Skip to content

Commit 1b72173

Browse files
committed
提交进度
1 parent cd286bb commit 1b72173

30 files changed

Lines changed: 2706 additions & 94 deletions

.github/workflows/website.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Deploy Website
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- 'website/**'
9+
- '.github/workflows/website.yml'
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
pages: write
15+
id-token: write
16+
17+
concurrency:
18+
group: 'pages'
19+
cancel-in-progress: false
20+
21+
jobs:
22+
build:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v4
27+
28+
- name: Setup pnpm
29+
uses: pnpm/action-setup@v4
30+
with:
31+
version: 10.6.2
32+
33+
- name: Setup Node.js
34+
uses: actions/setup-node@v4
35+
with:
36+
node-version: 20
37+
cache: 'pnpm'
38+
cache-dependency-path: website/pnpm-lock.yaml
39+
40+
- name: Install dependencies
41+
working-directory: website
42+
run: pnpm install --frozen-lockfile
43+
44+
- name: Build
45+
working-directory: website
46+
run: pnpm build
47+
48+
- name: Upload artifact
49+
uses: actions/upload-pages-artifact@v3
50+
with:
51+
path: website/dist
52+
53+
deploy:
54+
needs: build
55+
runs-on: ubuntu-latest
56+
environment:
57+
name: github-pages
58+
url: ${{ steps.deploy-pages.outputs.page_url }}
59+
steps:
60+
- name: Deploy to GitHub Pages
61+
id: deploy-pages
62+
uses: actions/deploy-pages@v4

src-tauri/src/main.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ use tauri_plugin_single_instance::init as single_instance;
1818
// use tauri_plugin_updater::UpdaterExt;
1919
use tauri::menu::{MenuBuilder, MenuItemBuilder};
2020
use tauri::tray::TrayIconBuilder;
21-
use tauri::Url;
2221
use tauri::webview::WebviewWindowBuilder;
2322
use tauri::WebviewUrl;
2423
use tauri::{LogicalSize, Size};
@@ -148,6 +147,11 @@ fn main() {
148147
let _ = win.set_size(Size::Logical(LogicalSize::new(base_w, base_h)));
149148
}
150149
}
150+
if let Some(float_win) = app.get_webview_window("float") {
151+
let float_w = 128.0f64;
152+
let float_h = 128.0f64;
153+
let _ = float_win.set_size(Size::Logical(LogicalSize::new(float_w, float_h)));
154+
}
151155

152156
// 开发模式下显式导航至 Vite 开发服务器,避免资源协议映射异常
153157
// 不再使用 dev server 导航

src-tauri/tauri.conf.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@
6262
"label": "float",
6363
"title": "BPM",
6464
"url": "index.html#float",
65-
"width": 84,
66-
"height": 84,
65+
"width": 128,
66+
"height": 128,
6767
"resizable": false,
6868
"visible": false,
6969
"decorations": false,
@@ -94,4 +94,4 @@
9494
"requireLiteralLeadingDot": false
9595
}
9696
}
97-
}
97+
}

src/App.tsx

Lines changed: 110 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -601,95 +601,105 @@ function FloatBall({ themeName, bpm, conf, viz, onExit, isLockedHighlight }: { t
601601
const [hover, setHover] = React.useState(false)
602602
const lastClickRef = React.useRef<number>(0)
603603
const dragStartRef = React.useRef<{x:number,y:number}|null>(null)
604-
const mode: 'bars' = 'bars'
605-
// 悬浮球固定尺寸,需在依赖它的 effect 之前声明
606-
const ballSize = 56
607-
// 中等夸张参数(与绘制一致),据此给出透明边距,避免裁剪
608-
const baseStroke = 2.2
609-
const widthGain = 2.2
610-
const radiusGain = 1.5
611-
const shadowBlur = 6
612-
const innerRadiusGain = 0.9
613-
const marginPx = Math.ceil(shadowBlur + (baseStroke + widthGain)/2 + radiusGain + 1)
604+
const accent = theme.ring || '#eb1a50'
605+
const ballSize = 58
606+
const baseStroke = 1.68
607+
const widthGain = 2.24
608+
const radiusGain = 2.56
609+
const innerRadiusGain = 0.96
610+
const shadowBlur = 9.6
611+
const segments = 64
612+
const segmentGap = 0.12
613+
const marginPx = Math.ceil(shadowBlur + (baseStroke + widthGain) / 2 + radiusGain + 2)
614614
const canvasSize = ballSize + marginPx * 2
615615

616-
const canvasRef = React.useRef<HTMLCanvasElement|null>(null)
617-
// rAF 驱动:以最新 viz 作为源,在屏幕刷新节奏下重绘,保证前端帧率不受事件抖动影响
616+
const toRgba = React.useCallback(
617+
(alpha: number) => {
618+
const hex = accent.startsWith('#') ? accent.slice(1) : null
619+
if (!hex || (hex.length !== 3 && hex.length !== 6)) {
620+
return `rgba(235,26,80,${alpha})`
621+
}
622+
const full = hex.length === 3 ? hex.split('').map((c) => c + c).join('') : hex
623+
const num = parseInt(full, 16)
624+
const r = (num >> 16) & 255
625+
const g = (num >> 8) & 255
626+
const b = num & 255
627+
return `rgba(${r},${g},${b},${alpha})`
628+
},
629+
[accent]
630+
)
631+
632+
const canvasRef = React.useRef<HTMLCanvasElement | null>(null)
633+
// rAF ?????? viz ?????????????????????????????
618634
const lastVizRef = React.useRef<AudioViz | null>(null)
619635
React.useEffect(() => { lastVizRef.current = viz }, [viz])
620636
React.useEffect(() => {
621637
let rafId = 0
622-
let phase = 0 // 中等夸张:轻微相位流动
623-
function tick() {
638+
const tick = () => {
624639
const cvs = canvasRef.current
625640
if (cvs) {
626641
const ctx = cvs.getContext('2d')
627642
if (ctx) {
628643
const dpr = Math.max(1, Math.floor(window.devicePixelRatio || 1))
629-
const cssW = canvasSize, cssH = canvasSize
644+
const cssW = canvasSize
645+
const cssH = canvasSize
630646
if (cvs.width !== cssW * dpr || cvs.height !== cssH * dpr) {
631647
cvs.width = cssW * dpr; cvs.height = cssH * dpr
632648
}
633649
cvs.style.width = cssW + 'px'; cvs.style.height = cssH + 'px'
634650
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
635-
const W = cssW, H = cssH
636-
ctx.clearRect(0,0,W,H)
637-
const cx=W/2, cy=H/2
638-
// 以视觉圆直径为基准,确保可视化与圆边对齐(静止外缘≈圆半径,稍作+0.25px外扩防露底)
639-
const rBase = Math.max(1, ballSize/2 - baseStroke/2 + 0.25)
640-
ctx.lineCap = 'round' as CanvasLineCap
641-
ctx.lineJoin = 'round' as CanvasLineJoin
642-
const N = 64
643-
const gap = 0.08
651+
ctx.clearRect(0,0,cssW,cssH)
652+
const cx = cssW / 2
653+
const cy = cssH / 2
644654
const samples = lastVizRef.current?.samples ?? []
645655
const rms = lastVizRef.current?.rms ?? 0
646-
const energyBoost = Math.min(2.0, 0.9 + rms * 2.0)
647-
ctx.shadowBlur = shadowBlur
648-
ctx.shadowColor = 'rgba(235,26,80,0.80)'
649-
const silent = (rms <= 0.001)
650-
// 基础环:静音时更亮但不过分厚
651-
{
652-
ctx.save()
653-
ctx.shadowBlur = silent ? (shadowBlur + 1) : shadowBlur
654-
// 让静止时外缘与 rBase 一致:取与动态线宽同一“视觉厚度”级别
655-
ctx.lineWidth = silent ? Math.max(1.6, baseStroke * 0.9) : Math.max(1.0, baseStroke * 0.7)
656-
ctx.strokeStyle = `rgba(235,26,80,${silent ? 0.55 : 0.20})`
657-
ctx.beginPath(); ctx.arc(cx,cy,rBase,0,Math.PI*2,false); ctx.stroke()
658-
ctx.restore()
659-
}
660-
for (let i=0;i<N;i++){
661-
let v=0, cnt=0
662-
if (samples.length){
663-
const i0=Math.floor(i*samples.length/N); const i1=Math.floor((i+1)*samples.length/N)
664-
for (let j=i0;j<i1;j++){ v+=Math.abs(samples[j]||0); cnt++ }
665-
v = cnt? v/cnt : 0
656+
const silent = rms <= 0.002
657+
const energyBoost = Math.min(2.2, 0.85 + rms * 2.4)
658+
const rBase = Math.max(8, ballSize / 2 - baseStroke / 2 + radiusGain)
659+
ctx.lineCap = 'round' as CanvasLineCap
660+
ctx.lineJoin = 'round' as CanvasLineJoin
661+
662+
ctx.save()
663+
ctx.shadowBlur = 0
664+
ctx.lineWidth = Math.max(1.1, baseStroke * (silent ? 0.85 : 0.7))
665+
ctx.strokeStyle = toRgba(silent ? 0.5 : 0.25)
666+
ctx.beginPath(); ctx.arc(cx, cy, rBase, 0, Math.PI * 2, false); ctx.stroke()
667+
ctx.restore()
668+
669+
const seg = (Math.PI * 2) / segments
670+
for (let i = 0; i < segments; i++) {
671+
let acc = 0
672+
let cnt = 0
673+
if (samples.length) {
674+
const i0 = Math.floor((i / segments) * samples.length)
675+
const i1 = Math.floor(((i + 1) / segments) * samples.length)
676+
for (let j = i0; j < i1; j++) { acc += Math.abs(samples[j] || 0); cnt++ }
666677
}
667-
const v2 = Math.pow(Math.min(1, v * energyBoost * 2.0), 0.58)
668-
const baseA = silent ? 0.40 : 0.26
669-
const alpha = Math.min(1, baseA + v2 * (silent ? 0.45 : 0.60))
670-
const seg = (2*Math.PI)/N
671-
const a0 = -Math.PI/2 + i*seg + seg*gap*0.5 + phase
672-
const a1 = a0 + seg*(1-gap)
673-
const r2 = rBase + v2 * radiusGain
674-
ctx.lineWidth = baseStroke + v2 * widthGain
675-
ctx.strokeStyle = `rgba(235,26,80,${alpha})`
676-
ctx.beginPath(); ctx.arc(cx,cy,r2,a0,a1,false); ctx.stroke()
677-
// 轻微向内扩一圈,增强“环厚度”质感(更细、更淡)
678-
const r3 = Math.max(2, rBase - v2 * innerRadiusGain)
679-
const alpha2 = Math.min(1, (silent ? 0.36 : 0.22) + v2 * (silent ? 0.32 : 0.38))
680-
ctx.lineWidth = Math.max(1, 1.0 + v2 * 1.0)
681-
ctx.strokeStyle = `rgba(235,26,80,${alpha2})`
682-
ctx.beginPath(); ctx.arc(cx,cy,r3,a0,a1,false); ctx.stroke()
678+
const avg = cnt ? acc / cnt : 0
679+
const amp = Math.pow(Math.min(1, avg * energyBoost * 1.8), 0.6)
680+
const alpha = Math.min(1, (silent ? 0.32 : 0.2) + amp * (silent ? 0.55 : 0.75))
681+
const a0 = -Math.PI / 2 + i * seg + seg * segmentGap * 0.5
682+
const a1 = a0 + seg * (1 - segmentGap)
683+
const outerR = rBase + amp * radiusGain
684+
ctx.lineWidth = baseStroke + amp * widthGain
685+
ctx.strokeStyle = toRgba(alpha)
686+
ctx.beginPath(); ctx.arc(cx, cy, outerR, a0, a1, false); ctx.stroke()
687+
688+
const innerR = Math.max(4, rBase - amp * innerRadiusGain)
689+
const innerAlpha = Math.min(1, (silent ? 0.25 : 0.16) + amp * 0.55)
690+
ctx.lineWidth = 1 + amp * 1.1
691+
ctx.strokeStyle = toRgba(innerAlpha)
692+
ctx.beginPath(); ctx.arc(cx, cy, innerR, a0, a1, false); ctx.stroke()
683693
}
684694
ctx.shadowBlur = 0
685-
phase += 0.025
686695
}
687696
}
688697
rafId = requestAnimationFrame(tick)
689698
}
690699
rafId = requestAnimationFrame(tick)
691700
return () => { if (rafId) cancelAnimationFrame(rafId) }
692-
}, [themeName, ballSize, canvasSize])
701+
}, [themeName, ballSize, canvasSize, toRgba, segments, segmentGap, baseStroke, radiusGain, widthGain, innerRadiusGain, shadowBlur])
702+
693703

694704
async function handlePointerDown(e: React.PointerEvent) {
695705
dragStartRef.current = { x: e.clientX, y: e.clientY }
@@ -741,7 +751,9 @@ function FloatBall({ themeName, bpm, conf, viz, onExit, isLockedHighlight }: { t
741751
const color = isLockedHighlight ? theme.textPrimary : (conf == null ? confGray : (conf >= 0.5 ? theme.textPrimary : confGray))
742752
const fontPx = 22
743753
const rootStyle: React.CSSProperties = {height:'100vh',display:'flex',alignItems:'center',justifyContent:'center',background:'transparent', cursor:'default'}
754+
const bpmLabelColor = hover ? accent : (themeName === 'dark' ? '#c8d2df' : '#5c606d')
744755
const textStyle: React.CSSProperties = {fontSize:fontPx,fontWeight:700,color,letterSpacing:1,lineHeight:fontPx + 'px'}
756+
const bpmLabelStyle: React.CSSProperties = {fontSize:8,letterSpacing:2,color: bpmLabelColor,transition:'color 0.2s ease'}
745757
return (
746758
<main style={rootStyle}>
747759
<div
@@ -750,37 +762,45 @@ function FloatBall({ themeName, bpm, conf, viz, onExit, isLockedHighlight }: { t
750762
onMouseEnter={() => setHover(true)}
751763
onMouseLeave={() => setHover(false)}
752764
style={{
753-
// 包裹层:更大的真实矩形用于容纳发光/外扩,保持透明
754-
width:canvasSize,
755-
height:canvasSize,
756-
position:'relative',
757-
display:'flex',
758-
alignItems:'center',
759-
justifyContent:'center',
760-
cursor:'default',
761-
background:'transparent'
765+
width: canvasSize,
766+
height: canvasSize + 32,
767+
position: 'relative',
768+
display: 'flex',
769+
flexDirection: 'column',
770+
alignItems: 'center',
771+
justifyContent: 'center',
772+
gap: 10,
773+
cursor: 'default',
774+
background: 'transparent'
762775
}}
763-
// 去掉“单击刷新”文案,保留双击行为
764776
>
765-
{/* 可视化画布:更大尺寸,避免裁剪;置于最上层以显示“向内”弧线 */}
766-
<canvas ref={canvasRef} width={canvasSize} height={canvasSize} style={{position:'absolute', inset:0, pointerEvents:'none', zIndex:2}} />
767-
{/* 视觉圆:保持 56px,不变 */}
768-
<div
769-
style={{
770-
width:ballSize,
771-
height:ballSize,
772-
borderRadius:ballSize/2,
773-
background: theme.background,
774-
display:'flex',
775-
alignItems:'center',
776-
justifyContent:'center',
777-
position:'relative',
778-
zIndex:1
779-
}}
780-
>
781-
<div style={textStyle}>{Math.round(bpm||0)}</div>
777+
<div style={{ position: 'relative', width: canvasSize, height: canvasSize, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
778+
<canvas
779+
ref={canvasRef}
780+
width={canvasSize}
781+
height={canvasSize}
782+
style={{ position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: 2, filter: 'none' }}
783+
/>
784+
<div
785+
style={{
786+
width: ballSize,
787+
height: ballSize,
788+
borderRadius: ballSize / 2,
789+
background: theme.background,
790+
display: 'flex',
791+
flexDirection: 'column',
792+
alignItems: 'center',
793+
justifyContent: 'center',
794+
gap: 2,
795+
position: 'relative',
796+
zIndex: 3,
797+
boxShadow: 'none'
798+
}}
799+
>
800+
<div style={textStyle}>{Math.round(bpm || 0)}</div>
801+
<div style={bpmLabelStyle}>BPM</div>
802+
</div>
782803
</div>
783-
{/* 悬浮球刷新动画已移除 */}
784804
</div>
785805
<style>{`@keyframes spin360{from{transform:rotate(0)}to{transform:rotate(360deg)}}`}</style>
786806
</main>

website/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

website/.vscode/extensions.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"recommendations": ["Vue.volar"]
3+
}

website/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Vue 3 + Vite
2+
3+
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
4+
5+
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

0 commit comments

Comments
 (0)