fix(做对了): 点评弹窗等烟花放完再出来,不再放到一半就被盖住;星星雨、彩虹喷泉收短一点,十种都在两三秒内放完
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
655af07ca7
commit
4d6df4ab0c
2 files changed
+80
-36
No files matched your search
@@ -35,7 +35,7 @@ const { celebrate } = useFireworks()
|
|||||||
// 只对已经能评价、且还没评过的人弹:后端要求先有 AC 才收评价,这里刚 AC 完正好;
|
// 只对已经能评价、且还没评过的人弹:后端要求先有 AC 才收评价,这里刚 AC 完正好;
|
||||||
// mine 非 null 说明早就评过了,别再打扰。
|
// mine 非 null 说明早就评过了,别再打扰。
|
||||||
//
|
//
|
||||||
// 点评是强制的,但从 AC 到弹窗出来有 1.5 秒(加上查一次「评过没有」),而换题会把
|
// 点评是强制的,但弹窗要等烟花放完才出来(两三秒,加上查一次「评过没有」),而换题会把
|
||||||
// 定时器取消(见下面换题的 watch)—— 「下一题」在通过的那一刻就出现了,学生手快点
|
// 定时器取消(见下面换题的 watch)—— 「下一题」在通过的那一刻就出现了,学生手快点
|
||||||
// 下去,点评就跳过了。所以这段时间里欠着一次点评(reviewOwed),离开这道题的导航
|
// 下去,点评就跳过了。所以这段时间里欠着一次点评(reviewOwed),离开这道题的导航
|
||||||
// 先拦下来:立刻弹点评,选完再去原来要去的地方。
|
// 先拦下来:立刻弹点评,选完再去原来要去的地方。
|
||||||
@@ -128,11 +128,24 @@ function continueAfterReview() {
|
|||||||
closeCommentPanel()
|
closeCommentPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
const { start: showCommentPanelDelayed, stop: cancelCommentPanel } = useTimeoutFn(
|
/**
|
||||||
openReview,
|
* 烟花放完再弹点评,两边一个时间(十种烟花长短不一,两到三秒)。
|
||||||
1500,
|
* 每次通过记一个号,换题、被导航拦下提前弹了,就把号作废,放完也不再弹。
|
||||||
{ immediate: false },
|
*/
|
||||||
)
|
let reviewAfterFireworks = 0
|
||||||
|
|
||||||
|
function showCommentPanelAfter(fireworks: Promise<void>) {
|
||||||
|
const run = ++reviewAfterFireworks
|
||||||
|
fireworks.then(() => {
|
||||||
|
if (run === reviewAfterFireworks) openReview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelCommentPanel() {
|
||||||
|
reviewAfterFireworks++
|
||||||
|
}
|
||||||
|
// 离开题目页时烟花那边当作放完了,不能接着去查、去弹
|
||||||
|
onScopeDispose(cancelCommentPanel)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 只拦「离开这道题」:换 query(左栏切页签会改 `?tab=`)不算。弹窗已经开着的时候
|
* 只拦「离开这道题」:换 query(左栏切页签会改 `?tab=`)不算。弹窗已经开着的时候
|
||||||
@@ -166,7 +179,7 @@ const { start: goToProblemSetDelayed, stop: cancelGoToProblemSet } = useTimeoutF
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 换题时收掉通过之后那两个 1.5 秒的定时器 —— 评价弹窗是按**当前**题目去查、去弹的,
|
* 换题时收掉通过之后等着的点评和回题单 —— 评价弹窗是按**当前**题目去查、去弹的,
|
||||||
* 不取消的话就会给一道还没做的题弹评价。欠着的点评走不到这里:离开这道题的导航
|
* 不取消的话就会给一道还没做的题弹评价。欠着的点评走不到这里:离开这道题的导航
|
||||||
* 已经被 holdForReview 拦下、评完才放行;还能走到这里的只有弹窗开着时按了浏览器后退。
|
* 已经被 holdForReview 拦下、评完才放行;还能走到这里的只有弹窗开着时按了浏览器后退。
|
||||||
* 结果面板、还在跟的那条提交由 submission store 自己收。
|
* 结果面板、还在跟的那条提交由 submission store 自己收。
|
||||||
@@ -226,12 +239,12 @@ watch(
|
|||||||
if (result !== SubmissionStatus.accepted) return
|
if (result !== SubmissionStatus.accepted) return
|
||||||
|
|
||||||
// 2. 放烟花
|
// 2. 放烟花
|
||||||
celebrate()
|
const fireworks = celebrate()
|
||||||
|
|
||||||
// 3. 弹出评价框。比赛里不打扰;题单里 1.5 秒后要跳回题单页,弹了也会被冲掉
|
// 3. 烟花放完弹出评价框。比赛里不打扰;题单里 1.5 秒后要跳回题单页,弹了也会被冲掉
|
||||||
if (ctx.value.reviewAfterAccepted) {
|
if (ctx.value.reviewAfterAccepted) {
|
||||||
reviewOwed.value = true
|
reviewOwed.value = true
|
||||||
showCommentPanelDelayed()
|
showCommentPanelAfter(fireworks)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.value.backToProblemSet) {
|
if (ctx.value.backToProblemSet) {
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ const PALETTES = [
|
|||||||
const RAINBOW = [COLORS.pink, COLORS.orange, COLORS.gold, COLORS.green, COLORS.blue, COLORS.purple]
|
const RAINBOW = [COLORS.pink, COLORS.orange, COLORS.gold, COLORS.green, COLORS.blue, COLORS.purple]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 盖在点评弹窗上面放:通过之后 1.5 秒点评弹窗就出来了,这十种要放 2~3 秒,
|
* 盖在弹层上面放。点评弹窗平时等烟花放完才出来(见 celebrate 的返回值),但学生在放的时候
|
||||||
* 默认的 zIndex 100 在 Naive 弹层(2000 起)底下,后半段会被遮罩挡住。
|
* 点「下一题」,点评会被提前叫出来 —— 默认的 zIndex 100 在 Naive 弹层(2000 起)底下,
|
||||||
* 画布本身 pointer-events: none,不挡点评的点击。
|
* 剩下的半场就被遮罩挡住了。画布本身 pointer-events: none,不挡点评的点击。
|
||||||
*/
|
*/
|
||||||
const Z_INDEX = 3000
|
const Z_INDEX = 3000
|
||||||
|
|
||||||
@@ -113,6 +113,11 @@ export function useFireworks() {
|
|||||||
// 动画期间创建的定时器,组件卸载时统一清理,避免离开页面后动画继续
|
// 动画期间创建的定时器,组件卸载时统一清理,避免离开页面后动画继续
|
||||||
const timers = new Set<ReturnType<typeof setTimeout>>()
|
const timers = new Set<ReturnType<typeof setTimeout>>()
|
||||||
let disposed = false
|
let disposed = false
|
||||||
|
// 这一场最后一次发射的 promise:canvas-confetti 同一块画布上的粒子是一场动画,
|
||||||
|
// 后发的接进去,promise 在所有粒子都落完的时候才 resolve
|
||||||
|
let lastShot: Promise<unknown> | null = null
|
||||||
|
// 还没发出去的异步准备(表情包位图在解码),算「还在放」
|
||||||
|
let preparing = 0
|
||||||
|
|
||||||
function runLater(fn: () => void, ms: number) {
|
function runLater(fn: () => void, ms: number) {
|
||||||
const id = setTimeout(() => {
|
const id = setTimeout(() => {
|
||||||
@@ -146,7 +151,22 @@ export function useFireworks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fire(options: confetti.Options) {
|
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 */
|
/** 没有真火箭:沿上升路线一路撒往下掉的小火星,当作尾焰。x0 → x、从屏幕底到 y */
|
||||||
@@ -228,15 +248,15 @@ export function useFireworks() {
|
|||||||
|
|
||||||
// 效果2: 星星雨 —— 从顶上落下一片星星
|
// 效果2: 星星雨 —— 从顶上落下一片星星
|
||||||
() => {
|
() => {
|
||||||
every(1200, () => {
|
every(1000, () => {
|
||||||
fire({
|
fire({
|
||||||
particleCount: 2,
|
particleCount: 2,
|
||||||
angle: 270,
|
angle: 270,
|
||||||
spread: 20,
|
spread: 20,
|
||||||
startVelocity: 8,
|
startVelocity: 10,
|
||||||
gravity: 0.7,
|
gravity: 0.7,
|
||||||
decay: 0.97,
|
decay: 0.97,
|
||||||
ticks: 180,
|
ticks: 120,
|
||||||
scalar: 1.1,
|
scalar: 1.1,
|
||||||
shapes: ["star"],
|
shapes: ["star"],
|
||||||
colors: [COLORS.gold, COLORS.gold, COLORS.pink, COLORS.blue, COLORS.silver],
|
colors: [COLORS.gold, COLORS.gold, COLORS.pink, COLORS.blue, COLORS.silver],
|
||||||
@@ -263,7 +283,7 @@ export function useFireworks() {
|
|||||||
// 效果4: 彩虹喷泉 —— 两个下角往里喷,颜色按彩虹轮换
|
// 效果4: 彩虹喷泉 —— 两个下角往里喷,颜色按彩虹轮换
|
||||||
() => {
|
() => {
|
||||||
const start = performance.now()
|
const start = performance.now()
|
||||||
every(1500, () => {
|
every(1300, () => {
|
||||||
const color = RAINBOW[Math.floor((performance.now() - start) / 100) % RAINBOW.length]
|
const color = RAINBOW[Math.floor((performance.now() - start) / 100) % RAINBOW.length]
|
||||||
const base: confetti.Options = {
|
const base: confetti.Options = {
|
||||||
particleCount: 3,
|
particleCount: 3,
|
||||||
@@ -271,7 +291,7 @@ export function useFireworks() {
|
|||||||
startVelocity: 55,
|
startVelocity: 55,
|
||||||
gravity: 1,
|
gravity: 1,
|
||||||
decay: 0.92,
|
decay: 0.92,
|
||||||
ticks: 120,
|
ticks: 95,
|
||||||
scalar: 0.8,
|
scalar: 0.8,
|
||||||
flat: true,
|
flat: true,
|
||||||
shapes: ["circle"],
|
shapes: ["circle"],
|
||||||
@@ -420,29 +440,40 @@ export function useFireworks() {
|
|||||||
|
|
||||||
// 效果10: 表情包烟花 —— 一左一右炸出两团表情
|
// 效果10: 表情包烟花 —— 一左一右炸出两团表情
|
||||||
() => {
|
() => {
|
||||||
loadEmojiShapes().then((emojiShapes) => {
|
preparing++
|
||||||
if (disposed) return
|
loadEmojiShapes()
|
||||||
const options: confetti.Options = {
|
.then((emojiShapes) => {
|
||||||
shapes: emojiShapes,
|
if (disposed) return
|
||||||
flat: true,
|
const options: confetti.Options = {
|
||||||
scalar: 5,
|
shapes: emojiShapes,
|
||||||
particleCount: 16,
|
flat: true,
|
||||||
spread: 360,
|
scalar: 5,
|
||||||
startVelocity: 40,
|
particleCount: 16,
|
||||||
gravity: 0.7,
|
spread: 360,
|
||||||
ticks: 150,
|
startVelocity: 40,
|
||||||
}
|
gravity: 0.7,
|
||||||
fire({ ...options, origin: { x: 0.35, y: 0.5 } })
|
ticks: 150,
|
||||||
runLater(() => fire({ ...options, origin: { x: 0.68, y: 0.42 } }), 300)
|
}
|
||||||
})
|
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)()
|
pick(fireworkTypes)()
|
||||||
|
return Promise.race([
|
||||||
|
finished(),
|
||||||
|
new Promise<void>((resolve) => setTimeout(resolve, MAX_WAIT_MS)),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in new issue
Block a user