mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Migrate MetallicPaint
This commit is contained in:
@@ -33,6 +33,7 @@ export const CATEGORIES = [
|
|||||||
'Glare Hover',
|
'Glare Hover',
|
||||||
'Magnet Lines',
|
'Magnet Lines',
|
||||||
'Count Up',
|
'Count Up',
|
||||||
|
'Metallic Paint',
|
||||||
'Click Spark',
|
'Click Spark',
|
||||||
'Magnet',
|
'Magnet',
|
||||||
'Cubes'
|
'Cubes'
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const animations = {
|
|||||||
'glare-hover': () => import('../demo/Animations/GlareHoverDemo.vue'),
|
'glare-hover': () => import('../demo/Animations/GlareHoverDemo.vue'),
|
||||||
'magnet-lines': () => import('../demo/Animations/MagnetLinesDemo.vue'),
|
'magnet-lines': () => import('../demo/Animations/MagnetLinesDemo.vue'),
|
||||||
'click-spark': () => import('../demo/Animations/ClickSparkDemo.vue'),
|
'click-spark': () => import('../demo/Animations/ClickSparkDemo.vue'),
|
||||||
|
'metallic-paint': () => import('../demo/Animations/MetallicPaintDemo.vue'),
|
||||||
'magnet': () => import('../demo/Animations/MagnetDemo.vue'),
|
'magnet': () => import('../demo/Animations/MagnetDemo.vue'),
|
||||||
'cubes': () => import('../demo/Animations/CubesDemo.vue'),
|
'cubes': () => import('../demo/Animations/CubesDemo.vue'),
|
||||||
'count-up': () => import('../demo/Animations/CountUpDemo.vue')
|
'count-up': () => import('../demo/Animations/CountUpDemo.vue')
|
||||||
|
|||||||
47
src/constants/code/Animations/metallicPaintCode.ts
Normal file
47
src/constants/code/Animations/metallicPaintCode.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import code from '@content/Animations/MetallicPaint/MetallicPaint.vue?raw'
|
||||||
|
import utility from '@content/Animations/MetallicPaint/parseImage.ts?raw'
|
||||||
|
import type { CodeObject } from '../../../types/code'
|
||||||
|
|
||||||
|
export const metallicPaint: CodeObject = {
|
||||||
|
cli: `npx jsrepo add https://vue-bits.dev/ui/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>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import MetallicPaint from "./MetallicPaint.vue";
|
||||||
|
|
||||||
|
// copy and import the parseImage utility from the correct path
|
||||||
|
import { parseImage } from './parseImage';
|
||||||
|
|
||||||
|
const imageData = ref<ImageData | null>(null);
|
||||||
|
|
||||||
|
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>`,
|
||||||
|
code,
|
||||||
|
utility
|
||||||
|
}
|
||||||
386
src/content/Animations/MetallicPaint/MetallicPaint.vue
Normal file
386
src/content/Animations/MetallicPaint/MetallicPaint.vue
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
<template>
|
||||||
|
<canvas ref="canvasRef" class="block w-full h-full object-contain" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch, nextTick } 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 = ref<HTMLCanvasElement | null>(null)
|
||||||
|
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;
|
||||||
|
|
||||||
|
in vec2 a_position;
|
||||||
|
out vec2 vUv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vUv = .5 * (a_position + 1.);
|
||||||
|
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||||
|
}`
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 get_img_uv() {
|
||||||
|
vec2 img_uv = vUv;
|
||||||
|
img_uv -= .5;
|
||||||
|
if (u_ratio > u_img_ratio) {
|
||||||
|
img_uv.x = img_uv.x * u_ratio / u_img_ratio;
|
||||||
|
} else {
|
||||||
|
img_uv.y = img_uv.y * u_img_ratio / u_ratio;
|
||||||
|
}
|
||||||
|
float scale_factor = 1.;
|
||||||
|
img_uv *= scale_factor;
|
||||||
|
img_uv += .5;
|
||||||
|
img_uv.y = 1. - img_uv.y;
|
||||||
|
return img_uv;
|
||||||
|
}
|
||||||
|
vec2 rotate(vec2 uv, float th) {
|
||||||
|
return mat2(cos(th), sin(th), -sin(th), cos(th)) * uv;
|
||||||
|
}
|
||||||
|
float get_color_channel(float c1, float c2, float stripe_p, vec3 w, float extra_blur, float b) {
|
||||||
|
float ch = c2;
|
||||||
|
float border = 0.;
|
||||||
|
float blur = u_patternBlur + extra_blur;
|
||||||
|
ch = mix(ch, c1, smoothstep(.0, blur, stripe_p));
|
||||||
|
border = w[0];
|
||||||
|
ch = mix(ch, c2, smoothstep(border - blur, border + blur, stripe_p));
|
||||||
|
b = smoothstep(.2, .8, b);
|
||||||
|
border = w[0] + .4 * (1. - b) * w[1];
|
||||||
|
ch = mix(ch, c1, smoothstep(border - blur, border + blur, stripe_p));
|
||||||
|
border = w[0] + .5 * (1. - b) * w[1];
|
||||||
|
ch = mix(ch, c2, smoothstep(border - blur, border + blur, stripe_p));
|
||||||
|
border = w[0] + w[1];
|
||||||
|
ch = mix(ch, c1, smoothstep(border - blur, border + blur, stripe_p));
|
||||||
|
float gradient_t = (stripe_p - w[0] - w[1]) / w[2];
|
||||||
|
float gradient = mix(c1, c2, smoothstep(0., 1., gradient_t));
|
||||||
|
ch = mix(ch, gradient, smoothstep(border - blur, border + blur, stripe_p));
|
||||||
|
return ch;
|
||||||
|
}
|
||||||
|
float get_img_frame_alpha(vec2 uv, float img_frame_width) {
|
||||||
|
float img_frame_alpha = smoothstep(0., img_frame_width, uv.x) * smoothstep(1., 1. - img_frame_width, uv.x);
|
||||||
|
img_frame_alpha *= smoothstep(0., img_frame_width, uv.y) * smoothstep(1., 1. - img_frame_width, uv.y);
|
||||||
|
return img_frame_alpha;
|
||||||
|
}
|
||||||
|
void main() {
|
||||||
|
vec2 uv = vUv;
|
||||||
|
uv.y = 1. - uv.y;
|
||||||
|
uv.x *= u_ratio;
|
||||||
|
float diagonal = uv.x - uv.y;
|
||||||
|
float t = .001 * u_time;
|
||||||
|
vec2 img_uv = get_img_uv();
|
||||||
|
vec4 img = texture(u_image_texture, img_uv);
|
||||||
|
vec3 color = vec3(0.);
|
||||||
|
float opacity = 1.;
|
||||||
|
vec3 color1 = vec3(.98, 0.98, 1.);
|
||||||
|
vec3 color2 = vec3(.1, .1, .1 + .1 * smoothstep(.7, 1.3, uv.x + uv.y));
|
||||||
|
float edge = img.r;
|
||||||
|
vec2 grad_uv = uv;
|
||||||
|
grad_uv -= .5;
|
||||||
|
float dist = length(grad_uv + vec2(0., .2 * diagonal));
|
||||||
|
grad_uv = rotate(grad_uv, (.25 - .2 * diagonal) * PI);
|
||||||
|
float bulge = pow(1.8 * dist, 1.2);
|
||||||
|
bulge = 1. - bulge;
|
||||||
|
bulge *= pow(uv.y, .3);
|
||||||
|
float cycle_width = u_patternScale;
|
||||||
|
float thin_strip_1_ratio = .12 / cycle_width * (1. - .4 * bulge);
|
||||||
|
float thin_strip_2_ratio = .07 / cycle_width * (1. + .4 * bulge);
|
||||||
|
float wide_strip_ratio = (1. - thin_strip_1_ratio - thin_strip_2_ratio);
|
||||||
|
float thin_strip_1_width = cycle_width * thin_strip_1_ratio;
|
||||||
|
float thin_strip_2_width = cycle_width * thin_strip_2_ratio;
|
||||||
|
opacity = 1. - smoothstep(.9 - .5 * u_edge, 1. - .5 * u_edge, edge);
|
||||||
|
opacity *= get_img_frame_alpha(img_uv, 0.01);
|
||||||
|
float noise = snoise(uv - t);
|
||||||
|
edge += (1. - edge) * u_liquid * noise;
|
||||||
|
float refr = 0.;
|
||||||
|
refr += (1. - bulge);
|
||||||
|
refr = clamp(refr, 0., 1.);
|
||||||
|
float dir = grad_uv.x;
|
||||||
|
dir += diagonal;
|
||||||
|
dir -= 2. * noise * diagonal * (smoothstep(0., 1., edge) * smoothstep(1., 0., edge));
|
||||||
|
bulge *= clamp(pow(uv.y, .1), .3, 1.);
|
||||||
|
dir *= (.1 + (1.1 - edge) * bulge);
|
||||||
|
dir *= smoothstep(1., .7, edge);
|
||||||
|
dir += .18 * (smoothstep(.1, .2, uv.y) * smoothstep(.4, .2, uv.y));
|
||||||
|
dir += .03 * (smoothstep(.1, .2, 1. - uv.y) * smoothstep(.4, .2, 1. - uv.y));
|
||||||
|
dir *= (.5 + .5 * pow(uv.y, 2.));
|
||||||
|
dir *= cycle_width;
|
||||||
|
dir -= t;
|
||||||
|
float refr_r = refr;
|
||||||
|
refr_r += .03 * bulge * noise;
|
||||||
|
float refr_b = 1.3 * refr;
|
||||||
|
refr_r += 5. * (smoothstep(-.1, .2, uv.y) * smoothstep(.5, .1, uv.y)) * (smoothstep(.4, .6, bulge) * smoothstep(1., .4, bulge));
|
||||||
|
refr_r -= diagonal;
|
||||||
|
refr_b += (smoothstep(0., .4, uv.y) * smoothstep(.8, .1, uv.y)) * (smoothstep(.4, .6, bulge) * smoothstep(.8, .4, bulge));
|
||||||
|
refr_b -= .2 * edge;
|
||||||
|
refr_r *= u_refraction;
|
||||||
|
refr_b *= u_refraction;
|
||||||
|
vec3 w = vec3(thin_strip_1_width, thin_strip_2_width, wide_strip_ratio);
|
||||||
|
w[1] -= .02 * smoothstep(.0, 1., edge + bulge);
|
||||||
|
float stripe_r = mod(dir + refr_r, 1.);
|
||||||
|
float r = get_color_channel(color1.r, color2.r, stripe_r, w, 0.02 + .03 * u_refraction * bulge, bulge);
|
||||||
|
float stripe_g = mod(dir, 1.);
|
||||||
|
float g = get_color_channel(color1.g, color2.g, stripe_g, w, 0.01 / (1. - diagonal), bulge);
|
||||||
|
float stripe_b = mod(dir - refr_b, 1.);
|
||||||
|
float b = get_color_channel(color1.b, color2.b, stripe_b, w, .01, bulge);
|
||||||
|
color = vec3(r, g, b);
|
||||||
|
color *= opacity;
|
||||||
|
fragColor = vec4(color, opacity);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
function 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)
|
||||||
|
|
||||||
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||||
|
console.error('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader))
|
||||||
|
gl.deleteShader(shader)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (animationId.value) {
|
||||||
|
cancelAnimationFrame(animationId.value)
|
||||||
|
}
|
||||||
|
window.removeEventListener('resize', resizeCanvas)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.params, () => {
|
||||||
|
updateUniforms()
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
watch(() => props.imageData, () => {
|
||||||
|
setupTexture()
|
||||||
|
resizeCanvas()
|
||||||
|
}, { deep: true })
|
||||||
|
</script>
|
||||||
180
src/content/Animations/MetallicPaint/parseImage.ts
Normal file
180
src/content/Animations/MetallicPaint/parseImage.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
141
src/demo/Animations/MetallicPaintDemo.vue
Normal file
141
src/demo/Animations/MetallicPaintDemo.vue
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<div class="metallic-paint-demo">
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="demo-container relative h-[500px] overflow-hidden">
|
||||||
|
<MetallicPaint
|
||||||
|
v-if="imageData"
|
||||||
|
:key="rerenderKey"
|
||||||
|
:image-data="imageData"
|
||||||
|
:params="{
|
||||||
|
edge,
|
||||||
|
patternBlur,
|
||||||
|
patternScale,
|
||||||
|
refraction,
|
||||||
|
speed,
|
||||||
|
liquid
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<PreviewSlider
|
||||||
|
title="Edge"
|
||||||
|
v-model="edge"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.1"
|
||||||
|
@update:model-value="forceRerender"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Pattern Scale"
|
||||||
|
v-model="patternScale"
|
||||||
|
:min="1"
|
||||||
|
:max="5"
|
||||||
|
:step="0.1"
|
||||||
|
@update:model-value="forceRerender"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Pattern Blur"
|
||||||
|
v-model="patternBlur"
|
||||||
|
:min="0"
|
||||||
|
:max="0.1"
|
||||||
|
:step="0.001"
|
||||||
|
@update:model-value="forceRerender"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Refraction"
|
||||||
|
v-model="refraction"
|
||||||
|
:min="0"
|
||||||
|
:max="0.1"
|
||||||
|
:step="0.01"
|
||||||
|
@update:model-value="forceRerender"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Liquid"
|
||||||
|
v-model="liquid"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
@update:model-value="forceRerender"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Speed"
|
||||||
|
v-model="speed"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
@update:model-value="forceRerender"
|
||||||
|
/>
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="metallicPaint" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="metallicPaint.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</div>
|
||||||
|
</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 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 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 { rerenderKey, forceRerender } = useForceRerender()
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'imageData',
|
||||||
|
type: 'ImageData',
|
||||||
|
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.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'params',
|
||||||
|
type: 'ShaderParams',
|
||||||
|
default: '',
|
||||||
|
description: 'An object to configure the shader effect. Properties include: patternScale, refraction, edge, patternBlur, liquid, speed'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
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