Merge branch 'main' into feat/dome-gallery

This commit is contained in:
David
2025-09-25 10:34:08 +03:00
committed by GitHub
8 changed files with 970 additions and 24 deletions

View File

@@ -19,10 +19,10 @@
/>
<div class="hero-main-content">
<router-link to="/backgrounds/liquid-ether" class="hero-new-badge-container">
<router-link to="/backgrounds/laser-flow" class="hero-new-badge-container">
<span class="hero-new-badge">New 🎉</span>
<div class="hero-new-badge-text">
<span>Liquid Ether</span>
<span>Laser Flow</span>
<i class="pi-arrow-right pi" style="font-size: 0.8rem"></i>
</div>
</router-link>

View File

@@ -1,5 +1,5 @@
// Highlighted sidebar items
export const NEW = ['Dome Gallery', 'Liquid Ether', 'Staggered Menu', 'Pixel Blast', 'Gradual Blur', 'Gradient Blinds', 'Bubble Menu', 'Prism', 'Plasma', 'Electric Border', 'Target Cursor', 'Pill Nav', 'Card Nav', 'Logo Loop', 'Prismatic Burst'];
export const NEW = ['Laser Flow', 'Dome Gallery', 'Liquid Ether', 'Staggered Menu', 'Pixel Blast', 'Gradual Blur', 'Gradient Blinds', 'Bubble Menu', 'Prism', 'Plasma', 'Electric Border', 'Target Cursor', 'Pill Nav', 'Card Nav', 'Logo Loop', 'Prismatic Burst'];
export const UPDATED = [];
// Used for main sidebar navigation
@@ -37,6 +37,7 @@ export const CATEGORIES = [
'Animated Content',
'Fade Content',
'Gradual Blur',
'Laser Flow',
'Noise',
'Splash Cursor',
'Logo Loop',

View File

@@ -23,6 +23,7 @@ const animations = {
'sticker-peel': () => import('../demo/Animations/StickerPeelDemo.vue'),
'electric-border': () => import('../demo/Animations/ElectricBorderDemo.vue'),
'gradual-blur': () => import('../demo/Animations/GradualBlurDemo.vue'),
'laser-flow': () => import('../demo/Animations/LaserFlowDemo.vue'),
};
const textAnimations = {

View File

@@ -3,7 +3,24 @@ import { createCodeObject } from '@/types/code';
export const gradualBlur = createCodeObject(code, 'Animations/GradualBlur', {
installation: `npm install mathjs`,
usage: `
usage: `<template>
<section style="position: relative; height: 500px; overflow: hidden;">
<div style="height: 100%; overflow-y: auto; padding: 6rem 2rem;">
<!-- Content Here - such as an image or text -->
</div>
<GradualBlur
target="parent"
position="bottom"
height="6rem"
:strength="2"
:divCount="5"
curve="bezier"
:exponential="true"
:opacity="1"
/>
</section>
</template>
<script setup lang="ts">
import GradualBlur from "./GradualBlur.vue";

View File

@@ -0,0 +1,99 @@
import code from '@/content/Animations/LaserFlow/LaserFlow.vue?raw';
import { createCodeObject } from '@/types/code';
export const laserFlow = createCodeObject(code, 'Animations/LaserFlow', {
installation: `npm install three`,
usage: `<template>
<div
style="height: 800px; position: relative; overflow: hidden; background-color: #060010;"
@mousemove="handleMouseMove"
@mouseleave="handleMouseLeave"
>
<LaserFlow
:horizontalBeamOffset="0.1"
:verticalBeamOffset="0.0"
color="#FF79C6"
/>
<div
style="
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%);
width: 86%;
height: 60%;
background-color: #060010;
border-radius: 20px;
border: 2px solid #FF79C6;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 2rem;
z-index: 6;
"
>
<!-- Your content here -->
Example Box Content
</div>
<img
ref="revealImgRef"
src="/path/to/image.jpg"
alt="Reveal effect"
style="
position: absolute;
width: 100%;
top: -50%;
z-index: 5;
mix-blend-mode: lighten;
opacity: 0.3;
pointer-events: none;
--mx: -9999px;
--my: -9999px;
-webkit-mask-image: radial-gradient(circle at var(--mx) var(--my),
rgba(255,255,255,1) 0px,
rgba(255,255,255,0.95) 60px,
rgba(255,255,255,0.6) 120px,
rgba(255,255,255,0.25) 180px,
rgba(255,255,255,0) 240px);
mask-image: radial-gradient(circle at var(--mx) var(--my),
rgba(255,255,255,1) 0px,
rgba(255,255,255,0.95) 60px,
rgba(255,255,255,0.6) 120px,
rgba(255,255,255,0.25) 180px,
rgba(255,255,255,0) 240px);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
"
/>
</div>
</template>
<script setup>
import { useTemplateRef } from 'vue'
import LaserFlow from './LaserFlow.vue'
const revealImgRef = useTemplateRef('revealImgRef')
function handleMouseMove(e) {
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const el = revealImgRef.value
if (el) {
el.style.setProperty('--mx', '\${x}px')
el.style.setProperty('--my', '\${y + rect.height * 0.5}px')
}
}
function handleMouseLeave() {
const el = revealImgRef.value
if (el) {
el.style.setProperty('--mx', '-9999px')
el.style.setProperty('--my', '-9999px')
}
}
</script>`
});

View File

@@ -0,0 +1,615 @@
<script setup lang="ts">
import * as THREE from 'three';
import { onBeforeUnmount, onMounted, ref, useTemplateRef, watch, type CSSProperties } from 'vue';
type Props = {
className?: string;
style?: CSSProperties;
wispDensity?: number;
dpr?: number;
mouseSmoothTime?: number;
mouseTiltStrength?: number;
horizontalBeamOffset?: number;
verticalBeamOffset?: number;
flowSpeed?: number;
verticalSizing?: number;
horizontalSizing?: number;
fogIntensity?: number;
fogScale?: number;
wispSpeed?: number;
wispIntensity?: number;
flowStrength?: number;
decay?: number;
falloffStart?: number;
fogFallSpeed?: number;
color?: string;
};
const VERT = `
precision highp float;
attribute vec3 position;
void main(){
gl_Position = vec4(position, 1.0);
}
`;
const FRAG = `
#ifdef GL_ES
#extension GL_OES_standard_derivatives : enable
#endif
precision highp float;
precision mediump int;
uniform float iTime;
uniform vec3 iResolution;
uniform vec4 iMouse;
uniform float uWispDensity;
uniform float uTiltScale;
uniform float uFlowTime;
uniform float uFogTime;
uniform float uBeamXFrac;
uniform float uBeamYFrac;
uniform float uFlowSpeed;
uniform float uVLenFactor;
uniform float uHLenFactor;
uniform float uFogIntensity;
uniform float uFogScale;
uniform float uWSpeed;
uniform float uWIntensity;
uniform float uFlowStrength;
uniform float uDecay;
uniform float uFalloffStart;
uniform float uFogFallSpeed;
uniform vec3 uColor;
uniform float uFade;
// Core beam/flare shaping and dynamics
#define PI 3.14159265359
#define TWO_PI 6.28318530718
#define EPS 1e-6
#define EDGE_SOFT (DT_LOCAL*4.0)
#define DT_LOCAL 0.0038
#define TAP_RADIUS 6
#define R_H 150.0
#define R_V 150.0
#define FLARE_HEIGHT 16.0
#define FLARE_AMOUNT 8.0
#define FLARE_EXP 2.0
#define TOP_FADE_START 0.1
#define TOP_FADE_EXP 1.0
#define FLOW_PERIOD 0.5
#define FLOW_SHARPNESS 1.5
// Wisps (animated micro-streaks) that travel along the beam
#define W_BASE_X 1.5
#define W_LAYER_GAP 0.25
#define W_LANES 10
#define W_SIDE_DECAY 0.5
#define W_HALF 0.01
#define W_AA 0.15
#define W_CELL 20.0
#define W_SEG_MIN 0.01
#define W_SEG_MAX 0.55
#define W_CURVE_AMOUNT 15.0
#define W_CURVE_RANGE (FLARE_HEIGHT - 3.0)
#define W_BOTTOM_EXP 10.0
// Volumetric fog controls
#define FOG_ON 1
#define FOG_CONTRAST 1.2
#define FOG_SPEED_U 0.1
#define FOG_SPEED_V -0.1
#define FOG_OCTAVES 5
#define FOG_BOTTOM_BIAS 0.8
#define FOG_TILT_TO_MOUSE 0.05
#define FOG_TILT_DEADZONE 0.01
#define FOG_TILT_MAX_X 0.35
#define FOG_TILT_SHAPE 1.5
#define FOG_BEAM_MIN 0.0
#define FOG_BEAM_MAX 0.75
#define FOG_MASK_GAMMA 0.5
#define FOG_EXPAND_SHAPE 12.2
#define FOG_EDGE_MIX 0.5
// Horizontal vignette for the fog volume
#define HFOG_EDGE_START 0.20
#define HFOG_EDGE_END 0.98
#define HFOG_EDGE_GAMMA 1.4
#define HFOG_Y_RADIUS 25.0
#define HFOG_Y_SOFT 60.0
// Beam extents and edge masking
#define EDGE_X0 0.22
#define EDGE_X1 0.995
#define EDGE_X_GAMMA 1.25
#define EDGE_LUMA_T0 0.0
#define EDGE_LUMA_T1 2.0
#define DITHER_STRENGTH 1.0
float g(float x){return x<=0.00031308?12.92*x:1.055*pow(x,1.0/2.4)-0.055;}
float bs(vec2 p,vec2 q,float powr){
float d=distance(p,q),f=powr*uFalloffStart,r=(f*f)/(d*d+EPS);
return powr*min(1.0,r);
}
float bsa(vec2 p,vec2 q,float powr,vec2 s){
vec2 d=p-q; float dd=(d.x*d.x)/(s.x*s.x)+(d.y*d.y)/(s.y*s.y),f=powr*uFalloffStart,r=(f*f)/(dd+EPS);
return powr*min(1.0,r);
}
float tri01(float x){float f=fract(x);return 1.0-abs(f*2.0-1.0);}
float tauWf(float t,float tmin,float tmax){float a=smoothstep(tmin,tmin+EDGE_SOFT,t),b=1.0-smoothstep(tmax-EDGE_SOFT,tmax,t);return max(0.0,a*b);}
float h21(vec2 p){p=fract(p*vec2(123.34,456.21));p+=dot(p,p+34.123);return fract(p.x*p.y);}
float vnoise(vec2 p){
vec2 i=floor(p),f=fract(p);
float a=h21(i),b=h21(i+vec2(1,0)),c=h21(i+vec2(0,1)),d=h21(i+vec2(1,1));
vec2 u=f*f*(3.0-2.0*f);
return mix(mix(a,b,u.x),mix(c,d,u.x),u.y);
}
float fbm2(vec2 p){
float v=0.0,amp=0.6; mat2 m=mat2(0.86,0.5,-0.5,0.86);
for(int i=0;i<FOG_OCTAVES;++i){v+=amp*vnoise(p); p=m*p*2.03+17.1; amp*=0.52;}
return v;
}
float rGate(float x,float l){float a=smoothstep(0.0,W_AA,x),b=1.0-smoothstep(l,l+W_AA,x);return max(0.0,a*b);}
float flareY(float y){float t=clamp(1.0-(clamp(y,0.0,FLARE_HEIGHT)/max(FLARE_HEIGHT,EPS)),0.0,1.0);return pow(t,FLARE_EXP);}
float vWisps(vec2 uv,float topF){
float y=uv.y,yf=(y+uFlowTime*uWSpeed)/W_CELL;
float dRaw=clamp(uWispDensity,0.0,2.0),d=dRaw<=0.0?1.0:dRaw;
float lanesF=floor(float(W_LANES)*min(d,1.0)+0.5); // WebGL1-safe
int lanes=int(max(1.0,lanesF));
float sp=min(d,1.0),ep=max(d-1.0,0.0);
float fm=flareY(max(y,0.0)),rm=clamp(1.0-(y/max(W_CURVE_RANGE,EPS)),0.0,1.0),cm=fm*rm;
const float G=0.05; float xS=1.0+(FLARE_AMOUNT*W_CURVE_AMOUNT*G)*cm;
float sPix=clamp(y/R_V,0.0,1.0),bGain=pow(1.0-sPix,W_BOTTOM_EXP),sum=0.0;
for(int s=0;s<2;++s){
float sgn=s==0?-1.0:1.0;
for(int i=0;i<W_LANES;++i){
if(i>=lanes) break;
float off=W_BASE_X+float(i)*W_LAYER_GAP,xc=sgn*(off*xS);
float dx=abs(uv.x-xc),lat=1.0-smoothstep(W_HALF,W_HALF+W_AA,dx),amp=exp(-off*W_SIDE_DECAY);
float seed=h21(vec2(off,sgn*17.0)),yf2=yf+seed*7.0,ci=floor(yf2),fy=fract(yf2);
float seg=mix(W_SEG_MIN,W_SEG_MAX,h21(vec2(ci,off*2.3)));
float spR=h21(vec2(ci,off+sgn*31.0)),seg1=rGate(fy,seg)*step(spR,sp);
if(ep>0.0){float spR2=h21(vec2(ci*3.1+7.0,off*5.3+sgn*13.0)); float f2=fract(fy+0.5); seg1+=rGate(f2,seg*0.9)*step(spR2,ep);}
sum+=amp*lat*seg1;
}
}
float span=smoothstep(-3.0,0.0,y)*(1.0-smoothstep(R_V-6.0,R_V,y));
return uWIntensity*sum*topF*bGain*span;
}
void mainImage(out vec4 fc,in vec2 frag){
vec2 C=iResolution.xy*.5; float invW=1.0/max(C.x,1.0);
float sc=512.0/iResolution.x*.4;
vec2 uv=(frag-C)*sc,off=vec2(uBeamXFrac*iResolution.x*sc,uBeamYFrac*iResolution.y*sc);
vec2 uvc = uv - off;
float a=0.0,b=0.0;
float basePhase=1.5*PI+uDecay*.5; float tauMin=basePhase-uDecay; float tauMax=basePhase;
float cx=clamp(uvc.x/(R_H*uHLenFactor),-1.0,1.0),tH=clamp(TWO_PI-acos(cx),tauMin,tauMax);
for(int k=-TAP_RADIUS;k<=TAP_RADIUS;++k){
float tu=tH+float(k)*DT_LOCAL,wt=tauWf(tu,tauMin,tauMax); if(wt<=0.0) continue;
float spd=max(abs(sin(tu)),0.02),u=clamp((basePhase-tu)/max(uDecay,EPS),0.0,1.0),env=pow(1.0-abs(u*2.0-1.0),0.8);
vec2 p=vec2((R_H*uHLenFactor)*cos(tu),0.0);
a+=wt*bs(uvc,p,env*spd);
}
float yPix=uvc.y,cy=clamp(-yPix/(R_V*uVLenFactor),-1.0,1.0),tV=clamp(TWO_PI-acos(cy),tauMin,tauMax);
for(int k=-TAP_RADIUS;k<=TAP_RADIUS;++k){
float tu=tV+float(k)*DT_LOCAL,wt=tauWf(tu,tauMin,tauMax); if(wt<=0.0) continue;
float yb=(-R_V)*cos(tu),s=clamp(yb/R_V,0.0,1.0),spd=max(abs(sin(tu)),0.02);
float env=pow(1.0-s,0.6)*spd;
float cap=1.0-smoothstep(TOP_FADE_START,1.0,s); cap=pow(cap,TOP_FADE_EXP); env*=cap;
float ph=s/max(FLOW_PERIOD,EPS)+uFlowTime*uFlowSpeed;
float fl=pow(tri01(ph),FLOW_SHARPNESS);
env*=mix(1.0-uFlowStrength,1.0,fl);
float yp=(-R_V*uVLenFactor)*cos(tu),m=pow(smoothstep(FLARE_HEIGHT,0.0,yp),FLARE_EXP),wx=1.0+FLARE_AMOUNT*m;
vec2 sig=vec2(wx,1.0),p=vec2(0.0,yp);
float mask=step(0.0,yp);
b+=wt*bsa(uvc,p,mask*env,sig);
}
float sPix=clamp(yPix/R_V,0.0,1.0),topA=pow(1.0-smoothstep(TOP_FADE_START,1.0,sPix),TOP_FADE_EXP);
float L=a+b*topA;
float w=vWisps(vec2(uvc.x,yPix),topA);
float fog=0.0;
#if FOG_ON
vec2 fuv=uvc*uFogScale;
float mAct=step(1.0,length(iMouse.xy)),nx=((iMouse.x-C.x)*invW)*mAct;
float ax = abs(nx);
float stMag = mix(ax, pow(ax, FOG_TILT_SHAPE), 0.35);
float st = sign(nx) * stMag * uTiltScale;
st = clamp(st, -FOG_TILT_MAX_X, FOG_TILT_MAX_X);
vec2 dir=normalize(vec2(st,1.0));
fuv+=uFogTime*uFogFallSpeed*dir;
vec2 prp=vec2(-dir.y,dir.x);
fuv+=prp*(0.08*sin(dot(uvc,prp)*0.08+uFogTime*0.9));
float n=fbm2(fuv+vec2(fbm2(fuv+vec2(7.3,2.1)),fbm2(fuv+vec2(-3.7,5.9)))*0.6);
n=pow(clamp(n,0.0,1.0),FOG_CONTRAST);
float pixW = 1.0 / max(iResolution.y, 1.0);
#ifdef GL_OES_standard_derivatives
float wL = max(fwidth(L), pixW);
#else
float wL = pixW;
#endif
float m0=pow(smoothstep(FOG_BEAM_MIN - wL, FOG_BEAM_MAX + wL, L),FOG_MASK_GAMMA);
float bm=1.0-pow(1.0-m0,FOG_EXPAND_SHAPE); bm=mix(bm*m0,bm,FOG_EDGE_MIX);
float yP=1.0-smoothstep(HFOG_Y_RADIUS,HFOG_Y_RADIUS+HFOG_Y_SOFT,abs(yPix));
float nxF=abs((frag.x-C.x)*invW),hE=1.0-smoothstep(HFOG_EDGE_START,HFOG_EDGE_END,nxF); hE=pow(clamp(hE,0.0,1.0),HFOG_EDGE_GAMMA);
float hW=mix(1.0,hE,clamp(yP,0.0,1.0));
float bBias=mix(1.0,1.0-sPix,FOG_BOTTOM_BIAS);
float browserFogIntensity = uFogIntensity;
browserFogIntensity *= 1.8;
float radialFade = 1.0 - smoothstep(0.0, 0.7, length(uvc) / 120.0);
float safariFog = n * browserFogIntensity * bBias * bm * hW * radialFade;
fog = safariFog;
#endif
float LF=L+fog;
float dith=(h21(frag)-0.5)*(DITHER_STRENGTH/255.0);
float tone=g(LF+w);
vec3 col=tone*uColor+dith;
float alpha=clamp(g(L+w*0.6)+dith*0.6,0.0,1.0);
float nxE=abs((frag.x-C.x)*invW),xF=pow(clamp(1.0-smoothstep(EDGE_X0,EDGE_X1,nxE),0.0,1.0),EDGE_X_GAMMA);
float scene=LF+max(0.0,w)*0.5,hi=smoothstep(EDGE_LUMA_T0,EDGE_LUMA_T1,scene);
float eM=mix(xF,1.0,hi);
col*=eM; alpha*=eM;
col*=uFade; alpha*=uFade;
fc=vec4(col,alpha);
}
void main(){
vec4 fc;
mainImage(fc, gl_FragCoord.xy);
gl_FragColor = fc;
}
`;
const props = withDefaults(defineProps<Props>(), {
wispDensity: 1,
mouseSmoothTime: 0.0,
mouseTiltStrength: 0.01,
horizontalBeamOffset: 0.1,
verticalBeamOffset: 0.0,
flowSpeed: 0.35,
verticalSizing: 2.0,
horizontalSizing: 0.5,
fogIntensity: 0.45,
fogScale: 0.3,
wispSpeed: 15.0,
wispIntensity: 5.0,
flowStrength: 0.25,
decay: 1.1,
falloffStart: 1.2,
fogFallSpeed: 0.6,
color: '#A0FFBC'
});
const mountRef = useTemplateRef('mountRef');
const rendererRef = ref<THREE.WebGLRenderer | null>(null);
const uniformsRef = ref<any>(null);
const hasFadedRef = ref(false);
const rectRef = ref<DOMRect | null>(null);
const baseDprRef = ref<number>(1);
const currentDprRef = ref<number>(1);
const fpsSamplesRef = ref<number[]>([]);
const lastFpsCheckRef = ref<number>(performance.now());
const emaDtRef = ref<number>(16.7); // ms
const pausedRef = ref<boolean>(false);
const inViewRef = ref<boolean>(true);
const hexToRGB = (hex: string) => {
let c = hex.trim();
if (c[0] === '#') c = c.slice(1);
if (c.length === 3)
c = c
.split('')
.map(x => x + x)
.join('');
const n = parseInt(c, 16) || 0xffffff;
return { r: ((n >> 16) & 255) / 255, g: ((n >> 8) & 255) / 255, b: (n & 255) / 255 };
};
let cleanup: (() => void) | null = null;
const setup = () => {
const mount = mountRef.value!;
const renderer = new THREE.WebGLRenderer({
antialias: false,
alpha: false,
depth: false,
stencil: false,
powerPreference: 'high-performance',
premultipliedAlpha: false,
preserveDrawingBuffer: false,
failIfMajorPerformanceCaveat: false,
logarithmicDepthBuffer: false
});
rendererRef.value = renderer;
baseDprRef.value = Math.min(props.dpr ?? (window.devicePixelRatio || 1), 2);
currentDprRef.value = baseDprRef.value;
renderer.setPixelRatio(currentDprRef.value);
renderer.shadowMap.enabled = false;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setClearColor(0x000000, 1);
const canvas = renderer.domElement;
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
mount.appendChild(canvas);
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array([-1, -1, 0, 3, -1, 0, -1, 3, 0]), 3));
const { r, g, b } = hexToRGB(props.color || '#FFFFFF');
const uniforms = {
iTime: { value: 0 },
iResolution: { value: new THREE.Vector3(1, 1, 1) },
iMouse: { value: new THREE.Vector4(0, 0, 0, 0) },
uWispDensity: { value: props.wispDensity },
uTiltScale: { value: props.mouseTiltStrength },
uFlowTime: { value: 0 },
uFogTime: { value: 0 },
uBeamXFrac: { value: props.horizontalBeamOffset },
uBeamYFrac: { value: props.verticalBeamOffset },
uFlowSpeed: { value: props.flowSpeed },
uVLenFactor: { value: props.verticalSizing },
uHLenFactor: { value: props.horizontalSizing },
uFogIntensity: { value: props.fogIntensity },
uFogScale: { value: props.fogScale },
uWSpeed: { value: props.wispSpeed },
uWIntensity: { value: props.wispIntensity },
uFlowStrength: { value: props.flowStrength },
uDecay: { value: props.decay },
uFalloffStart: { value: props.falloffStart },
uFogFallSpeed: { value: props.fogFallSpeed },
uColor: { value: new THREE.Vector3(r, g, b) },
uFade: { value: hasFadedRef.value ? 1 : 0 }
};
uniformsRef.value = uniforms;
const material = new THREE.RawShaderMaterial({
vertexShader: VERT,
fragmentShader: FRAG,
uniforms,
transparent: false,
depthTest: false,
depthWrite: false,
blending: THREE.NormalBlending
});
const mesh = new THREE.Mesh(geometry, material);
mesh.frustumCulled = false;
scene.add(mesh);
const clock = new THREE.Clock();
let prevTime = 0;
let fade = hasFadedRef.value ? 1 : 0;
const mouseTarget = new THREE.Vector2(0, 0);
const mouseSmooth = new THREE.Vector2(0, 0);
const setSizeNow = () => {
const w = mount.clientWidth || 1;
const h = mount.clientHeight || 1;
const pr = currentDprRef.value;
renderer.setPixelRatio(pr);
renderer.setSize(w, h, false);
uniforms.iResolution.value.set(w * pr, h * pr, pr);
rectRef.value = canvas.getBoundingClientRect();
};
let resizeRaf = 0;
const scheduleResize = () => {
if (resizeRaf) cancelAnimationFrame(resizeRaf);
resizeRaf = requestAnimationFrame(setSizeNow);
};
setSizeNow();
const ro = new ResizeObserver(scheduleResize);
ro.observe(mount);
const io = new IntersectionObserver(
entries => {
inViewRef.value = entries[0]?.isIntersecting ?? true;
},
{ root: null, threshold: 0 }
);
io.observe(mount);
const onVis = () => {
pausedRef.value = document.hidden;
};
document.addEventListener('visibilitychange', onVis, { passive: true });
const updateMouse = (clientX: number, clientY: number) => {
const rect = rectRef.value;
if (!rect) return;
const x = clientX - rect.left;
const y = clientY - rect.top;
const ratio = currentDprRef.value;
const hb = rect.height * ratio;
mouseTarget.set(x * ratio, hb - y * ratio);
};
const onMove = (ev: PointerEvent | MouseEvent) => updateMouse(ev.clientX, ev.clientY);
const onLeave = () => mouseTarget.set(0, 0);
canvas.addEventListener('pointermove', onMove as any, { passive: true });
canvas.addEventListener('pointerdown', onMove as any, { passive: true });
canvas.addEventListener('pointerenter', onMove as any, { passive: true });
canvas.addEventListener('pointerleave', onLeave as any, { passive: true });
const onCtxLost = (e: Event) => {
e.preventDefault();
pausedRef.value = true;
};
const onCtxRestored = () => {
pausedRef.value = false;
scheduleResize();
};
canvas.addEventListener('webglcontextlost', onCtxLost, false);
canvas.addEventListener('webglcontextrestored', onCtxRestored, false);
let raf = 0;
const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));
const dprFloor = 0.6;
const lowerThresh = 50;
const upperThresh = 58;
const adjustDprIfNeeded = (now: number) => {
const elapsed = now - lastFpsCheckRef.value;
if (elapsed < 750) return;
const samples = fpsSamplesRef.value;
if (samples.length === 0) {
lastFpsCheckRef.value = now;
return;
}
const avgFps = samples.reduce((a, b) => a + b, 0) / samples.length;
let next = currentDprRef.value;
const base = baseDprRef.value;
if (avgFps < lowerThresh) {
next = clamp(currentDprRef.value * 0.9, dprFloor, base);
} else if (avgFps > upperThresh && currentDprRef.value < base) {
next = clamp(currentDprRef.value * 1.05, dprFloor, base);
}
if (Math.abs(next - currentDprRef.value) > 0.01) {
currentDprRef.value = next;
setSizeNow();
}
fpsSamplesRef.value = [];
lastFpsCheckRef.value = now;
};
const animate = () => {
raf = requestAnimationFrame(animate);
if (pausedRef.value || !inViewRef.value) return;
const t = clock.getElapsedTime();
const dt = Math.max(0, t - prevTime);
prevTime = t;
const dtMs = dt * 1000;
emaDtRef.value = emaDtRef.value * 0.9 + dtMs * 0.1;
const instFps = 1000 / Math.max(1, emaDtRef.value);
fpsSamplesRef.value.push(instFps);
uniforms.iTime.value = t;
const cdt = Math.min(0.033, Math.max(0.001, dt));
(uniforms.uFlowTime.value as number) += cdt;
(uniforms.uFogTime.value as number) += cdt;
if (!hasFadedRef.value) {
const fadeDur = 1.0;
fade = Math.min(1, fade + cdt / fadeDur);
uniforms.uFade.value = fade;
if (fade >= 1) hasFadedRef.value = true;
}
const tau = Math.max(1e-3, props.mouseSmoothTime);
const alpha = 1 - Math.exp(-cdt / tau);
mouseSmooth.lerp(mouseTarget, alpha);
uniforms.iMouse.value.set(mouseSmooth.x, mouseSmooth.y, 0, 0);
renderer.render(scene, camera);
adjustDprIfNeeded(performance.now());
};
animate();
cleanup = () => {
cancelAnimationFrame(raf);
ro.disconnect();
io.disconnect();
document.removeEventListener('visibilitychange', onVis);
canvas.removeEventListener('pointermove', onMove as any);
canvas.removeEventListener('pointerdown', onMove as any);
canvas.removeEventListener('pointerenter', onMove as any);
canvas.removeEventListener('pointerleave', onLeave as any);
canvas.removeEventListener('webglcontextlost', onCtxLost);
canvas.removeEventListener('webglcontextrestored', onCtxRestored);
geometry.dispose();
material.dispose();
renderer.dispose();
if (mount.contains(canvas)) mount.removeChild(canvas);
};
};
onMounted(() => {
setup();
});
onBeforeUnmount(() => {
cleanup?.();
});
watch(
() => [props.dpr],
() => {
cleanup?.();
setup();
},
{ deep: true }
);
watch(
() => [
props.wispDensity,
props.mouseTiltStrength,
props.horizontalBeamOffset,
props.verticalBeamOffset,
props.flowSpeed,
props.verticalSizing,
props.horizontalSizing,
props.fogIntensity,
props.fogScale,
props.wispSpeed,
props.wispIntensity,
props.flowStrength,
props.decay,
props.falloffStart,
props.fogFallSpeed,
props.color
],
() => {
const uniforms = uniformsRef.value;
if (!uniforms) return;
uniforms.uWispDensity.value = props.wispDensity;
uniforms.uTiltScale.value = props.mouseTiltStrength;
uniforms.uBeamXFrac.value = props.horizontalBeamOffset;
uniforms.uBeamYFrac.value = props.verticalBeamOffset;
uniforms.uFlowSpeed.value = props.flowSpeed;
uniforms.uVLenFactor.value = props.verticalSizing;
uniforms.uHLenFactor.value = props.horizontalSizing;
uniforms.uFogIntensity.value = props.fogIntensity;
uniforms.uFogScale.value = props.fogScale;
uniforms.uWSpeed.value = props.wispSpeed;
uniforms.uWIntensity.value = props.wispIntensity;
uniforms.uFlowStrength.value = props.flowStrength;
uniforms.uDecay.value = props.decay;
uniforms.uFalloffStart.value = props.falloffStart;
uniforms.uFogFallSpeed.value = props.fogFallSpeed;
const { r, g, b } = hexToRGB(props.color || '#FFFFFF');
console.log(props.color);
uniforms.uColor.value.set(r, g, b);
},
{ deep: true }
);
</script>
<template>
<div ref="mountRef" :class="['w-full h-full relative', className]" :style="style" />
</template>

View File

@@ -31,26 +31,6 @@ const focusRect = ref({ x: 0, y: 0, width: 0, height: 0 });
let interval: number | null = null;
watch(
[() => props.manualMode, () => props.animationDuration, () => props.pauseBetweenAnimations, () => words.value],
() => {
if (interval) {
clearInterval(interval);
interval = null;
}
if (!props.manualMode) {
interval = setInterval(
() => {
currentIndex.value = (currentIndex.value + 1) % words.value.length;
},
(props.animationDuration + props.pauseBetweenAnimations) * 1000
);
}
},
{ immediate: true }
);
watch(
[currentIndex, () => words.value.length],
async () => {
@@ -105,6 +85,26 @@ onMounted(async () => {
height: activeRect.height
};
}
watch(
[() => props.manualMode, () => props.animationDuration, () => props.pauseBetweenAnimations, () => words.value],
() => {
if (interval) {
clearInterval(interval);
interval = null;
}
if (!props.manualMode) {
interval = setInterval(
() => {
currentIndex.value = (currentIndex.value + 1) % words.value.length;
},
(props.animationDuration + props.pauseBetweenAnimations) * 1000
);
}
},
{ immediate: true }
);
});
onUnmounted(() => {

View File

@@ -0,0 +1,213 @@
<template>
<TabbedLayout>
<template #preview>
<div
class="relative w-full h-[600px] overflow-hidden demo-container"
@mousemove="handleMouseMove"
@mouseleave="handleMouseLeave"
ref="containerRef"
>
<RefreshButton @click="forceRerender" />
<LaserFlow
:horizontal-beam-offset="selectedExample === 'box' ? 0.1 : 0.0"
:vertical-beam-offset="selectedExample === 'box' ? -0.2 : -0.5"
:horizontal-sizing="horizontalSizing"
:vertical-sizing="verticalSizing"
:wisp-density="wispDensity"
:wisp-speed="wispSpeed"
:wisp-intensity="wispIntensity"
:flow-speed="flowSpeed"
:flow-strength="flowStrength"
:fog-intensity="fogIntensity"
:fog-scale="fogScale"
:fog-fall-speed="fogFallSpeed"
:decay="decay"
:dpr="1"
:falloff-start="falloffStart"
:color="laserColor"
:key="key"
:class-name="`laser-flow-demo-${selectedExample}`"
/>
<template v-if="selectedExample === 'box'">
<div
class="top-[70%] left-1/2 z-[6] absolute flex justify-center items-center bg-black border-2 rounded-[20px] w-[86%] h-[60%] text-white text-2xl -translate-x-1/2"
:style="{
borderColor: laserColor,
backgroundImage: 'radial-gradient(circle, #165f2b 1px, transparent 1px)',
backgroundSize: '20px 20px'
}"
></div>
<img
ref="revealImgRef"
src="https://cdn.dribbble.com/userupload/15325964/file/original-25ae735b5d9255a4a31d3471fd1c346a.png?resize=1024x768&vertical=center"
class="-top-1/2 z-2 absolute opacity-30 w-full pointer-events-none mix-blend-lighten"
:style="{
['--mx']: '-9999px',
['--my']: '-9999px',
WebkitMaskImage:
'radial-gradient(circle at var(--mx) var(--my), rgba(255,255,255,1) 0px, rgba(255,255,255,0.95) 60px, rgba(255,255,255,0.6) 120px, rgba(255,255,255,0.25) 180px, rgba(255,255,255,0) 240px)',
maskImage:
'radial-gradient(circle at var(--mx) var(--my), rgba(255,255,255,1) 0px, rgba(255,255,255,0.95) 60px, rgba(255,255,255,0.6) 120px, rgba(255,255,255,0.25) 180px, rgba(255,255,255,0) 240px)',
WebkitMaskRepeat: 'no-repeat',
maskRepeat: 'no-repeat'
}"
/>
</template>
</div>
<Customize>
<PreviewSelect title="Example:" v-model="selectedExample" :options="exampleOptions" />
<PreviewColor title="Color" v-model="laserColor" />
<PreviewSlider title="Horizontal Sizing" :min="0.1" :max="2" :step="0.01" v-model="horizontalSizing" />
<PreviewSlider title="Vertical Sizing" :min="0.1" :max="5" :step="0.1" v-model="verticalSizing" />
<PreviewSlider title="Wisp Density" :min="0" :max="5" :step="0.1" v-model="wispDensity" />
<PreviewSlider title="Wisp Speed" :min="1" :max="50" :step="0.5" v-model="wispSpeed" />
<PreviewSlider title="Wisp Intensity" :min="0" :max="20" :step="0.1" v-model="wispIntensity" />
<PreviewSlider title="Flow Speed" :min="0" :max="2" :step="0.02" v-model="flowSpeed" />
<PreviewSlider title="Flow Strength" :min="0" :max="1" :step="0.01" v-model="flowStrength" />
<PreviewSlider title="Fog Intensity" :min="0" :max="1" :step="0.01" v-model="fogIntensity" />
<PreviewSlider title="Fog Scale" :min="0.1" :max="1" :step="0.01" v-model="fogScale" />
<PreviewSlider title="Fog Fall Speed" :min="0" :max="2" :step="0.01" v-model="fogFallSpeed" />
<PreviewSlider title="Decay" :min="0.5" :max="3" :step="0.01" v-model="decay" />
<PreviewSlider title="Falloff Start" :min="0.5" :max="3" :step="0.01" v-model="falloffStart" />
</Customize>
<PropTable :data="propData" />
<Dependencies :dependency-list="['three']" />
</template>
<template #code>
<CodeExample :code-object="laserFlow" />
</template>
<template #cli>
<CliInstallation :command="laserFlow.cli" />
</template>
</TabbedLayout>
</template>
<script setup lang="ts">
import { useForceRerender } from '@/composables/useForceRerender';
import { laserFlow } from '@/constants/code/Animations/laserFlowCode';
import { ref, useTemplateRef } from 'vue';
import CliInstallation from '../../components/code/CliInstallation.vue';
import CodeExample from '../../components/code/CodeExample.vue';
import Dependencies from '../../components/code/Dependencies.vue';
import Customize from '../../components/common/Customize.vue';
import PreviewColor from '../../components/common/PreviewColor.vue';
import PreviewSelect from '../../components/common/PreviewSelect.vue';
import PreviewSlider from '../../components/common/PreviewSlider.vue';
import PropTable from '../../components/common/PropTable.vue';
import RefreshButton from '../../components/common/RefreshButton.vue';
import TabbedLayout from '../../components/common/TabbedLayout.vue';
import LaserFlow from '../../content/Animations/LaserFlow/LaserFlow.vue';
const { rerenderKey: key, forceRerender } = useForceRerender();
type ExampleKey = 'box' | 'basic';
const exampleOptions = [
{ label: 'Box', value: 'box' },
{ label: 'Basic', value: 'basic' }
];
const containerRef = useTemplateRef('containerRef');
const revealImgRef = useTemplateRef('revealImgRef');
const selectedExample = ref<ExampleKey>('box');
const laserColor = ref('#A0FFBC');
const horizontalSizing = ref(0.5);
const verticalSizing = ref(2.0);
const wispDensity = ref(1);
const wispSpeed = ref(15.0);
const wispIntensity = ref(5.0);
const flowSpeed = ref(0.35);
const flowStrength = ref(0.25);
const fogIntensity = ref(0.45);
const fogScale = ref(0.3);
const fogFallSpeed = ref(0.6);
const decay = ref(1.1);
const falloffStart = ref(1.2);
function handleMouseMove(e: MouseEvent) {
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const el = revealImgRef.value;
if (el) {
el.style.setProperty('--mx', `${x}px`);
el.style.setProperty('--my', `${y + rect.height * 0.5}px`);
}
}
function handleMouseLeave() {
const el = revealImgRef.value;
if (el) {
el.style.setProperty('--mx', `-9999px`);
el.style.setProperty('--my', `-9999px`);
}
}
const propData = [
{
name: 'horizontalBeamOffset',
type: 'number',
default: '0.1',
description: 'Horizontal offset of the beam (01 of canvas width).'
},
{
name: 'verticalBeamOffset',
type: 'number',
default: '0.0',
description: 'Vertical offset of the beam (01 of canvas height).'
},
{
name: 'horizontalSizing',
type: 'number',
default: '0.5',
description: 'Horizontal sizing factor of the beam footprint.'
},
{
name: 'verticalSizing',
type: 'number',
default: '2.0',
description: 'Vertical sizing factor of the beam footprint.'
},
{ name: 'wispDensity', type: 'number', default: '1', description: 'Density of micro-streak wisps.' },
{ name: 'wispSpeed', type: 'number', default: '15.0', description: 'Speed of wisp motion.' },
{ name: 'wispIntensity', type: 'number', default: '5.0', description: 'Brightness of wisps.' },
{ name: 'flowSpeed', type: 'number', default: '0.35', description: 'Speed of the beams flow modulation.' },
{ name: 'flowStrength', type: 'number', default: '0.25', description: 'Strength of the beams flow modulation.' },
{ name: 'fogIntensity', type: 'number', default: '0.45', description: 'Overall volumetric fog intensity.' },
{ name: 'fogScale', type: 'number', default: '0.3', description: 'Spatial scale for the fog noise.' },
{ name: 'fogFallSpeed', type: 'number', default: '0.6', description: 'Drift speed for the fog field.' },
{
name: 'mouseTiltStrength',
type: 'number',
default: '0.01',
description: 'How much mouse x tilts the fog volume.'
},
{ name: 'mouseSmoothTime', type: 'number', default: '0.0', description: 'Pointer smoothing time (seconds).' },
{ name: 'decay', type: 'number', default: '1.1', description: 'Beam decay shaping for sampling envelope.' },
{
name: 'falloffStart',
type: 'number',
default: '1.2',
description: 'Falloff start radius used in inverse-square blending.'
},
{
name: 'dpr',
type: 'number',
default: 'auto',
description: 'Device pixel ratio override (defaults to window.devicePixelRatio).'
},
{ name: 'color', type: 'string', default: '#A0FFBC', description: 'Beam color (hex).' }
];
</script>
<style scoped>
.demo-container {
padding: 0;
}
</style>