Seidenhintergrund im Dev-Modus reparieren, Resize-Warnung beheben
- 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBHsCnWNXAB2Le3gAEsivF
This commit is contained in:
2 files changed
+84
-34
No files matched your search
@@ -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])
|
||||
|
||||
|
||||
+16
-2
@@ -22,9 +22,23 @@ export function useFitText<T extends HTMLElement>(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
|
||||
|
||||
Reference in new issue
Block a user