mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-09 08:29:30 -06:00
Migrate MetallicPaint
This commit is contained in:
180
src/content/Animations/MetallicPaint/parseImage.ts
Normal file
180
src/content/Animations/MetallicPaint/parseImage.ts
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user