{ "name": "dither-gradient", "type": "registry:ui", "dependencies": [], "registryDependencies": [], "description": "An animated dithered gradient background effect using canvas with Bayer matrix dithering.", "files": [ { "path": "components/ui/dither-gradient.tsx", "content": "\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport { cn } from \"@/lib/utils\"\n\ninterface DitherGradientProps {\n className?: string\n colorFrom?: string\n colorTo?: string\n colorMid?: string\n intensity?: number\n speed?: number\n angle?: number\n}\n\nexport function DitherGradient({\n className,\n colorFrom = \"#4f46e5\",\n colorTo = \"#ec4899\",\n colorMid = \"#a855f7\",\n intensity = 0.15,\n speed = 3,\n angle = 45,\n}: DitherGradientProps) {\n const canvasRef = useRef(null)\n const animationRef = useRef(0)\n\n useEffect(() => {\n const canvas = canvasRef.current\n if (!canvas) return\n\n const ctx = canvas.getContext(\"2d\")\n if (!ctx) return\n\n const resize = () => {\n const rect = canvas.getBoundingClientRect()\n canvas.width = rect.width\n canvas.height = rect.height\n }\n\n resize()\n window.addEventListener(\"resize\", resize)\n\n let time = 0\n const bayerMatrix = [\n [0, 8, 2, 10],\n [12, 4, 14, 6],\n [3, 11, 1, 9],\n [15, 7, 13, 5],\n ]\n\n const hexToRgb = (hex: string) => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex)\n if (!result) return { r: 0, g: 0, b: 0 }\n return {\n r: parseInt(result[1]!, 16),\n g: parseInt(result[2]!, 16),\n b: parseInt(result[3]!, 16),\n }\n }\n\n const smoothstep = (t: number) => t * t * (3 - 2 * t)\n\n const animate = () => {\n const { width, height } = canvas\n const imageData = ctx.createImageData(width, height)\n const data = imageData.data\n\n const from = hexToRgb(colorFrom)\n const mid = hexToRgb(colorMid)\n const to = hexToRgb(colorTo)\n const rad = (angle * Math.PI) / 180\n\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const normalizedX = x / width\n const normalizedY = y / height\n \n const gradientPos = \n (normalizedX * Math.cos(rad) + normalizedY * Math.sin(rad)) * 0.8 + \n 0.1 + \n Math.sin(time * speed * 0.0008) * 0.1\n\n const clampedPos = Math.max(0, Math.min(1, gradientPos))\n\n let r: number, g: number, b: number\n if (clampedPos < 0.5) {\n const t = smoothstep(clampedPos * 2)\n r = from.r + (mid.r - from.r) * t\n g = from.g + (mid.g - from.g) * t\n b = from.b + (mid.b - from.b) * t\n } else {\n const t = smoothstep((clampedPos - 0.5) * 2)\n r = mid.r + (to.r - mid.r) * t\n g = mid.g + (to.g - mid.g) * t\n b = mid.b + (to.b - mid.b) * t\n }\n\n const threshold = (bayerMatrix[y % 4]![x % 4]! / 16 - 0.5) * intensity * 180\n const noise = (Math.random() - 0.5) * intensity * 60\n\n const idx = (y * width + x) * 4\n data[idx] = Math.min(255, Math.max(0, r + threshold + noise))\n data[idx + 1] = Math.min(255, Math.max(0, g + threshold + noise))\n data[idx + 2] = Math.min(255, Math.max(0, b + threshold + noise))\n data[idx + 3] = 255\n }\n }\n\n ctx.putImageData(imageData, 0, 0)\n time += 16\n animationRef.current = requestAnimationFrame(animate)\n }\n\n animate()\n\n return () => {\n window.removeEventListener(\"resize\", resize)\n cancelAnimationFrame(animationRef.current)\n }\n }, [colorFrom, colorTo, colorMid, intensity, speed, angle])\n\n return (\n \n )\n}\n", "type": "registry:ui" } ] }