diff --git a/src/constants/Categories.ts b/src/constants/Categories.ts
index 88230a1..021944a 100644
--- a/src/constants/Categories.ts
+++ b/src/constants/Categories.ts
@@ -33,6 +33,7 @@ export const CATEGORIES = [
'Glare Hover',
'Magnet Lines',
'Count Up',
+ 'Metallic Paint',
'Click Spark',
'Magnet',
'Cubes'
diff --git a/src/constants/Components.ts b/src/constants/Components.ts
index 6c1a5f2..6f5a3cf 100644
--- a/src/constants/Components.ts
+++ b/src/constants/Components.ts
@@ -5,6 +5,7 @@ const animations = {
'glare-hover': () => import('../demo/Animations/GlareHoverDemo.vue'),
'magnet-lines': () => import('../demo/Animations/MagnetLinesDemo.vue'),
'click-spark': () => import('../demo/Animations/ClickSparkDemo.vue'),
+ 'metallic-paint': () => import('../demo/Animations/MetallicPaintDemo.vue'),
'magnet': () => import('../demo/Animations/MagnetDemo.vue'),
'cubes': () => import('../demo/Animations/CubesDemo.vue'),
'count-up': () => import('../demo/Animations/CountUpDemo.vue')
diff --git a/src/constants/code/Animations/metallicPaintCode.ts b/src/constants/code/Animations/metallicPaintCode.ts
new file mode 100644
index 0000000..de663c3
--- /dev/null
+++ b/src/constants/code/Animations/metallicPaintCode.ts
@@ -0,0 +1,47 @@
+import code from '@content/Animations/MetallicPaint/MetallicPaint.vue?raw'
+import utility from '@content/Animations/MetallicPaint/parseImage.ts?raw'
+import type { CodeObject } from '../../../types/code'
+
+export const metallicPaint: CodeObject = {
+ cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/MetallicPaint`,
+ usage: `
+
+
+
+`,
+ code,
+ utility
+}
diff --git a/src/content/Animations/MetallicPaint/MetallicPaint.vue b/src/content/Animations/MetallicPaint/MetallicPaint.vue
new file mode 100644
index 0000000..47a8afa
--- /dev/null
+++ b/src/content/Animations/MetallicPaint/MetallicPaint.vue
@@ -0,0 +1,386 @@
+
+
+
+
+
diff --git a/src/content/Animations/MetallicPaint/parseImage.ts b/src/content/Animations/MetallicPaint/parseImage.ts
new file mode 100644
index 0000000..4627bee
--- /dev/null
+++ b/src/content/Animations/MetallicPaint/parseImage.ts
@@ -0,0 +1,180 @@
+export function parseImage(file: File): Promise<{ imageData: ImageData; pngBlob: Blob }> {
+ const canvas = document.createElement('canvas')
+ const ctx = canvas.getContext('2d')
+
+ return new Promise((resolve, reject) => {
+ if (!file || !ctx) {
+ reject(new Error('Invalid file or context'))
+ return
+ }
+
+ const img = new Image()
+ img.crossOrigin = 'anonymous'
+ img.onload = function () {
+ if (file.type === 'image/svg+xml') {
+ img.width = 1000
+ img.height = 1000
+ }
+
+ const MAX_SIZE = 1000
+ const MIN_SIZE = 500
+ let width = img.naturalWidth
+ let height = img.naturalHeight
+
+ if (width > MAX_SIZE || height > MAX_SIZE || width < MIN_SIZE || height < MIN_SIZE) {
+ if (width > height) {
+ if (width > MAX_SIZE) {
+ height = Math.round((height * MAX_SIZE) / width)
+ width = MAX_SIZE
+ } else if (width < MIN_SIZE) {
+ height = Math.round((height * MIN_SIZE) / width)
+ width = MIN_SIZE
+ }
+ } else {
+ if (height > MAX_SIZE) {
+ width = Math.round((width * MAX_SIZE) / height)
+ height = MAX_SIZE
+ } else if (height < MIN_SIZE) {
+ width = Math.round((width * MIN_SIZE) / height)
+ height = MIN_SIZE
+ }
+ }
+ }
+
+ canvas.width = width
+ canvas.height = height
+
+ const shapeCanvas = document.createElement('canvas')
+ shapeCanvas.width = width
+ shapeCanvas.height = height
+ const shapeCtx = shapeCanvas.getContext('2d')!
+ shapeCtx.drawImage(img, 0, 0, width, height)
+
+ const shapeImageData = shapeCtx.getImageData(0, 0, width, height)
+ const data = shapeImageData.data
+ const shapeMask = new Array(width * height).fill(false)
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ const idx4 = (y * width + x) * 4
+ const r = data[idx4]
+ const g = data[idx4 + 1]
+ const b = data[idx4 + 2]
+ const a = data[idx4 + 3]
+ shapeMask[y * width + x] = !((r === 255 && g === 255 && b === 255 && a === 255) || a === 0)
+ }
+ }
+
+ function inside(x: number, y: number) {
+ if (x < 0 || x >= width || y < 0 || y >= height) return false
+ return shapeMask[y * width + x]
+ }
+
+ const boundaryMask = new Array(width * height).fill(false)
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ const idx = y * width + x
+ if (!shapeMask[idx]) continue
+ let isBoundary = false
+ for (let ny = y - 1; ny <= y + 1 && !isBoundary; ny++) {
+ for (let nx = x - 1; nx <= x + 1 && !isBoundary; nx++) {
+ if (!inside(nx, ny)) {
+ isBoundary = true
+ }
+ }
+ }
+ if (isBoundary) {
+ boundaryMask[idx] = true
+ }
+ }
+ }
+
+ const interiorMask = new Array(width * height).fill(false)
+ for (let y = 1; y < height - 1; y++) {
+ for (let x = 1; x < width - 1; x++) {
+ const idx = y * width + x
+ if (
+ shapeMask[idx] &&
+ shapeMask[idx - 1] &&
+ shapeMask[idx + 1] &&
+ shapeMask[idx - width] &&
+ shapeMask[idx + width]
+ ) {
+ interiorMask[idx] = true
+ }
+ }
+ }
+
+ const u = new Float32Array(width * height).fill(0)
+ const newU = new Float32Array(width * height).fill(0)
+ const C = 0.01
+ const ITERATIONS = 300
+
+ function getU(x: number, y: number, arr: Float32Array) {
+ if (x < 0 || x >= width || y < 0 || y >= height) return 0
+ if (!shapeMask[y * width + x]) return 0
+ return arr[y * width + x]
+ }
+
+ for (let iter = 0; iter < ITERATIONS; iter++) {
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ const idx = y * width + x
+ if (!shapeMask[idx] || boundaryMask[idx]) {
+ newU[idx] = 0
+ continue
+ }
+ const sumN = getU(x + 1, y, u) + getU(x - 1, y, u) + getU(x, y + 1, u) + getU(x, y - 1, u)
+ newU[idx] = (C + sumN) / 4
+ }
+ }
+ u.set(newU)
+ }
+
+ let maxVal = 0
+ for (let i = 0; i < width * height; i++) {
+ if (u[i] > maxVal) maxVal = u[i]
+ }
+ const alpha = 2.0
+ const outImg = ctx.createImageData(width, height)
+
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ const idx = y * width + x
+ const px = idx * 4
+ if (!shapeMask[idx]) {
+ outImg.data[px] = 255
+ outImg.data[px + 1] = 255
+ outImg.data[px + 2] = 255
+ outImg.data[px + 3] = 255
+ } else {
+ const raw = u[idx] / maxVal
+ const remapped = Math.pow(raw, alpha)
+ const gray = 255 * (1 - remapped)
+ outImg.data[px] = gray
+ outImg.data[px + 1] = gray
+ outImg.data[px + 2] = gray
+ outImg.data[px + 3] = 255
+ }
+ }
+ }
+ ctx.putImageData(outImg, 0, 0)
+
+ canvas.toBlob(
+ blob => {
+ if (!blob) {
+ reject(new Error('Failed to create PNG blob'))
+ return
+ }
+ resolve({
+ imageData: outImg,
+ pngBlob: blob
+ })
+ },
+ 'image/png'
+ )
+ }
+
+ img.onerror = () => reject(new Error('Failed to load image'))
+ img.src = URL.createObjectURL(file)
+ })
+}
diff --git a/src/demo/Animations/MetallicPaintDemo.vue b/src/demo/Animations/MetallicPaintDemo.vue
new file mode 100644
index 0000000..438b53d
--- /dev/null
+++ b/src/demo/Animations/MetallicPaintDemo.vue
@@ -0,0 +1,141 @@
+
+
+
+
+