From a81ea3f973c14feef06221fb5993f4a53b9faa1e Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 9 May 2026 04:14:54 -0400 Subject: [PATCH] fix(decnet_web/Layout): theme swap animation no longer flashes opposite mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Growing the NEW theme layer from circle(0) outward leaves a one-frame gap where the new pseudo is fully opaque at full size (the default state) before the clip-path animation registers. Result: a flash of the destination theme right before the reveal starts. Inverted the layering and animation direction: - NEW theme snapshot sits on the bottom (z-index 0), static - OLD theme snapshot sits on top (z-index 1), shrinks via clip-path from circle(N) at click point down to circle(0) The new layer is now hidden behind the old one until the old shrinks away — no flash possible because the new layer was never visible before the animation. Same 520ms duration, same ease curve, same direction-of-travel from the user's POV (circle expanding from cursor). --- decnet_web/src/index.css | 18 +++++++++++++----- decnet_web/src/lib/useThemeToggle.ts | 9 +++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/decnet_web/src/index.css b/decnet_web/src/index.css index 95079177..65e1f99a 100644 --- a/decnet_web/src/index.css +++ b/decnet_web/src/index.css @@ -340,18 +340,26 @@ input:focus { /* ── Theme transition ─────────────────────────── * Disables the default cross-fade so the JS-driven * circle clip-path in useThemeToggle.ts owns the - * reveal entirely. The new theme grows from the - * click point; the old theme stays put and is - * uncovered by the expanding circle. */ + * 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). */ ::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; } -::view-transition-old(root) { +::view-transition-new(root) { z-index: 0; } -::view-transition-new(root) { +::view-transition-old(root) { z-index: 1; } diff --git a/decnet_web/src/lib/useThemeToggle.ts b/decnet_web/src/lib/useThemeToggle.ts index 62d9fb2d..0a0ff10d 100644 --- a/decnet_web/src/lib/useThemeToggle.ts +++ b/decnet_web/src/lib/useThemeToggle.ts @@ -70,17 +70,22 @@ 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. */ document.documentElement.animate( { clipPath: [ - `circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`, + `circle(0px at ${x}px ${y}px)`, ], }, { duration: 520, easing: 'cubic-bezier(0.4, 0, 0.2, 1)', - pseudoElement: '::view-transition-new(root)', + pseudoElement: '::view-transition-old(root)', }, ); }).catch(() => { /* user-cancelled or unsupported pseudo, ignore */ });