{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "dither-shader",
	"title": "Dither Shader",
	"type": "registry:block",
	"description": "A canvas-based component that applies dithering effects to images with customizable patterns and color modes.",
	"files": [
		{
			"content": "<script lang=\"ts\">\n\timport { onMount } from \"svelte\";\n\timport { cn } from \"$UTILS$\";\n\n\ttype DitheringMode = \"bayer\" | \"halftone\" | \"noise\" | \"crosshatch\";\n\ttype ColorMode = \"original\" | \"grayscale\" | \"duotone\" | \"custom\";\n\n\tinterface DitherShaderProps {\n\t\t/** Source image URL */\n\t\tsrc: string;\n\t\t/** Size of the dithering grid cells */\n\t\tgridSize?: number;\n\t\t/** Type of dithering pattern */\n\t\tditherMode?: DitheringMode;\n\t\t/** Color processing mode */\n\t\tcolorMode?: ColorMode;\n\t\t/** Invert the dithered output colors */\n\t\tinvert?: boolean;\n\t\t/** Pixelation multiplier (1 = no pixelation, higher = more pixelated) */\n\t\tpixelRatio?: number;\n\t\t/** Primary color for duotone mode */\n\t\tprimaryColor?: string;\n\t\t/** Secondary color for duotone mode */\n\t\tsecondaryColor?: string;\n\t\t/** Custom color palette array for custom mode */\n\t\tcustomPalette?: string[];\n\t\t/** Brightness adjustment (-1 to 1) */\n\t\tbrightness?: number;\n\t\t/** Contrast adjustment (0 to 2, 1 = normal) */\n\t\tcontrast?: number;\n\t\t/** Background color behind the dithered image */\n\t\tbackgroundColor?: string;\n\t\t/** Object fit behavior */\n\t\tobjectFit?: \"cover\" | \"contain\" | \"fill\" | \"none\";\n\t\t/** Threshold bias for dithering (0 to 1) */\n\t\tthreshold?: number;\n\t\t/** Enable animation effect */\n\t\tanimated?: boolean;\n\t\t/** Animation speed (lower = slower) */\n\t\tanimationSpeed?: number;\n\t\t/** Additional CSS classes for the container (use this to set size via Tailwind) */\n\t\tclass?: string;\n\t}\n\n\tlet {\n\t\tsrc,\n\t\tgridSize = 4,\n\t\tditherMode = \"bayer\",\n\t\tcolorMode = \"original\",\n\t\tinvert = false,\n\t\tpixelRatio = 1,\n\t\tprimaryColor = \"#000000\",\n\t\tsecondaryColor = \"#ffffff\",\n\t\tcustomPalette = [\"#000000\", \"#ffffff\"],\n\t\tbrightness = 0,\n\t\tcontrast = 1,\n\t\tbackgroundColor = \"transparent\",\n\t\tobjectFit = \"cover\",\n\t\tthreshold = 0.5,\n\t\tanimated = false,\n\t\tanimationSpeed = 0.02,\n\t\tclass: className,\n\t}: DitherShaderProps = $props();\n\n\t// 4x4 Bayer matrix for ordered dithering\n\tconst BAYER_MATRIX_4x4 = [\n\t\t[0, 8, 2, 10],\n\t\t[12, 4, 14, 6],\n\t\t[3, 11, 1, 9],\n\t\t[15, 7, 13, 5],\n\t];\n\n\t// 8x8 Bayer matrix for finer dithering\n\tconst BAYER_MATRIX_8x8 = [\n\t\t[0, 32, 8, 40, 2, 34, 10, 42],\n\t\t[48, 16, 56, 24, 50, 18, 58, 26],\n\t\t[12, 44, 4, 36, 14, 46, 6, 38],\n\t\t[60, 28, 52, 20, 62, 30, 54, 22],\n\t\t[3, 35, 11, 43, 1, 33, 9, 41],\n\t\t[51, 19, 59, 27, 49, 17, 57, 25],\n\t\t[15, 47, 7, 39, 13, 45, 5, 37],\n\t\t[63, 31, 55, 23, 61, 29, 53, 21],\n\t];\n\n\tlet containerRef: HTMLDivElement;\n\tlet canvasRef: HTMLCanvasElement;\n\tlet animationFrameId: number | null = null;\n\tlet timeRef = 0;\n\tlet imageRef: HTMLImageElement | null = null;\n\tlet imageDataRef: ImageData | null = null;\n\tlet dimensions = $state({ width: 0, height: 0 });\n\n\tconst parsedPrimaryColor = $derived(parseColor(primaryColor));\n\tconst parsedSecondaryColor = $derived(parseColor(secondaryColor));\n\tconst parsedCustomPalette = $derived(customPalette.map(parseColor));\n\n\tfunction parseColor(color: string): [number, number, number] {\n\t\tif (color.startsWith(\"#\")) {\n\t\t\tconst hex = color.slice(1);\n\t\t\tif (hex.length === 3) {\n\t\t\t\treturn [\n\t\t\t\t\tparseInt(hex[0] + hex[0], 16),\n\t\t\t\t\tparseInt(hex[1] + hex[1], 16),\n\t\t\t\t\tparseInt(hex[2] + hex[2], 16),\n\t\t\t\t];\n\t\t\t}\n\t\t\treturn [\n\t\t\t\tparseInt(hex.slice(0, 2), 16),\n\t\t\t\tparseInt(hex.slice(2, 4), 16),\n\t\t\t\tparseInt(hex.slice(4, 6), 16),\n\t\t\t];\n\t\t}\n\t\tconst match = color.match(/rgb\\((\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\)/i);\n\t\tif (match) {\n\t\t\treturn [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];\n\t\t}\n\t\treturn [0, 0, 0];\n\t}\n\n\tfunction getLuminance(r: number, g: number, b: number): number {\n\t\treturn 0.299 * r + 0.587 * g + 0.114 * b;\n\t}\n\n\tfunction clamp(value: number, min: number, max: number): number {\n\t\treturn Math.max(min, Math.min(max, value));\n\t}\n\n\tfunction applyDithering(\n\t\tctx: CanvasRenderingContext2D,\n\t\tdisplayWidth: number,\n\t\tdisplayHeight: number,\n\t\ttime: number = 0\n\t) {\n\t\tif (!canvasRef || !imageDataRef) return;\n\n\t\t// Clear with background\n\t\tif (backgroundColor !== \"transparent\") {\n\t\t\tctx.fillStyle = backgroundColor;\n\t\t\tctx.fillRect(0, 0, displayWidth, displayHeight);\n\t\t} else {\n\t\t\tctx.clearRect(0, 0, displayWidth, displayHeight);\n\t\t}\n\n\t\tconst sourceData = imageDataRef.data;\n\t\tconst sourceWidth = imageDataRef.width;\n\t\tconst sourceHeight = imageDataRef.height;\n\n\t\tconst effectivePixelSize = Math.max(1, Math.floor(gridSize * pixelRatio));\n\t\tconst matrixSize = gridSize <= 4 ? 4 : 8;\n\t\tconst bayerMatrix = gridSize <= 4 ? BAYER_MATRIX_4x4 : BAYER_MATRIX_8x8;\n\t\tconst matrixScale = matrixSize === 4 ? 16 : 64;\n\n\t\t// Process pixels\n\t\tfor (let y = 0; y < displayHeight; y += effectivePixelSize) {\n\t\t\tfor (let x = 0; x < displayWidth; x += effectivePixelSize) {\n\t\t\t\t// Map display coordinates to source image coordinates\n\t\t\t\tconst srcX = Math.floor((x / displayWidth) * sourceWidth);\n\t\t\t\tconst srcY = Math.floor((y / displayHeight) * sourceHeight);\n\t\t\t\tconst srcIdx = (srcY * sourceWidth + srcX) * 4;\n\n\t\t\t\tlet r = sourceData[srcIdx] || 0;\n\t\t\t\tlet g = sourceData[srcIdx + 1] || 0;\n\t\t\t\tlet b = sourceData[srcIdx + 2] || 0;\n\t\t\t\tconst a = sourceData[srcIdx + 3] || 0;\n\n\t\t\t\tif (a < 10) continue; // Skip fully transparent pixels\n\n\t\t\t\t// Apply brightness and contrast\n\t\t\t\tr = clamp((r - 128) * contrast + 128 + brightness * 255, 0, 255);\n\t\t\t\tg = clamp((g - 128) * contrast + 128 + brightness * 255, 0, 255);\n\t\t\t\tb = clamp((b - 128) * contrast + 128 + brightness * 255, 0, 255);\n\n\t\t\t\t// Calculate luminance\n\t\t\t\tconst luminance = getLuminance(r, g, b) / 255;\n\n\t\t\t\t// Get dither threshold based on mode\n\t\t\t\tlet ditherThreshold: number;\n\t\t\t\tconst matrixX = Math.floor(x / gridSize) % matrixSize;\n\t\t\t\tconst matrixY = Math.floor(y / gridSize) % matrixSize;\n\n\t\t\t\tswitch (ditherMode) {\n\t\t\t\t\tcase \"bayer\":\n\t\t\t\t\t\tditherThreshold = bayerMatrix[matrixY][matrixX] / matrixScale;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"halftone\": {\n\t\t\t\t\t\tconst angle = Math.PI / 4;\n\t\t\t\t\t\tconst scale = gridSize * 2;\n\t\t\t\t\t\tconst rotX = x * Math.cos(angle) + y * Math.sin(angle);\n\t\t\t\t\t\tconst rotY = -x * Math.sin(angle) + y * Math.cos(angle);\n\t\t\t\t\t\tconst pattern = (Math.sin(rotX / scale) + Math.sin(rotY / scale) + 2) / 4;\n\t\t\t\t\t\tditherThreshold = pattern;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase \"noise\": {\n\t\t\t\t\t\tconst noiseVal =\n\t\t\t\t\t\t\tMath.sin(x * 12.9898 + y * 78.233 + time * 100) * 43758.5453;\n\t\t\t\t\t\tditherThreshold = noiseVal - Math.floor(noiseVal);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase \"crosshatch\": {\n\t\t\t\t\t\tconst line1 = (x + y) % (gridSize * 2) < gridSize ? 1 : 0;\n\t\t\t\t\t\tconst line2 = (x - y + gridSize * 4) % (gridSize * 2) < gridSize ? 1 : 0;\n\t\t\t\t\t\tditherThreshold = (line1 + line2) / 2;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tditherThreshold = bayerMatrix[matrixY][matrixX] / matrixScale;\n\t\t\t\t}\n\n\t\t\t\t// Adjust threshold with user setting\n\t\t\t\tditherThreshold = ditherThreshold * (1 - threshold) + threshold * 0.5;\n\n\t\t\t\t// Determine output color based on color mode\n\t\t\t\tlet outputColor: [number, number, number];\n\n\t\t\t\tswitch (colorMode) {\n\t\t\t\t\tcase \"grayscale\": {\n\t\t\t\t\t\tconst shouldBeDark = luminance < ditherThreshold;\n\t\t\t\t\t\toutputColor = shouldBeDark ? [0, 0, 0] : [255, 255, 255];\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase \"duotone\": {\n\t\t\t\t\t\tconst shouldBeDark = luminance < ditherThreshold;\n\t\t\t\t\t\toutputColor = shouldBeDark ? parsedPrimaryColor : parsedSecondaryColor;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase \"custom\": {\n\t\t\t\t\t\tif (parsedCustomPalette.length === 2) {\n\t\t\t\t\t\t\tconst shouldBeDark = luminance < ditherThreshold;\n\t\t\t\t\t\t\toutputColor = shouldBeDark\n\t\t\t\t\t\t\t\t? parsedCustomPalette[0]\n\t\t\t\t\t\t\t\t: parsedCustomPalette[1];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Quantize to closest palette color with dithering\n\t\t\t\t\t\t\tconst adjustedLuminance = luminance + (ditherThreshold - 0.5) * 0.5;\n\t\t\t\t\t\t\tconst paletteIndex = Math.floor(\n\t\t\t\t\t\t\t\tclamp(adjustedLuminance, 0, 1) * (parsedCustomPalette.length - 1)\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\toutputColor = parsedCustomPalette[paletteIndex];\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase \"original\":\n\t\t\t\t\tdefault: {\n\t\t\t\t\t\t// Apply dithering while preserving colors\n\t\t\t\t\t\tconst ditherAmount = ditherThreshold - 0.5;\n\t\t\t\t\t\tconst adjustedR = clamp(r + ditherAmount * 64, 0, 255);\n\t\t\t\t\t\tconst adjustedG = clamp(g + ditherAmount * 64, 0, 255);\n\t\t\t\t\t\tconst adjustedB = clamp(b + ditherAmount * 64, 0, 255);\n\n\t\t\t\t\t\t// Quantize to fewer levels for dithered look\n\t\t\t\t\t\tconst levels = 4;\n\t\t\t\t\t\toutputColor = [\n\t\t\t\t\t\t\tMath.round(adjustedR / (255 / levels)) * (255 / levels),\n\t\t\t\t\t\t\tMath.round(adjustedG / (255 / levels)) * (255 / levels),\n\t\t\t\t\t\t\tMath.round(adjustedB / (255 / levels)) * (255 / levels),\n\t\t\t\t\t\t];\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Apply inversion\n\t\t\t\tif (invert) {\n\t\t\t\t\toutputColor = [\n\t\t\t\t\t\t255 - outputColor[0],\n\t\t\t\t\t\t255 - outputColor[1],\n\t\t\t\t\t\t255 - outputColor[2],\n\t\t\t\t\t];\n\t\t\t\t}\n\n\t\t\t\t// Draw the pixel\n\t\t\t\tctx.fillStyle = `rgb(${outputColor[0]}, ${outputColor[1]}, ${outputColor[2]})`;\n\t\t\t\tctx.fillRect(x, y, effectivePixelSize, effectivePixelSize);\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction processImage(img: HTMLImageElement) {\n\t\tif (!canvasRef || dimensions.width === 0 || dimensions.height === 0) return;\n\n\t\tconst dpr = typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1;\n\t\tconst displayWidth = dimensions.width;\n\t\tconst displayHeight = dimensions.height;\n\n\t\tcanvasRef.width = Math.floor(displayWidth * dpr);\n\t\tcanvasRef.height = Math.floor(displayHeight * dpr);\n\n\t\tconst ctx = canvasRef.getContext(\"2d\");\n\t\tif (!ctx) return;\n\t\tctx.resetTransform();\n\t\tctx.scale(dpr, dpr);\n\n\t\t// Create offscreen canvas to get image data\n\t\tconst offscreen = document.createElement(\"canvas\");\n\t\tconst iw = img.naturalWidth || displayWidth;\n\t\tconst ih = img.naturalHeight || displayHeight;\n\n\t\tlet dw = displayWidth;\n\t\tlet dh = displayHeight;\n\t\tlet dx = 0;\n\t\tlet dy = 0;\n\n\t\tif (objectFit === \"cover\") {\n\t\t\tconst scale = Math.max(displayWidth / iw, displayHeight / ih);\n\t\t\tdw = Math.ceil(iw * scale);\n\t\t\tdh = Math.ceil(ih * scale);\n\t\t\tdx = Math.floor((displayWidth - dw) / 2);\n\t\t\tdy = Math.floor((displayHeight - dh) / 2);\n\t\t} else if (objectFit === \"contain\") {\n\t\t\tconst scale = Math.min(displayWidth / iw, displayHeight / ih);\n\t\t\tdw = Math.ceil(iw * scale);\n\t\t\tdh = Math.ceil(ih * scale);\n\t\t\tdx = Math.floor((displayWidth - dw) / 2);\n\t\t\tdy = Math.floor((displayHeight - dh) / 2);\n\t\t} else if (objectFit === \"fill\") {\n\t\t\tdw = displayWidth;\n\t\t\tdh = displayHeight;\n\t\t} else {\n\t\t\tdw = iw;\n\t\t\tdh = ih;\n\t\t\tdx = Math.floor((displayWidth - dw) / 2);\n\t\t\tdy = Math.floor((displayHeight - dh) / 2);\n\t\t}\n\n\t\toffscreen.width = displayWidth;\n\t\toffscreen.height = displayHeight;\n\t\tconst offCtx = offscreen.getContext(\"2d\");\n\t\tif (!offCtx) return;\n\n\t\toffCtx.drawImage(img, dx, dy, dw, dh);\n\n\t\ttry {\n\t\t\timageDataRef = offCtx.getImageData(0, 0, displayWidth, displayHeight);\n\t\t} catch {\n\t\t\tconsole.error(\"Could not get image data. CORS issue?\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Initial render\n\t\tapplyDithering(ctx, displayWidth, displayHeight, 0);\n\n\t\t// Setup animation if enabled\n\t\tif (animated) {\n\t\t\tconst animate = () => {\n\t\t\t\ttimeRef += animationSpeed;\n\t\t\t\tapplyDithering(ctx, displayWidth, displayHeight, timeRef);\n\t\t\t\tanimationFrameId = requestAnimationFrame(animate);\n\t\t\t};\n\t\t\tanimationFrameId = requestAnimationFrame(animate);\n\t\t}\n\t}\n\n\t// Setup resize observer for responsive sizing\n\tonMount(() => {\n\t\tif (!containerRef) return;\n\n\t\tconst resizeObserver = new ResizeObserver((entries) => {\n\t\t\tfor (const entry of entries) {\n\t\t\t\tconst { width, height } = entry.contentRect;\n\t\t\t\tif (width > 0 && height > 0) {\n\t\t\t\t\tdimensions = { width, height };\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tresizeObserver.observe(containerRef);\n\n\t\treturn () => {\n\t\t\tresizeObserver.disconnect();\n\t\t\tif (animationFrameId) {\n\t\t\t\tcancelAnimationFrame(animationFrameId);\n\t\t\t}\n\t\t};\n\t});\n\n\t// Process image when dimensions or settings change\n\t$effect(() => {\n\t\tif (dimensions.width === 0 || dimensions.height === 0) return;\n\n\t\t// Cancel any existing animation\n\t\tif (animationFrameId) {\n\t\t\tcancelAnimationFrame(animationFrameId);\n\t\t\tanimationFrameId = null;\n\t\t}\n\n\t\t// If image is already loaded, reprocess it\n\t\tif (imageRef && imageRef.complete) {\n\t\t\tprocessImage(imageRef);\n\t\t} else {\n\t\t\t// Load the image\n\t\t\tconst img = new Image();\n\t\t\timg.crossOrigin = \"anonymous\";\n\t\t\timg.src = src;\n\n\t\t\timg.onload = () => {\n\t\t\t\timageRef = img;\n\t\t\t\tprocessImage(img);\n\t\t\t};\n\n\t\t\timg.onerror = () => {\n\t\t\t\tconsole.error(\"Failed to load image for DitherShader:\", src);\n\t\t\t};\n\t\t}\n\t});\n</script>\n\n<div bind:this={containerRef} class={cn(\"relative h-full w-full\", className)}>\n\t<canvas\n\t\tbind:this={canvasRef}\n\t\tclass=\"absolute inset-0 h-full w-full\"\n\t\tstyle=\"image-rendering: pixelated;\"\n\t\taria-label=\"Dithered image\"\n\t></canvas>\n</div>\n",
			"type": "registry:component",
			"target": "magic/dither-shader/dither-shader.svelte"
		},
		{
			"content": "import DitherShader from \"./dither-shader.svelte\";\nexport { DitherShader };\n",
			"type": "registry:file",
			"target": "magic/dither-shader/index.ts"
		}
	]
}