mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
Merge pull request #136 from Utkarsh-Singhal-26/refact/metallic-paint
[ REFACT ] : Metallic Paint Animation
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"name":"CountUp","title":"CountUp","description":"Animated number counter supporting formatting and decimals.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <span ref=\"elementRef\" :class=\"className\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch, computed, useTemplateRef } from 'vue';\n\ninterface Props {\n to: number;\n from?: number;\n direction?: 'up' | 'down';\n delay?: number;\n duration?: number;\n className?: string;\n startWhen?: boolean;\n separator?: string;\n onStart?: () => void;\n onEnd?: () => void;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n from: 0,\n direction: 'up',\n delay: 0,\n duration: 2,\n className: '',\n startWhen: true,\n separator: ''\n});\n\nconst elementRef = useTemplateRef<HTMLSpanElement>('elementRef');\nconst currentValue = ref(props.direction === 'down' ? props.to : props.from);\nconst isInView = ref(false);\nconst animationId = ref<number | null>(null);\nconst hasStarted = ref(false);\n\nlet intersectionObserver: IntersectionObserver | null = null;\n\nconst damping = computed(() => 20 + 40 * (1 / props.duration));\nconst stiffness = computed(() => 100 * (1 / props.duration));\n\nlet velocity = 0;\nlet startTime = 0;\n\nconst formatNumber = (value: number) => {\n const options = {\n useGrouping: !!props.separator,\n minimumFractionDigits: 0,\n maximumFractionDigits: 0\n };\n\n const formattedNumber = Intl.NumberFormat('en-US', options).format(Number(value.toFixed(0)));\n\n return props.separator ? formattedNumber.replace(/,/g, props.separator) : formattedNumber;\n};\n\nconst updateDisplay = () => {\n if (elementRef.value) {\n elementRef.value.textContent = formatNumber(currentValue.value);\n }\n};\n\nconst springAnimation = (timestamp: number) => {\n if (!startTime) startTime = timestamp;\n\n const target = props.direction === 'down' ? props.from : props.to;\n const current = currentValue.value;\n\n const displacement = target - current;\n const springForce = displacement * stiffness.value;\n const dampingForce = velocity * damping.value;\n const acceleration = springForce - dampingForce;\n\n velocity += acceleration * 0.016; // Assuming 60fps\n currentValue.value += velocity * 0.016;\n\n updateDisplay();\n\n if (Math.abs(displacement) > 0.01 || Math.abs(velocity) > 0.01) {\n animationId.value = requestAnimationFrame(springAnimation);\n } else {\n currentValue.value = target;\n updateDisplay();\n animationId.value = null;\n\n if (props.onEnd) {\n props.onEnd();\n }\n }\n};\n\nconst startAnimation = () => {\n if (hasStarted.value || !isInView.value || !props.startWhen) return;\n\n hasStarted.value = true;\n\n if (props.onStart) {\n props.onStart();\n }\n\n setTimeout(() => {\n startTime = 0;\n velocity = 0;\n animationId.value = requestAnimationFrame(springAnimation);\n }, props.delay * 1000);\n};\n\nconst setupIntersectionObserver = () => {\n if (!elementRef.value) return;\n\n intersectionObserver = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting && !isInView.value) {\n isInView.value = true;\n startAnimation();\n }\n },\n {\n threshold: 0,\n rootMargin: '0px'\n }\n );\n\n intersectionObserver.observe(elementRef.value);\n};\n\nconst cleanup = () => {\n if (animationId.value) {\n cancelAnimationFrame(animationId.value);\n animationId.value = null;\n }\n\n if (intersectionObserver) {\n intersectionObserver.disconnect();\n intersectionObserver = null;\n }\n};\n\nwatch(\n [() => props.from, () => props.to, () => props.direction],\n () => {\n currentValue.value = props.direction === 'down' ? props.to : props.from;\n updateDisplay();\n hasStarted.value = false;\n },\n { immediate: true }\n);\n\nwatch(\n () => props.startWhen,\n () => {\n if (props.startWhen && isInView.value && !hasStarted.value) {\n startAnimation();\n }\n }\n);\n\nonMounted(() => {\n updateDisplay();\n setupIntersectionObserver();\n});\n\nonUnmounted(() => {\n cleanup();\n});\n</script>\n","path":"CountUp/CountUp.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[],"devDependencies":[],"categories":["Animations"]}
|
||||
{"name":"CountUp","title":"CountUp","description":"Animated number counter supporting formatting and decimals.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <span ref=\"elementRef\" :class=\"className\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch, computed, useTemplateRef } from 'vue';\n\ninterface Props {\n to: number;\n from?: number;\n direction?: 'up' | 'down';\n delay?: number;\n duration?: number;\n className?: string;\n startWhen?: boolean;\n separator?: string;\n onStart?: () => void;\n onEnd?: () => void;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n from: 0,\n direction: 'up',\n delay: 0,\n duration: 2,\n className: '',\n startWhen: true,\n separator: ''\n});\n\nconst elementRef = useTemplateRef<HTMLSpanElement>('elementRef');\nconst currentValue = ref(props.direction === 'down' ? props.to : props.from);\nconst isInView = ref(false);\nconst animationId = ref<number | null>(null);\nconst hasStarted = ref(false);\n\nlet intersectionObserver: IntersectionObserver | null = null;\n\nconst damping = computed(() => 20 + 40 * (1 / props.duration));\nconst stiffness = computed(() => 100 * (1 / props.duration));\n\nlet velocity = 0;\nlet startTime = 0;\n\nconst formatNumber = (value: number) => {\n const options = {\n useGrouping: !!props.separator,\n minimumFractionDigits: 0,\n maximumFractionDigits: 0\n };\n\n const formattedNumber = Intl.NumberFormat('en-US', options).format(Number(value.toFixed(0)));\n\n return props.separator ? formattedNumber.replace(/,/g, props.separator) : formattedNumber;\n};\n\nconst updateDisplay = () => {\n if (elementRef.value) {\n elementRef.value.textContent = formatNumber(currentValue.value);\n }\n};\n\nconst springAnimation = (timestamp: number) => {\n if (!startTime) startTime = timestamp;\n\n const target = props.direction === 'down' ? props.from : props.to;\n const current = currentValue.value;\n\n const displacement = target - current;\n const springForce = displacement * stiffness.value;\n const dampingForce = velocity * damping.value;\n const acceleration = springForce - dampingForce;\n\n velocity += acceleration * 0.016; // Assuming 60fps\n currentValue.value += velocity * 0.016;\n\n updateDisplay();\n\n if (Math.abs(displacement) > 0.01 || Math.abs(velocity) > 0.01) {\n animationId.value = requestAnimationFrame(springAnimation);\n } else {\n currentValue.value = target;\n updateDisplay();\n animationId.value = null;\n\n if (props.onEnd) {\n props.onEnd();\n }\n }\n};\n\nconst startAnimation = () => {\n if (hasStarted.value || !isInView.value || !props.startWhen) return;\n\n hasStarted.value = true;\n\n if (props.onStart) {\n props.onStart();\n }\n\n setTimeout(() => {\n startTime = 0;\n velocity = 0;\n animationId.value = requestAnimationFrame(springAnimation);\n }, props.delay * 1000);\n};\n\nconst setupIntersectionObserver = () => {\n if (!elementRef.value) return;\n\n intersectionObserver = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting && !isInView.value) {\n isInView.value = true;\n startAnimation();\n }\n },\n {\n threshold: 0,\n rootMargin: '0px'\n }\n );\n\n intersectionObserver.observe(elementRef.value);\n};\n\nconst cleanup = () => {\n if (animationId.value) {\n cancelAnimationFrame(animationId.value);\n animationId.value = null;\n }\n\n if (intersectionObserver) {\n intersectionObserver.disconnect();\n intersectionObserver = null;\n }\n};\n\nwatch(\n [() => props.from, () => props.to, () => props.direction],\n () => {\n currentValue.value = props.direction === 'down' ? props.to : props.from;\n updateDisplay();\n hasStarted.value = false;\n },\n { immediate: true }\n);\n\nwatch(\n () => props.startWhen,\n () => {\n if (props.startWhen && isInView.value && !hasStarted.value) {\n startAnimation();\n }\n }\n);\n\nonMounted(() => {\n updateDisplay();\n setupIntersectionObserver();\n});\n\nonUnmounted(() => {\n cleanup();\n});\n</script>\n","path":"CountUp/CountUp.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[],"devDependencies":[],"categories":["TextAnimations"]}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
public/r/PixelSnow.json
Normal file
1
public/r/PixelSnow.json
Normal file
File diff suppressed because one or more lines are too long
1
public/r/PixelTrail.json
Normal file
1
public/r/PixelTrail.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
public/r/Shuffle.json
Normal file
1
public/r/Shuffle.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,7 @@ export const NEW = [
|
||||
'Pixel Blast',
|
||||
'Pixel Snow',
|
||||
];
|
||||
|
||||
export const UPDATED = [];
|
||||
export const UPDATED = ['Metallic Paint'];
|
||||
|
||||
// Used for main sidebar navigation
|
||||
export const CATEGORIES = [
|
||||
|
||||
@@ -42,7 +42,7 @@ const textAnimations = {
|
||||
'falling-text': () => import("../demo/TextAnimations/FallingTextDemo.vue"),
|
||||
'text-cursor': () => import("../demo/TextAnimations/TextCursorDemo.vue"),
|
||||
'decrypted-text': () => import("../demo/TextAnimations/DecryptedTextDemo.vue"),
|
||||
'ascii-text': () => import("../demo/TextAnimations/AsciiTextDemo.vue"),
|
||||
'ascii-text': () => import("../demo/TextAnimations/ASCIITextDemo.vue"),
|
||||
'scramble-text': () => import("../demo/TextAnimations/ScrambleTextDemo.vue"),
|
||||
'true-focus': () => import("../demo/TextAnimations/TrueFocusDemo.vue"),
|
||||
'scroll-float': () => import("../demo/TextAnimations/ScrollFloatDemo.vue"),
|
||||
|
||||
@@ -225,11 +225,11 @@ export const componentMetadata: ComponentMetadata = {
|
||||
|
||||
//! Text Animations -------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
'TextAnimations/AsciiText': {
|
||||
'TextAnimations/ASCIIText': {
|
||||
videoUrl: '/assets/videos/asciitext.webm',
|
||||
description: 'Renders text with an animated ASCII background for a retro feel.',
|
||||
category: 'TextAnimations',
|
||||
name: 'AsciiText',
|
||||
name: 'ASCIIText',
|
||||
docsUrl: 'https://vue-bits.dev/text-animations/ascii-text',
|
||||
tags: []
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
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>`
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import code from '@/content/TextAnimations/AsciiText/AsciiText.vue?raw';
|
||||
import code from '@/content/TextAnimations/ASCIIText/ASCIIText.vue?raw';
|
||||
import { createCodeObject } from '../../../types/code';
|
||||
|
||||
export const asciiText = createCodeObject(code, 'TextAnimations/AsciiText', {
|
||||
export const asciiText = createCodeObject(code, 'TextAnimations/ASCIIText', {
|
||||
installation: `npm install three @types/three`,
|
||||
usage: `<!-- Component ported and enhanced from https://codepen.io/JuanFuentes/pen/eYEeoyE -->
|
||||
|
||||
<template>
|
||||
<AsciiText
|
||||
<ASCIIText
|
||||
text="Hey!"
|
||||
:ascii-font-size="8"
|
||||
:text-font-size="200"
|
||||
@@ -18,6 +18,6 @@ export const asciiText = createCodeObject(code, 'TextAnimations/AsciiText', {
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AsciiText from "./AsciiText.vue";
|
||||
import ASCIIText from "./ASCIIText.vue";
|
||||
</script>`
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
import { onMounted, onUnmounted, watch, useTemplateRef } from 'vue';
|
||||
import * as THREE from 'three';
|
||||
|
||||
interface AsciiTextProps {
|
||||
interface ASCIITextProps {
|
||||
text?: string;
|
||||
asciiFontSize?: number;
|
||||
textFontSize?: number;
|
||||
@@ -14,7 +14,7 @@ interface AsciiTextProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<AsciiTextProps>(), {
|
||||
const props = withDefaults(defineProps<ASCIITextProps>(), {
|
||||
text: 'David!',
|
||||
asciiFontSize: 8,
|
||||
textFontSize: 200,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="demo-container h-[400px] overflow-hidden">
|
||||
<AsciiText
|
||||
<ASCIIText
|
||||
:key="rerenderKey"
|
||||
:text="text"
|
||||
:ascii-font-size="asciiFontSize"
|
||||
@@ -62,7 +62,7 @@ import Customize from '@/components/common/Customize.vue';
|
||||
import PreviewSlider from '@/components/common/PreviewSlider.vue';
|
||||
import PreviewSwitch from '@/components/common/PreviewSwitch.vue';
|
||||
import PreviewText from '@/components/common/PreviewText.vue';
|
||||
import AsciiText from '@/content/TextAnimations/AsciiText/AsciiText.vue';
|
||||
import ASCIIText from '@/content/TextAnimations/ASCIIText/ASCIIText.vue';
|
||||
import { asciiText } from '@/constants/code/TextAnimations/asciiTextCode';
|
||||
import { useForceRerender } from '@/composables/useForceRerender';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user