mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
19 KiB
JSON
1 line
19 KiB
JSON
{"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":"<template>\n <canvas ref=\"canvasRef\" class=\"block w-full h-full object-contain\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch, nextTick, useTemplateRef } from 'vue';\n\ninterface ShaderParams {\n patternScale: number;\n refraction: number;\n edge: number;\n patternBlur: number;\n liquid: number;\n speed: number;\n}\n\ninterface Props {\n imageData: ImageData;\n params?: ShaderParams;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n params: () => ({\n patternScale: 2,\n refraction: 0.015,\n edge: 1,\n patternBlur: 0.005,\n liquid: 0.07,\n speed: 0.3\n })\n});\n\nconst canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef');\nconst gl = ref<WebGL2RenderingContext | null>(null);\nconst uniforms = ref<Record<string, WebGLUniformLocation>>({});\nconst totalAnimationTime = ref(0);\nconst lastRenderTime = ref(0);\nconst animationId = ref<number>();\n\nconst vertexShaderSource = `#version 300 es\nprecision mediump float;\n\nin vec2 a_position;\nout vec2 vUv;\n\nvoid main() {\n vUv = .5 * (a_position + 1.);\n gl_Position = vec4(a_position, 0.0, 1.0);\n}`;\n\nconst liquidFragSource = `#version 300 es\nprecision mediump float;\n\nin vec2 vUv;\nout vec4 fragColor;\n\nuniform sampler2D u_image_texture;\nuniform float u_time;\nuniform float u_ratio;\nuniform float u_img_ratio;\nuniform float u_patternScale;\nuniform float u_refraction;\nuniform float u_edge;\nuniform float u_patternBlur;\nuniform float u_liquid;\n\n#define TWO_PI 6.28318530718\n#define PI 3.14159265358979323846\n\nvec3 mod289(vec3 x) { return x - floor(x * (1. / 289.)) * 289.; }\nvec2 mod289(vec2 x) { return x - floor(x * (1. / 289.)) * 289.; }\nvec3 permute(vec3 x) { return mod289(((x*34.)+1.)*x); }\nfloat snoise(vec2 v) {\n const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);\n vec2 i = floor(v + dot(v, C.yy));\n vec2 x0 = v - i + dot(i, C.xx);\n vec2 i1;\n i1 = (x0.x > x0.y) ? vec2(1., 0.) : vec2(0., 1.);\n vec4 x12 = x0.xyxy + C.xxzz;\n x12.xy -= i1;\n i = mod289(i);\n vec3 p = permute(permute(i.y + vec3(0., i1.y, 1.)) + i.x + vec3(0., i1.x, 1.));\n vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.);\n m = m*m;\n m = m*m;\n vec3 x = 2. * fract(p * C.www) - 1.;\n vec3 h = abs(x) - 0.5;\n vec3 ox = floor(x + 0.5);\n vec3 a0 = x - ox;\n m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);\n vec3 g;\n g.x = a0.x * x0.x + h.x * x0.y;\n g.yz = a0.yz * x12.xz + h.yz * x12.yw;\n return 130. * dot(m, g);\n}\n\nvec2 get_img_uv() {\n vec2 img_uv = vUv;\n img_uv -= .5;\n if (u_ratio > u_img_ratio) {\n img_uv.x = img_uv.x * u_ratio / u_img_ratio;\n } else {\n img_uv.y = img_uv.y * u_img_ratio / u_ratio;\n }\n float scale_factor = 1.;\n img_uv *= scale_factor;\n img_uv += .5;\n img_uv.y = 1. - img_uv.y;\n return img_uv;\n}\nvec2 rotate(vec2 uv, float th) {\n return mat2(cos(th), sin(th), -sin(th), cos(th)) * uv;\n}\nfloat get_color_channel(float c1, float c2, float stripe_p, vec3 w, float extra_blur, float b) {\n float ch = c2;\n float border = 0.;\n float blur = u_patternBlur + extra_blur;\n ch = mix(ch, c1, smoothstep(.0, blur, stripe_p));\n border = w[0];\n ch = mix(ch, c2, smoothstep(border - blur, border + blur, stripe_p));\n b = smoothstep(.2, .8, b);\n border = w[0] + .4 * (1. - b) * w[1];\n ch = mix(ch, c1, smoothstep(border - blur, border + blur, stripe_p));\n border = w[0] + .5 * (1. - b) * w[1];\n ch = mix(ch, c2, smoothstep(border - blur, border + blur, stripe_p));\n border = w[0] + w[1];\n ch = mix(ch, c1, smoothstep(border - blur, border + blur, stripe_p));\n float gradient_t = (stripe_p - w[0] - w[1]) / w[2];\n float gradient = mix(c1, c2, smoothstep(0., 1., gradient_t));\n ch = mix(ch, gradient, smoothstep(border - blur, border + blur, stripe_p));\n return ch;\n}\nfloat get_img_frame_alpha(vec2 uv, float img_frame_width) {\n float img_frame_alpha = smoothstep(0., img_frame_width, uv.x) * smoothstep(1., 1. - img_frame_width, uv.x);\n img_frame_alpha *= smoothstep(0., img_frame_width, uv.y) * smoothstep(1., 1. - img_frame_width, uv.y);\n return img_frame_alpha;\n}\nvoid main() {\n vec2 uv = vUv;\n uv.y = 1. - uv.y;\n uv.x *= u_ratio;\n float diagonal = uv.x - uv.y;\n float t = .001 * u_time;\n vec2 img_uv = get_img_uv();\n vec4 img = texture(u_image_texture, img_uv);\n vec3 color = vec3(0.);\n float opacity = 1.;\n vec3 color1 = vec3(.98, 0.98, 1.);\n vec3 color2 = vec3(.1, .1, .1 + .1 * smoothstep(.7, 1.3, uv.x + uv.y));\n float edge = img.r;\n vec2 grad_uv = uv;\n grad_uv -= .5;\n float dist = length(grad_uv + vec2(0., .2 * diagonal));\n grad_uv = rotate(grad_uv, (.25 - .2 * diagonal) * PI);\n float bulge = pow(1.8 * dist, 1.2);\n bulge = 1. - bulge;\n bulge *= pow(uv.y, .3);\n float cycle_width = u_patternScale;\n float thin_strip_1_ratio = .12 / cycle_width * (1. - .4 * bulge);\n float thin_strip_2_ratio = .07 / cycle_width * (1. + .4 * bulge);\n float wide_strip_ratio = (1. - thin_strip_1_ratio - thin_strip_2_ratio);\n float thin_strip_1_width = cycle_width * thin_strip_1_ratio;\n float thin_strip_2_width = cycle_width * thin_strip_2_ratio;\n opacity = 1. - smoothstep(.9 - .5 * u_edge, 1. - .5 * u_edge, edge);\n opacity *= get_img_frame_alpha(img_uv, 0.01);\n float noise = snoise(uv - t);\n edge += (1. - edge) * u_liquid * noise;\n float refr = 0.;\n refr += (1. - bulge);\n refr = clamp(refr, 0., 1.);\n float dir = grad_uv.x;\n dir += diagonal;\n dir -= 2. * noise * diagonal * (smoothstep(0., 1., edge) * smoothstep(1., 0., edge));\n bulge *= clamp(pow(uv.y, .1), .3, 1.);\n dir *= (.1 + (1.1 - edge) * bulge);\n dir *= smoothstep(1., .7, edge);\n dir += .18 * (smoothstep(.1, .2, uv.y) * smoothstep(.4, .2, uv.y));\n dir += .03 * (smoothstep(.1, .2, 1. - uv.y) * smoothstep(.4, .2, 1. - uv.y));\n dir *= (.5 + .5 * pow(uv.y, 2.));\n dir *= cycle_width;\n dir -= t;\n float refr_r = refr;\n refr_r += .03 * bulge * noise;\n float refr_b = 1.3 * refr;\n refr_r += 5. * (smoothstep(-.1, .2, uv.y) * smoothstep(.5, .1, uv.y)) * (smoothstep(.4, .6, bulge) * smoothstep(1., .4, bulge));\n refr_r -= diagonal;\n refr_b += (smoothstep(0., .4, uv.y) * smoothstep(.8, .1, uv.y)) * (smoothstep(.4, .6, bulge) * smoothstep(.8, .4, bulge));\n refr_b -= .2 * edge;\n refr_r *= u_refraction;\n refr_b *= u_refraction;\n vec3 w = vec3(thin_strip_1_width, thin_strip_2_width, wide_strip_ratio);\n w[1] -= .02 * smoothstep(.0, 1., edge + bulge);\n float stripe_r = mod(dir + refr_r, 1.);\n float r = get_color_channel(color1.r, color2.r, stripe_r, w, 0.02 + .03 * u_refraction * bulge, bulge);\n float stripe_g = mod(dir, 1.);\n float g = get_color_channel(color1.g, color2.g, stripe_g, w, 0.01 / (1. - diagonal), bulge);\n float stripe_b = mod(dir - refr_b, 1.);\n float b = get_color_channel(color1.b, color2.b, stripe_b, w, .01, bulge);\n color = vec3(r, g, b);\n color *= opacity;\n fragColor = vec4(color, opacity);\n}\n`;\n\nfunction updateUniforms() {\n if (!gl.value || !uniforms.value) return;\n gl.value.uniform1f(uniforms.value.u_edge, props.params.edge);\n gl.value.uniform1f(uniforms.value.u_patternBlur, props.params.patternBlur);\n gl.value.uniform1f(uniforms.value.u_time, 0);\n gl.value.uniform1f(uniforms.value.u_patternScale, props.params.patternScale);\n gl.value.uniform1f(uniforms.value.u_refraction, props.params.refraction);\n gl.value.uniform1f(uniforms.value.u_liquid, props.params.liquid);\n}\n\nfunction createShader(gl: WebGL2RenderingContext, sourceCode: string, type: number) {\n const shader = gl.createShader(type);\n if (!shader) {\n return null;\n }\n\n gl.shaderSource(shader, sourceCode);\n gl.compileShader(shader);\n\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n }\n\n return shader;\n}\n\nfunction getUniforms(program: WebGLProgram, gl: WebGL2RenderingContext) {\n const uniformsObj: Record<string, WebGLUniformLocation> = {};\n const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);\n for (let i = 0; i < uniformCount; i++) {\n const uniformName = gl.getActiveUniform(program, i)?.name;\n if (!uniformName) continue;\n uniformsObj[uniformName] = gl.getUniformLocation(program, uniformName) as WebGLUniformLocation;\n }\n return uniformsObj;\n}\n\nfunction initShader() {\n const canvas = canvasRef.value;\n const glContext = canvas?.getContext('webgl2', {\n antialias: true,\n alpha: true\n });\n if (!canvas || !glContext) {\n return;\n }\n\n const vertexShader = createShader(glContext, vertexShaderSource, glContext.VERTEX_SHADER);\n const fragmentShader = createShader(glContext, liquidFragSource, glContext.FRAGMENT_SHADER);\n const program = glContext.createProgram();\n if (!program || !vertexShader || !fragmentShader) {\n return;\n }\n\n glContext.attachShader(program, vertexShader);\n glContext.attachShader(program, fragmentShader);\n glContext.linkProgram(program);\n\n if (!glContext.getProgramParameter(program, glContext.LINK_STATUS)) {\n console.error('Unable to initialize the shader program: ' + glContext.getProgramInfoLog(program));\n return null;\n }\n\n const uniformsObj = getUniforms(program, glContext);\n uniforms.value = uniformsObj;\n\n const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);\n const vertexBuffer = glContext.createBuffer();\n glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuffer);\n glContext.bufferData(glContext.ARRAY_BUFFER, vertices, glContext.STATIC_DRAW);\n\n glContext.useProgram(program);\n\n const positionLocation = glContext.getAttribLocation(program, 'a_position');\n glContext.enableVertexAttribArray(positionLocation);\n\n glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuffer);\n glContext.vertexAttribPointer(positionLocation, 2, glContext.FLOAT, false, 0, 0);\n\n gl.value = glContext;\n}\n\nfunction resizeCanvas() {\n if (!canvasRef.value || !gl.value || !uniforms.value || !props.imageData) return;\n const imgRatio = props.imageData.width / props.imageData.height;\n gl.value.uniform1f(uniforms.value.u_img_ratio, imgRatio);\n\n const side = 1000;\n canvasRef.value.width = side * devicePixelRatio;\n canvasRef.value.height = side * devicePixelRatio;\n gl.value.viewport(0, 0, canvasRef.value.height, canvasRef.value.height);\n gl.value.uniform1f(uniforms.value.u_ratio, 1);\n gl.value.uniform1f(uniforms.value.u_img_ratio, imgRatio);\n}\n\nfunction setupTexture() {\n if (!gl.value || !uniforms.value) return;\n\n const existingTexture = gl.value.getParameter(gl.value.TEXTURE_BINDING_2D);\n if (existingTexture) {\n gl.value.deleteTexture(existingTexture);\n }\n\n const imageTexture = gl.value.createTexture();\n gl.value.activeTexture(gl.value.TEXTURE0);\n gl.value.bindTexture(gl.value.TEXTURE_2D, imageTexture);\n\n gl.value.texParameteri(gl.value.TEXTURE_2D, gl.value.TEXTURE_MIN_FILTER, gl.value.LINEAR);\n gl.value.texParameteri(gl.value.TEXTURE_2D, gl.value.TEXTURE_MAG_FILTER, gl.value.LINEAR);\n gl.value.texParameteri(gl.value.TEXTURE_2D, gl.value.TEXTURE_WRAP_S, gl.value.CLAMP_TO_EDGE);\n gl.value.texParameteri(gl.value.TEXTURE_2D, gl.value.TEXTURE_WRAP_T, gl.value.CLAMP_TO_EDGE);\n\n gl.value.pixelStorei(gl.value.UNPACK_ALIGNMENT, 1);\n\n try {\n gl.value.texImage2D(\n gl.value.TEXTURE_2D,\n 0,\n gl.value.RGBA,\n props.imageData?.width,\n props.imageData?.height,\n 0,\n gl.value.RGBA,\n gl.value.UNSIGNED_BYTE,\n props.imageData?.data\n );\n\n gl.value.uniform1i(uniforms.value.u_image_texture, 0);\n } catch (e) {\n console.error('Error uploading texture:', e);\n }\n}\n\nfunction render(currentTime: number) {\n if (!gl.value || !uniforms.value) return;\n\n const deltaTime = currentTime - lastRenderTime.value;\n lastRenderTime.value = currentTime;\n\n totalAnimationTime.value += deltaTime * props.params.speed;\n gl.value.uniform1f(uniforms.value.u_time, totalAnimationTime.value);\n gl.value.drawArrays(gl.value.TRIANGLE_STRIP, 0, 4);\n animationId.value = requestAnimationFrame(render);\n}\n\nfunction startAnimation() {\n if (animationId.value) {\n cancelAnimationFrame(animationId.value);\n }\n lastRenderTime.value = performance.now();\n animationId.value = requestAnimationFrame(render);\n}\n\nonMounted(async () => {\n await nextTick();\n initShader();\n updateUniforms();\n resizeCanvas();\n setupTexture();\n startAnimation();\n\n window.addEventListener('resize', resizeCanvas);\n});\n\nonUnmounted(() => {\n if (animationId.value) {\n cancelAnimationFrame(animationId.value);\n }\n window.removeEventListener('resize', resizeCanvas);\n});\n\nwatch(\n () => props.params,\n () => {\n updateUniforms();\n },\n { deep: true }\n);\n\nwatch(\n () => props.imageData,\n () => {\n setupTexture();\n resizeCanvas();\n },\n { deep: true }\n);\n</script>\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"]} |