mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
662 lines
21 KiB
Vue
662 lines
21 KiB
Vue
<template>
|
|
<div
|
|
ref="containerRef"
|
|
:class="['w-full h-full relative overflow-hidden', className]"
|
|
:style="style"
|
|
aria-label="PixelBlast interactive background"
|
|
/>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { Effect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';
|
|
import * as THREE from 'three';
|
|
import { onBeforeUnmount, onMounted, ref, useTemplateRef, watch, type CSSProperties } from 'vue';
|
|
|
|
export type PixelBlastVariant = 'square' | 'circle' | 'triangle' | 'diamond';
|
|
|
|
type PixelBlastProps = {
|
|
variant?: PixelBlastVariant;
|
|
pixelSize?: number;
|
|
color?: string;
|
|
className?: string;
|
|
style?: CSSProperties;
|
|
antialias?: boolean;
|
|
patternScale?: number;
|
|
patternDensity?: number;
|
|
liquid?: boolean;
|
|
liquidStrength?: number;
|
|
liquidRadius?: number;
|
|
pixelSizeJitter?: number;
|
|
enableRipples?: boolean;
|
|
rippleIntensityScale?: number;
|
|
rippleThickness?: number;
|
|
rippleSpeed?: number;
|
|
liquidWobbleSpeed?: number;
|
|
autoPauseOffscreen?: boolean;
|
|
speed?: number;
|
|
transparent?: boolean;
|
|
edgeFade?: number;
|
|
noiseAmount?: number;
|
|
};
|
|
|
|
const createTouchTexture = () => {
|
|
const size = 64;
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = size;
|
|
canvas.height = size;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) throw new Error('2D context not available');
|
|
ctx.fillStyle = 'black';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
const texture = new THREE.Texture(canvas);
|
|
texture.minFilter = THREE.LinearFilter;
|
|
texture.magFilter = THREE.LinearFilter;
|
|
texture.generateMipmaps = false;
|
|
const trail: {
|
|
x: number;
|
|
y: number;
|
|
vx: number;
|
|
vy: number;
|
|
force: number;
|
|
age: number;
|
|
}[] = [];
|
|
let last: { x: number; y: number } | null = null;
|
|
const maxAge = 64;
|
|
let radius = 0.1 * size;
|
|
const speed = 1 / maxAge;
|
|
const clear = () => {
|
|
ctx.fillStyle = 'black';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
};
|
|
const drawPoint = (p: { x: number; y: number; vx: number; vy: number; force: number; age: number }) => {
|
|
const pos = { x: p.x * size, y: (1 - p.y) * size };
|
|
let intensity = 1;
|
|
const easeOutSine = (t: number) => Math.sin((t * Math.PI) / 2);
|
|
const easeOutQuad = (t: number) => -t * (t - 2);
|
|
if (p.age < maxAge * 0.3) intensity = easeOutSine(p.age / (maxAge * 0.3));
|
|
else intensity = easeOutQuad(1 - (p.age - maxAge * 0.3) / (maxAge * 0.7)) || 0;
|
|
intensity *= p.force;
|
|
const color = `${((p.vx + 1) / 2) * 255}, ${((p.vy + 1) / 2) * 255}, ${intensity * 255}`;
|
|
const offset = size * 5;
|
|
ctx.shadowOffsetX = offset;
|
|
ctx.shadowOffsetY = offset;
|
|
ctx.shadowBlur = radius;
|
|
ctx.shadowColor = `rgba(${color},${0.22 * intensity})`;
|
|
ctx.beginPath();
|
|
ctx.fillStyle = 'rgba(255,0,0,1)';
|
|
ctx.arc(pos.x - offset, pos.y - offset, radius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
};
|
|
const addTouch = (norm: { x: number; y: number }) => {
|
|
let force = 0;
|
|
let vx = 0;
|
|
let vy = 0;
|
|
if (last) {
|
|
const dx = norm.x - last.x;
|
|
const dy = norm.y - last.y;
|
|
if (dx === 0 && dy === 0) return;
|
|
const dd = dx * dx + dy * dy;
|
|
const d = Math.sqrt(dd);
|
|
vx = dx / (d || 1);
|
|
vy = dy / (d || 1);
|
|
force = Math.min(dd * 10000, 1);
|
|
}
|
|
last = { x: norm.x, y: norm.y };
|
|
trail.push({ x: norm.x, y: norm.y, age: 0, force, vx, vy });
|
|
};
|
|
const update = () => {
|
|
clear();
|
|
for (let i = trail.length - 1; i >= 0; i--) {
|
|
const point = trail[i];
|
|
const f = point.force * speed * (1 - point.age / maxAge);
|
|
point.x += point.vx * f;
|
|
point.y += point.vy * f;
|
|
point.age++;
|
|
if (point.age > maxAge) trail.splice(i, 1);
|
|
}
|
|
for (let i = 0; i < trail.length; i++) drawPoint(trail[i]);
|
|
texture.needsUpdate = true;
|
|
};
|
|
return {
|
|
canvas,
|
|
texture,
|
|
addTouch,
|
|
update,
|
|
set radiusScale(v: number) {
|
|
radius = 0.1 * size * v;
|
|
},
|
|
get radiusScale() {
|
|
return radius / (0.1 * size);
|
|
},
|
|
size
|
|
};
|
|
};
|
|
|
|
const createLiquidEffect = (texture: THREE.Texture, opts?: { strength?: number; freq?: number }) => {
|
|
const fragment = `
|
|
uniform sampler2D uTexture;
|
|
uniform float uStrength;
|
|
uniform float uTime;
|
|
uniform float uFreq;
|
|
|
|
void mainUv(inout vec2 uv) {
|
|
vec4 tex = texture2D(uTexture, uv);
|
|
float vx = tex.r * 2.0 - 1.0;
|
|
float vy = tex.g * 2.0 - 1.0;
|
|
float intensity = tex.b;
|
|
|
|
float wave = 0.5 + 0.5 * sin(uTime * uFreq + intensity * 6.2831853);
|
|
|
|
float amt = uStrength * intensity * wave;
|
|
|
|
uv += vec2(vx, vy) * amt;
|
|
}
|
|
`;
|
|
return new Effect('LiquidEffect', fragment, {
|
|
uniforms: new Map<string, THREE.Uniform>([
|
|
['uTexture', new THREE.Uniform(texture)],
|
|
['uStrength', new THREE.Uniform(opts?.strength ?? 0.025)],
|
|
['uTime', new THREE.Uniform(0)],
|
|
['uFreq', new THREE.Uniform(opts?.freq ?? 4.5)]
|
|
])
|
|
});
|
|
};
|
|
|
|
const SHAPE_MAP: Record<PixelBlastVariant, number> = {
|
|
square: 0,
|
|
circle: 1,
|
|
triangle: 2,
|
|
diamond: 3
|
|
};
|
|
|
|
const VERTEX_SRC = `
|
|
void main() {
|
|
gl_Position = vec4(position, 1.0);
|
|
}
|
|
`;
|
|
|
|
const FRAGMENT_SRC = `
|
|
precision highp float;
|
|
|
|
uniform vec3 uColor;
|
|
uniform vec2 uResolution;
|
|
uniform float uTime;
|
|
uniform float uPixelSize;
|
|
uniform float uScale;
|
|
uniform float uDensity;
|
|
uniform float uPixelJitter;
|
|
uniform int uEnableRipples;
|
|
uniform float uRippleSpeed;
|
|
uniform float uRippleThickness;
|
|
uniform float uRippleIntensity;
|
|
uniform float uEdgeFade;
|
|
|
|
uniform int uShapeType;
|
|
const int SHAPE_SQUARE = 0;
|
|
const int SHAPE_CIRCLE = 1;
|
|
const int SHAPE_TRIANGLE = 2;
|
|
const int SHAPE_DIAMOND = 3;
|
|
|
|
const int MAX_CLICKS = 10;
|
|
|
|
uniform vec2 uClickPos [MAX_CLICKS];
|
|
uniform float uClickTimes[MAX_CLICKS];
|
|
|
|
out vec4 fragColor;
|
|
|
|
float Bayer2(vec2 a) {
|
|
a = floor(a);
|
|
return fract(a.x / 2. + a.y * a.y * .75);
|
|
}
|
|
#define Bayer4(a) (Bayer2(.5*(a))*0.25 + Bayer2(a))
|
|
#define Bayer8(a) (Bayer4(.5*(a))*0.25 + Bayer2(a))
|
|
|
|
#define FBM_OCTAVES 5
|
|
#define FBM_LACUNARITY 1.25
|
|
#define FBM_GAIN 1.0
|
|
|
|
float hash11(float n){ return fract(sin(n)*43758.5453); }
|
|
|
|
float vnoise(vec3 p){
|
|
vec3 ip = floor(p);
|
|
vec3 fp = fract(p);
|
|
float n000 = hash11(dot(ip + vec3(0.0,0.0,0.0), vec3(1.0,57.0,113.0)));
|
|
float n100 = hash11(dot(ip + vec3(1.0,0.0,0.0), vec3(1.0,57.0,113.0)));
|
|
float n010 = hash11(dot(ip + vec3(0.0,1.0,0.0), vec3(1.0,57.0,113.0)));
|
|
float n110 = hash11(dot(ip + vec3(1.0,1.0,0.0), vec3(1.0,57.0,113.0)));
|
|
float n001 = hash11(dot(ip + vec3(0.0,0.0,1.0), vec3(1.0,57.0,113.0)));
|
|
float n101 = hash11(dot(ip + vec3(1.0,0.0,1.0), vec3(1.0,57.0,113.0)));
|
|
float n011 = hash11(dot(ip + vec3(0.0,1.0,1.0), vec3(1.0,57.0,113.0)));
|
|
float n111 = hash11(dot(ip + vec3(1.0,1.0,1.0), vec3(1.0,57.0,113.0)));
|
|
vec3 w = fp*fp*fp*(fp*(fp*6.0-15.0)+10.0);
|
|
float x00 = mix(n000, n100, w.x);
|
|
float x10 = mix(n010, n110, w.x);
|
|
float x01 = mix(n001, n101, w.x);
|
|
float x11 = mix(n011, n111, w.x);
|
|
float y0 = mix(x00, x10, w.y);
|
|
float y1 = mix(x01, x11, w.y);
|
|
return mix(y0, y1, w.z) * 2.0 - 1.0;
|
|
}
|
|
|
|
float fbm2(vec2 uv, float t){
|
|
vec3 p = vec3(uv * uScale, t);
|
|
float amp = 1.0;
|
|
float freq = 1.0;
|
|
float sum = 1.0;
|
|
for (int i = 0; i < FBM_OCTAVES; ++i){
|
|
sum += amp * vnoise(p * freq);
|
|
freq *= FBM_LACUNARITY;
|
|
amp *= FBM_GAIN;
|
|
}
|
|
return sum * 0.5 + 0.5;
|
|
}
|
|
|
|
float maskCircle(vec2 p, float cov){
|
|
float r = sqrt(cov) * .25;
|
|
float d = length(p - 0.5) - r;
|
|
float aa = 0.5 * fwidth(d);
|
|
return cov * (1.0 - smoothstep(-aa, aa, d * 2.0));
|
|
}
|
|
|
|
float maskTriangle(vec2 p, vec2 id, float cov){
|
|
bool flip = mod(id.x + id.y, 2.0) > 0.5;
|
|
if (flip) p.x = 1.0 - p.x;
|
|
float r = sqrt(cov);
|
|
float d = p.y - r*(1.0 - p.x);
|
|
float aa = fwidth(d);
|
|
return cov * clamp(0.5 - d/aa, 0.0, 1.0);
|
|
}
|
|
|
|
float maskDiamond(vec2 p, float cov){
|
|
float r = sqrt(cov) * 0.564;
|
|
return step(abs(p.x - 0.49) + abs(p.y - 0.49), r);
|
|
}
|
|
|
|
void main(){
|
|
float pixelSize = uPixelSize;
|
|
vec2 fragCoord = gl_FragCoord.xy - uResolution * .5;
|
|
float aspectRatio = uResolution.x / uResolution.y;
|
|
|
|
vec2 pixelId = floor(fragCoord / pixelSize);
|
|
vec2 pixelUV = fract(fragCoord / pixelSize);
|
|
|
|
float cellPixelSize = 8.0 * pixelSize;
|
|
vec2 cellId = floor(fragCoord / cellPixelSize);
|
|
vec2 cellCoord = cellId * cellPixelSize;
|
|
vec2 uv = cellCoord / uResolution * vec2(aspectRatio, 1.0);
|
|
|
|
float base = fbm2(uv, uTime * 0.05);
|
|
base = base * 0.5 - 0.65;
|
|
|
|
float feed = base + (uDensity - 0.5) * 0.3;
|
|
|
|
float speed = uRippleSpeed;
|
|
float thickness = uRippleThickness;
|
|
const float dampT = 1.0;
|
|
const float dampR = 10.0;
|
|
|
|
if (uEnableRipples == 1) {
|
|
for (int i = 0; i < MAX_CLICKS; ++i){
|
|
vec2 pos = uClickPos[i];
|
|
if (pos.x < 0.0) continue;
|
|
float cellPixelSize = 8.0 * pixelSize;
|
|
vec2 cuv = (((pos - uResolution * .5 - cellPixelSize * .5) / (uResolution))) * vec2(aspectRatio, 1.0);
|
|
float t = max(uTime - uClickTimes[i], 0.0);
|
|
float r = distance(uv, cuv);
|
|
float waveR = speed * t;
|
|
float ring = exp(-pow((r - waveR) / thickness, 2.0));
|
|
float atten = exp(-dampT * t) * exp(-dampR * r);
|
|
feed = max(feed, ring * atten * uRippleIntensity);
|
|
}
|
|
}
|
|
|
|
float bayer = Bayer8(fragCoord / uPixelSize) - 0.5;
|
|
float bw = step(0.5, feed + bayer);
|
|
|
|
float h = fract(sin(dot(floor(fragCoord / uPixelSize), vec2(127.1, 311.7))) * 43758.5453);
|
|
float jitterScale = 1.0 + (h - 0.5) * uPixelJitter;
|
|
float coverage = bw * jitterScale;
|
|
float M;
|
|
if (uShapeType == SHAPE_CIRCLE) M = maskCircle (pixelUV, coverage);
|
|
else if (uShapeType == SHAPE_TRIANGLE) M = maskTriangle(pixelUV, pixelId, coverage);
|
|
else if (uShapeType == SHAPE_DIAMOND) M = maskDiamond(pixelUV, coverage);
|
|
else M = coverage;
|
|
|
|
if (uEdgeFade > 0.0) {
|
|
vec2 norm = gl_FragCoord.xy / uResolution;
|
|
float edge = min(min(norm.x, norm.y), min(1.0 - norm.x, 1.0 - norm.y));
|
|
float fade = smoothstep(0.0, uEdgeFade, edge);
|
|
M *= fade;
|
|
}
|
|
|
|
vec3 color = uColor;
|
|
fragColor = vec4(color, M);
|
|
}
|
|
`;
|
|
|
|
const MAX_CLICKS = 10;
|
|
|
|
const props = withDefaults(defineProps<PixelBlastProps>(), {
|
|
variant: 'square',
|
|
pixelSize: 3,
|
|
color: '#B19EEF',
|
|
antialias: true,
|
|
patternScale: 2,
|
|
patternDensity: 1,
|
|
liquid: false,
|
|
liquidStrength: 0.1,
|
|
liquidRadius: 1,
|
|
pixelSizeJitter: 0,
|
|
enableRipples: true,
|
|
rippleIntensityScale: 1,
|
|
rippleThickness: 0.1,
|
|
rippleSpeed: 0.3,
|
|
liquidWobbleSpeed: 4.5,
|
|
autoPauseOffscreen: true,
|
|
speed: 0.5,
|
|
transparent: true,
|
|
edgeFade: 0.5,
|
|
noiseAmount: 0
|
|
});
|
|
|
|
const containerRef = useTemplateRef<HTMLDivElement>('containerRef');
|
|
const visibilityRef = ref({ visible: true });
|
|
const speedRef = ref(props.speed);
|
|
|
|
const threeRef = ref<{
|
|
renderer: THREE.WebGLRenderer;
|
|
scene: THREE.Scene;
|
|
camera: THREE.OrthographicCamera;
|
|
material: THREE.ShaderMaterial;
|
|
clock: THREE.Clock;
|
|
clickIx: number;
|
|
uniforms: {
|
|
uResolution: { value: THREE.Vector2 };
|
|
uTime: { value: number };
|
|
uColor: { value: THREE.Color };
|
|
uClickPos: { value: THREE.Vector2[] };
|
|
uClickTimes: { value: Float32Array };
|
|
uShapeType: { value: number };
|
|
uPixelSize: { value: number };
|
|
uScale: { value: number };
|
|
uDensity: { value: number };
|
|
uPixelJitter: { value: number };
|
|
uEnableRipples: { value: number };
|
|
uRippleSpeed: { value: number };
|
|
uRippleThickness: { value: number };
|
|
uRippleIntensity: { value: number };
|
|
uEdgeFade: { value: number };
|
|
};
|
|
resizeObserver?: ResizeObserver;
|
|
raf?: number;
|
|
quad?: THREE.Mesh<THREE.PlaneGeometry, THREE.ShaderMaterial>;
|
|
timeOffset?: number;
|
|
composer?: EffectComposer;
|
|
touch?: ReturnType<typeof createTouchTexture>;
|
|
liquidEffect?: Effect;
|
|
} | null>(null);
|
|
const prevConfigRef = ref<any>(null);
|
|
|
|
let cleanup: (() => void) | null = null;
|
|
|
|
const setup = () => {
|
|
const container = containerRef.value;
|
|
if (!container) return;
|
|
speedRef.value = props.speed;
|
|
const needsReinitKeys = ['antialias', 'liquid', 'noiseAmount'];
|
|
const cfg = { antialias: props.antialias, liquid: props.liquid, noiseAmount: props.noiseAmount };
|
|
let mustReinit = false;
|
|
if (!threeRef.value) mustReinit = true;
|
|
else if (prevConfigRef.value) {
|
|
for (const k of needsReinitKeys)
|
|
if (prevConfigRef.value[k] !== (cfg as any)[k]) {
|
|
mustReinit = true;
|
|
break;
|
|
}
|
|
}
|
|
if (mustReinit) {
|
|
if (threeRef.value) {
|
|
const t = threeRef.value;
|
|
t.resizeObserver?.disconnect();
|
|
cancelAnimationFrame(t.raf!);
|
|
t.quad?.geometry.dispose();
|
|
t.material.dispose();
|
|
t.composer?.dispose();
|
|
t.renderer.dispose();
|
|
if (t.renderer.domElement.parentElement === container) container.removeChild(t.renderer.domElement);
|
|
threeRef.value = null;
|
|
}
|
|
const canvas = document.createElement('canvas');
|
|
const gl = canvas.getContext('webgl2', { antialias: props.antialias, alpha: true });
|
|
if (!gl) return;
|
|
const renderer = new THREE.WebGLRenderer({
|
|
canvas,
|
|
context: gl as WebGL2RenderingContext,
|
|
antialias: props.antialias,
|
|
alpha: true
|
|
});
|
|
renderer.domElement.style.width = '100%';
|
|
renderer.domElement.style.height = '100%';
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
container.appendChild(renderer.domElement);
|
|
const uniforms = {
|
|
uResolution: { value: new THREE.Vector2(0, 0) },
|
|
uTime: { value: 0 },
|
|
uColor: { value: new THREE.Color(props.color) },
|
|
uClickPos: {
|
|
value: Array.from({ length: MAX_CLICKS }, () => new THREE.Vector2(-1, -1))
|
|
},
|
|
uClickTimes: { value: new Float32Array(MAX_CLICKS) },
|
|
uShapeType: { value: SHAPE_MAP[props.variant] ?? 0 },
|
|
uPixelSize: { value: props.pixelSize * renderer.getPixelRatio() },
|
|
uScale: { value: props.patternScale },
|
|
uDensity: { value: props.patternDensity },
|
|
uPixelJitter: { value: props.pixelSizeJitter },
|
|
uEnableRipples: { value: props.enableRipples ? 1 : 0 },
|
|
uRippleSpeed: { value: props.rippleSpeed },
|
|
uRippleThickness: { value: props.rippleThickness },
|
|
uRippleIntensity: { value: props.rippleIntensityScale },
|
|
uEdgeFade: { value: props.edgeFade }
|
|
};
|
|
const scene = new THREE.Scene();
|
|
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
const material = new THREE.ShaderMaterial({
|
|
vertexShader: VERTEX_SRC,
|
|
fragmentShader: FRAGMENT_SRC,
|
|
uniforms,
|
|
transparent: true,
|
|
glslVersion: THREE.GLSL3,
|
|
depthTest: false,
|
|
depthWrite: false
|
|
});
|
|
const quadGeom = new THREE.PlaneGeometry(2, 2);
|
|
const quad = new THREE.Mesh(quadGeom, material);
|
|
scene.add(quad);
|
|
const clock = new THREE.Clock();
|
|
const setSize = () => {
|
|
const w = container.clientWidth || 1;
|
|
const h = container.clientHeight || 1;
|
|
renderer.setSize(w, h, false);
|
|
uniforms.uResolution.value.set(renderer.domElement.width, renderer.domElement.height);
|
|
if (threeRef.value?.composer)
|
|
threeRef.value.composer.setSize(renderer.domElement.width, renderer.domElement.height);
|
|
uniforms.uPixelSize.value = props.pixelSize * renderer.getPixelRatio();
|
|
};
|
|
setSize();
|
|
const ro = new ResizeObserver(setSize);
|
|
ro.observe(container);
|
|
const randomFloat = () => {
|
|
if (typeof window !== 'undefined' && (window as any).crypto?.getRandomValues) {
|
|
const u32 = new Uint32Array(1);
|
|
window.crypto.getRandomValues(u32);
|
|
return u32[0] / 0xffffffff;
|
|
}
|
|
return Math.random();
|
|
};
|
|
const timeOffset = randomFloat() * 1000;
|
|
let composer: EffectComposer | undefined;
|
|
let touch: ReturnType<typeof createTouchTexture> | undefined;
|
|
let liquidEffect: Effect | undefined;
|
|
if (props.liquid) {
|
|
touch = createTouchTexture();
|
|
touch.radiusScale = props.liquidRadius;
|
|
composer = new EffectComposer(renderer);
|
|
const renderPass = new RenderPass(scene, camera);
|
|
liquidEffect = createLiquidEffect(touch.texture, {
|
|
strength: props.liquidStrength,
|
|
freq: props.liquidWobbleSpeed
|
|
});
|
|
const effectPass = new EffectPass(camera, liquidEffect);
|
|
effectPass.renderToScreen = true;
|
|
composer.addPass(renderPass);
|
|
composer.addPass(effectPass);
|
|
}
|
|
if (props.noiseAmount > 0) {
|
|
if (!composer) {
|
|
composer = new EffectComposer(renderer);
|
|
composer.addPass(new RenderPass(scene, camera));
|
|
}
|
|
const noiseEffect = new Effect(
|
|
'NoiseEffect',
|
|
`uniform float uTime; uniform float uAmount; float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453);} void mainUv(inout vec2 uv){} void mainImage(const in vec4 inputColor,const in vec2 uv,out vec4 outputColor){ float n=hash(floor(uv*vec2(1920.0,1080.0))+floor(uTime*60.0)); float g=(n-0.5)*uAmount; outputColor=inputColor+vec4(vec3(g),0.0);} `,
|
|
{
|
|
uniforms: new Map<string, THREE.Uniform>([
|
|
['uTime', new THREE.Uniform(0)],
|
|
['uAmount', new THREE.Uniform(props.noiseAmount)]
|
|
])
|
|
}
|
|
);
|
|
const noisePass = new EffectPass(camera, noiseEffect);
|
|
noisePass.renderToScreen = true;
|
|
if (composer && composer.passes.length > 0) composer.passes.forEach(p => ((p as any).renderToScreen = false));
|
|
composer.addPass(noisePass);
|
|
}
|
|
if (composer) composer.setSize(renderer.domElement.width, renderer.domElement.height);
|
|
const mapToPixels = (e: PointerEvent) => {
|
|
const rect = renderer.domElement.getBoundingClientRect();
|
|
const scaleX = renderer.domElement.width / rect.width;
|
|
const scaleY = renderer.domElement.height / rect.height;
|
|
const fx = (e.clientX - rect.left) * scaleX;
|
|
const fy = (rect.height - (e.clientY - rect.top)) * scaleY;
|
|
return {
|
|
fx,
|
|
fy,
|
|
w: renderer.domElement.width,
|
|
h: renderer.domElement.height
|
|
};
|
|
};
|
|
const onPointerDown = (e: PointerEvent) => {
|
|
const { fx, fy } = mapToPixels(e);
|
|
const ix = threeRef.value?.clickIx ?? 0;
|
|
uniforms.uClickPos.value[ix].set(fx, fy);
|
|
uniforms.uClickTimes.value[ix] = uniforms.uTime.value;
|
|
if (threeRef.value) threeRef.value.clickIx = (ix + 1) % MAX_CLICKS;
|
|
};
|
|
const onPointerMove = (e: PointerEvent) => {
|
|
if (!touch) return;
|
|
const { fx, fy, w, h } = mapToPixels(e);
|
|
touch.addTouch({ x: fx / w, y: fy / h });
|
|
};
|
|
renderer.domElement.addEventListener('pointerdown', onPointerDown, {
|
|
passive: true
|
|
});
|
|
renderer.domElement.addEventListener('pointermove', onPointerMove, {
|
|
passive: true
|
|
});
|
|
let raf = 0;
|
|
const animate = () => {
|
|
if (props.autoPauseOffscreen && !visibilityRef.value.visible) {
|
|
raf = requestAnimationFrame(animate);
|
|
return;
|
|
}
|
|
uniforms.uTime.value = timeOffset + clock.getElapsedTime() * speedRef.value;
|
|
if (liquidEffect) (liquidEffect as any).uniforms.get('uTime').value = uniforms.uTime.value;
|
|
if (composer) {
|
|
if (touch) touch.update();
|
|
composer.passes.forEach(p => {
|
|
const effs = (p as any).effects;
|
|
if (effs)
|
|
effs.forEach((eff: any) => {
|
|
const u = eff.uniforms?.get('uTime');
|
|
if (u) u.value = uniforms.uTime.value;
|
|
});
|
|
});
|
|
composer.render();
|
|
} else renderer.render(scene, camera);
|
|
raf = requestAnimationFrame(animate);
|
|
};
|
|
raf = requestAnimationFrame(animate);
|
|
threeRef.value = {
|
|
renderer,
|
|
scene,
|
|
camera,
|
|
material,
|
|
clock,
|
|
clickIx: 0,
|
|
uniforms,
|
|
resizeObserver: ro,
|
|
raf,
|
|
quad,
|
|
timeOffset,
|
|
composer,
|
|
touch,
|
|
liquidEffect
|
|
};
|
|
} else {
|
|
const t = threeRef.value!;
|
|
t.uniforms.uShapeType.value = SHAPE_MAP[props.variant] ?? 0;
|
|
t.uniforms.uPixelSize.value = props.pixelSize * t.renderer.getPixelRatio();
|
|
t.uniforms.uColor.value.set(props.color);
|
|
t.uniforms.uScale.value = props.patternScale;
|
|
t.uniforms.uDensity.value = props.patternDensity;
|
|
t.uniforms.uPixelJitter.value = props.pixelSizeJitter;
|
|
t.uniforms.uEnableRipples.value = props.enableRipples ? 1 : 0;
|
|
t.uniforms.uRippleIntensity.value = props.rippleIntensityScale;
|
|
t.uniforms.uRippleThickness.value = props.rippleThickness;
|
|
t.uniforms.uRippleSpeed.value = props.rippleSpeed;
|
|
t.uniforms.uEdgeFade.value = props.edgeFade;
|
|
if (props.transparent) t.renderer.setClearAlpha(0);
|
|
else t.renderer.setClearColor(0x000000, 1);
|
|
if (t.liquidEffect) {
|
|
const uStrength = (t.liquidEffect as any).uniforms.get('uStrength');
|
|
if (uStrength) uStrength.value = props.liquidStrength;
|
|
const uFreq = (t.liquidEffect as any).uniforms.get('uFreq');
|
|
if (uFreq) uFreq.value = props.liquidWobbleSpeed;
|
|
}
|
|
if (t.touch) t.touch.radiusScale = props.liquidRadius;
|
|
}
|
|
prevConfigRef.value = cfg;
|
|
|
|
cleanup = () => {
|
|
if (threeRef.value && mustReinit) return;
|
|
if (!threeRef.value) return;
|
|
const t = threeRef.value;
|
|
t.resizeObserver?.disconnect();
|
|
cancelAnimationFrame(t.raf!);
|
|
t.quad?.geometry.dispose();
|
|
t.material.dispose();
|
|
t.composer?.dispose();
|
|
t.renderer.dispose();
|
|
if (t.renderer.domElement.parentElement === container) container.removeChild(t.renderer.domElement);
|
|
threeRef.value = null;
|
|
};
|
|
};
|
|
|
|
onMounted(() => {
|
|
setup();
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
cleanup?.();
|
|
});
|
|
|
|
watch(
|
|
props,
|
|
() => {
|
|
cleanup?.();
|
|
setup();
|
|
},
|
|
{ deep: true }
|
|
);
|
|
</script>
|