From ccff1467b164504af182c6c327e6461b9007b763 Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 9 May 2026 04:17:07 -0400 Subject: [PATCH] fix(decnet_web/Layout): outward theme reveal, no flash either end ANTI prefers the new theme growing outward from the click point (visually clearer cause-and-effect than the old theme burning away). The original outward implementation flashed at the start because the new pseudo defaulted to its computed style (no clip-path = fully visible) for one frame before the JS animation registered. Switching the animation's fill from 'forwards' to 'both' enforces the start keyframe (circle(0) at click point) before the first paint, in addition to pinning the end keyframe through pseudo teardown. New layer is invisible until the animation begins, fully visible until cleanup. No flash either end. --- decnet_web/src/index.css | 20 +++++++++----------- decnet_web/src/lib/useThemeToggle.ts | 22 ++++++++++------------ 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/decnet_web/src/index.css b/decnet_web/src/index.css index 65e1f99a..d5f6ff18 100644 --- a/decnet_web/src/index.css +++ b/decnet_web/src/index.css @@ -342,24 +342,22 @@ input:focus { * circle clip-path in useThemeToggle.ts owns the * reveal entirely. * - * Layering: the NEW theme snapshot sits behind, the - * OLD theme snapshot sits on top. The JS animation - * shrinks the old layer's clip-path from the - * viewport-covering circle down to circle(0) at the - * click point — burning away to reveal the new - * theme underneath. This avoids the one-frame flash - * you'd get by growing the new layer (where the - * default fully-opaque pseudo is visible for a tick - * before the clip-path animation registers). */ + * Layering: the OLD theme sits behind, the NEW theme + * sits on top and grows outward from the click point + * via a clip-path animation. The JS animation runs + * with fill: 'both' so the new layer starts clipped + * to circle(0) immediately (no opening flash) and + * stays at the final keyframe until pseudo teardown + * (no closing flash). */ ::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; } -::view-transition-new(root) { +::view-transition-old(root) { z-index: 0; } -::view-transition-old(root) { +::view-transition-new(root) { z-index: 1; } diff --git a/decnet_web/src/lib/useThemeToggle.ts b/decnet_web/src/lib/useThemeToggle.ts index 663a292f..9ae992f9 100644 --- a/decnet_web/src/lib/useThemeToggle.ts +++ b/decnet_web/src/lib/useThemeToggle.ts @@ -70,27 +70,25 @@ function animateSwap(next: Theme, x: number, y: number): void { Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y), ); - /* Shrink the OLD layer (on top per index.css z-index rules) - * away from the click point, uncovering the NEW layer that - * sits behind. Going outside-in instead of inside-out avoids - * the one-frame flash where the default-opaque new pseudo - * would otherwise be visible before the clip-path registers. */ + /* Grow the NEW layer (on top per index.css z-index rules) + * outward from the click point, covering the OLD layer that + * sits behind. fill: 'both' pins both ends of the keyframe + * range so the start state (clipped to a 0-radius circle) + * is enforced before the first paint and the end state + * (full-viewport circle) holds through pseudo teardown — + * killing the flash on either end of the animation. */ document.documentElement.animate( { clipPath: [ - `circle(${endRadius}px at ${x}px ${y}px)`, `circle(0px at ${x}px ${y}px)`, + `circle(${endRadius}px at ${x}px ${y}px)`, ], }, { duration: 520, easing: 'cubic-bezier(0.4, 0, 0.2, 1)', - pseudoElement: '::view-transition-old(root)', - /* Without 'forwards' the keyframes release on completion - * and the pseudo reverts to its computed style (no - * clip-path), making the old layer flash back at full - * size for a frame before View Transitions tears it down. */ - fill: 'forwards', + pseudoElement: '::view-transition-new(root)', + fill: 'both', }, ); }).catch(() => { /* user-cancelled or unsupported pseudo, ignore */ });