Files

89 lines
5.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: scale-swap-transition
description: Coordinated shrink-out + spring pop-in morph-like transition between two elements — no SVG path interpolation needed.
metadata:
tags: transition, morph, scale, swap, spring, pop
---
# Scale-Swap Transition
Simulates a "morph" between two DOM elements by overlapping exit and entrance scale animations. Lighter weight than [card-morph-anchor.md](card-morph-anchor.md) (which morphs container dimensions — use that for SHAPE changes; this rule is for SAME-shape state swaps) and easier than SVG path interpolation.
At a single trigger, two coordinated tweens fire:
1. **Outgoing**: scale `1.0 → EXIT_SCALE` + opacity `1 → 0`, fast `power2.in` (rushing away).
2. **Incoming**: scale `EXIT_SCALE → 1.0` + opacity `0 → 1`, `back.out(BOUNCE_FACTOR)` (arriving with weight).
A small `OVERLAP` window during which both are mid-tween creates the morph illusion; the incoming sits on top via z-index so the outgoing's fade-tail doesn't bleed through.
## Recipe
```html
<!-- Both cards position: absolute; inset: 0 in one fixed-size wrapper — same
footprint, same transform-origin: 50% 50%. Incoming starts opacity: 0,
transform: scale(EXIT_SCALE), z-index above the outgoing. -->
<div class="swap-wrap">
<div class="card outgoing" id="outgoing">{outgoingIcon} {outgoingLabel}</div>
<div class="card incoming" id="incoming">
{incomingIcon} {incomingLabel}
<div class="sub" id="sub">{incomingSubline}</div>
</div>
</div>
```
```js
// Outgoing: shrink + fade fast
tl.to(
"#outgoing",
{ scale: EXIT_SCALE, opacity: 0, duration: EXIT_DUR, ease: "power2.in" },
TRIGGER,
);
// Incoming: pops in with overshoot, starting OVERLAP before the exit finishes
tl.to(
"#incoming",
{ scale: 1.0, opacity: 1, duration: ENTER_DUR, ease: `back.out(${BOUNCE_FACTOR})` },
TRIGGER + EXIT_DUR - OVERLAP,
);
// Inner content reveals AFTER the incoming settles
tl.fromTo(
"#sub",
{ opacity: 0, y: SUB_REVEAL_Y_PX },
{ opacity: 1, y: 0, duration: SUB_REVEAL_DUR, ease: "power3.out" },
TRIGGER + EXIT_DUR + SUB_REVEAL_DELAY,
);
```
## Variations
- **Delayed inner content reveal** — the classic pattern above: morph the container, then reveal inner text once it settles; the 0.2–0.4 s gap lets the eye land on the new shape before reading.
- **Triple swap (3-state cycle)** — chain A→B→C with triggers `TRIGGER_AB` / `TRIGGER_BC`; each transition is its own tween pair, the previous incoming becoming the next outgoing. State-evolution narratives (early → mid → final labels).
- **Color-shift transition (no scale)** — for a flat morph between same-shape states, drop the scale and keep opacity + a brief background hue tween; less dramatic, more product-UI tone.
## Values
| token | range | notes |
| ---------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| TRIGGER | ≥ outgoing settled + a presence-dwell | the outgoing must "land" before transforming |
| EXIT_DUR | 0.3–0.5 s | |
| ENTER_DUR | 0.45–0.7 s | longer than `EXIT_DUR` so the overshoot can settle |
| OVERLAP | 0.1–0.2 s | >0.3 s both are clearly visible together (no morph); <0.05 s leaves a visible empty gap |
| EXIT_SCALE | 0.6–0.8 | smaller exits feel dramatic but risk reading as "vanish" instead of "morph" |
| BOUNCE_FACTOR | 1.4 soft · 1.8 firm · 2.2 cartoony | |
| SUB_REVEAL_DELAY | 0.2–0.4 s | reveals during the morph compete with the swap for attention |
| BRAND_REVEAL_AT | < TRIGGER | context (brand, eyebrow) sets the stage early; revealed AT the swap it competes with the headline beat |
## Critical Constraints
- **Incoming z-index ABOVE outgoing** — otherwise the outgoing's fade-tail (opacity 0.3–0.5) bleeds through and double-exposes the frame.
- **Both elements share `transform-origin: 50% 50%`** — different origins make the morph read as one thing teleporting elsewhere.
- **Bouncy ease ONLY on the incoming** — outgoing `power2.in`, incoming `back.out`; reversed, the swap feels mechanical.
- **Both cards `position: absolute; inset: 0`** in the same fixed-size wrapper (sized to fit both states; the wrap never resizes).
- **Don't `display: none` the outgoing** after the fade — leave it at `opacity: 0` so layout doesn't reflow.
- **Inner content reveals after the container settles**; **climax dwell ≥ 1 s** after the final state + subline land.
## See also
`press-release-spring` (a button press TRIGGERS the swap — cause and effect) · `card-morph-anchor` (shape-changing alternative) · `reactive-displacement` (when the replacement should read as a causal collision) · `sine-wave-loop` (idle breathing on the final state).