feat: add confetti fireworks effect on tour completion
This commit is contained in:
parent
78a9a5c8d0
commit
def787e6d4
2 changed files with 61 additions and 0 deletions
|
|
@ -18,6 +18,7 @@ import {
|
|||
useState,
|
||||
} from "react";
|
||||
import { cssEscape, toRect } from "../lib/dom";
|
||||
import { fireConfettiFireworks } from "../lib/confetti-fireworks";
|
||||
import { computeCardPos, padded, pickPlacement } from "../lib/layout";
|
||||
import type { Placement, Rect, TourStep } from "../types";
|
||||
|
||||
|
|
@ -230,6 +231,7 @@ export function GuidedTour({
|
|||
function requestClose(reason: "skip" | "complete") {
|
||||
if (closeLockRef.current) return;
|
||||
closeLockRef.current = true;
|
||||
void fireConfettiFireworks();
|
||||
if (reason === "skip") onSkip();
|
||||
else onComplete();
|
||||
onOpenChange(false);
|
||||
|
|
|
|||
59
studio/frontend/src/features/tour/lib/confetti-fireworks.ts
Normal file
59
studio/frontend/src/features/tour/lib/confetti-fireworks.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"use client";
|
||||
|
||||
type FireworksOpts = {
|
||||
durationMs?: number;
|
||||
intervalMs?: number;
|
||||
zIndex?: number;
|
||||
};
|
||||
|
||||
export async function fireConfettiFireworks(opts: FireworksOpts = {}) {
|
||||
try {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const prefersReduce =
|
||||
window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
|
||||
if (prefersReduce) return;
|
||||
|
||||
const confetti = (await import("canvas-confetti")).default;
|
||||
|
||||
const duration = opts.durationMs ?? 1200;
|
||||
const intervalMs = opts.intervalMs ?? 240;
|
||||
const animationEnd = Date.now() + duration;
|
||||
const defaults = {
|
||||
startVelocity: 28,
|
||||
spread: 360,
|
||||
ticks: 58,
|
||||
zIndex: opts.zIndex ?? 99999,
|
||||
disableForReducedMotion: true,
|
||||
} as const;
|
||||
|
||||
const randomInRange = (min: number, max: number) =>
|
||||
Math.random() * (max - min) + min;
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
const timeLeft = animationEnd - Date.now();
|
||||
if (timeLeft <= 0) {
|
||||
window.clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
const particleCount = Math.max(
|
||||
10,
|
||||
Math.floor(36 * (timeLeft / duration)),
|
||||
);
|
||||
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount,
|
||||
origin: { x: randomInRange(0.12, 0.3), y: Math.random() - 0.2 },
|
||||
});
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount,
|
||||
origin: { x: randomInRange(0.7, 0.88), y: Math.random() - 0.2 },
|
||||
});
|
||||
}, intervalMs);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue