Skin Swap

Lucian Labs system B — two universes, one DOM, and the wipe between them. Shipped in mood.

Press the button

The stage below is the real thing: two token blocks on one selector axis, swapped behind mood's actual wipe algorithm. Every swap randomises the angle, the curve depth and the wave count — press it a few times and no two are alike.

skin · wave
WifeMood
she is, in fact, fine
Standing
720
rested dishes done listened
the meter decays. so does everything.

Watch the order: the new skin wipes in behind a curved front, the wordmark blurs through its font change, then the meter fills from zero. It reads as booting into the other mode, not repainting.

The two skins

Mood's husband mode isn't like House Pour — it is the same palette and the same faces, lifted directly. That's the proof: a house look is portable enough to become one mode of an unrelated app.

body.wavebody.pour
descends fromConsole (WaveLoop)House Pour (DrinkTix)
ground#101014#f7f5ef
panel#11141e#fffefa
ink#eaecf2#262319
accent#ffa40a#a4813b
radius014px
displayMicro 5Cormorant Garamond
wordmarkMicro 5Kaushan Script
bodyFinlandicaAvenir Next
reads asMMO raid HUDa printed card

How it's wired

The transition, in four moves

1 · curved wipe, random angle

buildWipeKeyframes() regenerates a @keyframes wipe-dyn per swap: pick an angle, curve depth (5–16), wave count (1–3.5) and phase; sample 16 points along a front offset by amp · sin(…); close the polygon 500 units behind so the revealed side is solid; emit three clip-path frames at sweep −260 / 0 / +260, advancing the phase 1.4 each step so the curve travels instead of sliding rigidly.

2 · freeze the tweens

Colours and radii normally tween over 0.6s so a skin change morphs. During a wipe that's wrong — you'd reveal a half-transitioned skin. no-tween kills every transition for the duration and comes off in vt.finished. Those 0.6s tweens still earn their keep: they're the fallback.

3 · crossfade what can't tween

The wordmark changes face — Micro 5 to Kaushan Script — which no transition can interpolate. skinfade blurs it out and back over 0.6s (opacity: 0 + blur(2px) at 45%). A font swap that cuts looks like a bug; blurred through the middle it looks deliberate.

4 · land empty, then fill

zeroFill() snaps the meter to 0% inside the view-transition callback with transitions off, so the snapshot the wipe reveals shows an empty bar. When vt.finished resolves, it sweeps up to the real value over 0.85s. The arrival is the payoff.

One path, reused

withWipe(apply) takes the mutation as a callback, so the same choreography serves the mode toggle and the language picker. Whenever the whole screen changes meaning, it wipes.

function withWipe(apply) {
  var reduced = matchMedia('(prefers-reduced-motion: reduce)').matches
  if (document.startViewTransition && !reduced) {
    buildWipeKeyframes()
    document.body.classList.add('no-tween')
    var vt = document.startViewTransition(apply)
    // aborted transitions (hidden tab) are harmless — swallow them
    vt.finished.catch(noop).then(function () {
      document.body.classList.remove('no-tween')
      animateFillIn(); sketchIn()
    })
  } else {
    // no View Transitions, or reduced motion: crossfade the wordmark,
    // let the 0.6s token tweens do the morph, still fill on land
    header.classList.add('skin-swap')
    apply(); animateFillIn(); sketchIn()
  }
}

Porting this

You need two token blocks on one selector axis, components that only ever read tokens, and a single function that owns the swap. Then: