634 lines
20 KiB
JavaScript
634 lines
20 KiB
JavaScript
import {
|
|
Scene,
|
|
OrthographicCamera,
|
|
WebGLRenderer,
|
|
PlaneGeometry,
|
|
Mesh,
|
|
ShaderMaterial,
|
|
Vector3,
|
|
Vector2,
|
|
} from 'three'
|
|
|
|
const vertexShader = `
|
|
precision highp float;
|
|
|
|
void main() {
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`
|
|
|
|
const fragmentShader = `
|
|
precision mediump float;
|
|
|
|
uniform float iTime;
|
|
uniform vec3 iResolution;
|
|
uniform float animationSpeed;
|
|
|
|
uniform bool enableTop;
|
|
uniform bool enableMiddle;
|
|
uniform bool enableBottom;
|
|
|
|
uniform int topLineCount;
|
|
uniform int middleLineCount;
|
|
uniform int bottomLineCount;
|
|
|
|
uniform float topLineDistance;
|
|
uniform float bottomLineDistance;
|
|
|
|
uniform vec3 topWavePosition;
|
|
uniform vec3 bottomWavePosition;
|
|
|
|
uniform int numPoints;
|
|
uniform float pointSpacingX;
|
|
uniform float pointsOffsetX;
|
|
uniform float pointY[8];
|
|
uniform float lineSpread;
|
|
uniform float fanSpread;
|
|
uniform float lineSharpness;
|
|
uniform float waveFrequency;
|
|
uniform float bezierCurvature;
|
|
uniform float circleRadiusPx;
|
|
uniform float circleGlowSize;
|
|
uniform float circleGlowStrength;
|
|
|
|
uniform vec2 iMouse;
|
|
uniform bool interactive;
|
|
uniform float bendRadius;
|
|
uniform float bendStrength;
|
|
uniform float bendInfluence;
|
|
|
|
uniform int horizonMode; // 0=off 1=fog 2=split 3=glow
|
|
uniform float horizonOpacity; // Nebel + Glow: Helligkeit/Dichte
|
|
uniform float horizonBlend; // Trennung: 0=scharf, 1=weicher Übergang
|
|
|
|
uniform bool parallax;
|
|
uniform float parallaxStrength;
|
|
uniform vec2 parallaxOffset;
|
|
|
|
uniform float lineBrightness;
|
|
uniform vec3 lineGradient[8];
|
|
uniform int lineGradientCount;
|
|
uniform vec3 bgColorCenter;
|
|
uniform vec3 bgColorEdge;
|
|
|
|
mat2 rotate(float r) {
|
|
return mat2(cos(r), sin(r), -sin(r), cos(r));
|
|
}
|
|
|
|
vec3 getLineColor(float t, vec3 baseColor) {
|
|
if (lineGradientCount <= 0) {
|
|
return baseColor;
|
|
}
|
|
|
|
vec3 gradientColor;
|
|
|
|
if (lineGradientCount == 1) {
|
|
gradientColor = lineGradient[0];
|
|
} else {
|
|
float clampedT = clamp(t, 0.0, 0.9999);
|
|
float scaled = clampedT * float(lineGradientCount - 1);
|
|
int idx = int(floor(scaled));
|
|
float f = fract(scaled);
|
|
int idx2 = min(idx + 1, lineGradientCount - 1);
|
|
|
|
vec3 c1 = lineGradient[idx];
|
|
vec3 c2 = lineGradient[idx2];
|
|
|
|
gradientColor = mix(c1, c2, f);
|
|
}
|
|
|
|
return gradientColor;
|
|
}
|
|
|
|
vec3 drawCircle(vec2 uv, vec2 center, float r, vec3 color) {
|
|
float d = length(uv - center);
|
|
|
|
// Glow: Größe und Stärke per Uniform steuerbar
|
|
float glowW = circleGlowSize / iResolution.y * 2.0;
|
|
float glow = exp(-pow(max(d - r, 0.0) / glowW, 2.0)) * circleGlowStrength;
|
|
float fog = 0.008 / max(d * d * 3.0 + 0.016, 0.001);
|
|
|
|
// Weißer Kreis: harte Kante, 1px Antialiasing
|
|
float aa = 1.5 / iResolution.y;
|
|
float core = 1.0 - smoothstep(r - aa, r + aa, d);
|
|
|
|
// Glow nur außerhalb des weißen Kreises
|
|
vec3 result = color * (glow + fog) * (1.0 - core);
|
|
result += vec3(core);
|
|
return result;
|
|
}
|
|
|
|
// Nächsten t-Parameter auf quadratischer Bézier (Newton + Coarse-Search)
|
|
float bezierClosestT(vec2 q, vec2 p0, vec2 pc, vec2 p1) {
|
|
// Grobe Suche über 8 Samples für guten Startwert
|
|
float bestT = 0.0;
|
|
float bestD = 1e9;
|
|
for (int k = 0; k <= 8; ++k) {
|
|
float t = float(k) / 8.0;
|
|
float mt = 1.0 - t;
|
|
vec2 b = mt*mt*p0 + 2.0*mt*t*pc + t*t*p1;
|
|
float d = dot(q - b, q - b);
|
|
if (d < bestD) { bestD = d; bestT = t; }
|
|
}
|
|
|
|
// Newton-Verfahren: minimiert |B(t)-q|²
|
|
// f(t) = a·t³ + b·t² + c·t + d, f'(t) = 3a·t² + 2b·t + c
|
|
vec2 A = pc - p0;
|
|
vec2 B = p0 - 2.0*pc + p1;
|
|
vec2 D = p0 - q;
|
|
float a = 2.0*dot(B,B);
|
|
float bco = 6.0*dot(A,B);
|
|
float c = 4.0*dot(A,A) + 2.0*dot(D,B);
|
|
float dco = 2.0*dot(D,A);
|
|
|
|
// Etwas breiterer Bereich erlaubt leichten Überlauf in benachbarte Segmente
|
|
float t = clamp(bestT, 0.001, 0.999);
|
|
for (int k = 0; k < 4; ++k) {
|
|
float f = a*t*t*t + bco*t*t + c*t + dco;
|
|
float fp = 3.0*a*t*t + 2.0*bco*t + c;
|
|
if (abs(fp) > 1e-8) t -= f / fp;
|
|
t = clamp(t, -0.08, 1.08);
|
|
}
|
|
return t;
|
|
}
|
|
|
|
// Accepts precomputed bezier values (bt, bPos, bNorm) — computed once per segment
|
|
float waveFocal(vec2 uv, float fi, float totalLines, float t, vec2 bPos, vec2 bNorm) {
|
|
float s = dot(uv - bPos, bNorm);
|
|
|
|
float time = iTime * animationSpeed;
|
|
float normalizedI = totalLines > 1.0 ? fi / (totalLines - 1.0) : 0.5;
|
|
|
|
float envelope = sin(t * 3.14159265359);
|
|
float linePos = (normalizedI - 0.5) * fanSpread * envelope;
|
|
float amp = lineSpread * 0.3 * envelope;
|
|
float waveDisp = sin(t * waveFrequency + fi * 1.3 + time * 0.4) * amp
|
|
* sin(fi * 0.9 + time * 0.18);
|
|
|
|
float dist = s - linePos - waveDisp;
|
|
float fade = smoothstep(-0.06, 0.04, t) * smoothstep(1.06, 0.96, t);
|
|
|
|
return fade * (0.013 / max(abs(dist) * lineSharpness + 0.004, 1e-4) + 0.003);
|
|
}
|
|
|
|
float wave(vec2 uv, float offset, vec2 screenUv, vec2 mouseUv, bool shouldBend) {
|
|
float time = iTime * animationSpeed;
|
|
|
|
float x_offset = offset;
|
|
float x_movement = time * 0.1;
|
|
float amp = sin(offset + time * 0.2) * 0.3;
|
|
float y = sin(uv.x + x_offset + x_movement) * amp;
|
|
|
|
if (shouldBend) {
|
|
vec2 d = screenUv - mouseUv;
|
|
float influence = exp(-dot(d, d) * bendRadius);
|
|
float bendOffset = (mouseUv.y - screenUv.y) * influence * bendStrength * bendInfluence;
|
|
y += bendOffset;
|
|
}
|
|
|
|
float m = uv.y - y;
|
|
return 0.0175 / max(abs(m) + 0.01, 1e-3) + 0.01;
|
|
}
|
|
|
|
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
|
|
baseUv.y *= -1.0;
|
|
|
|
if (parallax) {
|
|
baseUv += parallaxOffset;
|
|
}
|
|
|
|
vec3 col = vec3(0.0);
|
|
|
|
vec3 b = bgColorCenter;
|
|
|
|
vec2 mouseUv = vec2(0.0);
|
|
if (interactive) {
|
|
mouseUv = (2.0 * iMouse - iResolution.xy) / iResolution.y;
|
|
mouseUv.y *= -1.0;
|
|
}
|
|
|
|
if (enableBottom) {
|
|
for (int i = 0; i < bottomLineCount; ++i) {
|
|
float fi = float(i);
|
|
float t = fi / max(float(bottomLineCount - 1), 1.0);
|
|
vec3 lineCol = getLineColor(t, b);
|
|
|
|
float angle = bottomWavePosition.z * log(length(baseUv) + 1.0);
|
|
vec2 ruv = baseUv * rotate(angle);
|
|
col += lineCol * wave(
|
|
ruv + vec2(bottomLineDistance * fi + bottomWavePosition.x, bottomWavePosition.y),
|
|
1.5 + 0.2 * fi,
|
|
baseUv,
|
|
mouseUv,
|
|
interactive
|
|
) * 0.2;
|
|
}
|
|
}
|
|
|
|
if (enableMiddle) {
|
|
const int MAX_PTS = 8;
|
|
const int MAX_SEGS = 7;
|
|
float r = circleRadiusPx / iResolution.y * 2.0;
|
|
float tScale = numPoints > 1 ? 1.0 / float(numPoints - 1) : 1.0;
|
|
|
|
// Segmente: Punkt s → Punkt s+1
|
|
for (int s = 0; s < MAX_SEGS; ++s) {
|
|
if (s >= numPoints - 1) break;
|
|
|
|
float x0 = pointsOffsetX + (float(s) - float(numPoints - 1) * 0.5) * pointSpacingX;
|
|
float x1 = pointsOffsetX + (float(s + 1) - float(numPoints - 1) * 0.5) * pointSpacingX;
|
|
|
|
vec2 sp = vec2(x0, pointY[s]);
|
|
vec2 ep = vec2(x1, pointY[s + 1]);
|
|
|
|
// Segment-Geometrie (einmalig berechnet, von Gradient + Bézier genutzt)
|
|
vec2 seg = ep - sp;
|
|
float segL = length(seg);
|
|
vec2 segDir = segL > 0.001 ? seg / segL : vec2(1.0, 0.0);
|
|
vec2 sPerp = vec2(-segDir.y, segDir.x);
|
|
vec2 pc = (sp + ep) * 0.5 + sPerp * segL * bezierCurvature;
|
|
|
|
// Gradient
|
|
float t_seg = clamp(dot(baseUv - sp, segDir) / segL, 0.0, 1.0);
|
|
float t_global = (float(s) + t_seg) * tScale;
|
|
vec3 lineCol = getLineColor(t_global, b);
|
|
|
|
// Bézier einmal pro Segment — geteilt von Nebel + allen Linien
|
|
float bt = bezierClosestT(baseUv, sp, pc, ep);
|
|
float bmt = 1.0 - bt;
|
|
vec2 bPos = bmt*bmt*sp + 2.0*bmt*bt*pc + bt*bt*ep;
|
|
vec2 bTang = normalize(2.0*bmt*(pc - sp) + 2.0*bt*(ep - pc));
|
|
vec2 bNorm = vec2(-bTang.y, bTang.x);
|
|
|
|
// Weicher Nebel entlang der Kurve
|
|
float bDist = length(baseUv - bPos);
|
|
float fogFade = smoothstep(-0.06, 0.05, bt) * smoothstep(1.06, 0.95, bt);
|
|
float fogEnv = sin(bt * 3.14159265359);
|
|
float segFog = fogFade * fogEnv * 0.0018 / max(bDist * bDist * 4.0 + 0.012, 0.001);
|
|
col += lineCol * segFog;
|
|
|
|
for (int i = 0; i < middleLineCount; ++i) {
|
|
col += lineCol * waveFocal(baseUv, float(i), float(middleLineCount), bt, bPos, bNorm);
|
|
}
|
|
}
|
|
|
|
// Kreise an jedem Punkt
|
|
for (int p = 0; p < MAX_PTS; ++p) {
|
|
if (p >= numPoints) break;
|
|
float px = pointsOffsetX + (float(p) - float(numPoints - 1) * 0.5) * pointSpacingX;
|
|
float t_pt = numPoints > 1 ? float(p) * tScale : 0.0;
|
|
vec3 circCol = getLineColor(t_pt, b) * 1.5;
|
|
col += drawCircle(baseUv, vec2(px, pointY[p]), r, circCol);
|
|
}
|
|
}
|
|
|
|
if (enableTop) {
|
|
for (int i = 0; i < topLineCount; ++i) {
|
|
float fi = float(i);
|
|
float t = fi / max(float(topLineCount - 1), 1.0);
|
|
vec3 lineCol = getLineColor(t, b);
|
|
|
|
float angle = topWavePosition.z * log(length(baseUv) + 1.0);
|
|
vec2 ruv = baseUv * rotate(angle);
|
|
ruv.x *= -1.0;
|
|
col += lineCol * wave(
|
|
ruv + vec2(topLineDistance * fi + topWavePosition.x, topWavePosition.y),
|
|
1.0 + 0.2 * fi,
|
|
baseUv,
|
|
mouseUv,
|
|
interactive
|
|
) * 0.1;
|
|
}
|
|
}
|
|
|
|
col *= lineBrightness;
|
|
|
|
// Hintergrundverlauf: radial von bgColorCenter (Mitte) nach bgColorEdge (Rand)
|
|
float dist = length(baseUv) / 1.8;
|
|
vec3 bg = mix(bgColorCenter, bgColorEdge, clamp(dist, 0.0, 1.0));
|
|
|
|
if (horizonMode == 1) {
|
|
// Nebel: breites weiches Gaussband in Gradient-Mittelfarbe
|
|
float band = exp(-baseUv.y * baseUv.y * 5.0);
|
|
vec3 fogColor = getLineColor(0.5, bg) * 2.0;
|
|
col += fogColor * band * horizonOpacity;
|
|
} else if (horizonMode == 2) {
|
|
// Farbtrennung: vertikaler Split an Y=0
|
|
// bgColorCenter → oben (positives UV-Y), bgColorEdge → unten
|
|
// horizonBlend: 0=harter Schnitt, 1=sehr weicher Übergang
|
|
float blendW = max(horizonBlend * 0.7, 0.001);
|
|
float t = smoothstep(-blendW, blendW, baseUv.y);
|
|
bg = mix(bgColorEdge, bgColorCenter, t);
|
|
} else if (horizonMode == 3) {
|
|
// Glow: konzentriertes Leuchten in Gradient-Mittelfarbe
|
|
float d2 = baseUv.y * baseUv.y;
|
|
float softGlow = exp(-d2 * 10.0);
|
|
float coreGlow = exp(-d2 * 70.0) * 0.7;
|
|
vec3 glowColor = getLineColor(0.5, bg) * 3.0;
|
|
col += glowColor * (softGlow + coreGlow) * horizonOpacity;
|
|
}
|
|
|
|
fragColor = vec4(clamp(bg + col, 0.0, 1.0), 1.0);
|
|
}
|
|
|
|
void main() {
|
|
vec4 color = vec4(0.0);
|
|
mainImage(color, gl_FragCoord.xy);
|
|
gl_FragColor = color;
|
|
}
|
|
`
|
|
|
|
const MAX_GRADIENT_STOPS = 8
|
|
|
|
function hexToVec3(hex) {
|
|
let value = hex.trim()
|
|
|
|
if (value.startsWith('#')) {
|
|
value = value.slice(1)
|
|
}
|
|
|
|
let r = 255
|
|
let g = 255
|
|
let b = 255
|
|
|
|
if (value.length === 3) {
|
|
r = parseInt(value[0] + value[0], 16)
|
|
g = parseInt(value[1] + value[1], 16)
|
|
b = parseInt(value[2] + value[2], 16)
|
|
} else if (value.length === 6) {
|
|
r = parseInt(value.slice(0, 2), 16)
|
|
g = parseInt(value.slice(2, 4), 16)
|
|
b = parseInt(value.slice(4, 6), 16)
|
|
}
|
|
|
|
return new Vector3(r / 255, g / 255, b / 255)
|
|
}
|
|
|
|
export default class FloatingLines {
|
|
constructor(
|
|
container,
|
|
{
|
|
linesGradient,
|
|
enabledWaves = ['top', 'middle', 'bottom'],
|
|
lineCount = [6],
|
|
lineDistance = [5],
|
|
topWavePosition,
|
|
bottomWavePosition = { x: 2.0, y: -0.7, rotate: -1 },
|
|
numPoints = 4,
|
|
pointSpacingX = 0.8,
|
|
pointsOffsetX = 0.0,
|
|
pointYValues = [0.75, -0.5, 0.3, -0.75, 0.6, -0.4, 0.5, -0.6],
|
|
lineSpread = 0.6,
|
|
fanSpread = 0.25,
|
|
lineSharpness = 1.0,
|
|
waveFrequency = 8.0,
|
|
bezierCurvature = 0.3,
|
|
circleRadiusPx = 50,
|
|
animationSpeed = 1,
|
|
lineBrightness = 1.0,
|
|
interactive = true,
|
|
bendRadius = 5.0,
|
|
bendStrength = -0.5,
|
|
mouseDamping = 0.05,
|
|
parallax = true,
|
|
parallaxStrength = 0.2,
|
|
circleGlowSize = 18.0,
|
|
circleGlowStrength = 1.5,
|
|
horizonMode = 'off',
|
|
horizonOpacity = 0.5,
|
|
horizonBlend = 0.2,
|
|
bgColorCenter = '#0a0514',
|
|
bgColorEdge = '#000000',
|
|
mixBlendMode = 'screen',
|
|
} = {},
|
|
) {
|
|
this.container = container
|
|
this.interactive = interactive
|
|
this.parallax = parallax
|
|
this.mouseDamping = mouseDamping
|
|
this.parallaxStrength = parallaxStrength
|
|
|
|
this.targetMouse = new Vector2(-1000, -1000)
|
|
this.currentMouse = new Vector2(-1000, -1000)
|
|
this.targetInfluence = 0
|
|
this.currentInfluence = 0
|
|
this.targetParallax = new Vector2(0, 0)
|
|
this.currentParallax = new Vector2(0, 0)
|
|
|
|
const getLineCount = (waveType) => {
|
|
if (typeof lineCount === 'number') return lineCount
|
|
if (!enabledWaves.includes(waveType)) return 0
|
|
const index = enabledWaves.indexOf(waveType)
|
|
return lineCount[index] ?? 6
|
|
}
|
|
|
|
const getLineDistance = (waveType) => {
|
|
if (typeof lineDistance === 'number') return lineDistance
|
|
if (!enabledWaves.includes(waveType)) return 0.1
|
|
const index = enabledWaves.indexOf(waveType)
|
|
return lineDistance[index] ?? 0.1
|
|
}
|
|
|
|
const topLineCount = getLineCount('top')
|
|
const middleLineCount = getLineCount('middle')
|
|
const bottomLineCount = getLineCount('bottom')
|
|
|
|
const topLineDistance = getLineDistance('top') * 0.01
|
|
const bottomLineDistance = getLineDistance('bottom') * 0.01
|
|
|
|
this.scene = new Scene()
|
|
this.camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1)
|
|
this.camera.position.z = 1
|
|
|
|
this.renderer = new WebGLRenderer({ antialias: true, alpha: false })
|
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
|
|
this.renderer.domElement.style.width = '100%'
|
|
this.renderer.domElement.style.height = '100%'
|
|
this.renderer.domElement.style.mixBlendMode = mixBlendMode
|
|
container.appendChild(this.renderer.domElement)
|
|
|
|
this.uniforms = {
|
|
iTime: { value: 0 },
|
|
iResolution: { value: new Vector3(1, 1, 1) },
|
|
animationSpeed: { value: animationSpeed },
|
|
lineBrightness: { value: lineBrightness },
|
|
|
|
enableTop: { value: enabledWaves.includes('top') },
|
|
enableMiddle: { value: enabledWaves.includes('middle') },
|
|
enableBottom: { value: enabledWaves.includes('bottom') },
|
|
|
|
topLineCount: { value: topLineCount },
|
|
middleLineCount: { value: middleLineCount },
|
|
bottomLineCount: { value: bottomLineCount },
|
|
|
|
topLineDistance: { value: topLineDistance },
|
|
bottomLineDistance: { value: bottomLineDistance },
|
|
|
|
topWavePosition: {
|
|
value: new Vector3(
|
|
topWavePosition?.x ?? 10.0,
|
|
topWavePosition?.y ?? 0.5,
|
|
topWavePosition?.rotate ?? -0.4,
|
|
),
|
|
},
|
|
bottomWavePosition: {
|
|
value: new Vector3(
|
|
bottomWavePosition?.x ?? 2.0,
|
|
bottomWavePosition?.y ?? -0.7,
|
|
bottomWavePosition?.rotate ?? 0.4,
|
|
),
|
|
},
|
|
|
|
numPoints: { value: numPoints },
|
|
pointSpacingX: { value: pointSpacingX },
|
|
pointsOffsetX: { value: pointsOffsetX },
|
|
pointY: { value: [...pointYValues].slice(0, 8).concat(Array(8).fill(0)).slice(0, 8) },
|
|
lineSpread: { value: lineSpread },
|
|
fanSpread: { value: fanSpread },
|
|
lineSharpness: { value: lineSharpness },
|
|
waveFrequency: { value: waveFrequency },
|
|
bezierCurvature: { value: bezierCurvature },
|
|
circleRadiusPx: { value: circleRadiusPx },
|
|
circleGlowSize: { value: circleGlowSize },
|
|
circleGlowStrength: { value: circleGlowStrength },
|
|
|
|
iMouse: { value: new Vector2(-1000, -1000) },
|
|
interactive: { value: interactive },
|
|
bendRadius: { value: bendRadius },
|
|
bendStrength: { value: bendStrength },
|
|
bendInfluence: { value: 0 },
|
|
|
|
horizonMode: { value: { off: 0, fog: 1, split: 2, glow: 3 }[horizonMode] ?? 0 },
|
|
horizonOpacity: { value: horizonOpacity },
|
|
horizonBlend: { value: horizonBlend },
|
|
|
|
parallax: { value: parallax },
|
|
parallaxStrength: { value: parallaxStrength },
|
|
parallaxOffset: { value: new Vector2(0, 0) },
|
|
|
|
lineGradient: {
|
|
value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1)),
|
|
},
|
|
lineGradientCount: { value: 0 },
|
|
bgColorCenter: { value: new Vector3(0, 0, 0) },
|
|
bgColorEdge: { value: new Vector3(0, 0, 0) },
|
|
}
|
|
|
|
if (linesGradient && linesGradient.length > 0) {
|
|
const stops = linesGradient.slice(0, MAX_GRADIENT_STOPS)
|
|
this.uniforms.lineGradientCount.value = stops.length
|
|
stops.forEach((hex, i) => {
|
|
const color = hexToVec3(hex)
|
|
this.uniforms.lineGradient.value[i].set(color.x, color.y, color.z)
|
|
})
|
|
}
|
|
|
|
const center = hexToVec3(bgColorCenter)
|
|
this.uniforms.bgColorCenter.value.set(center.x, center.y, center.z)
|
|
const edge = hexToVec3(bgColorEdge)
|
|
this.uniforms.bgColorEdge.value.set(edge.x, edge.y, edge.z)
|
|
|
|
const material = new ShaderMaterial({
|
|
uniforms: this.uniforms,
|
|
vertexShader,
|
|
fragmentShader,
|
|
})
|
|
|
|
const geometry = new PlaneGeometry(2, 2)
|
|
this.mesh = new Mesh(geometry, material)
|
|
this.scene.add(this.mesh)
|
|
|
|
this.geometry = geometry
|
|
this.material = material
|
|
this._startTime = performance.now()
|
|
|
|
this._setSize = () => {
|
|
const width = container.clientWidth || 1
|
|
const height = container.clientHeight || 1
|
|
this.renderer.setSize(width, height, false)
|
|
const canvasWidth = this.renderer.domElement.width
|
|
const canvasHeight = this.renderer.domElement.height
|
|
this.uniforms.iResolution.value.set(canvasWidth, canvasHeight, 1)
|
|
}
|
|
this._setSize()
|
|
|
|
this.ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(this._setSize) : null
|
|
if (this.ro) this.ro.observe(container)
|
|
|
|
this._handlePointerMove = (event) => {
|
|
const rect = this.renderer.domElement.getBoundingClientRect()
|
|
const x = event.clientX - rect.left
|
|
const y = event.clientY - rect.top
|
|
const dpr = this.renderer.getPixelRatio()
|
|
|
|
this.targetMouse.set(x * dpr, (rect.height - y) * dpr)
|
|
this.targetInfluence = 1.0
|
|
|
|
if (this.parallax) {
|
|
const centerX = rect.width / 2
|
|
const centerY = rect.height / 2
|
|
const offsetX = (x - centerX) / rect.width
|
|
const offsetY = -(y - centerY) / rect.height
|
|
this.targetParallax.set(offsetX * this.parallaxStrength, offsetY * this.parallaxStrength)
|
|
}
|
|
}
|
|
|
|
this._handlePointerLeave = () => {
|
|
this.targetInfluence = 0.0
|
|
}
|
|
|
|
this.renderer.domElement.addEventListener('pointermove', this._handlePointerMove)
|
|
this.renderer.domElement.addEventListener('pointerleave', this._handlePointerLeave)
|
|
|
|
this.raf = 0
|
|
const renderLoop = () => {
|
|
this.uniforms.iTime.value = (performance.now() - this._startTime) * 0.001
|
|
|
|
if (this.interactive) {
|
|
this.currentMouse.lerp(this.targetMouse, this.mouseDamping)
|
|
this.uniforms.iMouse.value.copy(this.currentMouse)
|
|
|
|
this.currentInfluence += (this.targetInfluence - this.currentInfluence) * this.mouseDamping
|
|
this.uniforms.bendInfluence.value = this.currentInfluence
|
|
}
|
|
|
|
if (this.parallax) {
|
|
this.currentParallax.lerp(this.targetParallax, this.mouseDamping)
|
|
this.uniforms.parallaxOffset.value.copy(this.currentParallax)
|
|
}
|
|
|
|
this.renderer.render(this.scene, this.camera)
|
|
this.raf = requestAnimationFrame(renderLoop)
|
|
}
|
|
|
|
this._handleVisibility = () => {
|
|
if (document.hidden) {
|
|
cancelAnimationFrame(this.raf)
|
|
this.raf = 0
|
|
} else if (!this.raf) {
|
|
renderLoop()
|
|
}
|
|
}
|
|
document.addEventListener('visibilitychange', this._handleVisibility)
|
|
|
|
renderLoop()
|
|
}
|
|
|
|
destroy() {
|
|
cancelAnimationFrame(this.raf)
|
|
if (this.ro) this.ro.disconnect()
|
|
document.removeEventListener('visibilitychange', this._handleVisibility)
|
|
|
|
this.renderer.domElement.removeEventListener('pointermove', this._handlePointerMove)
|
|
this.renderer.domElement.removeEventListener('pointerleave', this._handlePointerLeave)
|
|
|
|
this.geometry.dispose()
|
|
this.material.dispose()
|
|
this.renderer.dispose()
|
|
|
|
if (this.renderer.domElement.parentElement) {
|
|
this.renderer.domElement.parentElement.removeChild(this.renderer.domElement)
|
|
}
|
|
}
|
|
}
|