{"name":"MetallicPaint","title":"MetallicPaint","description":"Liquid metallic paint shader which can be applied to SVG elements.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"\n\n\n","path":"MetallicPaint/MetallicPaint.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]},{"type":"registry:component","role":"file","content":"export function parseImage(file: File): Promise<{ imageData: ImageData; pngBlob: Blob }> {\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n\n return new Promise((resolve, reject) => {\n if (!file || !ctx) {\n reject(new Error('Invalid file or context'));\n return;\n }\n\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = function () {\n if (file.type === 'image/svg+xml') {\n img.width = 1000;\n img.height = 1000;\n }\n\n const MAX_SIZE = 1000;\n const MIN_SIZE = 500;\n let width = img.naturalWidth;\n let height = img.naturalHeight;\n\n if (width > MAX_SIZE || height > MAX_SIZE || width < MIN_SIZE || height < MIN_SIZE) {\n if (width > height) {\n if (width > MAX_SIZE) {\n height = Math.round((height * MAX_SIZE) / width);\n width = MAX_SIZE;\n } else if (width < MIN_SIZE) {\n height = Math.round((height * MIN_SIZE) / width);\n width = MIN_SIZE;\n }\n } else {\n if (height > MAX_SIZE) {\n width = Math.round((width * MAX_SIZE) / height);\n height = MAX_SIZE;\n } else if (height < MIN_SIZE) {\n width = Math.round((width * MIN_SIZE) / height);\n height = MIN_SIZE;\n }\n }\n }\n\n canvas.width = width;\n canvas.height = height;\n\n const shapeCanvas = document.createElement('canvas');\n shapeCanvas.width = width;\n shapeCanvas.height = height;\n const shapeCtx = shapeCanvas.getContext('2d')!;\n shapeCtx.drawImage(img, 0, 0, width, height);\n\n const shapeImageData = shapeCtx.getImageData(0, 0, width, height);\n const data = shapeImageData.data;\n const shapeMask = new Array(width * height).fill(false);\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx4 = (y * width + x) * 4;\n const r = data[idx4];\n const g = data[idx4 + 1];\n const b = data[idx4 + 2];\n const a = data[idx4 + 3];\n shapeMask[y * width + x] = !((r === 255 && g === 255 && b === 255 && a === 255) || a === 0);\n }\n }\n\n function inside(x: number, y: number) {\n if (x < 0 || x >= width || y < 0 || y >= height) return false;\n return shapeMask[y * width + x];\n }\n\n const boundaryMask = new Array(width * height).fill(false);\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = y * width + x;\n if (!shapeMask[idx]) continue;\n let isBoundary = false;\n for (let ny = y - 1; ny <= y + 1 && !isBoundary; ny++) {\n for (let nx = x - 1; nx <= x + 1 && !isBoundary; nx++) {\n if (!inside(nx, ny)) {\n isBoundary = true;\n }\n }\n }\n if (isBoundary) {\n boundaryMask[idx] = true;\n }\n }\n }\n\n const interiorMask = new Array(width * height).fill(false);\n for (let y = 1; y < height - 1; y++) {\n for (let x = 1; x < width - 1; x++) {\n const idx = y * width + x;\n if (\n shapeMask[idx] &&\n shapeMask[idx - 1] &&\n shapeMask[idx + 1] &&\n shapeMask[idx - width] &&\n shapeMask[idx + width]\n ) {\n interiorMask[idx] = true;\n }\n }\n }\n\n const u = new Float32Array(width * height).fill(0);\n const newU = new Float32Array(width * height).fill(0);\n const C = 0.01;\n const ITERATIONS = 300;\n\n function getU(x: number, y: number, arr: Float32Array) {\n if (x < 0 || x >= width || y < 0 || y >= height) return 0;\n if (!shapeMask[y * width + x]) return 0;\n return arr[y * width + x];\n }\n\n for (let iter = 0; iter < ITERATIONS; iter++) {\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = y * width + x;\n if (!shapeMask[idx] || boundaryMask[idx]) {\n newU[idx] = 0;\n continue;\n }\n const sumN = getU(x + 1, y, u) + getU(x - 1, y, u) + getU(x, y + 1, u) + getU(x, y - 1, u);\n newU[idx] = (C + sumN) / 4;\n }\n }\n u.set(newU);\n }\n\n let maxVal = 0;\n for (let i = 0; i < width * height; i++) {\n if (u[i] > maxVal) maxVal = u[i];\n }\n const alpha = 2.0;\n const outImg = ctx.createImageData(width, height);\n\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = y * width + x;\n const px = idx * 4;\n if (!shapeMask[idx]) {\n outImg.data[px] = 255;\n outImg.data[px + 1] = 255;\n outImg.data[px + 2] = 255;\n outImg.data[px + 3] = 255;\n } else {\n const raw = u[idx] / maxVal;\n const remapped = Math.pow(raw, alpha);\n const gray = 255 * (1 - remapped);\n outImg.data[px] = gray;\n outImg.data[px + 1] = gray;\n outImg.data[px + 2] = gray;\n outImg.data[px + 3] = 255;\n }\n }\n }\n ctx.putImageData(outImg, 0, 0);\n\n canvas.toBlob(blob => {\n if (!blob) {\n reject(new Error('Failed to create PNG blob'));\n return;\n }\n resolve({\n imageData: outImg,\n pngBlob: blob\n });\n }, 'image/png');\n };\n\n img.onerror = () => reject(new Error('Failed to load image'));\n img.src = URL.createObjectURL(file);\n });\n}\n","path":"MetallicPaint/parseImage.ts","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[],"devDependencies":[],"categories":["Animations"]}