fix(做对了): 点评弹窗等烟花放完再出来,不再放到一半就被盖住;星星雨、彩虹喷泉收短一点,十种都在两三秒内放完

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-14 13:17:27 +08:00
1 parent 655af07ca7
commit 4d6df4ab0c
2 files changed
+80 -36

No files matched your search

@@ -74,9 +74,9 @@ const PALETTES = [
const RAINBOW = [COLORS.pink, COLORS.orange, COLORS.gold, COLORS.green, COLORS.blue, COLORS.purple]
/**
* 盖在点评弹窗上面放:通过之后 1.5 秒点评弹窗就出来了,这十种要放 2~3 秒,
* 默认的 zIndex 100 在 Naive 弹层(2000 起)底下,后半段会被遮罩挡住。
* 画布本身 pointer-events: none,不挡点评的点击。
* 盖在弹层上面放。点评弹窗平时等烟花放完才出来(见 celebrate 的返回值),但学生在放的时候
* 点「下一题」,点评会被提前叫出来 —— 默认的 zIndex 100 在 Naive 弹层(2000 起)底下,
* 剩下的半场就被遮罩挡住了。画布本身 pointer-events: none,不挡点评的点击。
*/
const Z_INDEX = 3000
@@ -113,6 +113,11 @@ export function useFireworks() {
// 动画期间创建的定时器,组件卸载时统一清理,避免离开页面后动画继续
const timers = new Set<ReturnType<typeof setTimeout>>()
let disposed = false
// 这一场最后一次发射的 promise:canvas-confetti 同一块画布上的粒子是一场动画,
// 后发的接进去,promise 在所有粒子都落完的时候才 resolve
let lastShot: Promise<unknown> | null = null
// 还没发出去的异步准备(表情包位图在解码),算「还在放」
let preparing = 0
function runLater(fn: () => void, ms: number) {
const id = setTimeout(() => {
@@ -146,7 +151,22 @@ export function useFireworks() {
}
function fire(options: confetti.Options) {
confetti({ zIndex: Z_INDEX, ...options })
lastShot = confetti({ zIndex: Z_INDEX, ...options }) ?? lastShot
}
/** 定时器都走完、最后一把粒子也落完。离开页面(定时器被清掉)就当放完了 */
function finished(): Promise<void> {
return new Promise((resolve) => {
const check = () => {
if (disposed) return resolve()
if (timers.size > 0 || preparing > 0) {
setTimeout(check, 50)
return
}
Promise.resolve(lastShot).then(() => resolve())
}
check()
})
}
/** 没有真火箭:沿上升路线一路撒往下掉的小火星,当作尾焰。x0 → x、从屏幕底到 y */
@@ -228,15 +248,15 @@ export function useFireworks() {
// 效果2: 星星雨 —— 从顶上落下一片星星
() => {
every(1200, () => {
every(1000, () => {
fire({
particleCount: 2,
angle: 270,
spread: 20,
startVelocity: 8,
startVelocity: 10,
gravity: 0.7,
decay: 0.97,
ticks: 180,
ticks: 120,
scalar: 1.1,
shapes: ["star"],
colors: [COLORS.gold, COLORS.gold, COLORS.pink, COLORS.blue, COLORS.silver],
@@ -263,7 +283,7 @@ export function useFireworks() {
// 效果4: 彩虹喷泉 —— 两个下角往里喷,颜色按彩虹轮换
() => {
const start = performance.now()
every(1500, () => {
every(1300, () => {
const color = RAINBOW[Math.floor((performance.now() - start) / 100) % RAINBOW.length]
const base: confetti.Options = {
particleCount: 3,
@@ -271,7 +291,7 @@ export function useFireworks() {
startVelocity: 55,
gravity: 1,
decay: 0.92,
ticks: 120,
ticks: 95,
scalar: 0.8,
flat: true,
shapes: ["circle"],
@@ -420,29 +440,40 @@ export function useFireworks() {
// 效果10: 表情包烟花 —— 一左一右炸出两团表情
() => {
loadEmojiShapes().then((emojiShapes) => {
if (disposed) return
const options: confetti.Options = {
shapes: emojiShapes,
flat: true,
scalar: 5,
particleCount: 16,
spread: 360,
startVelocity: 40,
gravity: 0.7,
ticks: 150,
}
fire({ ...options, origin: { x: 0.35, y: 0.5 } })
runLater(() => fire({ ...options, origin: { x: 0.68, y: 0.42 } }), 300)
})
preparing++
loadEmojiShapes()
.then((emojiShapes) => {
if (disposed) return
const options: confetti.Options = {
shapes: emojiShapes,
flat: true,
scalar: 5,
particleCount: 16,
spread: 360,
startVelocity: 40,
gravity: 0.7,
ticks: 150,
}
fire({ ...options, origin: { x: 0.35, y: 0.5 } })
runLater(() => fire({ ...options, origin: { x: 0.68, y: 0.42 } }), 300)
})
.catch(() => {})
.finally(() => preparing--)
},
]
/** 最多等这么久:粒子落完的回调万一没来,别让点评弹窗一直不出来 */
const MAX_WAIT_MS = 4000
/**
* 触发随机烟花效果
* 触发随机烟花效果,返回的 promise 在这一场放完时 resolve(点评弹窗等它)
*/
function celebrate() {
function celebrate(): Promise<void> {
pick(fireworkTypes)()
return Promise.race([
finished(),
new Promise<void>((resolve) => setTimeout(resolve, MAX_WAIT_MS)),
])
}
return {