@@ -32,7 +32,7 @@ const props = withDefaults(
3232 themeToggle: ' 切换主题' ,
3333 pinOn: ' 已置顶' ,
3434 pinOff: ' 置顶' ,
35- floatingOn: ' 悬浮中 ' ,
35+ floatingOn: ' 双击返回 ' ,
3636 floatingOff: ' 悬浮球'
3737 })
3838 }
@@ -80,6 +80,18 @@ const lightTheme = {
8080}
8181
8282const theme = computed (() => (themeName .value === ' dark' ? darkTheme : lightTheme ))
83+ const floatingWindowStyle = computed (() => {
84+ if (! floating .value ) return {}
85+ const height = floatingCanvasEdge + 48
86+ return {
87+ background: ' transparent' ,
88+ border: ' none' ,
89+ boxShadow: ' none' ,
90+ width: ` ${floatingCanvasEdge + 8 }px ` ,
91+ height: ` ${height }px ` ,
92+ padding: ' 24px 0 10px'
93+ }
94+ })
8395
8496const displayBpm = computed (() => ' 124' )
8597const metaText = computed (() => i18n .value .metaText )
@@ -99,6 +111,19 @@ function toggleFloating() {
99111 floating .value = ! floating .value
100112}
101113
114+ function exitFloatingMode() {
115+ floating .value = false
116+ }
117+
118+ function handleFloatingDblClick() {
119+ exitFloatingMode ()
120+ }
121+
122+ function handleFloatingKeydown(event : KeyboardEvent ) {
123+ event .preventDefault ()
124+ exitFloatingMode ()
125+ }
126+
102127function handleRefresh() {
103128 if (refreshSpin .value ) return
104129 refreshSpin .value = true
@@ -156,8 +181,6 @@ const pulseEnvelope = (phase: number, attack: number, decay: number, curve = 1.5
156181 return Math .exp (- fall * (1 + curve * 0.55 ))
157182}
158183
159- const floatingCanvasRef = ref <HTMLCanvasElement | null >(null )
160-
161184const floatingBallSize = 58
162185const floatingBaseStroke = 1.68
163186const floatingWidthGain = 2.24
@@ -169,6 +192,9 @@ const floatingGap = 0.12
169192const floatingMargin = Math .ceil (floatingShadowBlur + (floatingBaseStroke + floatingWidthGain ) / 2 + floatingRadiusGain + 2 )
170193const FLOATING_CANVAS_EDGE = floatingBallSize + floatingMargin * 2
171194
195+ const floatingCanvasRef = ref <HTMLCanvasElement | null >(null )
196+ const floatingCanvasEdge = FLOATING_CANVAS_EDGE
197+
172198function hexToRgba(hex : string , alpha : number ) {
173199 const h = hex .replace (' #' , ' ' )
174200 if (h .length !== 3 && h .length !== 6 ) return ` rgba(235,26,80,${alpha }) `
@@ -180,6 +206,10 @@ function hexToRgba(hex: string, alpha: number) {
180206 return ` rgba(${r },${g },${b },${alpha }) `
181207}
182208
209+ function colorWithAlpha(color : string , alpha : number ) {
210+ return color .startsWith (' #' ) ? hexToRgba (color , alpha ) : color
211+ }
212+
183213let floatingRafId: number | null = null
184214
185215function stopFloatingLoop() {
@@ -214,6 +244,16 @@ function renderFloatingFrame() {
214244 const energyBoost = Math .min (2.2 , 0.85 + rms * 2.4 )
215245 const rBase = Math .max (8 , floatingBallSize / 2 - floatingBaseStroke / 2 + floatingRadiusGain )
216246
247+ const ballBg = theme .value .panel ?? theme .value .background ?? ' #14060a'
248+ ctx .save ()
249+ ctx .shadowColor = hexToRgba (accent , 0.35 )
250+ ctx .shadowBlur = floatingShadowBlur * 0.8
251+ ctx .fillStyle = colorWithAlpha (ballBg , 0.92 )
252+ ctx .beginPath ()
253+ ctx .arc (cx , cy , floatingBallSize / 2 , 0 , Math .PI * 2 , false )
254+ ctx .fill ()
255+ ctx .restore ()
256+
217257 ctx .save ()
218258 ctx .shadowBlur = 0
219259 ctx .lineWidth = Math .max (1.1 , floatingBaseStroke * (silent ? 0.85 : 0.7 ))
@@ -255,6 +295,19 @@ function renderFloatingFrame() {
255295 ctx .arc (cx , cy , innerR , a0 , a1 , false )
256296 ctx .stroke ()
257297 }
298+
299+ const textColor = theme .value .textPrimary ?? ' #ffffff'
300+ const bpmFontSize = floatingBallSize * 0.38
301+ const labelFontSize = floatingBallSize * 0.14
302+ ctx .textAlign = ' center'
303+ ctx .textBaseline = ' middle'
304+ ctx .fillStyle = textColor
305+ ctx .font = ` 700 ${bpmFontSize }px 'Space Grotesk', 'Inter', 'Segoe UI', sans-serif `
306+ ctx .fillText (displayBpm .value , cx , cy - floatingBallSize * 0.04 )
307+
308+ ctx .font = ` 600 ${labelFontSize }px 'Space Grotesk', 'Inter', 'Segoe UI', sans-serif `
309+ ctx .fillStyle = hexToRgba (textColor , 0.75 )
310+ ctx .fillText (' BPM' , cx , cy + floatingBallSize * 0.3 )
258311}
259312
260313async function startFloatingLoop() {
@@ -398,27 +451,51 @@ onUnmounted(() => {
398451 </script >
399452
400453<template >
401- <div class =" demo-window" :style =" { background: theme.background, color: theme.textPrimary }" >
402- <div class =" title" >BPM</div >
403- <div class =" bpm-display" :style =" { color: bpmColor }" >
404- {{ displayBpm }}
405- </div >
406- <div class =" meta-line" >
407- {{ metaText }}
408- <span class =" conf" :style =" { color: confColor }" >{{ confText }}</span >
409- </div >
410- <VizPanel
411- class =" viz-panel-shell"
412- :theme =" theme"
413- :hide-rms =" false"
414- :viz =" viz"
415- :mode =" vizMode"
416- :theme-name =" themeName"
417- :width =" 350"
418- :base-height =" 150"
419- @toggle =" cycleVizMode"
420- />
421- <div class =" actions" >
454+ <div
455+ class =" demo-window"
456+ :class =" { 'floating-mode': floating }"
457+ :style =" [{ background: theme.background, color: theme.textPrimary }, floatingWindowStyle]"
458+ >
459+ <transition name =" demo-fade" >
460+ <div v-if =" !floating" class =" demo-body" >
461+ <div class =" title" >BPM</div >
462+ <div class =" bpm-display" :style =" { color: bpmColor }" >
463+ {{ displayBpm }}
464+ </div >
465+ <div class =" meta-line" >
466+ {{ metaText }}
467+ <span class =" conf" :style =" { color: confColor }" >{{ confText }}</span >
468+ </div >
469+ <VizPanel
470+ class =" viz-panel-shell"
471+ :theme =" theme"
472+ :hide-rms =" false"
473+ :viz =" viz"
474+ :mode =" vizMode"
475+ :theme-name =" themeName"
476+ :width =" 350"
477+ :base-height =" 150"
478+ @toggle =" cycleVizMode"
479+ />
480+ </div >
481+ </transition >
482+ <transition name =" floating-fade" >
483+ <div
484+ v-if =" floating"
485+ class =" floating-preview"
486+ role =" button"
487+ tabindex =" 0"
488+ :title =" i18n.floatingOn"
489+ :aria-label =" i18n.floatingOn"
490+ @dblclick =" handleFloatingDblClick"
491+ @keydown.enter.prevent =" handleFloatingKeydown"
492+ @keydown.space.prevent =" handleFloatingKeydown"
493+ >
494+ <canvas ref =" floatingCanvasRef" class =" floating-canvas" aria-hidden =" true" ></canvas >
495+ <span class =" floating-label" >{{ i18n.floatingOn }}</span >
496+ </div >
497+ </transition >
498+ <div class =" actions" v-if =" !floating" >
422499 <button class =" icon-btn" :title =" i18n.refresh" @click =" handleRefresh" >
423500 <img
424501 :src =" refreshIcon"
@@ -452,6 +529,7 @@ onUnmounted(() => {
452529 border-radius : 5px ;
453530 padding : 50px 10px 24px ;
454531 position : relative ;
532+ overflow : hidden ;
455533 box-shadow :
456534 0 25px 60px rgba (0 , 0 , 0 , 0.55 ),
457535 inset 0 1px 0 rgba (255 , 255 , 255 , 0.06 );
@@ -462,6 +540,26 @@ onUnmounted(() => {
462540 gap : 0 ;
463541}
464542
543+ .demo-window.floating-mode {
544+ width : auto ;
545+ height : auto ;
546+ min-height : 0 ;
547+ padding : 24px 0 14px ;
548+ border : none ;
549+ box-shadow : none ;
550+ background : transparent ;
551+ overflow : visible ;
552+ gap : 12px ;
553+ }
554+
555+ .demo-body {
556+ width : 100% ;
557+ height : 100% ;
558+ display : flex ;
559+ flex-direction : column ;
560+ align-items : center ;
561+ }
562+
465563.actions {
466564 position : absolute ;
467565 top : 12px ;
@@ -559,4 +657,60 @@ onUnmounted(() => {
559657 display : flex ;
560658 justify-content : center ;
561659}
660+
661+ .floating-preview {
662+ width : 100% ;
663+ min-width : 110px ;
664+ min-height : 120px ;
665+ display : flex ;
666+ flex-direction : column ;
667+ align-items : center ;
668+ justify-content : center ;
669+ gap : 18px ;
670+ cursor : pointer ;
671+ user-select : none ;
672+ text-align : center ;
673+ }
674+
675+ .floating-canvas {
676+ display : block ;
677+ pointer-events : none ;
678+ filter : drop-shadow (0 10px 18px rgba (0 , 0 , 0 , 0.4 ));
679+ border-radius : 999px ;
680+ }
681+
682+ .floating-label {
683+ font-size : 13px ;
684+ letter-spacing : 0.08em ;
685+ text-transform : none ;
686+ font-weight : 600 ;
687+ color : v-bind(' theme.subduedText' );
688+ }
689+
690+ .floating-fade-enter-active ,
691+ .floating-fade-leave-active {
692+ transition : opacity 160ms ease , transform 160ms ease ;
693+ }
694+
695+ .floating-fade-enter-from ,
696+ .floating-fade-leave-to {
697+ opacity : 0 ;
698+ transform : translateY (6px ) scale (0.96 );
699+ }
700+
701+ .demo-fade-enter-active ,
702+ .demo-fade-leave-active {
703+ transition : opacity 160ms ease , transform 160ms ease ;
704+ }
705+
706+ .demo-fade-enter-from ,
707+ .demo-fade-leave-to {
708+ opacity : 0 ;
709+ transform : translateY (14px );
710+ }
711+
712+ .floating-preview :focus-visible {
713+ outline : 1px dashed rgba (235 , 26 , 80 , 0.6 );
714+ outline-offset : 6px ;
715+ }
562716 </style >
0 commit comments