Stage 17 (#9): animated shuffle — tiles hop along a low parabola to new slots
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 28s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 53s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 28s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 53s
Give each rack slot a stable id permuted with the letters on shuffle, so the keyed rack reorders (rather than relabelling in place) and Svelte's animate directive fires. hop flies each tile along a parabola (apogee ~half a tile height) with a duration that scales with the horizontal distance (arc length): the longest 1<->7 swap takes ~0.5s, shorter swaps land sooner. Ordinary reflow (place/recall) stays instant via a guard. e2e locks that a shuffle preserves the rack's tile multiset.
This commit is contained in:
+25
-2
@@ -8,25 +8,48 @@
|
||||
slots,
|
||||
variant,
|
||||
selected,
|
||||
shuffling = false,
|
||||
ondown,
|
||||
}: {
|
||||
slots: RackSlot[];
|
||||
// Each slot carries a stable id that travels with its tile through a shuffle, so the
|
||||
// keyed list reorders (rather than relabelling in place) and the hop animation fires.
|
||||
slots: (RackSlot & { id: number })[];
|
||||
variant: Variant;
|
||||
selected: number | null;
|
||||
shuffling?: boolean;
|
||||
ondown: (e: PointerEvent, index: number) => void;
|
||||
} = $props();
|
||||
|
||||
// Used slots are hidden (the rack shifts left, freeing room on the right for the
|
||||
// MakeMove control); the slot still exists in the model for per-tile recall.
|
||||
const visible = $derived(slots.filter((s) => !s.used));
|
||||
|
||||
// hop flies a tile to its shuffled position along a low parabola (apogee ≈ half a tile
|
||||
// height). The duration scales with the horizontal distance — i.e. the arc length — so
|
||||
// the longest swap (slot 1 ↔ 7) takes ~0.5s and shorter swaps land sooner. It runs only
|
||||
// while a shuffle is in progress; ordinary reflow (placing/recalling a tile) is instant.
|
||||
function hop(node: HTMLElement, { from, to }: { from: DOMRect; to: DOMRect }, active: boolean) {
|
||||
const dx = from.left - to.left;
|
||||
const dy = from.top - to.top;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (!active || dist < 2) return { duration: 0 };
|
||||
const span = node.parentElement?.getBoundingClientRect().width || dist;
|
||||
const lift = (to.height || from.height) * 0.5;
|
||||
return {
|
||||
duration: Math.max(160, Math.min(500, (dist / span) * 560)),
|
||||
css: (t: number, u: number) =>
|
||||
`transform: translate(${dx * u}px, ${dy * u - Math.sin(Math.PI * t) * lift}px);`,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rack" data-rack>
|
||||
{#each visible as slot (slot.index)}
|
||||
{#each visible as slot (slot.id)}
|
||||
<button
|
||||
class="tile"
|
||||
class:selected={selected === slot.index}
|
||||
data-rack-index={slot.index}
|
||||
animate:hop={shuffling}
|
||||
onpointerdown={(e) => ondown(e, slot.index)}
|
||||
>
|
||||
<span class="letter">{slot.letter === BLANK ? '' : slot.letter}</span>
|
||||
|
||||
Reference in New Issue
Block a user