Skip to content

Commit 1d7ab71

Browse files
committed
feat(pomodoro): Chronos 时间生物可视化(P0-P2)
- 新增 ChronosSphere 3D 生物主体:有机球体(顶点扰动)+ 粒子光环聚散 + 呼吸波纹环,useFrame 驱动与 tick 解耦 - 双风格深度定制:深潜(深海蓝绿冷光)/ 极光(紫青渐变),跟随 useSceneTheme 自动切换 - 六阶段状态色板:沉睡/呼吸/专注/短休/长休/濒临结束(最后 5 分钟暖色渐变) - 接入普通视图(compact)与沉浸模式(full),可回退经典 TimerRing/ImmersiveRing(chronosEnabled 开关) - 专注完成绽放动画:粒子爆发 + 波纹扩散(bloom 边沿触发) - P2 守护灵联动:focusScore 驱动呼吸节奏/光效速度 - P2 环境光自适应:摄像头亮度采样(缺省关闭,隐私优先),暗环境自发光补偿 - P2 AI 规划师:PlannerBlock 本地节律规划(任务数 → 推荐时长 → 分组计划) - 设置页新增「时间生物」区块与「今日下潜计划」 - 死代码清理:ChronosPhasePalette 导出降级为内部 interface
1 parent 317d84b commit 1d7ab71

9 files changed

Lines changed: 797 additions & 11 deletions

File tree

‎client/src/features/pomodoro/components/ImmersiveTimer.tsx‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useReducedMotion } from '@/hooks/useReducedMotion';
1717
import { BEAT } from '@/lib/animation/springConfig';
1818
import { useAudioPrefsStore } from '@/lib/audio/audioPrefsStore';
1919
import { AnchorReminderOverlay } from './AnchorReminderOverlay';
20+
import { ChronosCanvas } from './chronos/ChronosCanvas';
2021
import { ImmersiveRing } from './ImmersiveRing';
2122
import { ImmersiveControls } from './ImmersiveControls';
2223
import { useFlowMusic } from '@/hooks/useFlowMusic';
@@ -42,6 +43,12 @@ interface ImmersiveTimerProps {
4243
focusScore?: number;
4344
/** 3.8 心流音乐引擎是否激活 */
4445
flowMusicEnabled?: boolean;
46+
/** 专注完成绽放触发(Chronos 时间生物) */
47+
bloom?: boolean;
48+
/** 环境光亮度 0-1(P2 暗环境自发光补偿) */
49+
ambientLight?: number;
50+
/** Chronos 生物形态开关(缺省 true,关闭回退经典环) */
51+
chronosEnabled?: boolean;
4552
}
4653

4754
/** 3.13 具身学习休息活动列表 */
@@ -94,6 +101,9 @@ export default function ImmersiveTimer({
94101
lastSessionKeywords,
95102
focusScore = 0,
96103
flowMusicEnabled = false,
104+
bloom = false,
105+
ambientLight = 0.5,
106+
chronosEnabled = true,
97107
}: ImmersiveTimerProps) {
98108
const prefersReduced = useReducedMotion();
99109

@@ -137,7 +147,9 @@ export default function ImmersiveTimer({
137147
[progressPercent],
138148
);
139149

140-
// 呼吸动画参数 — 匹配 BEAT.x5 (600ms 周期 → 实际用6s完整呼吸)
150+
const isBreak = phase === 'short_break' || phase === 'long_break';
151+
152+
// 经典环呼吸动画参数(chronosEnabled=false 时回退)
141153
const breatheAnimation = prefersReduced
142154
? {}
143155
: {
@@ -149,8 +161,6 @@ export default function ImmersiveTimer({
149161
},
150162
};
151163

152-
const isBreak = phase === 'short_break' || phase === 'long_break';
153-
154164
// 3.13 休息活动轮换索引
155165
const [activityIndex, setActivityIndex] = useState(0);
156166
useEffect(() => {
@@ -220,8 +230,23 @@ export default function ImmersiveTimer({
220230
</motion.div>
221231
)}
222232

223-
{/* 中央计时器区域 */}
224-
<ImmersiveRing progress={progress} timeStr={timeStr} label={label} breatheAnimation={breatheAnimation} />
233+
{/* 中央计时器区域 — Chronos 时间生物(可回退经典环) */}
234+
{chronosEnabled ? (
235+
<ChronosCanvas
236+
mode="full"
237+
phase={phase}
238+
isRunning={isRunning}
239+
remainingSeconds={remainingSeconds}
240+
started
241+
intensity={focusScore > 0 ? focusScore : 50}
242+
ambientLight={ambientLight}
243+
bloom={bloom}
244+
timeStr={timeStr}
245+
label={label}
246+
/>
247+
) : (
248+
<ImmersiveRing progress={progress} timeStr={timeStr} label={label} breatheAnimation={breatheAnimation} />
249+
)}
225250

226251
{/* T2 记忆锚点提醒浮层 — work 阶段每 12 分钟一句话要点,15 秒自动消失 */}
227252
<AnchorReminderOverlay />
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* ChronosCanvas — 时间生物容器
3+
*
4+
* R3F Canvas 包裹 ChronosSphere,按主题自动切换双风格(深潜/极光)。
5+
* 支持 full(沉浸全屏)与 compact(普通视图表盘尺寸)两种模式。
6+
* prefersReducedMotion 时回退 2D 静态形态(颜色随阶段变化,无动画)。
7+
*
8+
* @ai-context: Chronos 时间生物渲染容器,替代 TimerRing / ImmersiveRing 视觉层。
9+
*/
10+
import { Canvas } from '@react-three/fiber';
11+
import { useReducedMotion } from 'framer-motion';
12+
import { useSceneTheme } from '@/lib/3d/hooks/useSceneTheme';
13+
import { CHRONOS_STYLES, CHRONOS_PHASES, toChronosPhase, type ChronosPhase } from './chronosStyles';
14+
import { ChronosSphere } from './ChronosSphere';
15+
16+
interface ChronosCanvasProps {
17+
/** full = 沉浸全屏;compact = 普通视图表盘尺寸 */
18+
mode?: 'full' | 'compact';
19+
phase: 'work' | 'short_break' | 'long_break';
20+
isRunning: boolean;
21+
remainingSeconds: number;
22+
started: boolean;
23+
/** 守护灵分心分数 0-100(可选,P2) */
24+
intensity?: number;
25+
/** 环境光亮度 0-1(可选,P2 暗环境自发光补偿) */
26+
ambientLight?: number;
27+
/** 完成绽放触发(P1) */
28+
bloom?: boolean;
29+
onTap?: () => void;
30+
/** 中央显示内容(时间字符串) */
31+
timeStr: string;
32+
/** 中央显示标签(如 专注中) */
33+
label: string;
34+
}
35+
36+
/** 2D 静态回退形态(reduced motion / 极端低配) */
37+
function ChronosStatic({ phase, timeStr, label }: {
38+
phase: ChronosPhase;
39+
timeStr: string;
40+
label: string;
41+
}) {
42+
const phaseColor = CHRONOS_PHASES[phase].body;
43+
return (
44+
<div className="relative flex items-center justify-center w-full h-full">
45+
{/* 静态光环 + 渐变球体 */}
46+
<div
47+
className="rounded-full"
48+
style={{
49+
width: 'min(60%, 240px)',
50+
aspectRatio: '1',
51+
background: `radial-gradient(circle at 35% 30%, ${phaseColor}66 0%, #0C152433 55%, transparent 75%)`,
52+
border: `2px solid ${phaseColor}55`,
53+
boxShadow: `0 0 40px ${phaseColor}33`,
54+
}}
55+
/>
56+
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
57+
<span className="font-timer font-light tracking-tight leading-none text-text-primary" style={{ fontSize: 'clamp(2rem, 8vmin, 4.5rem)', fontVariantNumeric: 'tabular-nums' }}>
58+
{timeStr}
59+
</span>
60+
<span className="text-[11px] mt-2 font-medium tracking-[0.15em] uppercase text-text-tertiary">
61+
{label}
62+
</span>
63+
</div>
64+
</div>
65+
);
66+
}
67+
68+
export function ChronosCanvas({
69+
mode = 'full',
70+
phase,
71+
isRunning,
72+
remainingSeconds,
73+
started,
74+
intensity,
75+
ambientLight,
76+
bloom,
77+
onTap,
78+
timeStr,
79+
label,
80+
}: ChronosCanvasProps) {
81+
const theme = useSceneTheme();
82+
const reduced = useReducedMotion();
83+
const style = CHRONOS_STYLES[theme];
84+
const chronosPhase = toChronosPhase(phase, isRunning, remainingSeconds, started);
85+
86+
// 降级:减少动效偏好 → 2D 静态形态
87+
if (reduced) {
88+
return <ChronosStatic phase={chronosPhase} timeStr={timeStr} label={label} />;
89+
}
90+
91+
return (
92+
<div
93+
className={mode === 'full'
94+
? 'absolute inset-0 flex items-center justify-center pointer-events-none'
95+
: 'relative w-[clamp(150px,34vmin,280px)] h-[clamp(150px,34vmin,280px)] pointer-events-none'}
96+
aria-label={`${label} ${timeStr}`}
97+
role="timer"
98+
>
99+
<Canvas
100+
camera={{ position: [0, 0, 4.2], fov: 45 }}
101+
dpr={[1, 1.5]}
102+
gl={{ antialias: true, alpha: true }}
103+
className={mode === 'full' ? '!absolute inset-0' : ''}
104+
>
105+
<ambientLight intensity={0.6} />
106+
<pointLight position={[3, 3, 4]} intensity={1.2} />
107+
<pointLight position={[-3, -2, -3]} intensity={0.4} color={style.emissiveColor} />
108+
<ChronosSphere
109+
phase={chronosPhase}
110+
style={style}
111+
intensity={intensity}
112+
ambientLight={ambientLight}
113+
bloom={bloom}
114+
onTap={onTap}
115+
/>
116+
</Canvas>
117+
{/* 中央数字(HTML 层,与 3D 叠加) */}
118+
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
119+
<span
120+
className="font-timer font-light tracking-tight leading-none text-text-primary"
121+
style={{ fontSize: mode === 'full' ? 'clamp(3rem, 9vmin, 6rem)' : 'clamp(1.75rem, 7vmin, 3.5rem)', fontVariantNumeric: 'tabular-nums' }}
122+
>
123+
{timeStr}
124+
</span>
125+
<span className="text-[11px] mt-2 font-medium tracking-[0.15em] uppercase text-text-tertiary">
126+
{label}
127+
</span>
128+
</div>
129+
</div>
130+
);
131+
}

0 commit comments

Comments
 (0)