Skip to content

Commit 1ec8b37

Browse files
committed
feat(3d): switch rendering backend from WebGL to WebGPU and restore Chronos 3D sphere
- Replace WebGLRenderer with WebGPURenderer in SceneProvider and ChronosCanvas (auto-fallback to WebGL2 when WebGPU unavailable) - Add WebGPU flag to Electron main.ts, remove WebGLDraftExtensions - Update ContextRecovery to handle both GPUDevice.lost and webglcontextlost - Fix MemoryManager info structure for WebGPU compatibility - Create ChronosSphere.tsx — 3D particle sphere using THREE.Points, preserving all 2D particle behaviors (distribution, motion, color, focus-gather) - Switch ChronosCanvas from 2D Canvas back to R3F 3D rendering
1 parent a734883 commit 1ec8b37

6 files changed

Lines changed: 436 additions & 119 deletions

File tree

‎client/electron/main.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,21 @@ import { registerShortcuts, unregisterShortcuts } from './shortcutManager.js';
4848
import { registerRecordingIpcHandlers } from './recordingStorage.js';
4949

5050
// ================================================================
51-
// 性能优化:启用 GPU 光栅化与零拷贝
51+
// 性能优化:启用 GPU 光栅化、零拷贝与 WebGPU
5252
// ================================================================
5353
app.commandLine.appendSwitch('enable-gpu-rasterization');
5454
app.commandLine.appendSwitch('enable-zero-copy');
55-
app.commandLine.appendSwitch('enable-features', 'WebGLDraftExtensions,SharedArrayBuffer');
55+
// WebGPU 支持(Three.js WebGPURenderer 自动检测,不可用时回退 WebGL2)
56+
app.commandLine.appendSwitch('enable-features', 'WebGPU,SharedArrayBuffer');
5657

57-
// Windows: ANGLE + Direct3D 11
58+
// Windows: ANGLE + Direct3D 11(WebGPU 不可用时作为 WebGL 回退)
5859
// 注意:值必须是 'd3d11',曾误写为 'gl' 导致 ANGLE 被锁定 OpenGL 后端,
5960
// 引发 WEBGL_lose_context 缺失、上下文异常及渲染卡顿
6061
if (process.platform === 'win32') {
6162
app.commandLine.appendSwitch('use-angle', 'd3d11');
6263
}
6364

64-
// macOS: Metal
65+
// macOS: Metal(WebGPU 原生使用 Metal API)
6566
if (process.platform === 'darwin') {
6667
app.commandLine.appendSwitch('enable-metal', '1');
6768
}

‎client/src/features/pomodoro/components/chronos/ChronosCanvas.tsx‎

Lines changed: 37 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
11
/**
2-
* ChronosCanvas — 时间生物容器
2+
* ChronosCanvas — 时间生物容器(R3F 3D 渲染版)
33
*
4-
* R3F Canvas 包裹 ChronosParticleField(粒子主体)+ ChronosCreature(辅助形态)。
5-
* compact 尺寸上限提升至 480px(需求 1:普通视图 3D 足够大)。
6-
* 中央 overlay 仅保留时间数字(需求 4:提示语移出球体,由父组件状态行承担)。
4+
* R3F + WebGPU 渲染的 3D 粒子球体(ChronosSphere),替代 2D Canvas 版:
5+
* 保留 3D 球面坐标 + 透视投影的立体观感,渲染走 THREE.Points(GPU 高效),
6+
* 1500 粒子轻松 60fps,位置算法与 2D 版一致(静态分布 + 增量角度)。
77
*
8-
* 3D 空间:camera z=5.5 fov=50(视锥半高 ≈2.57),比原 z=5.0 fov=45 增大约 24%,
9-
* 粒子场全态(asleep canopy≤2.0, radius=1.8)完整落在视锥内。
8+
* compact 尺寸上限 480px;中央 overlay 仅保留时间数字(提示语移出球体)。
9+
* 时间显示:阶段开始 5s + hover 显示(父组件 showTime prop)。
1010
*
11-
* 时间显示:使用 CSS 动画实现呼吸级辉光脉冲(每周期 3s),与 60bpm 心跳同频反射。
11+
* 降级策略:系统 reduced-motion / 性能 low 档 → degraded(减粒子),动画保留不静态化。
1212
*
13-
* 降级策略:系统 reduced-motion / 性能 low 档 → degraded(减粒子+跳帧+降 dpr),
14-
* 但保留粒子形态与动画(永不静态化);仅 WebGL 不可用时回退 2D 静态。
15-
*
16-
* @ai-context: Chronos 渲染容器;描述符 particleMorphs,色板 chronosStyles。
13+
* @ai-context: Chronos 渲染容器;3D 粒子球 ChronosSphere,WebGPU 渲染后端。
1714
*/
18-
import { useMemo, useRef, memo } from 'react';
1915
import { Canvas } from '@react-three/fiber';
16+
import { useRef, useState, memo } from 'react';
17+
import * as THREE from 'three';
18+
import WebGPURenderer from 'three/src/renderers/webgpu/WebGPURenderer.js';
2019
import { useSceneTheme } from '@/lib/3d/hooks/useSceneTheme';
2120
import { useReducedMotion } from '@/hooks/useReducedMotion';
2221
import { usePerformanceModeStore } from '@/lib/performance/usePerformanceMode';
2322
import { toChronosState } from './chronosState';
2423
import { CHRONOS_PALETTES } from './chronosStyles';
2524
import type { Mood } from './particleMorphs';
26-
import { ChronosCreature } from './ChronosCreature';
27-
import { ChronosParticleField } from './ChronosParticleField';
25+
import { ChronosSphere } from './ChronosSphere';
2826

2927
/** 长按判定阈值(ms):长按=中止/放弃(回沉睡) */
3028
const LONG_PRESS_MS = 800;
@@ -48,50 +46,12 @@ interface ChronosCanvasProps {
4846
onLongPress?: () => void;
4947
/** 右键点击生物(沉浸模式入口,父组件处理) */
5048
onContextMenu?: (e: React.MouseEvent) => void;
49+
/** 阶段开始 5s 显示标记(父组件检测运行起始沿传入;hover 时同样显示) */
50+
showTime?: boolean;
5151
/** 中央显示内容(时间字符串) */
5252
timeStr: string;
5353
}
5454

55-
/** WebGL 可用性检测(不可用时回退 2D 静态) */
56-
function isWebGLAvailable(): boolean {
57-
try {
58-
const c = document.createElement('canvas');
59-
return !!(c.getContext('webgl') || c.getContext('experimental-webgl'));
60-
} catch {
61-
return false;
62-
}
63-
}
64-
65-
/** 2D 静态兜底(仅 WebGL 不可用):显式宽高,只显示时间(状态行由父组件渲染) */
66-
function ChronosStatic({ mode, timeStr }: { mode: 'full' | 'compact'; timeStr: string }) {
67-
return (
68-
<div
69-
className={mode === 'full'
70-
? 'absolute inset-0 flex items-center justify-center'
71-
: 'relative flex items-center justify-center w-[clamp(240px,52vmin,480px)] h-[clamp(240px,52vmin,480px)]'}
72-
aria-label={timeStr}
73-
role="timer"
74-
>
75-
<div
76-
className="rounded-full shrink-0"
77-
style={{
78-
width: 'min(70%, 320px)',
79-
minWidth: '160px',
80-
aspectRatio: '1',
81-
background: 'radial-gradient(circle at 35% 30%, rgba(34,211,238,0.25) 0%, #0C152433 55%, transparent 75%)',
82-
border: '2px solid rgba(34,211,238,0.35)',
83-
boxShadow: '0 0 40px rgba(34,211,238,0.2)',
84-
}}
85-
/>
86-
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
87-
<span className="font-timer font-light tracking-tight leading-none text-text-primary" style={{ fontSize: mode === 'full' ? 'clamp(3rem, 9vmin, 6rem)' : 'clamp(2rem, 8vmin, 4.5rem)', fontVariantNumeric: 'tabular-nums' }}>
88-
{timeStr}
89-
</span>
90-
</div>
91-
</div>
92-
);
93-
}
94-
9555
// P0-2 React.memo:TimerFace 因 completedCount/currentGoal 等非 tick 字段
9656
// 重渲染时,粒子容器跳过 reconcile(timeStr 变化时 props 不等,正常重渲染)
9757
export const ChronosCanvas = memo(function ChronosCanvas({
@@ -107,22 +67,24 @@ export const ChronosCanvas = memo(function ChronosCanvas({
10767
onTap,
10868
onLongPress,
10969
onContextMenu,
70+
showTime = false,
11071
timeStr,
11172
}: ChronosCanvasProps) {
11273
const theme = useSceneTheme();
113-
// 降级(不静态化):系统 reduced-motion 或性能 low 档 → 减粒子/跳帧/降 dpr
74+
// 降级(不静态化):系统 reduced-motion 或性能 low 档 → 减粒子
11475
const reduced = useReducedMotion();
11576
const perfMode = usePerformanceModeStore((s) => s.mode);
11677
const degraded = reduced || perfMode === 'low';
117-
const webgl = useMemo(() => isWebGLAvailable(), []);
11878

11979
const chronosState = toChronosState({ isArmed, isRunning, isPaused, phase, isStepDive });
12080
const stateColor = CHRONOS_PALETTES[theme][chronosState].glow;
12181
const progress = totalSeconds > 0 ? remainingSeconds / totalSeconds : 1;
12282

123-
// P0-9:秒数变化心跳脉冲改 CSS animation 重启(key={remainingSeconds} 触发),
124-
// 不再每秒 2 次 setState(脉冲开/关)造成额外 React 渲染
125-
// 容器层统一手势(3D 与静态回退交互一致)
83+
// ── 时间显示隐藏化:父组件传入 showTime(阶段开始 5s),hover 粒子团时显示 ──
84+
const [hovering, setHovering] = useState(false);
85+
const timeVisible = showTime || hovering;
86+
87+
// 容器层统一手势
12688
const pointerDownRef = useRef(0);
12789
const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
12890
const longPressFiredRef = useRef(false);
@@ -153,51 +115,45 @@ export const ChronosCanvas = memo(function ChronosCanvas({
153115
? 'absolute inset-0 flex items-center justify-center'
154116
: 'relative w-[clamp(240px,52vmin,480px)] h-[clamp(240px,52vmin,480px)] overflow-hidden';
155117

156-
// WebGL 不可用 → 2D 静态兜底(唯一静态化路径)
157-
if (!webgl) {
158-
return <ChronosStatic mode={mode} timeStr={timeStr} />;
159-
}
160-
161118
return (
162119
<div
163120
className={`${containerClass} cursor-pointer`}
164121
onPointerDown={handlePointerDown}
165122
onPointerUp={handlePointerUp}
166123
onPointerLeave={cancelLongPress}
167124
onContextMenu={onContextMenu}
125+
onMouseEnter={() => setHovering(true)}
126+
onMouseLeave={() => setHovering(false)}
168127
role="timer"
169128
aria-label={timeStr}
170129
>
130+
{/* 3D 粒子球(R3F + WebGPU;THREE.Points 高效渲染,canvas 填满容器) */}
171131
<Canvas
172-
/* camera z=5.5 fov=50:视锥半高 ≈2.57,比原 z=5.0 fov=45 增大约 24%,
173-
asleep 半径 1.8 + 漂移余量 ≈2.0 完整落在视锥内;
174-
扩大空间让粒子聚集动画有更大的运动范围 */
175-
camera={{ position: [0, 0, 5.5], fov: 50 }}
176-
dpr={degraded ? [1, 1] : [1, 1.5]}
177-
gl={{ antialias: true, alpha: true }}
178-
className="!absolute inset-0"
179-
style={{ pointerEvents: 'none' }}
132+
gl={(c) => new WebGPURenderer({ canvas: c, antialias: true, alpha: true, stencil: false })}
133+
dpr={[1, degraded ? 1 : 1.5]}
134+
camera={{ fov: 50, near: 0.1, far: 20, position: [0, 0, 4.5] }}
135+
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', background: 'transparent' }}
136+
onCreated={({ gl }) => {
137+
gl.toneMapping = THREE.ACESFilmicToneMapping;
138+
gl.toneMappingExposure = 1.0;
139+
}}
180140
>
181-
<ambientLight intensity={0.6} />
182-
<pointLight position={[3, 3, 4]} intensity={1.0} color={stateColor} />
183-
<pointLight position={[-3, -2, -3]} intensity={0.4} color={stateColor} />
184-
<ChronosParticleField
141+
<ChronosSphere
185142
state={chronosState}
186-
theme={theme}
187143
mood={mood}
188144
progress={progress}
189145
degraded={degraded}
190146
/>
191-
<ChronosCreature state={chronosState} theme={theme} />
192147
</Canvas>
193148
{/* 进度条:容器顶部边缘细条(剩余时间比例,图形化时间感知) */}
194149
<div className="absolute top-0 left-0 right-0 pointer-events-none" aria-hidden="true">
195150
<div className="h-[2px] rounded-full" style={{ width: `${progress * 100}%`, background: stateColor, opacity: 0.45, transition: 'width 1s linear' }} />
196151
</div>
197-
{/* 中央状态区:仅时间数字(需求 4:状态名/引导语移出球体)。
198-
时间与粒子球同源和谐:状态辉光色 + 同色 textShadow,半透明胶囊背景提升可读性
152+
{/* 中央状态区:仅时间数字。
153+
时间显示隐藏化:阶段开始显示 5s + 鼠标悬停粒子团时显示,否则隐藏
199154
辉光脉冲动画:3s 周期呼吸级脉冲,与 60bpm 心跳同频反射
200155
秒数心跳脉冲:key 随秒变化重置 chronos-second-pulse 动画(scale 1.05 → 1) */}
156+
{timeVisible && (
201157
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
202158
<span
203159
key={remainingSeconds}
@@ -213,6 +169,7 @@ export const ChronosCanvas = memo(function ChronosCanvas({
213169
{timeStr}
214170
</span>
215171
</div>
172+
)}
216173
{/* 时间辉光脉冲动画 keyframes */}
217174
<style>{`
218175
@keyframes chronos-glow-pulse {

0 commit comments

Comments
 (0)