99 :style =" { zIndex: 9999 }"
1010 @pointerdown =" onStagePointerDown"
1111 >
12- <!-- 控制按钮 -->
13- <div class =" absolute top-3 right-3 z-40 flex items-center gap-2" data-no-accel >
14- <button
15- type =" button"
16- class =" kw-chip"
17- :class =" privacyMode ? 'kw-chip--on' : ''"
18- @click =" privacyMode = !privacyMode"
19- >
20- {{ privacyMode ? '隐私:开' : '隐私:关' }}
21- </button >
22- <button type =" button" class =" kw-chip" @click =" skipToCloud" >跳过</button >
23- <button type =" button" class =" kw-chip" @click =" replay" >重播</button >
24- </div >
25-
2612 <!-- 提示(accelerated 默认开启,此提示基本不显示) -->
2713 <div
2814 v-if =" showHint"
2915 class =" absolute bottom-3 right-3 z-30 wrapped-label text-[10px] text-[#00000055] bg-white/55 backdrop-blur rounded-lg px-2 py-1 border border-[#0000000a]"
3016 data-no-accel
3117 >
32- 点击空白处加速 · 右上角可重播
18+ 点击空白处加速
3319 </div >
3420
3521 <!-- 气泡层 -->
4430 >
4531 <div
4632 class =" px-3 py-2 text-sm max-w-sm relative msg-bubble whitespace-pre-wrap break-words leading-relaxed bg-[#95EC69] text-black bubble-tail-r"
47- :class =" privacyMode ? 'privacy-blur' : ''"
4833 >
4934 <span v-if =" Array.isArray(b.segments) && b.segments.length > 0" >
5035 <span v-for =" (seg, idx) in b.segments" :key =" `${b.id}-${idx}`" >
5742 </div >
5843 </div >
5944
60- <!-- 粒子 (burst) -->
61- <canvas
62- v-show =" showParticles"
63- ref =" particleCanvas"
64- class =" absolute inset-0 z-20 pointer-events-none"
65- />
6645 </div >
6746 </Teleport >
6847
7554 你的话,正在涌来。
7655 </template >
7756 <template v-else >
78- 每一句话都是一个气泡,最终爆开成你的年度关键词词云。点击关键词,回看它出现的瞬间。
57+ 这一年,你一共发出了 <span class =" font-medium text-[#07C160]" >{{ card.data?.meta?.matchedCandidates || 0 }}</span > 句简短的表达,其中 <span class =" font-medium text-[#07C160]" >{{ card.data?.meta?.uniquePhrases || 0 }}</span > 句话成了你的专属口头禅。
58+ <template v-if =" card .data ?.topKeyword " >
59+ 「<span class =" font-medium text-[#07C160]" >{{ card.data.topKeyword.word }}</span >」是你最常说的话,足足被你重复了 <span class =" font-medium text-[#07C160]" >{{ card.data.topKeyword.count }}</span > 次。
60+ </template >
61+ 点击气泡,找回当时的心情。
7962 </template >
8063 </p >
8164 </div >
8770 class =" kw-stage relative w-full h-[56vh] min-h-[360px] max-h-[680px] rounded-[28px] overflow-hidden"
8871 >
8972
90- <!-- cloud 阶段的控制按钮 -->
91- <div v-if =" phase === 'cloud'" class =" absolute top-3 right-3 z-40 flex items-center gap-2" >
92- <button
93- type =" button"
94- class =" kw-chip"
95- :class =" privacyMode ? 'kw-chip--on' : ''"
96- @click =" privacyMode = !privacyMode"
97- >
98- {{ privacyMode ? '隐私:开' : '隐私:关' }}
99- </button >
100- <button type =" button" class =" kw-chip" @click =" replay" >重播</button >
101- </div >
102-
10373 <!-- 词云 -->
10474 <transition name =" cloud-fade" >
10575 <div v-if =" phase === 'cloud'" class =" absolute inset-0 z-30 p-3 sm:p-5" >
10676 <KeywordWordCloud
10777 :keywords =" keywords "
10878 :examples =" examples "
109- :privacy-mode =" privacyMode "
11079 :animate =" true "
11180 :reduced-motion =" reducedMotion "
11281 />
11382 </div >
11483 </transition >
11584 </div >
85+
86+ <div v-if =" phase === 'cloud'" class =" mt-3 flex justify-center" >
87+ <button type =" button" class =" kw-chip" @click =" replay" >再看一遍</button >
88+ </div >
11689 </div >
11790 </WrappedCardShell >
11891 </div >
@@ -132,13 +105,10 @@ const props = defineProps({
132105const cardRoot = ref (null )
133106const stageEl = ref (null )
134107const overlayEl = ref (null )
135- const particleCanvas = ref (null )
136108
137109const phase = ref (' idle' ) // 'idle' | 'storm' | 'packed' | 'merge' | 'burst' | 'cloud'
138110const hasPlayed = ref (false )
139- const privacyMode = ref (false )
140111const accelerated = ref (true ) // 默认加速
141- const showParticles = ref (false )
142112
143113// 通知父级 deck 隐藏顶部 UI
144114const deckChromeHidden = inject (' deckChromeHidden' , ref (false ))
@@ -271,7 +241,6 @@ const bubbleSizeForText = (text, compact = false) => {
271241let stormTimer = null
272242let packedTimer = null
273243let mainTl = null
274- let particleRaf = null
275244let hardStopTimer = null
276245let animationStartedAt = 0
277246let animationDeadlineAt = 0
@@ -299,18 +268,7 @@ const armHardStop = () => {
299268 }, remain + 8 )
300269}
301270
302- const stopParticles = () => {
303- showParticles .value = false
304- if (particleRaf) cancelAnimationFrame (particleRaf)
305- particleRaf = null
306- const c = particleCanvas .value
307- if (c) {
308- try {
309- const ctx = c .getContext (' 2d' )
310- ctx? .clearRect ? .(0 , 0 , c .width , c .height )
311- } catch {}
312- }
313- }
271+ const stopParticles = () => {}
314272
315273const killTimeline = () => {
316274 if (mainTl) {
@@ -362,71 +320,6 @@ const isVisible = ref(false)
362320let io = null
363321const updateVisibility = (v ) => { isVisible .value = !! v }
364322
365- const startParticles = (rng , centerX , centerY ) => {
366- if (! import .meta.client) return
367- const canvas = particleCanvas .value
368- if (! canvas || ! curViewW || ! curViewH) return
369-
370- const dpr = Math .max (1 , Math .min (3 , Number (window .devicePixelRatio || 1 )))
371- canvas .width = Math .max (1 , Math .round (curViewW * dpr))
372- canvas .height = Math .max (1 , Math .round (curViewH * dpr))
373- canvas .style .width = ` ${ curViewW} px`
374- canvas .style .height = ` ${ curViewH} px`
375- const ctx = canvas .getContext (' 2d' )
376- if (! ctx) return
377- ctx .setTransform (dpr, 0 , 0 , dpr, 0 , 0 )
378-
379- const N = 80
380- const particles = []
381- for (let i = 0 ; i < N ; i += 1 ) {
382- const ang = rng () * Math .PI * 2
383- const sp = 120 + rng () * 320
384- particles .push ({
385- x: centerX,
386- y: centerY,
387- vx: Math .cos (ang) * sp,
388- vy: Math .sin (ang) * sp,
389- size: 0.8 + rng () * 2.4 ,
390- life: 1 ,
391- color: rng () < 0.66 ? ' rgba(7,193,96,0.65)' : (rng () < 0.5 ? ' rgba(242,170,0,0.55)' : ' rgba(14,165,233,0.55)' )
392- })
393- }
394-
395- showParticles .value = true
396- const duration = 600
397- let last = 0
398- const started = performance .now ()
399-
400- const tick = (now ) => {
401- const t = now - started
402- const dt = last > 0 ? Math .min ((now - last) / 1000 , 0.05 ) : 0.016
403- last = now
404-
405- ctx .clearRect (0 , 0 , curViewW, curViewH)
406- const p = clamp (t / duration, 0 , 1 )
407- const alpha = 1 - p
408- for (const it of particles) {
409- it .x += it .vx * dt
410- it .y += it .vy * dt
411- it .vx *= 0.86
412- it .vy *= 0.86
413- const a = alpha * 0.9
414- ctx .fillStyle = it .color .replace (/ [\d. ] + \) $ / g , ` ${ a} )` )
415- ctx .beginPath ()
416- ctx .arc (it .x , it .y , it .size , 0 , Math .PI * 2 )
417- ctx .fill ()
418- }
419-
420- if (t < duration) {
421- particleRaf = requestAnimationFrame (tick)
422- } else {
423- stopParticles ()
424- }
425- }
426-
427- particleRaf = requestAnimationFrame (tick)
428- }
429-
430323const maybeStart = () => {
431324 if (! import .meta.client) return
432325 detectReducedMotion ()
@@ -849,10 +742,7 @@ const runMergeBurst = (rng, centerX, centerY) => {
849742 opacity: 0,
850743 scale: 0.92,
851744 ease: 'power3.out',
852- stagger: staggerBurst,
853- onStart: () => {
854- startParticles(rng, centerX, centerY)
855- }
745+ stagger: staggerBurst
856746 })
857747
858748 const tlTotal = mainTl.totalDuration()
@@ -941,12 +831,6 @@ onBeforeUnmount(() => {
941831 transform: translateY(-1px);
942832}
943833
944- .kw-chip--on {
945- background: rgba(7, 193, 96, 0.12);
946- border-color: rgba(7, 193, 96, 0.22);
947- color: rgba(0, 0, 0, 0.75);
948- }
949-
950834.kw-bubble {
951835 will-change: transform, opacity;
952836 transform: translate3d(0, 0, 0);
0 commit comments