mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
1 line
17 KiB
JSON
1 line
17 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":"<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue';\n\nconst vertexShader = `#version 300 es\nprecision highp float;\nin vec2 a_position;\nout vec2 vP;\nvoid main(){vP=a_position*.5+.5;gl_Position=vec4(a_position,0.,1.);}`;\n\nconst fragmentShader = `#version 300 es\nprecision highp float;\nin vec2 vP;\nout vec4 oC;\nuniform sampler2D u_tex;\nuniform float u_time,u_ratio,u_imgRatio,u_seed,u_scale,u_refract,u_blur,u_liquid;\nuniform float u_bright,u_contrast,u_angle,u_fresnel,u_sharp,u_wave,u_noise,u_chroma;\nuniform float u_distort,u_contour;\nuniform vec3 u_lightColor,u_darkColor,u_tint;\n\nvec3 sC,sM;\n\nvec3 pW(vec3 v){\n vec3 i=floor(v),f=fract(v),s=sign(fract(v*.5)-.5),h=fract(sM*i+i.yzx),c=f*(f-1.);\n return s*c*((h*16.-4.)*c-1.);\n}\n\nvec3 aF(vec3 b,vec3 c){return pW(b+c.zxy-pW(b.zxy+c.yzx)+pW(b.yzx+c.xyz));}\nvec3 lM(vec3 s,vec3 p){return(p+aF(s,p))*.5;}\n\nvec2 fA(){\n vec2 c=vP-.5;\n c.x*=u_ratio>u_imgRatio?u_ratio/u_imgRatio:1.;\n c.y*=u_ratio>u_imgRatio?1.:u_imgRatio/u_ratio;\n return vec2(c.x+.5,.5-c.y);\n}\n\nvec2 rot(vec2 p,float r){float c=cos(r),s=sin(r);return vec2(p.x*c+p.y*s,p.y*c-p.x*s);}\n\nfloat bM(vec2 c,float t){\n vec2 l=smoothstep(vec2(0.),vec2(t),c),u=smoothstep(vec2(0.),vec2(t),1.-c);\n return l.x*l.y*u.x*u.y;\n}\n\nfloat mG(float hi,float lo,float t,float sh,float cv){\n sh*=(2.-u_sharp);\n float ci=smoothstep(.15,.85,cv),r=lo;\n float e1=.08/u_scale;\n r=mix(r,hi,smoothstep(0.,sh*1.5,t));\n r=mix(r,lo,smoothstep(e1-sh,e1+sh,t));\n float e2=e1+.05/u_scale*(1.-ci*.35);\n r=mix(r,hi,smoothstep(e2-sh,e2+sh,t));\n float e3=e2+.025/u_scale*(1.-ci*.45);\n r=mix(r,lo,smoothstep(e3-sh,e3+sh,t));\n float e4=e1+.1/u_scale;\n r=mix(r,hi,smoothstep(e4-sh,e4+sh,t));\n float rm=1.-e4,gT=clamp((t-e4)/rm,0.,1.);\n r=mix(r,mix(hi,lo,smoothstep(0.,1.,gT)),smoothstep(e4-sh*.5,e4+sh*.5,t));\n return r;\n}\n\nvoid main(){\n sC=fract(vec3(.7548,.5698,.4154)*(u_seed+17.31))+.5;\n sM=fract(sC.zxy-sC.yzx*1.618);\n vec2 sc=vec2(vP.x*u_ratio,1.-vP.y);\n float angleRad=u_angle*3.14159/180.;\n sc=rot(sc-.5,angleRad)+.5;\n sc=clamp(sc,0.,1.);\n float sl=sc.x-sc.y,an=u_time*.001;\n vec2 iC=fA();\n vec4 texSample=texture(u_tex,iC);\n float dp=texSample.r;\n float shapeMask=texSample.a;\n vec3 hi=u_lightColor*u_bright;\n vec3 lo=u_darkColor*(2.-u_bright);\n lo.b+=smoothstep(.6,1.4,sc.x+sc.y)*.08;\n vec2 fC=sc-.5;\n float rd=length(fC+vec2(0.,sl*.15));\n vec2 ag=rot(fC,(.22-sl*.18)*3.14159);\n float cv=1.-pow(rd*1.65,1.15);\n cv*=pow(sc.y,.35);\n float vs=shapeMask;\n vs*=bM(iC,.01);\n float fr=pow(1.-cv,u_fresnel)*.3;\n vs=min(vs+fr*vs,1.);\n float mT=an*.0625;\n vec3 wO=vec3(-1.05,1.35,1.55);\n vec3 wA=aF(vec3(31.,73.,56.),mT+wO)*.22*u_wave;\n vec3 wB=aF(vec3(24.,64.,42.),mT-wO.yzx)*.22*u_wave;\n vec2 nC=sc*45.*u_noise;\n nC+=aF(sC.zxy,an*.17*sC.yzx-sc.yxy*.35).xy*18.*u_wave;\n vec3 tC=vec3(.00041,.00053,.00076)*mT+wB*nC.x+wA*nC.y;\n tC=lM(sC,tC);\n tC=lM(sC+1.618,tC);\n float tb=sin(tC.x*3.14159)*.5+.5;\n tb=tb*2.-1.;\n float noiseVal=pW(vec3(sc*8.+an,an*.5)).x;\n float edgeFactor=smoothstep(0.,.5,dp)*smoothstep(1.,.5,dp);\n float lD=dp+(1.-dp)*u_liquid*tb;\n lD+=noiseVal*u_distort*.15*edgeFactor;\n float rB=clamp(1.-cv,0.,1.);\n float fl=ag.x+sl;\n fl+=noiseVal*sl*u_distort*edgeFactor;\n fl*=mix(1.,1.-dp*.5,u_contour);\n fl-=dp*u_contour*.8;\n float eI=smoothstep(0.,1.,lD)*smoothstep(1.,0.,lD);\n fl-=tb*sl*1.8*eI;\n float cA=cv*clamp(pow(sc.y,.12),.25,1.);\n fl*=.12+(1.05-lD)*cA;\n fl*=smoothstep(1.,.65,lD);\n float vA1=smoothstep(.08,.18,sc.y)*smoothstep(.38,.18,sc.y);\n float vA2=smoothstep(.08,.18,1.-sc.y)*smoothstep(.38,.18,1.-sc.y);\n fl+=vA1*.16+vA2*.025;\n fl*=.45+pow(sc.y,2.)*.55;\n fl*=u_scale;\n fl-=an;\n float rO=rB+cv*tb*.025;\n float vM1=smoothstep(-.12,.18,sc.y)*smoothstep(.48,.08,sc.y);\n float cM1=smoothstep(.35,.55,cv)*smoothstep(.95,.35,cv);\n rO+=vM1*cM1*4.5;\n rO-=sl;\n float bO=rB*1.25;\n float vM2=smoothstep(-.02,.35,sc.y)*smoothstep(.75,.08,sc.y);\n float cM2=smoothstep(.35,.55,cv)*smoothstep(.75,.35,cv);\n bO+=vM2*cM2*.9;\n bO-=lD*.18;\n rO*=u_refract*u_chroma;\n bO*=u_refract*u_chroma;\n float sf=u_blur;\n float rP=fract(fl+rO);\n float rC=mG(hi.r,lo.r,rP,sf+.018+u_refract*cv*.025,cv);\n float gP=fract(fl);\n float gC=mG(hi.g,lo.g,gP,sf+.008/max(.01,1.-sl),cv);\n float bP=fract(fl-bO);\n float bC=mG(hi.b,lo.b,bP,sf+.008,cv);\n vec3 col=vec3(rC,gC,bC);\n col=(col-.5)*u_contrast+.5;\n col=clamp(col,0.,1.);\n col=mix(col,1.-min(vec3(1.),(1.-col)/max(u_tint,vec3(.001))),length(u_tint-1.)*.5);\n col=clamp(col,0.,1.);\n oC=vec4(col*vs,vs);\n}`;\n\ninterface MetallicPaintProps {\n imageSrc: string;\n seed?: number;\n scale?: number;\n refraction?: number;\n blur?: number;\n liquid?: number;\n speed?: number;\n brightness?: number;\n contrast?: number;\n angle?: number;\n fresnel?: number;\n lightColor?: string;\n darkColor?: string;\n patternSharpness?: number;\n waveAmplitude?: number;\n noiseScale?: number;\n chromaticSpread?: number;\n mouseAnimation?: boolean;\n distortion?: number;\n contour?: number;\n tintColor?: string;\n}\n\nfunction processImage(img: HTMLImageElement): ImageData {\n const MAX_SIZE = 1000;\n const MIN_SIZE = 500;\n let width = img.naturalWidth || img.width;\n let height = img.naturalHeight || img.height;\n\n if (width > MAX_SIZE || height > MAX_SIZE || width < MIN_SIZE || height < MIN_SIZE) {\n const scale =\n width > height\n ? width > MAX_SIZE\n ? MAX_SIZE / width\n : width < MIN_SIZE\n ? MIN_SIZE / width\n : 1\n : height > MAX_SIZE\n ? MAX_SIZE / height\n : height < MIN_SIZE\n ? MIN_SIZE / height\n : 1;\n width = Math.round(width * scale);\n height = Math.round(height * scale);\n }\n\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext('2d')!;\n ctx.drawImage(img, 0, 0, width, height);\n\n const imageData = ctx.getImageData(0, 0, width, height);\n const data = imageData.data;\n const size = width * height;\n const alphaValues = new Float32Array(size);\n const shapeMask = new Uint8Array(size);\n const boundaryMask = new Uint8Array(size);\n\n for (let i = 0; i < size; i++) {\n const idx = i * 4;\n const r = data[idx],\n g = data[idx + 1],\n b = data[idx + 2],\n a = data[idx + 3];\n const isBackground = (r > 250 && g > 250 && b > 250 && a === 255) || a < 5;\n alphaValues[i] = isBackground ? 0 : a / 255;\n shapeMask[i] = alphaValues[i] > 0.1 ? 1 : 0;\n }\n\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 if (\n x === 0 ||\n x === width - 1 ||\n y === 0 ||\n y === height - 1 ||\n !shapeMask[idx - 1] ||\n !shapeMask[idx + 1] ||\n !shapeMask[idx - width] ||\n !shapeMask[idx + width]\n ) {\n boundaryMask[idx] = 1;\n }\n }\n }\n\n const u = new Float32Array(size);\n const ITERATIONS = 200;\n const C = 0.01;\n const omega = 1.85;\n\n for (let iter = 0; iter < ITERATIONS; iter++) {\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 (!shapeMask[idx] || boundaryMask[idx]) continue;\n const sum =\n (shapeMask[idx + 1] ? u[idx + 1] : 0) +\n (shapeMask[idx - 1] ? u[idx - 1] : 0) +\n (shapeMask[idx + width] ? u[idx + width] : 0) +\n (shapeMask[idx - width] ? u[idx - width] : 0);\n const newVal = (C + sum) / 4;\n u[idx] = omega * newVal + (1 - omega) * u[idx];\n }\n }\n }\n\n let maxVal = 0;\n for (let i = 0; i < size; i++) if (u[i] > maxVal) maxVal = u[i];\n if (maxVal === 0) maxVal = 1;\n\n const outData = ctx.createImageData(width, height);\n for (let i = 0; i < size; i++) {\n const px = i * 4;\n const depth = u[i] / maxVal;\n const gray = Math.round(255 * (1 - depth * depth));\n outData.data[px] = outData.data[px + 1] = outData.data[px + 2] = gray;\n outData.data[px + 3] = Math.round(alphaValues[i] * 255);\n }\n\n return outData;\n}\n\nfunction hexToRgb(hex: string): [number, number, number] {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return result\n ? [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255]\n : [1, 1, 1];\n}\n\nconst props = withDefaults(defineProps<MetallicPaintProps>(), {\n seed: 42,\n scale: 4,\n refraction: 0.01,\n blur: 0.015,\n liquid: 0.75,\n speed: 0.3,\n brightness: 2,\n contrast: 0.5,\n angle: 0,\n fresnel: 1,\n lightColor: '#ffffff',\n darkColor: '#000000',\n patternSharpness: 1,\n waveAmplitude: 1,\n noiseScale: 0.5,\n chromaticSpread: 2,\n mouseAnimation: false,\n distortion: 1,\n contour: 0.2,\n tintColor: '#feb3ff'\n});\n\nconst canvasRef = ref<HTMLCanvasElement | null>(null);\nconst glRef = ref<WebGL2RenderingContext | null>(null);\nconst programRef = ref<WebGLProgram | null>(null);\nconst uniformsRef = ref<Record<string, WebGLUniformLocation | null>>({});\nconst textureRef = ref<WebGLTexture | null>(null);\nconst animTimeRef = ref(0);\nconst lastTimeRef = ref(0);\nconst rafRef = ref<number | null>(null);\nconst imgDataRef = ref<ImageData | null>(null);\nconst speedRef = ref(props.speed);\nconst mouseRef = ref({ x: 0.5, y: 0.5, targetX: 0.5, targetY: 0.5 });\nconst mouseAnimRef = ref(props.mouseAnimation);\n\nconst ready = ref<boolean>(false);\nconst textureReady = ref<boolean>(false);\n\nwatch(\n () => props.speed,\n speed => (speedRef.value = speed)\n);\n\nwatch(\n () => props.mouseAnimation,\n mouseAnimation => (mouseAnimRef.value = mouseAnimation)\n);\n\nconst initGL = (): boolean => {\n const canvas = canvasRef.value;\n if (!canvas) return false;\n\n const gl = canvas.getContext('webgl2', { antialias: true, alpha: true });\n if (!gl) return false;\n\n const compile = (src: string, type: number): WebGLShader | null => {\n const s = gl.createShader(type);\n if (!s) return null;\n gl.shaderSource(s, src);\n gl.compileShader(s);\n if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {\n console.error(gl.getShaderInfoLog(s));\n return null;\n }\n return s;\n };\n\n const vs = compile(vertexShader, gl.VERTEX_SHADER);\n const fs = compile(fragmentShader, gl.FRAGMENT_SHADER);\n if (!vs || !fs) return false;\n\n const prog = gl.createProgram();\n if (!prog) return false;\n gl.attachShader(prog, vs);\n gl.attachShader(prog, fs);\n gl.linkProgram(prog);\n if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {\n console.error(gl.getProgramInfoLog(prog));\n return false;\n }\n\n const uniforms: Record<string, WebGLUniformLocation | null> = {};\n const count = gl.getProgramParameter(prog, gl.ACTIVE_UNIFORMS);\n for (let i = 0; i < count; i++) {\n const info = gl.getActiveUniform(prog, i);\n if (info) {\n uniforms[info.name] = gl.getUniformLocation(prog, info.name);\n }\n }\n\n const verts = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);\n const buf = gl.createBuffer();\n if (!buf) return false;\n\n gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);\n\n gl.useProgram(prog);\n const pos = gl.getAttribLocation(prog, 'a_position');\n gl.enableVertexAttribArray(pos);\n gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);\n\n glRef.value = gl;\n programRef.value = prog;\n uniformsRef.value = uniforms;\n\n return true;\n};\n\nwatch(initGL, () => {\n if (!initGL()) return;\n\n const canvas = canvasRef.value;\n const gl = glRef.value;\n if (!canvas || !gl) return;\n\n const side = 1000 * devicePixelRatio;\n canvas.width = side;\n canvas.height = side;\n gl.viewport(0, 0, side, side);\n\n ready.value = true;\n\n return () => {\n if (rafRef.value) cancelAnimationFrame(rafRef.value);\n if (textureRef.value && glRef.value) {\n glRef.value.deleteTexture(textureRef.value);\n }\n };\n});\n\nconst uploadTexture = (imgData: ImageData): void => {\n const gl = glRef.value;\n const uniforms = uniformsRef.value;\n if (!gl || !imgData) return;\n\n if (textureRef.value) gl.deleteTexture(textureRef.value);\n\n const tex = gl.createTexture();\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, imgData.width, imgData.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, imgData.data);\n gl.uniform1i(uniforms.u_tex, 0);\n\n const ratio = imgData.width / imgData.height;\n gl.uniform1f(uniforms.u_imgRatio, ratio);\n gl.uniform1f(uniforms.u_ratio, 1);\n\n textureRef.value = tex;\n imgDataRef.value = imgData;\n};\n\nwatch(\n () => [ready.value, props.imageSrc, uploadTexture],\n () => {\n if (!ready.value || !props.imageSrc) return;\n\n textureReady.value = false;\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n const imgData = processImage(img);\n uploadTexture(imgData);\n textureReady.value = true;\n };\n img.src = props.imageSrc;\n }\n);\n\nwatch(\n () => [\n ready.value,\n props.seed,\n props.scale,\n props.refraction,\n props.blur,\n props.liquid,\n props.brightness,\n props.contrast,\n props.angle,\n props.fresnel,\n props.lightColor,\n props.darkColor,\n props.patternSharpness,\n props.waveAmplitude,\n props.noiseScale,\n props.chromaticSpread,\n props.distortion,\n props.contour,\n props.tintColor\n ],\n () => {\n const gl = glRef.value;\n const u = uniformsRef.value;\n if (!gl || !ready.value) return;\n\n gl.uniform1f(u.u_seed, props.seed);\n gl.uniform1f(u.u_scale, props.scale);\n gl.uniform1f(u.u_refract, props.refraction);\n gl.uniform1f(u.u_blur, props.blur);\n gl.uniform1f(u.u_liquid, props.liquid);\n gl.uniform1f(u.u_bright, props.brightness);\n gl.uniform1f(u.u_contrast, props.contrast);\n gl.uniform1f(u.u_angle, props.angle);\n gl.uniform1f(u.u_fresnel, props.fresnel);\n\n const light = hexToRgb(props.lightColor);\n const dark = hexToRgb(props.darkColor);\n const tint = hexToRgb(props.tintColor);\n gl.uniform3f(u.u_lightColor, light[0], light[1], light[2]);\n gl.uniform3f(u.u_darkColor, dark[0], dark[1], dark[2]);\n gl.uniform1f(u.u_sharp, props.patternSharpness);\n gl.uniform1f(u.u_wave, props.waveAmplitude);\n gl.uniform1f(u.u_noise, props.noiseScale);\n gl.uniform1f(u.u_chroma, props.chromaticSpread);\n gl.uniform1f(u.u_distort, props.distortion);\n gl.uniform1f(u.u_contour, props.contour);\n gl.uniform3f(u.u_tint, tint[0], tint[1], tint[2]);\n }\n);\n\nlet cleanup: (() => void) | null = null;\nconst setup = () => {\n if (!ready.value || !textureReady.value) return;\n\n const gl = glRef.value;\n const u = uniformsRef.value;\n const canvas = canvasRef.value;\n const mouse = mouseRef.value;\n if (!gl || !canvas) return;\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = canvas.getBoundingClientRect();\n mouse.targetX = (e.clientX - rect.left) / rect.width;\n mouse.targetY = (e.clientY - rect.top) / rect.height;\n };\n\n canvas.addEventListener('mousemove', handleMouseMove);\n\n const render = (time: number) => {\n const delta = time - lastTimeRef.value;\n lastTimeRef.value = time;\n\n if (mouseAnimRef.value) {\n mouse.x += (mouse.targetX - mouse.x) * 0.08;\n mouse.y += (mouse.targetY - mouse.y) * 0.08;\n animTimeRef.value = mouse.x * 3000 + mouse.y * 1500;\n } else {\n animTimeRef.value += delta * speedRef.value;\n }\n\n gl.uniform1f(u.u_time, animTimeRef.value);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n rafRef.value = requestAnimationFrame(render);\n };\n\n lastTimeRef.value = performance.now();\n rafRef.value = requestAnimationFrame(render);\n\n cleanup = () => {\n if (rafRef.value) cancelAnimationFrame(rafRef.value);\n canvas.removeEventListener('mousemove', handleMouseMove);\n };\n};\n\nonMounted(() => {\n setup();\n});\n\nonBeforeUnmount(() => {\n cleanup?.();\n});\n\nwatch(\n () => [ready.value, textureReady.value],\n () => {\n cleanup?.();\n setup();\n }\n);\n</script>\n\n<template>\n <canvas ref=\"canvasRef\" className=\"block h-full w-full object-contain\" />\n</template>\n","path":"MetallicPaint/MetallicPaint.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[],"devDependencies":[],"categories":["Animations"]} |