mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
FEAT: � New <PixelBlast /> background
This commit is contained in:
661
src/content/Backgrounds/PixelBlast/PixelBlast.vue
Normal file
661
src/content/Backgrounds/PixelBlast/PixelBlast.vue
Normal file
@@ -0,0 +1,661 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user