kasuti: don't autoplay on completion; default GIF to 0.5× pace

- Completion now fires confetti and leaves both fabrics on the final
  tour (replayIdx = totalStitches, paused). The user drives the
  replay from the control strip.
- Exported GIF frames render at 400 ms each (vs 200 ms on-screen) with
  a 1200 ms hold on the first and last frame — half speed by default,
  easier to follow when a platform auto-loops the preview.

https://claude.ai/code/session_01KNdXjQczMCRGMK7K3Qderw
This commit is contained in:
Claude 2026-04-22 11:45:39 +00:00
parent bd4f073cac
commit 0378978b1f
No known key found for this signature in database

View file

@ -974,9 +974,10 @@ const KasutiEmbroidery: React.FC = () => {
const encoder = GIFEncoder(); const encoder = GIFEncoder();
const totalFrames = stitches.length + 1; const totalFrames = stitches.length + 1;
// Timings: slightly longer hold on the first and last frames. // Export at 0.5× the on-screen replay pace — easier to follow in a loop
const frameDelayMs = 200; // and friendlier when the GIF auto-plays in a social-media preview.
const holdDelayMs = 900; const frameDelayMs = 400;
const holdDelayMs = 1200;
for (let k = 0; k <= stitches.length; k++) { for (let k = 0; k <= stitches.length; k++) {
const stitchesUpTo = stitches.slice(0, k); const stitchesUpTo = stitches.slice(0, k);
@ -1023,19 +1024,20 @@ const KasutiEmbroidery: React.FC = () => {
patternId, patternId,
]); ]);
// On first completion: confetti + kick off an autoplay replay from step 0. // On first completion: confetti + leave both fabrics showing the final tour.
// Replay stays paused until the user presses play or scrubs the slider.
// When the tour is un-completed (undo/reset/pattern change), tear down replay. // When the tour is un-completed (undo/reset/pattern change), tear down replay.
useEffect(() => { useEffect(() => {
if (completed && !celebratedRef.current) { if (completed && !celebratedRef.current) {
celebratedRef.current = true; celebratedRef.current = true;
confetti({ particleCount: 180, spread: 90, origin: { y: 0.6 } }); confetti({ particleCount: 180, spread: 90, origin: { y: 0.6 } });
setReplayIdx(0); setReplayIdx(stitches.length);
setReplayPlaying(true); setReplayPlaying(false);
} else if (!completed) { } else if (!completed) {
setReplayIdx(0); setReplayIdx(0);
setReplayPlaying(false); setReplayPlaying(false);
} }
}, [completed]); }, [completed, stitches.length]);
// Hydrate from ?replay=... on mount. Runs once client-side; if the payload // Hydrate from ?replay=... on mount. Runs once client-side; if the payload
// parses and the pattern is known (preset or carried inline), rebuild the // parses and the pattern is known (preset or carried inline), rebuild the