From f21d79c08d60bf9fd5eb0a20de57eec063b77a9d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 12:12:42 +0000 Subject: [PATCH] Seidenhintergrund im Dev-Modus reparieren, Resize-Warnung beheben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SilkBackground: beim Aufräumen nur Programm/Buffer löschen statt den WebGL-Kontext zu verwerfen. React Strict Mode mountet im Dev-Modus zweimal, der zweite Mount bekam sonst einen verlorenen Kontext und der Hintergrund blieb leer. Kontextverlust/-wiederherstellung wird jetzt behandelt. - useFitText: nur auf Breitenänderungen reagieren und die Anpassung in den nächsten Frame verschieben, damit keine "ResizeObserver loop"-Fehler entstehen. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KBHsCnWNXAB2Le3gAEsivF --- src/components/SilkBackground.tsx | 100 ++++++++++++++++++++---------- src/lib/useFitText.ts | 18 +++++- 2 files changed, 84 insertions(+), 34 deletions(-) diff --git a/src/components/SilkBackground.tsx b/src/components/SilkBackground.tsx index 4b99c24..e2ce037 100644 --- a/src/components/SilkBackground.tsx +++ b/src/components/SilkBackground.tsx @@ -106,37 +106,13 @@ export function SilkBackground({ return } - const program = gl.createProgram()! - gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERTEX)) - gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, FRAGMENT)) - gl.linkProgram(program) - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - canvas.style.display = 'none' - return - } - gl.useProgram(program) - - // Ein Dreieck, das den ganzen Viewport abdeckt - const buffer = gl.createBuffer() - gl.bindBuffer(gl.ARRAY_BUFFER, buffer) - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW) - const position = gl.getAttribLocation(program, 'position') - gl.enableVertexAttribArray(position) - gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0) - - const u = (name: string) => gl.getUniformLocation(program, name) - const uTime = u('uTime') - const uAspect = u('uAspect') - const uPointer = u('uPointer') - gl.uniform3fv(u('uColor'), hexToRgb(color)) - gl.uniform1f(u('uSpeed'), speed) - gl.uniform1f(u('uScale'), scale) - gl.uniform1f(u('uRotation'), rotation) - gl.uniform1f(u('uNoiseIntensity'), noiseIntensity) - - const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches // Seide ist weich — eine niedrigere Auflösung sieht gleich aus und spart Akku const dpr = Math.min(window.devicePixelRatio || 1, 1.25) + let program: WebGLProgram | null = null + let buffer: WebGLBuffer | null = null + let uTime: WebGLUniformLocation | null = null + let uAspect: WebGLUniformLocation | null = null + let uPointer: WebGLUniformLocation | null = null const resize = () => { const { clientWidth: w, clientHeight: h } = canvas @@ -145,7 +121,47 @@ export function SilkBackground({ gl.viewport(0, 0, canvas.width, canvas.height) gl.uniform1f(uAspect, w / Math.max(1, h)) } - resize() + + // Programm und Geometrie anlegen — auch erneut, falls der Browser den WebGL-Kontext zurücksetzt + const setup = () => { + program = gl.createProgram() + const vs = compile(gl, gl.VERTEX_SHADER, VERTEX) + const fs = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT) + gl.attachShader(program, vs) + gl.attachShader(program, fs) + gl.linkProgram(program) + gl.deleteShader(vs) + gl.deleteShader(fs) + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return false + gl.useProgram(program) + + // Ein Dreieck, das den ganzen Viewport abdeckt + buffer = gl.createBuffer() + gl.bindBuffer(gl.ARRAY_BUFFER, buffer) + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW) + const position = gl.getAttribLocation(program, 'position') + gl.enableVertexAttribArray(position) + gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0) + + const u = (name: string) => gl.getUniformLocation(program!, name) + uTime = u('uTime') + uAspect = u('uAspect') + uPointer = u('uPointer') + gl.uniform3fv(u('uColor'), hexToRgb(color)) + gl.uniform1f(u('uSpeed'), speed) + gl.uniform1f(u('uScale'), scale) + gl.uniform1f(u('uRotation'), rotation) + gl.uniform1f(u('uNoiseIntensity'), noiseIntensity) + resize() + return true + } + + if (!setup()) { + canvas.style.display = 'none' + return + } + canvas.style.display = '' + const ro = new ResizeObserver(resize) ro.observe(canvas) @@ -157,8 +173,10 @@ export function SilkBackground({ } window.addEventListener('pointermove', onPointer, { passive: true }) + const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches let raf = 0 let visible = true + let lost = false let elapsed = 0 let last = performance.now() @@ -178,7 +196,7 @@ export function SilkBackground({ } const start = () => { - if (reduceMotion || raf || !visible || document.hidden) return + if (reduceMotion || raf || lost || !visible || document.hidden) return last = performance.now() raf = requestAnimationFrame(loop) } @@ -200,13 +218,31 @@ export function SilkBackground({ const onVisibility = () => (document.hidden ? stop() : start()) document.addEventListener('visibilitychange', onVisibility) + const onLost = (e: Event) => { + e.preventDefault() // erlaubt dem Browser, den Kontext wiederherzustellen + lost = true + stop() + } + const onRestored = () => { + lost = false + if (!setup()) return + draw(performance.now()) + start() + } + canvas.addEventListener('webglcontextlost', onLost) + canvas.addEventListener('webglcontextrestored', onRestored) + return () => { stop() ro.disconnect() io.disconnect() window.removeEventListener('pointermove', onPointer) document.removeEventListener('visibilitychange', onVisibility) - gl.getExtension('WEBGL_lose_context')?.loseContext() + canvas.removeEventListener('webglcontextlost', onLost) + canvas.removeEventListener('webglcontextrestored', onRestored) + // Ressourcen freigeben, den Kontext aber behalten (React Strict Mode mountet im Dev-Modus zweimal) + gl.deleteBuffer(buffer) + gl.deleteProgram(program) } }, [color, speed, scale, rotation, noiseIntensity]) diff --git a/src/lib/useFitText.ts b/src/lib/useFitText.ts index 0d3cf8f..29ea88d 100644 --- a/src/lib/useFitText.ts +++ b/src/lib/useFitText.ts @@ -22,9 +22,23 @@ export function useFitText(max = 400) { fit() document.fonts?.ready.then(fit) - const ro = new ResizeObserver(fit) + + let frame = 0 + let lastWidth = parent.clientWidth + const ro = new ResizeObserver(() => { + // Höhenänderungen entstehen durch die Schriftgröße selbst — nur auf neue Breiten reagieren + const width = parent.clientWidth + if (width === lastWidth) return + lastWidth = width + // außerhalb der Observer-Runde anpassen, sonst meldet der Browser eine Resize-Schleife + cancelAnimationFrame(frame) + frame = requestAnimationFrame(fit) + }) ro.observe(parent) - return () => ro.disconnect() + return () => { + cancelAnimationFrame(frame) + ro.disconnect() + } }, [max]) return ref