[ REFACT ] : Metallic Paint Animation

This commit is contained in:
Utkarsh-Singhal-26
2026-01-21 15:54:00 +05:30
parent b764ca4c5c
commit 6d550f3a3e
6 changed files with 3173 additions and 637 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
// Highlighted sidebar items
export const NEW = ['Antigravity', 'Color Bends', 'Ghost Cursor', 'Laser Flow', 'Liquid Ether', 'Pixel Blast', 'Floating Lines', 'Light Pillar', 'Pixel Snow', 'Grid Scan'];
export const UPDATED = [];
export const UPDATED = ['Metallic Paint'];
// Used for main sidebar navigation
export const CATEGORIES = [

View File

@@ -1,45 +1,47 @@
import code from '@content/Animations/MetallicPaint/MetallicPaint.vue?raw';
import utility from '@content/Animations/MetallicPaint/parseImage.ts?raw';
import { createCodeObject } from '@/types/code';
export const metallicPaint = createCodeObject(code, 'Animations/MetallicPaint', {
usage: `<template>
<MetallicPaint
:image-data="imageData"
:params="{
patternScale: 2,
refraction: 0.015,
edge: 1,
patternBlur: 0.005,
liquid: 0.07,
speed: 0.3
}"
/>
</template>
usage: `<script setup>
// Effect inspired by Paper's Liquid Metal effect
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import MetallicPaint from "./MetallicPaint.vue";
// copy and import the parseImage utility from the correct path
import { parseImage } from './parseImage';
// Replace with your own SVG path
// NOTE: Your SVG should have padding around the shape to prevent cutoff
// It should have a black fill color to allow the metallic effect to show through
import logo from "./logo.svg";
</script>
const imageData = ref<ImageData | null>(null);
<template>
<div style="width: 100%; height: 400px;">
<MetallicPaint
:image-src="logo"
onMounted(async () => {
try {
// Example: Fetch an SVG image and parse it
// The SVG should have a transparent background and black fill color for the best effect
:seed="42"
:scale="4"
:pattern-sharpness="1"
:noise-scale="0.5"
const response = await fetch('/path/to/your/logo.svg');
const blob = await response.blob();
const file = new File([blob], 'logo.svg', { type: blob.type });
const { imageData: processedImageData } = await parseImage(file);
imageData.value = processedImageData;
} catch (err) {
console.error('Error loading image:', err);
}
});
</script>`,
utility
:speed="0.3"
:liquid="0.75"
:mouse-animation="false"
:brightness="2"
:contrast="0.5"
:refraction="0.01"
:blur="0.015"
:chromatic-spread="2"
:fresnel="1"
:angle="0"
:wave-amplitude="1"
:distortion="1"
:contour="0.2"
light-color="#ffffff"
dark-color="#000000"
tint-color="#27FF64"
/>
</div>
</template>`
});

View File

@@ -1,394 +1,566 @@
<template>
<canvas ref="canvasRef" class="block w-full h-full object-contain" />
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick, useTemplateRef } from 'vue';
interface ShaderParams {
patternScale: number;
refraction: number;
edge: number;
patternBlur: number;
liquid: number;
speed: number;
}
interface Props {
imageData: ImageData;
params?: ShaderParams;
}
const props = withDefaults(defineProps<Props>(), {
params: () => ({
patternScale: 2,
refraction: 0.015,
edge: 1,
patternBlur: 0.005,
liquid: 0.07,
speed: 0.3
})
});
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef');
const gl = ref<WebGL2RenderingContext | null>(null);
const uniforms = ref<Record<string, WebGLUniformLocation>>({});
const totalAnimationTime = ref(0);
const lastRenderTime = ref(0);
const animationId = ref<number>();
const vertexShaderSource = `#version 300 es
precision mediump float;
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
const vertexShader = `#version 300 es
precision highp float;
in vec2 a_position;
out vec2 vUv;
out vec2 vP;
void main(){vP=a_position*.5+.5;gl_Position=vec4(a_position,0.,1.);}`;
const fragmentShader = `#version 300 es
precision highp float;
in vec2 vP;
out vec4 oC;
uniform sampler2D u_tex;
uniform float u_time,u_ratio,u_imgRatio,u_seed,u_scale,u_refract,u_blur,u_liquid;
uniform float u_bright,u_contrast,u_angle,u_fresnel,u_sharp,u_wave,u_noise,u_chroma;
uniform float u_distort,u_contour;
uniform vec3 u_lightColor,u_darkColor,u_tint;
vec3 sC,sM;
vec3 pW(vec3 v){
vec3 i=floor(v),f=fract(v),s=sign(fract(v*.5)-.5),h=fract(sM*i+i.yzx),c=f*(f-1.);
return s*c*((h*16.-4.)*c-1.);
}
vec3 aF(vec3 b,vec3 c){return pW(b+c.zxy-pW(b.zxy+c.yzx)+pW(b.yzx+c.xyz));}
vec3 lM(vec3 s,vec3 p){return(p+aF(s,p))*.5;}
vec2 fA(){
vec2 c=vP-.5;
c.x*=u_ratio>u_imgRatio?u_ratio/u_imgRatio:1.;
c.y*=u_ratio>u_imgRatio?1.:u_imgRatio/u_ratio;
return vec2(c.x+.5,.5-c.y);
}
vec2 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);}
float bM(vec2 c,float t){
vec2 l=smoothstep(vec2(0.),vec2(t),c),u=smoothstep(vec2(0.),vec2(t),1.-c);
return l.x*l.y*u.x*u.y;
}
float mG(float hi,float lo,float t,float sh,float cv){
sh*=(2.-u_sharp);
float ci=smoothstep(.15,.85,cv),r=lo;
float e1=.08/u_scale;
r=mix(r,hi,smoothstep(0.,sh*1.5,t));
r=mix(r,lo,smoothstep(e1-sh,e1+sh,t));
float e2=e1+.05/u_scale*(1.-ci*.35);
r=mix(r,hi,smoothstep(e2-sh,e2+sh,t));
float e3=e2+.025/u_scale*(1.-ci*.45);
r=mix(r,lo,smoothstep(e3-sh,e3+sh,t));
float e4=e1+.1/u_scale;
r=mix(r,hi,smoothstep(e4-sh,e4+sh,t));
float rm=1.-e4,gT=clamp((t-e4)/rm,0.,1.);
r=mix(r,mix(hi,lo,smoothstep(0.,1.,gT)),smoothstep(e4-sh*.5,e4+sh*.5,t));
return r;
}
void main(){
vUv = .5 * (a_position + 1.);
gl_Position = vec4(a_position, 0.0, 1.0);
sC=fract(vec3(.7548,.5698,.4154)*(u_seed+17.31))+.5;
sM=fract(sC.zxy-sC.yzx*1.618);
vec2 sc=vec2(vP.x*u_ratio,1.-vP.y);
float angleRad=u_angle*3.14159/180.;
sc=rot(sc-.5,angleRad)+.5;
sc=clamp(sc,0.,1.);
float sl=sc.x-sc.y,an=u_time*.001;
vec2 iC=fA();
vec4 texSample=texture(u_tex,iC);
float dp=texSample.r;
float shapeMask=texSample.a;
vec3 hi=u_lightColor*u_bright;
vec3 lo=u_darkColor*(2.-u_bright);
lo.b+=smoothstep(.6,1.4,sc.x+sc.y)*.08;
vec2 fC=sc-.5;
float rd=length(fC+vec2(0.,sl*.15));
vec2 ag=rot(fC,(.22-sl*.18)*3.14159);
float cv=1.-pow(rd*1.65,1.15);
cv*=pow(sc.y,.35);
float vs=shapeMask;
vs*=bM(iC,.01);
float fr=pow(1.-cv,u_fresnel)*.3;
vs=min(vs+fr*vs,1.);
float mT=an*.0625;
vec3 wO=vec3(-1.05,1.35,1.55);
vec3 wA=aF(vec3(31.,73.,56.),mT+wO)*.22*u_wave;
vec3 wB=aF(vec3(24.,64.,42.),mT-wO.yzx)*.22*u_wave;
vec2 nC=sc*45.*u_noise;
nC+=aF(sC.zxy,an*.17*sC.yzx-sc.yxy*.35).xy*18.*u_wave;
vec3 tC=vec3(.00041,.00053,.00076)*mT+wB*nC.x+wA*nC.y;
tC=lM(sC,tC);
tC=lM(sC+1.618,tC);
float tb=sin(tC.x*3.14159)*.5+.5;
tb=tb*2.-1.;
float noiseVal=pW(vec3(sc*8.+an,an*.5)).x;
float edgeFactor=smoothstep(0.,.5,dp)*smoothstep(1.,.5,dp);
float lD=dp+(1.-dp)*u_liquid*tb;
lD+=noiseVal*u_distort*.15*edgeFactor;
float rB=clamp(1.-cv,0.,1.);
float fl=ag.x+sl;
fl+=noiseVal*sl*u_distort*edgeFactor;
fl*=mix(1.,1.-dp*.5,u_contour);
fl-=dp*u_contour*.8;
float eI=smoothstep(0.,1.,lD)*smoothstep(1.,0.,lD);
fl-=tb*sl*1.8*eI;
float cA=cv*clamp(pow(sc.y,.12),.25,1.);
fl*=.12+(1.05-lD)*cA;
fl*=smoothstep(1.,.65,lD);
float vA1=smoothstep(.08,.18,sc.y)*smoothstep(.38,.18,sc.y);
float vA2=smoothstep(.08,.18,1.-sc.y)*smoothstep(.38,.18,1.-sc.y);
fl+=vA1*.16+vA2*.025;
fl*=.45+pow(sc.y,2.)*.55;
fl*=u_scale;
fl-=an;
float rO=rB+cv*tb*.025;
float vM1=smoothstep(-.12,.18,sc.y)*smoothstep(.48,.08,sc.y);
float cM1=smoothstep(.35,.55,cv)*smoothstep(.95,.35,cv);
rO+=vM1*cM1*4.5;
rO-=sl;
float bO=rB*1.25;
float vM2=smoothstep(-.02,.35,sc.y)*smoothstep(.75,.08,sc.y);
float cM2=smoothstep(.35,.55,cv)*smoothstep(.75,.35,cv);
bO+=vM2*cM2*.9;
bO-=lD*.18;
rO*=u_refract*u_chroma;
bO*=u_refract*u_chroma;
float sf=u_blur;
float rP=fract(fl+rO);
float rC=mG(hi.r,lo.r,rP,sf+.018+u_refract*cv*.025,cv);
float gP=fract(fl);
float gC=mG(hi.g,lo.g,gP,sf+.008/max(.01,1.-sl),cv);
float bP=fract(fl-bO);
float bC=mG(hi.b,lo.b,bP,sf+.008,cv);
vec3 col=vec3(rC,gC,bC);
col=(col-.5)*u_contrast+.5;
col=clamp(col,0.,1.);
col=mix(col,1.-min(vec3(1.),(1.-col)/max(u_tint,vec3(.001))),length(u_tint-1.)*.5);
col=clamp(col,0.,1.);
oC=vec4(col*vs,vs);
}`;
const liquidFragSource = `#version 300 es
precision mediump float;
in vec2 vUv;
out vec4 fragColor;
uniform sampler2D u_image_texture;
uniform float u_time;
uniform float u_ratio;
uniform float u_img_ratio;
uniform float u_patternScale;
uniform float u_refraction;
uniform float u_edge;
uniform float u_patternBlur;
uniform float u_liquid;
#define TWO_PI 6.28318530718
#define PI 3.14159265358979323846
vec3 mod289(vec3 x) { return x - floor(x * (1. / 289.)) * 289.; }
vec2 mod289(vec2 x) { return x - floor(x * (1. / 289.)) * 289.; }
vec3 permute(vec3 x) { return mod289(((x*34.)+1.)*x); }
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1., 0.) : vec2(0., 1.);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i);
vec3 p = permute(permute(i.y + vec3(0., i1.y, 1.)) + i.x + vec3(0., i1.x, 1.));
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.);
m = m*m;
m = m*m;
vec3 x = 2. * fract(p * C.www) - 1.;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130. * dot(m, g);
interface MetallicPaintProps {
imageSrc: string;
seed?: number;
scale?: number;
refraction?: number;
blur?: number;
liquid?: number;
speed?: number;
brightness?: number;
contrast?: number;
angle?: number;
fresnel?: number;
lightColor?: string;
darkColor?: string;
patternSharpness?: number;
waveAmplitude?: number;
noiseScale?: number;
chromaticSpread?: number;
mouseAnimation?: boolean;
distortion?: number;
contour?: number;
tintColor?: string;
}
vec2 get_img_uv() {
vec2 img_uv = vUv;
img_uv -= .5;
if (u_ratio > u_img_ratio) {
img_uv.x = img_uv.x * u_ratio / u_img_ratio;
} else {
img_uv.y = img_uv.y * u_img_ratio / u_ratio;
}
float scale_factor = 1.;
img_uv *= scale_factor;
img_uv += .5;
img_uv.y = 1. - img_uv.y;
return img_uv;
}
vec2 rotate(vec2 uv, float th) {
return mat2(cos(th), sin(th), -sin(th), cos(th)) * uv;
}
float get_color_channel(float c1, float c2, float stripe_p, vec3 w, float extra_blur, float b) {
float ch = c2;
float border = 0.;
float blur = u_patternBlur + extra_blur;
ch = mix(ch, c1, smoothstep(.0, blur, stripe_p));
border = w[0];
ch = mix(ch, c2, smoothstep(border - blur, border + blur, stripe_p));
b = smoothstep(.2, .8, b);
border = w[0] + .4 * (1. - b) * w[1];
ch = mix(ch, c1, smoothstep(border - blur, border + blur, stripe_p));
border = w[0] + .5 * (1. - b) * w[1];
ch = mix(ch, c2, smoothstep(border - blur, border + blur, stripe_p));
border = w[0] + w[1];
ch = mix(ch, c1, smoothstep(border - blur, border + blur, stripe_p));
float gradient_t = (stripe_p - w[0] - w[1]) / w[2];
float gradient = mix(c1, c2, smoothstep(0., 1., gradient_t));
ch = mix(ch, gradient, smoothstep(border - blur, border + blur, stripe_p));
return ch;
}
float get_img_frame_alpha(vec2 uv, float img_frame_width) {
float img_frame_alpha = smoothstep(0., img_frame_width, uv.x) * smoothstep(1., 1. - img_frame_width, uv.x);
img_frame_alpha *= smoothstep(0., img_frame_width, uv.y) * smoothstep(1., 1. - img_frame_width, uv.y);
return img_frame_alpha;
}
void main() {
vec2 uv = vUv;
uv.y = 1. - uv.y;
uv.x *= u_ratio;
float diagonal = uv.x - uv.y;
float t = .001 * u_time;
vec2 img_uv = get_img_uv();
vec4 img = texture(u_image_texture, img_uv);
vec3 color = vec3(0.);
float opacity = 1.;
vec3 color1 = vec3(.98, 0.98, 1.);
vec3 color2 = vec3(.1, .1, .1 + .1 * smoothstep(.7, 1.3, uv.x + uv.y));
float edge = img.r;
vec2 grad_uv = uv;
grad_uv -= .5;
float dist = length(grad_uv + vec2(0., .2 * diagonal));
grad_uv = rotate(grad_uv, (.25 - .2 * diagonal) * PI);
float bulge = pow(1.8 * dist, 1.2);
bulge = 1. - bulge;
bulge *= pow(uv.y, .3);
float cycle_width = u_patternScale;
float thin_strip_1_ratio = .12 / cycle_width * (1. - .4 * bulge);
float thin_strip_2_ratio = .07 / cycle_width * (1. + .4 * bulge);
float wide_strip_ratio = (1. - thin_strip_1_ratio - thin_strip_2_ratio);
float thin_strip_1_width = cycle_width * thin_strip_1_ratio;
float thin_strip_2_width = cycle_width * thin_strip_2_ratio;
opacity = 1. - smoothstep(.9 - .5 * u_edge, 1. - .5 * u_edge, edge);
opacity *= get_img_frame_alpha(img_uv, 0.01);
float noise = snoise(uv - t);
edge += (1. - edge) * u_liquid * noise;
float refr = 0.;
refr += (1. - bulge);
refr = clamp(refr, 0., 1.);
float dir = grad_uv.x;
dir += diagonal;
dir -= 2. * noise * diagonal * (smoothstep(0., 1., edge) * smoothstep(1., 0., edge));
bulge *= clamp(pow(uv.y, .1), .3, 1.);
dir *= (.1 + (1.1 - edge) * bulge);
dir *= smoothstep(1., .7, edge);
dir += .18 * (smoothstep(.1, .2, uv.y) * smoothstep(.4, .2, uv.y));
dir += .03 * (smoothstep(.1, .2, 1. - uv.y) * smoothstep(.4, .2, 1. - uv.y));
dir *= (.5 + .5 * pow(uv.y, 2.));
dir *= cycle_width;
dir -= t;
float refr_r = refr;
refr_r += .03 * bulge * noise;
float refr_b = 1.3 * refr;
refr_r += 5. * (smoothstep(-.1, .2, uv.y) * smoothstep(.5, .1, uv.y)) * (smoothstep(.4, .6, bulge) * smoothstep(1., .4, bulge));
refr_r -= diagonal;
refr_b += (smoothstep(0., .4, uv.y) * smoothstep(.8, .1, uv.y)) * (smoothstep(.4, .6, bulge) * smoothstep(.8, .4, bulge));
refr_b -= .2 * edge;
refr_r *= u_refraction;
refr_b *= u_refraction;
vec3 w = vec3(thin_strip_1_width, thin_strip_2_width, wide_strip_ratio);
w[1] -= .02 * smoothstep(.0, 1., edge + bulge);
float stripe_r = mod(dir + refr_r, 1.);
float r = get_color_channel(color1.r, color2.r, stripe_r, w, 0.02 + .03 * u_refraction * bulge, bulge);
float stripe_g = mod(dir, 1.);
float g = get_color_channel(color1.g, color2.g, stripe_g, w, 0.01 / (1. - diagonal), bulge);
float stripe_b = mod(dir - refr_b, 1.);
float b = get_color_channel(color1.b, color2.b, stripe_b, w, .01, bulge);
color = vec3(r, g, b);
color *= opacity;
fragColor = vec4(color, opacity);
}
`;
function processImage(img: HTMLImageElement): ImageData {
const MAX_SIZE = 1000;
const MIN_SIZE = 500;
let width = img.naturalWidth || img.width;
let height = img.naturalHeight || img.height;
function updateUniforms() {
if (!gl.value || !uniforms.value) return;
gl.value.uniform1f(uniforms.value.u_edge, props.params.edge);
gl.value.uniform1f(uniforms.value.u_patternBlur, props.params.patternBlur);
gl.value.uniform1f(uniforms.value.u_time, 0);
gl.value.uniform1f(uniforms.value.u_patternScale, props.params.patternScale);
gl.value.uniform1f(uniforms.value.u_refraction, props.params.refraction);
gl.value.uniform1f(uniforms.value.u_liquid, props.params.liquid);
if (width > MAX_SIZE || height > MAX_SIZE || width < MIN_SIZE || height < MIN_SIZE) {
const scale =
width > height
? width > MAX_SIZE
? MAX_SIZE / width
: width < MIN_SIZE
? MIN_SIZE / width
: 1
: height > MAX_SIZE
? MAX_SIZE / height
: height < MIN_SIZE
? MIN_SIZE / height
: 1;
width = Math.round(width * scale);
height = Math.round(height * scale);
}
function createShader(gl: WebGL2RenderingContext, sourceCode: string, type: number) {
const shader = gl.createShader(type);
if (!shader) {
return null;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const size = width * height;
const alphaValues = new Float32Array(size);
const shapeMask = new Uint8Array(size);
const boundaryMask = new Uint8Array(size);
for (let i = 0; i < size; i++) {
const idx = i * 4;
const r = data[idx],
g = data[idx + 1],
b = data[idx + 2],
a = data[idx + 3];
const isBackground = (r > 250 && g > 250 && b > 250 && a === 255) || a < 5;
alphaValues[i] = isBackground ? 0 : a / 255;
shapeMask[i] = alphaValues[i] > 0.1 ? 1 : 0;
}
gl.shaderSource(shader, sourceCode);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = y * width + x;
if (!shapeMask[idx]) continue;
if (
x === 0 ||
x === width - 1 ||
y === 0 ||
y === height - 1 ||
!shapeMask[idx - 1] ||
!shapeMask[idx + 1] ||
!shapeMask[idx - width] ||
!shapeMask[idx + width]
) {
boundaryMask[idx] = 1;
}
}
}
return shader;
const u = new Float32Array(size);
const ITERATIONS = 200;
const C = 0.01;
const omega = 1.85;
for (let iter = 0; iter < ITERATIONS; iter++) {
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
const idx = y * width + x;
if (!shapeMask[idx] || boundaryMask[idx]) continue;
const sum =
(shapeMask[idx + 1] ? u[idx + 1] : 0) +
(shapeMask[idx - 1] ? u[idx - 1] : 0) +
(shapeMask[idx + width] ? u[idx + width] : 0) +
(shapeMask[idx - width] ? u[idx - width] : 0);
const newVal = (C + sum) / 4;
u[idx] = omega * newVal + (1 - omega) * u[idx];
}
}
}
function getUniforms(program: WebGLProgram, gl: WebGL2RenderingContext) {
const uniformsObj: Record<string, WebGLUniformLocation> = {};
const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < uniformCount; i++) {
const uniformName = gl.getActiveUniform(program, i)?.name;
if (!uniformName) continue;
uniformsObj[uniformName] = gl.getUniformLocation(program, uniformName) as WebGLUniformLocation;
}
return uniformsObj;
let maxVal = 0;
for (let i = 0; i < size; i++) if (u[i] > maxVal) maxVal = u[i];
if (maxVal === 0) maxVal = 1;
const outData = ctx.createImageData(width, height);
for (let i = 0; i < size; i++) {
const px = i * 4;
const depth = u[i] / maxVal;
const gray = Math.round(255 * (1 - depth * depth));
outData.data[px] = outData.data[px + 1] = outData.data[px + 2] = gray;
outData.data[px + 3] = Math.round(alphaValues[i] * 255);
}
function initShader() {
return outData;
}
function hexToRgb(hex: string): [number, number, number] {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255]
: [1, 1, 1];
}
const props = withDefaults(defineProps<MetallicPaintProps>(), {
seed: 42,
scale: 4,
refraction: 0.01,
blur: 0.015,
liquid: 0.75,
speed: 0.3,
brightness: 2,
contrast: 0.5,
angle: 0,
fresnel: 1,
lightColor: '#ffffff',
darkColor: '#000000',
patternSharpness: 1,
waveAmplitude: 1,
noiseScale: 0.5,
chromaticSpread: 2,
mouseAnimation: false,
distortion: 1,
contour: 0.2,
tintColor: '#feb3ff'
});
const canvasRef = ref<HTMLCanvasElement | null>(null);
const glRef = ref<WebGL2RenderingContext | null>(null);
const programRef = ref<WebGLProgram | null>(null);
const uniformsRef = ref<Record<string, WebGLUniformLocation | null>>({});
const textureRef = ref<WebGLTexture | null>(null);
const animTimeRef = ref(0);
const lastTimeRef = ref(0);
const rafRef = ref<number | null>(null);
const imgDataRef = ref<ImageData | null>(null);
const speedRef = ref(props.speed);
const mouseRef = ref({ x: 0.5, y: 0.5, targetX: 0.5, targetY: 0.5 });
const mouseAnimRef = ref(props.mouseAnimation);
const ready = ref<boolean>(false);
const textureReady = ref<boolean>(false);
watch(
() => props.speed,
speed => (speedRef.value = speed)
);
watch(
() => props.mouseAnimation,
mouseAnimation => (mouseAnimRef.value = mouseAnimation)
);
const initGL = (): boolean => {
const canvas = canvasRef.value;
const glContext = canvas?.getContext('webgl2', {
antialias: true,
alpha: true
});
if (!canvas || !glContext) {
return;
}
if (!canvas) return false;
const vertexShader = createShader(glContext, vertexShaderSource, glContext.VERTEX_SHADER);
const fragmentShader = createShader(glContext, liquidFragSource, glContext.FRAGMENT_SHADER);
const program = glContext.createProgram();
if (!program || !vertexShader || !fragmentShader) {
return;
}
const gl = canvas.getContext('webgl2', { antialias: true, alpha: true });
if (!gl) return false;
glContext.attachShader(program, vertexShader);
glContext.attachShader(program, fragmentShader);
glContext.linkProgram(program);
if (!glContext.getProgramParameter(program, glContext.LINK_STATUS)) {
console.error('Unable to initialize the shader program: ' + glContext.getProgramInfoLog(program));
const compile = (src: string, type: number): WebGLShader | null => {
const s = gl.createShader(type);
if (!s) return null;
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(s));
return null;
}
return s;
};
const uniformsObj = getUniforms(program, glContext);
uniforms.value = uniformsObj;
const vs = compile(vertexShader, gl.VERTEX_SHADER);
const fs = compile(fragmentShader, gl.FRAGMENT_SHADER);
if (!vs || !fs) return false;
const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
const vertexBuffer = glContext.createBuffer();
glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuffer);
glContext.bufferData(glContext.ARRAY_BUFFER, vertices, glContext.STATIC_DRAW);
glContext.useProgram(program);
const positionLocation = glContext.getAttribLocation(program, 'a_position');
glContext.enableVertexAttribArray(positionLocation);
glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuffer);
glContext.vertexAttribPointer(positionLocation, 2, glContext.FLOAT, false, 0, 0);
gl.value = glContext;
const prog = gl.createProgram();
if (!prog) return false;
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(prog));
return false;
}
function resizeCanvas() {
if (!canvasRef.value || !gl.value || !uniforms.value || !props.imageData) return;
const imgRatio = props.imageData.width / props.imageData.height;
gl.value.uniform1f(uniforms.value.u_img_ratio, imgRatio);
const side = 1000;
canvasRef.value.width = side * devicePixelRatio;
canvasRef.value.height = side * devicePixelRatio;
gl.value.viewport(0, 0, canvasRef.value.height, canvasRef.value.height);
gl.value.uniform1f(uniforms.value.u_ratio, 1);
gl.value.uniform1f(uniforms.value.u_img_ratio, imgRatio);
}
function setupTexture() {
if (!gl.value || !uniforms.value) return;
const existingTexture = gl.value.getParameter(gl.value.TEXTURE_BINDING_2D);
if (existingTexture) {
gl.value.deleteTexture(existingTexture);
}
const imageTexture = gl.value.createTexture();
gl.value.activeTexture(gl.value.TEXTURE0);
gl.value.bindTexture(gl.value.TEXTURE_2D, imageTexture);
gl.value.texParameteri(gl.value.TEXTURE_2D, gl.value.TEXTURE_MIN_FILTER, gl.value.LINEAR);
gl.value.texParameteri(gl.value.TEXTURE_2D, gl.value.TEXTURE_MAG_FILTER, gl.value.LINEAR);
gl.value.texParameteri(gl.value.TEXTURE_2D, gl.value.TEXTURE_WRAP_S, gl.value.CLAMP_TO_EDGE);
gl.value.texParameteri(gl.value.TEXTURE_2D, gl.value.TEXTURE_WRAP_T, gl.value.CLAMP_TO_EDGE);
gl.value.pixelStorei(gl.value.UNPACK_ALIGNMENT, 1);
try {
gl.value.texImage2D(
gl.value.TEXTURE_2D,
0,
gl.value.RGBA,
props.imageData?.width,
props.imageData?.height,
0,
gl.value.RGBA,
gl.value.UNSIGNED_BYTE,
props.imageData?.data
);
gl.value.uniform1i(uniforms.value.u_image_texture, 0);
} catch (e) {
console.error('Error uploading texture:', e);
const uniforms: Record<string, WebGLUniformLocation | null> = {};
const count = gl.getProgramParameter(prog, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < count; i++) {
const info = gl.getActiveUniform(prog, i);
if (info) {
uniforms[info.name] = gl.getUniformLocation(prog, info.name);
}
}
function render(currentTime: number) {
if (!gl.value || !uniforms.value) return;
const verts = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
const buf = gl.createBuffer();
if (!buf) return false;
const deltaTime = currentTime - lastRenderTime.value;
lastRenderTime.value = currentTime;
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);
totalAnimationTime.value += deltaTime * props.params.speed;
gl.value.uniform1f(uniforms.value.u_time, totalAnimationTime.value);
gl.value.drawArrays(gl.value.TRIANGLE_STRIP, 0, 4);
animationId.value = requestAnimationFrame(render);
gl.useProgram(prog);
const pos = gl.getAttribLocation(prog, 'a_position');
gl.enableVertexAttribArray(pos);
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
glRef.value = gl;
programRef.value = prog;
uniformsRef.value = uniforms;
return true;
};
watch(initGL, () => {
if (!initGL()) return;
const canvas = canvasRef.value;
const gl = glRef.value;
if (!canvas || !gl) return;
const side = 1000 * devicePixelRatio;
canvas.width = side;
canvas.height = side;
gl.viewport(0, 0, side, side);
ready.value = true;
return () => {
if (rafRef.value) cancelAnimationFrame(rafRef.value);
if (textureRef.value && glRef.value) {
glRef.value.deleteTexture(textureRef.value);
}
function startAnimation() {
if (animationId.value) {
cancelAnimationFrame(animationId.value);
}
lastRenderTime.value = performance.now();
animationId.value = requestAnimationFrame(render);
}
onMounted(async () => {
await nextTick();
initShader();
updateUniforms();
resizeCanvas();
setupTexture();
startAnimation();
window.addEventListener('resize', resizeCanvas);
};
});
onUnmounted(() => {
if (animationId.value) {
cancelAnimationFrame(animationId.value);
}
window.removeEventListener('resize', resizeCanvas);
});
const uploadTexture = (imgData: ImageData): void => {
const gl = glRef.value;
const uniforms = uniformsRef.value;
if (!gl || !imgData) return;
if (textureRef.value) gl.deleteTexture(textureRef.value);
const tex = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, imgData.width, imgData.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, imgData.data);
gl.uniform1i(uniforms.u_tex, 0);
const ratio = imgData.width / imgData.height;
gl.uniform1f(uniforms.u_imgRatio, ratio);
gl.uniform1f(uniforms.u_ratio, 1);
textureRef.value = tex;
imgDataRef.value = imgData;
};
watch(
() => props.params,
() => [ready.value, props.imageSrc, uploadTexture],
() => {
updateUniforms();
},
{ deep: true }
if (!ready.value || !props.imageSrc) return;
textureReady.value = false;
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const imgData = processImage(img);
uploadTexture(imgData);
textureReady.value = true;
};
img.src = props.imageSrc;
}
);
watch(
() => props.imageData,
() => [
ready.value,
props.seed,
props.scale,
props.refraction,
props.blur,
props.liquid,
props.brightness,
props.contrast,
props.angle,
props.fresnel,
props.lightColor,
props.darkColor,
props.patternSharpness,
props.waveAmplitude,
props.noiseScale,
props.chromaticSpread,
props.distortion,
props.contour,
props.tintColor
],
() => {
setupTexture();
resizeCanvas();
},
{ deep: true }
const gl = glRef.value;
const u = uniformsRef.value;
if (!gl || !ready.value) return;
gl.uniform1f(u.u_seed, props.seed);
gl.uniform1f(u.u_scale, props.scale);
gl.uniform1f(u.u_refract, props.refraction);
gl.uniform1f(u.u_blur, props.blur);
gl.uniform1f(u.u_liquid, props.liquid);
gl.uniform1f(u.u_bright, props.brightness);
gl.uniform1f(u.u_contrast, props.contrast);
gl.uniform1f(u.u_angle, props.angle);
gl.uniform1f(u.u_fresnel, props.fresnel);
const light = hexToRgb(props.lightColor);
const dark = hexToRgb(props.darkColor);
const tint = hexToRgb(props.tintColor);
gl.uniform3f(u.u_lightColor, light[0], light[1], light[2]);
gl.uniform3f(u.u_darkColor, dark[0], dark[1], dark[2]);
gl.uniform1f(u.u_sharp, props.patternSharpness);
gl.uniform1f(u.u_wave, props.waveAmplitude);
gl.uniform1f(u.u_noise, props.noiseScale);
gl.uniform1f(u.u_chroma, props.chromaticSpread);
gl.uniform1f(u.u_distort, props.distortion);
gl.uniform1f(u.u_contour, props.contour);
gl.uniform3f(u.u_tint, tint[0], tint[1], tint[2]);
}
);
let cleanup: (() => void) | null = null;
const setup = () => {
if (!ready.value || !textureReady.value) return;
const gl = glRef.value;
const u = uniformsRef.value;
const canvas = canvasRef.value;
const mouse = mouseRef.value;
if (!gl || !canvas) return;
const handleMouseMove = (e: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
mouse.targetX = (e.clientX - rect.left) / rect.width;
mouse.targetY = (e.clientY - rect.top) / rect.height;
};
canvas.addEventListener('mousemove', handleMouseMove);
const render = (time: number) => {
const delta = time - lastTimeRef.value;
lastTimeRef.value = time;
if (mouseAnimRef.value) {
mouse.x += (mouse.targetX - mouse.x) * 0.08;
mouse.y += (mouse.targetY - mouse.y) * 0.08;
animTimeRef.value = mouse.x * 3000 + mouse.y * 1500;
} else {
animTimeRef.value += delta * speedRef.value;
}
gl.uniform1f(u.u_time, animTimeRef.value);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
rafRef.value = requestAnimationFrame(render);
};
lastTimeRef.value = performance.now();
rafRef.value = requestAnimationFrame(render);
cleanup = () => {
if (rafRef.value) cancelAnimationFrame(rafRef.value);
canvas.removeEventListener('mousemove', handleMouseMove);
};
};
onMounted(() => {
setup();
});
onBeforeUnmount(() => {
cleanup?.();
});
watch(
() => [ready.value, textureReady.value],
() => {
cleanup?.();
setup();
}
);
</script>
<template>
<canvas ref="canvasRef" className="block h-full w-full object-contain" />
</template>

View File

@@ -1,177 +0,0 @@
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);
});
}

View File

@@ -1,34 +1,64 @@
<template>
<TabbedLayout>
<template #preview>
<div class="demo-container h-[500px] overflow-hidden">
<div class="demo-container h-[400px] overflow-hidden">
<MetallicPaint
v-if="imageData"
:key="rerenderKey"
:image-data="imageData"
:params="{
edge,
patternBlur,
patternScale,
refraction,
speed,
liquid
}"
:image-src="logo"
:seed="seed"
:scale="scale"
:refraction="refraction"
:blur="blur"
:liquid="liquid"
:speed="speed"
:brightness="brightness"
:contrast="contrast"
:angle="angle"
:fresnel="fresnel"
:light-color="lightColor"
:dark-color="darkColor"
:pattern-sharpness="patternSharpness"
:wave-amplitude="waveAmplitude"
:noise-scale="noiseScale"
:chromatic-spread="chromaticSpread"
:mouse-animation="mouseAnimation"
:distortion="distortion"
:contour="contour"
:tint-color="tintColor"
/>
</div>
<Customize>
<PreviewSlider title="Edge" v-model="edge" :min="0" :max="1" :step="0.1" />
<PreviewSlider title="Pattern Scale" v-model="patternScale" :min="1" :max="5" :step="0.1" />
<PreviewSlider title="Pattern Blur" v-model="patternBlur" :min="0" :max="0.1" :step="0.001" />
<PreviewSlider title="Refraction" v-model="refraction" :min="0" :max="0.1" :step="0.01" />
<PreviewSlider title="Liquid" v-model="liquid" :min="0" :max="1" :step="0.01" />
<div class="flex flex-wrap gap-4">
<div class="flex-1 min-w-full md:min-w-[200px] lg:min-w-[180px]">
<PreviewColor title="Tint Color" v-model="tintColor" class="mb-2" />
<PreviewColor title="Dark Color" v-model="darkColor" class="mb-2" />
<PreviewColor title="Light Color" v-model="lightColor" class="mb-2" />
<PreviewSlider title="Seed" v-model="seed" :min="0" :max="200" :step="0.01" />
<PreviewSlider title="Scale" v-model="scale" :min="0.5" :max="5" :step="0.1" />
<PreviewSlider title="Refraction" v-model="refraction" :min="0" :max="0.1" :step="0.001" />
<PreviewSlider title="Blur" v-model="blur" :min="0" :max="0.1" :step="0.001" />
</div>
<div class="flex-1 min-w-full md:min-w-[200px] lg:min-w-[180px]">
<PreviewSlider title="Speed" v-model="speed" :min="0" :max="1" :step="0.01" />
<PreviewSlider title="Brightness" v-model="brightness" :min="0.5" :max="2" :step="0.05" />
<PreviewSlider title="Contrast" v-model="contrast" :min="0.5" :max="2" :step="0.05" />
<PreviewSlider title="Angle" v-model="angle" :min="-180" :max="180" :step="1" />
<PreviewSlider title="Fresnel" v-model="fresnel" :min="0" :max="3" :step="0.1" />
<PreviewSlider title="Pattern Sharpness" v-model="patternSharpness" :min="0.1" :max="2" :step="0.1" />
<PreviewSlider title="Wave Amplitude" v-model="waveAmplitude" :min="0" :max="3" :step="0.1" />
</div>
<div class="flex-1 min-w-full md:min-w-[200px] lg:min-w-[180px]">
<PreviewSlider title="Liquid" v-model="liquid" :min="0" :max="1" :step="0.01" />
<PreviewSlider title="Noise Scale" v-model="noiseScale" :min="0" :max="3" :step="0.1" />
<PreviewSlider title="Chromatic Spread" v-model="chromaticSpread" :min="0" :max="3" :step="0.1" />
<PreviewSlider title="Distortion" v-model="distortion" :min="0" :max="1" :step="0.05" />
<PreviewSlider title="Contour" v-model="contour" :min="0" :max="1" :step="0.05" />
<PreviewSwitch title="Mouse Animation" v-model="mouseAnimation" />
</div>
</div>
</Customize>
<PropTable :data="propData" />
@@ -45,55 +75,169 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import TabbedLayout from '../../components/common/TabbedLayout.vue';
import PropTable from '../../components/common/PropTable.vue';
import logo from '@/assets/logos/vue-bits-logo-small-dark.svg';
import PreviewColor from '@/components/common/PreviewColor.vue';
import PreviewSlider from '@/components/common/PreviewSlider.vue';
import PreviewSwitch from '@/components/common/PreviewSwitch.vue';
import { useForceRerender } from '@/composables/useForceRerender';
import { metallicPaint } from '@/constants/code/Animations/metallicPaintCode';
import { ref } from 'vue';
import CliInstallation from '../../components/code/CliInstallation.vue';
import CodeExample from '../../components/code/CodeExample.vue';
import Customize from '../../components/common/Customize.vue';
import PreviewSlider from '../../components/common/PreviewSlider.vue';
import PropTable from '../../components/common/PropTable.vue';
import TabbedLayout from '../../components/common/TabbedLayout.vue';
import MetallicPaint from '../../content/Animations/MetallicPaint/MetallicPaint.vue';
import { metallicPaint } from '@/constants/code/Animations/metallicPaintCode';
import { parseImage } from '../../content/Animations/MetallicPaint/parseImage';
import { useForceRerender } from '@/composables/useForceRerender';
import logo from '@/assets/logos/vue-bits-logo-small-dark.svg';
const imageData = ref<ImageData | null>(null);
const edge = ref(0);
const patternScale = ref(2);
const refraction = ref(0.015);
const patternBlur = ref(0.005);
const liquid = ref(0.07);
const speed = ref(0.3);
const seed = ref<number>(42);
const scale = ref<number>(2);
const refraction = ref<number>(0.01);
const blur = ref<number>(0.015);
const liquid = ref<number>(0.75);
const speed = ref<number>(0.3);
const brightness = ref<number>(2);
const contrast = ref<number>(0.5);
const angle = ref<number>(0);
const fresnel = ref<number>(1);
const patternSharpness = ref<number>(1);
const waveAmplitude = ref<number>(1);
const noiseScale = ref<number>(0.5);
const chromaticSpread = ref<number>(2);
const distortion = ref<number>(1);
const contour = ref<number>(0.2);
const mouseAnimation = ref<boolean>(false);
const lightColor = ref<string>('#ffffff');
const darkColor = ref<string>('#000000');
const tintColor = ref<string>('#27FF64');
const { rerenderKey } = useForceRerender();
const propData = [
{
name: 'imageData',
type: 'ImageData',
name: 'imageSrc',
type: 'string',
default: 'none (required)',
description:
'The processed image data generated from the parseImage utility. This image data is used by the shader to create the liquid paper effect.'
description: 'URL or path to the image used for the metallic paint effect. The image is processed internally.'
},
{
name: 'params',
type: 'ShaderParams',
default: '',
description:
'An object to configure the shader effect. Properties include: patternScale, refraction, edge, patternBlur, liquid, speed'
name: 'seed',
type: 'number',
default: '42',
description: 'Random seed for pattern generation. Different values create different pattern variations.'
},
{
name: 'scale',
type: 'number',
default: '2',
description: 'Scale of the metallic pattern. Higher values create more repetitions.'
},
{
name: 'refraction',
type: 'number',
default: '0.015',
description: 'Amount of chromatic aberration (color separation). Creates the rainbow edge effect.'
},
{
name: 'blur',
type: 'number',
default: '0.005',
description: 'Blur amount for the pattern transitions. Higher values create softer gradients.'
},
{
name: 'liquid',
type: 'number',
default: '0.07',
description: 'Amount of liquid/wavy animation applied to the pattern.'
},
{
name: 'speed',
type: 'number',
default: '0.3',
description: 'Animation speed multiplier. Set to 0 to disable animation.'
},
{
name: 'brightness',
type: 'number',
default: '1',
description: 'Overall brightness of the metallic effect. Values above 1 increase brightness.'
},
{
name: 'contrast',
type: 'number',
default: '1',
description: 'Color contrast of the effect. Higher values create more distinct light/dark areas.'
},
{
name: 'angle',
type: 'number',
default: '0',
description: 'Rotation angle of the pattern in degrees.'
},
{
name: 'fresnel',
type: 'number',
default: '1',
description: 'Fresnel effect intensity. Controls edge highlighting based on viewing angle.'
},
{
name: 'lightColor',
type: 'string',
default: '#ffffff',
description: 'Hex color for the bright/highlight areas of the metallic effect.'
},
{
name: 'darkColor',
type: 'string',
default: '#111111',
description: 'Hex color for the dark/shadow areas of the metallic effect.'
},
{
name: 'patternSharpness',
type: 'number',
default: '1',
description: 'Controls the sharpness of metallic band transitions. Higher = sharper edges.'
},
{
name: 'waveAmplitude',
type: 'number',
default: '1',
description: 'Intensity of the wave distortion effect.'
},
{
name: 'noiseScale',
type: 'number',
default: '1',
description: 'Scale of the noise pattern. Higher = more detailed noise.'
},
{
name: 'chromaticSpread',
type: 'number',
default: '1',
description: 'Multiplier for chromatic aberration spread between RGB channels.'
},
{
name: 'mouseAnimation',
type: 'boolean',
default: 'false',
description: 'When true, mouse position controls wave animation instead of auto-loop.'
},
{
name: 'distortion',
type: 'number',
default: '0',
description: 'Amount of noise-based distortion applied to the pattern flow (0-1).'
},
{
name: 'contour',
type: 'number',
default: '0',
description: 'Intensity of edge contour effect that warps the pattern along shape boundaries (0-1).'
},
{
name: 'tintColor',
type: 'string',
default: '#ffffff',
description: 'Hex color for color burn tint effect. White = no tint.'
}
];
onMounted(async () => {
try {
const response = await fetch(logo);
const blob = await response.blob();
const file = new File([blob], 'default.png', { type: blob.type });
const { imageData: processedImageData } = await parseImage(file);
imageData.value = processedImageData;
} catch (err) {
console.error('Error loading default image:', err);
}
});
</script>