Keep WebGL background across theme changes

codex/redisenio-dominio-musical
dev 4 weeks ago
parent b84b81fc1d
commit 1f86097d97

@ -313,6 +313,52 @@ test.describe('Fondo animado de la portada', () => {
await expect(page.locator('canvas.activo')).toHaveCount(1);
});
test('cambia de paleta con el tema sin perder el contexto WebGL', async ({ page }) => {
await page.addInitScript(() => {
type Uniforme = (this: WebGL2RenderingContext, ...argumentos: unknown[]) => void;
type VentanaConPaletas = Window & { paletasDeFondo: number[][] };
const ventana = window as VentanaConPaletas;
const prototipo = WebGL2RenderingContext.prototype as unknown as { uniform3fv: Uniforme };
const original = prototipo.uniform3fv;
ventana.paletasDeFondo = [];
prototipo.uniform3fv = function (...argumentos) {
const valores = argumentos[1] as ArrayLike<number>;
const paleta = Array.from(valores);
if (paleta.length === 12) ventana.paletasDeFondo.push(paleta);
original.apply(this, argumentos);
};
});
await page.goto('/');
const lienzo = page.locator('canvas');
await expect(lienzo).toHaveClass(/activo/);
const primera = await page.evaluate(() =>
(window as unknown as { paletasDeFondo: number[][] }).paletasDeFondo.at(-1)!
);
await page.getByRole('button', { name: 'Cambiar al tema claro' }).click();
await expect(page.locator('html')).toHaveAttribute('data-tema', 'claro');
await expect(lienzo).toHaveClass(/activo/);
await expect
.poll(() =>
page.evaluate(
() => (window as unknown as { paletasDeFondo: number[][] }).paletasDeFondo.length
)
)
.toBeGreaterThan(1);
const ultima = await page.evaluate(() =>
(window as unknown as { paletasDeFondo: number[][] }).paletasDeFondo.at(-1)!
);
expect(ultima).not.toEqual(primera);
expect(
await lienzo.evaluate((canvas) =>
(canvas as HTMLCanvasElement).getContext('webgl2')?.isContextLost()
)
).toBe(false);
});
test('con movimiento reducido no se monta', async ({ page }) => {
// Quien pide menos movimiento no quiere una primera pantalla que ondula.
// En tema oscuro, porque es donde hay imagen debajo que comprobar: en

@ -222,4 +222,32 @@
),
linear-gradient(to top, var(--c-bg) 0%, transparent 35%);
}
/*
* El texto oscuro del tema claro necesita una base limpia a la izquierda,
* pero el velo anterior tapaba casi por completo las ondas: un 80 % de papel
* encima de una paleta ya pálida. Se conserva el contraste bajo el texto y se
* abre antes hacia la derecha, donde el fondo es el protagonista.
*/
:global(:root[data-tema='claro']) .velo:not(.foto):not(.cerrado) {
background:
linear-gradient(
to right,
color-mix(in oklab, var(--c-bg) 62%, transparent) 0%,
color-mix(in oklab, var(--c-bg) 18%, transparent) 48%,
transparent 82%
),
linear-gradient(to top, var(--c-bg) 0%, transparent 22%);
}
@media (max-width: 62rem) {
:global(:root[data-tema='claro']) .velo:not(.foto):not(.cerrado) {
background: linear-gradient(
to top,
var(--c-bg) 4%,
color-mix(in oklab, var(--c-bg) 38%, transparent) 72%,
transparent 100%
);
}
}
</style>

@ -25,13 +25,22 @@
* programa que no compila, el componente no pinta y debajo sigue la imagen
* de siempre. Un fondo decorativo no puede tumbar una portada.
*/
import { tema } from '$lib/tema.svelte';
import { tema, type Tema } from '$lib/tema.svelte';
/** Densidad máxima. En una pantalla 4K, pintar cada píxel real no compensa. */
const DPR_MAXIMO = 1.5;
let lienzo = $state<HTMLCanvasElement | null>(null);
let activo = $state(false);
/**
* Cambia solo los colores del programa que ya está dibujando.
*
* Va separado de la inicialización porque leer `tema.actual` dentro de aquel
* efecto hacía que Svelte desmontara WebGL al alternar el tema. Su limpieza
* pierde el contexto deliberadamente y un contexto perdido no se puede volver
* a montar de inmediato sobre el mismo canvas: por eso las ondas desaparecían.
*/
let cambiarPaleta = $state<((actual: Tema) => void) | null>(null);
/*
* La paleta sale de los colores de marca, convertida a RGB lineal: el shader
@ -51,10 +60,10 @@
[0.0636, 0.1453, 0.3199]
],
claro: [
[0.8849, 0.9147, 0.9744],
[0.6679, 0.7334, 0.8648],
[0.4187, 0.52, 0.7112],
[0.1813, 0.2974, 0.5092]
[0.35, 0.48, 0.72],
[0.18, 0.32, 0.58],
[0.07, 0.19, 0.42],
[0.02, 0.08, 0.24]
]
} as const;
@ -188,8 +197,11 @@ void main() {
const uTiempo = gl.getUniformLocation(programa, 'u_tiempo');
const uPaleta = gl.getUniformLocation(programa, 'u_paleta');
const paleta = PALETAS[tema.actual] ?? PALETAS.oscuro;
gl.uniform3fv(uPaleta, new Float32Array(paleta.flat()));
const aplicarPaleta = (actual: Tema) => {
const paleta = PALETAS[actual] ?? PALETAS.oscuro;
gl.uniform3fv(uPaleta, new Float32Array(paleta.flat()));
};
cambiarPaleta = aplicarPaleta;
function medir() {
if (!lienzo || !gl) return;
@ -253,6 +265,7 @@ void main() {
arrancar();
return () => {
if (cambiarPaleta === aplicarPaleta) cambiarPaleta = null;
parar();
observador.disconnect();
document.removeEventListener('visibilitychange', alCambiarDePestana);
@ -266,6 +279,15 @@ void main() {
gl.getExtension('WEBGL_lose_context')?.loseContext();
};
});
/*
* Este efecto sí sigue el tema, pero no toca el ciclo de vida del contexto:
* doce números nuevos en un uniforme bastan para recolorear el siguiente
* fotograma sin recompilar shaders, perder animación ni dejar un parpadeo.
*/
$effect(() => {
cambiarPaleta?.(tema.actual);
});
</script>
<!--

Loading…
Cancel
Save

Powered by TurnKey Linux.