mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
[ REFACT ] : Metallic Paint Animation
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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 = [
|
||||
|
||||
@@ -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";
|
||||
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"
|
||||
|
||||
:seed="42"
|
||||
:scale="4"
|
||||
:pattern-sharpness="1"
|
||||
:noise-scale="0.5"
|
||||
|
||||
: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"
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
light-color="#ffffff"
|
||||
dark-color="#000000"
|
||||
tint-color="#27FF64"
|
||||
/>
|
||||
</div>
|
||||
</template>`
|
||||
});
|
||||
|
||||
@@ -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.);}`;
|
||||
|
||||
void main() {
|
||||
vUv = .5 * (a_position + 1.);
|
||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||
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(){
|
||||
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;
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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 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);
|
||||
}
|
||||
|
||||
function createShader(gl: WebGL2RenderingContext, sourceCode: string, type: number) {
|
||||
const shader = gl.createShader(type);
|
||||
if (!shader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
gl.shaderSource(shader, sourceCode);
|
||||
gl.compileShader(shader);
|
||||
const u = new Float32Array(size);
|
||||
const ITERATIONS = 200;
|
||||
const C = 0.01;
|
||||
const omega = 1.85;
|
||||
|
||||
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 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return shader;
|
||||
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);
|
||||
}
|
||||
|
||||
return outData;
|
||||
}
|
||||
|
||||
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;
|
||||
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];
|
||||
}
|
||||
|
||||
function initShader() {
|
||||
const canvas = canvasRef.value;
|
||||
const glContext = canvas?.getContext('webgl2', {
|
||||
antialias: true,
|
||||
alpha: true
|
||||
});
|
||||
if (!canvas || !glContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
return null;
|
||||
}
|
||||
|
||||
const uniformsObj = getUniforms(program, glContext);
|
||||
uniforms.value = uniformsObj;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function render(currentTime: number) {
|
||||
if (!gl.value || !uniforms.value) return;
|
||||
|
||||
const deltaTime = currentTime - lastRenderTime.value;
|
||||
lastRenderTime.value = currentTime;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
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'
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId.value) {
|
||||
cancelAnimationFrame(animationId.value);
|
||||
}
|
||||
window.removeEventListener('resize', resizeCanvas);
|
||||
});
|
||||
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.params,
|
||||
() => {
|
||||
updateUniforms();
|
||||
},
|
||||
{ deep: true }
|
||||
() => props.speed,
|
||||
speed => (speedRef.value = speed)
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.imageData,
|
||||
() => props.mouseAnimation,
|
||||
mouseAnimation => (mouseAnimRef.value = mouseAnimation)
|
||||
);
|
||||
|
||||
const initGL = (): boolean => {
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return false;
|
||||
|
||||
const gl = canvas.getContext('webgl2', { antialias: true, alpha: true });
|
||||
if (!gl) return false;
|
||||
|
||||
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 vs = compile(vertexShader, gl.VERTEX_SHADER);
|
||||
const fs = compile(fragmentShader, gl.FRAGMENT_SHADER);
|
||||
if (!vs || !fs) return false;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const verts = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
|
||||
const buf = gl.createBuffer();
|
||||
if (!buf) return false;
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
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(
|
||||
() => [ready.value, props.imageSrc, uploadTexture],
|
||||
() => {
|
||||
setupTexture();
|
||||
resizeCanvas();
|
||||
},
|
||||
{ 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(
|
||||
() => [
|
||||
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
|
||||
],
|
||||
() => {
|
||||
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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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" />
|
||||
<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>
|
||||
|
||||
<PreviewSlider title="Pattern Scale" v-model="patternScale" :min="1" :max="5" :step="0.1" />
|
||||
<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>
|
||||
|
||||
<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" />
|
||||
|
||||
<PreviewSlider title="Speed" v-model="speed" :min="0" :max="1" :step="0.01" />
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user