Skip to content

Commit 2f96df2

Browse files
committed
feat(pomodoro): Chronos 粒子球全面修复与 TimerFace 高频区重构
- 呼吸阶段 30s:awaken 启动倒计时、新增 startBreathingDive/skipBreathingDive(专注→呼吸缓解,完成自动恢复原剩余时间) - skip() 修复:休息阶段跳过后 isArmed=false 不再误入呼吸态 - 粒子双主题纹理:deep-sea 生物荧光软光晕(Additive)/ aurora 水晶碎屑六边形(Normal) - 专注聚集动画 + 休息态 river 循环河流动画(沿 Y 轴环绕 + 垂直起伏) - 动画流畅性:prevTimeRef 手动 delta、material 每帧更新、accel 幂级缓动 - 粒子错位修复:聚集计算改用 morph.visibleRatio 固定分母 - 卫星按钮 ResizeObserver 定位(容器实际尺寸)并移入共享坐标系 - 时间显示秒数心跳脉冲 + 辉光呼吸动画 - 计时结束反馈:breathing_resume 动作信号区分语义,不误触发涟漪/计数/庆祝层
1 parent df90cf2 commit 2f96df2

26 files changed

Lines changed: 1905 additions & 1597 deletions

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

Lines changed: 0 additions & 98 deletions
This file was deleted.

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

Lines changed: 56 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,23 @@
22
* ImmersiveTimer — 「潮汐穹顶」沉浸模式
33
*
44
* 全屏渐变色场背景(随进度变化),
5-
* 弧形光带进度条 + 呼吸缩放大字号倒计时(ImmersiveRing),
6-
* 底部极简 icon-only 操作(ImmersiveControls)。
5+
* 中央 Chronos 时间生物(full 模式)+ 底部极简 icon-only 操作(ImmersiveControls)。
76
*
87
* @ai-context: 3.8 心流音乐引擎 + 3.13 具身学习休息引导集成于此;
9-
* 中央圆环与底部操作区已拆至 ImmersiveRing / ImmersiveControls
10-
* (单文件 ≤300 行规范)。
8+
* 底部操作区已拆至 ImmersiveControls(单文件 ≤300 行规范)。
119
*/
1210
import { useMemo, useState, useEffect } from 'react';
1311
import { motion } from 'framer-motion';
1412
import { usePomodoroStore } from '../store/usePomodoroStore';
15-
import { useShallow } from 'zustand/react/shallow';
16-
import { useReducedMotion } from '@/hooks/useReducedMotion';
1713
import { BEAT } from '@/lib/animation/springConfig';
1814
import { useAudioPrefsStore } from '@/lib/audio/audioPrefsStore';
1915
import { AnchorReminderOverlay } from './AnchorReminderOverlay';
2016
import { ChronosCanvas } from './chronos/ChronosCanvas';
21-
import { ImmersiveRing } from './ImmersiveRing';
17+
import { ChronosStateRow } from './chronos/ChronosStateRow';
18+
import { toChronosState } from './chronos/chronosState';
2219
import { ImmersiveControls } from './ImmersiveControls';
2320
import { useFlowMusic } from '@/hooks/useFlowMusic';
2421

25-
const PHASE_LABELS: Record<string, string> = {
26-
work: '专注中',
27-
short_break: '休息中',
28-
long_break: '休息中',
29-
};
30-
3122
interface ImmersiveTimerProps {
3223
/** 背景音(白噪音)开关状态 */
3324
whiteNoiseEnabled?: boolean;
@@ -43,12 +34,6 @@ interface ImmersiveTimerProps {
4334
focusScore?: number;
4435
/** 3.8 心流音乐引擎是否激活 */
4536
flowMusicEnabled?: boolean;
46-
/** 专注完成绽放触发(Chronos 时间生物) */
47-
bloom?: boolean;
48-
/** 环境光亮度 0-1(P2 暗环境自发光补偿) */
49-
ambientLight?: number;
50-
/** Chronos 生物形态开关(缺省 true,关闭回退经典环) */
51-
chronosEnabled?: boolean;
5237
}
5338

5439
/** 3.13 具身学习休息活动列表 */
@@ -101,33 +86,42 @@ export default function ImmersiveTimer({
10186
lastSessionKeywords,
10287
focusScore = 0,
10388
flowMusicEnabled = false,
104-
bloom = false,
105-
ambientLight = 0.5,
106-
chronosEnabled = true,
10789
}: ImmersiveTimerProps) {
108-
const prefersReduced = useReducedMotion();
109-
110-
const {
111-
remainingSeconds,
112-
totalSeconds,
113-
phase,
114-
isRunning,
115-
currentGoal,
116-
pause,
117-
resume,
118-
reset,
119-
skip,
120-
} = usePomodoroStore(useShallow(s => s));
90+
// P0-1 细粒度订阅:整 store 订阅(useShallow(s => s))会在任何字段变化时
91+
// 重渲染沉浸层;remainingSeconds 每秒 tick 不可避免,但其余字段独立订阅
92+
// 避免 settings/presets 等无关变化波及本层
93+
const remainingSeconds = usePomodoroStore((s) => s.remainingSeconds);
94+
const totalSeconds = usePomodoroStore((s) => s.totalSeconds);
95+
const phase = usePomodoroStore((s) => s.phase);
96+
const isRunning = usePomodoroStore((s) => s.isRunning);
97+
const isPaused = usePomodoroStore((s) => s.isPaused);
98+
const isArmed = usePomodoroStore((s) => s.isArmed);
99+
const activePreset = usePomodoroStore((s) => s.activePreset);
100+
const isStepDive = usePomodoroStore((s) => s.isStepDive);
101+
const currentGoal = usePomodoroStore((s) => s.currentGoal);
102+
// 动作(稳定引用)
103+
const pause = usePomodoroStore((s) => s.pause);
104+
const startBreathingDive = usePomodoroStore((s) => s.startBreathingDive);
105+
const skipBreathingDive = usePomodoroStore((s) => s.skipBreathingDive);
106+
const resume = usePomodoroStore((s) => s.resume);
107+
const reset = usePomodoroStore((s) => s.reset);
108+
const skip = usePomodoroStore((s) => s.skip);
121109

122-
// Chronos 点击交互(时间生物 = 核心交互点):
123-
// 专注 → 暂停/继续;休息 → 提前结束休息;长按 → 沉睡
110+
// Chronos 点击交互(设计详解状态机):
111+
// 专注 → 呼吸缓解(30s)/继续;呼吸(运行中) → 跳过呼吸直接专注;休息 → 提前结束;长按 → 沉睡
124112
const handleChronosTap = () => {
125-
if (phase === 'work') {
126-
if (isRunning) pause();
113+
const cs = toChronosState({ isArmed, isRunning, isPaused, phase, isStepDive });
114+
if (cs === 'focus') {
115+
if (isRunning) startBreathingDive();
127116
else resume();
128-
} else {
117+
} else if (cs === 'breathing') {
118+
// 呼吸态(迈步/呼吸缓解)运行中 → 跳过呼吸直接进入专注
119+
if (isRunning) skipBreathingDive();
120+
else resume();
121+
} else if (cs === 'short_break' || cs === 'long_break') {
129122
skip();
130123
}
124+
// asleep 在沉浸模式不应出现(进入沉浸必经运行/暂停),忽略
131125
};
132126
const handleChronosLongPress = () => {
133127
reset();
@@ -155,7 +149,6 @@ export default function ImmersiveTimer({
155149
const minutes = Math.floor(remainingSeconds / 60);
156150
const seconds = remainingSeconds % 60;
157151
const timeStr = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
158-
const label = PHASE_LABELS[phase] ?? '专注中';
159152

160153
const backgroundGradient = useMemo(
161154
() => getBackgroundGradient(progressPercent),
@@ -164,18 +157,6 @@ export default function ImmersiveTimer({
164157

165158
const isBreak = phase === 'short_break' || phase === 'long_break';
166159

167-
// 经典环呼吸动画参数(chronosEnabled=false 时回退)
168-
const breatheAnimation = prefersReduced
169-
? {}
170-
: {
171-
scale: [1, 1.02, 1],
172-
transition: {
173-
duration: BEAT.x5 / 100,
174-
repeat: Infinity,
175-
ease: 'easeInOut' as const,
176-
},
177-
};
178-
179160
// 3.13 休息活动轮换索引
180161
const [activityIndex, setActivityIndex] = useState(0);
181162
useEffect(() => {
@@ -245,23 +226,29 @@ export default function ImmersiveTimer({
245226
</motion.div>
246227
)}
247228

248-
{/* 中央计时器区域 — Chronos 时间生物(可回退经典环) */}
249-
{chronosEnabled ? (
250-
<ChronosCanvas
251-
mode="full"
252-
phase={phase}
253-
isRunning={isRunning}
254-
remainingSeconds={remainingSeconds}
255-
started
256-
intensity={focusScore > 0 ? focusScore : 50}
257-
ambientLight={ambientLight}
258-
bloom={bloom}
259-
onTap={handleChronosTap}
260-
onLongPress={handleChronosLongPress}
261-
timeStr={timeStr}
229+
{/* 中央计时器区域 — Chronos 时间生物(唯一形态) */}
230+
<ChronosCanvas
231+
mode="full"
232+
phase={phase}
233+
isRunning={isRunning}
234+
isPaused={isPaused}
235+
isArmed={isArmed}
236+
remainingSeconds={remainingSeconds}
237+
totalSeconds={totalSeconds}
238+
isStepDive={isStepDive}
239+
mood={activePreset?.mood}
240+
onTap={handleChronosTap}
241+
onLongPress={handleChronosLongPress}
242+
timeStr={timeStr}
243+
/>
244+
{/* 状态行(沉浸深色背景专用变体):仅 work 阶段显示(休息时由活动建议卡占据该区域) */}
245+
{phase === 'work' && (
246+
<ChronosStateRow
247+
input={{ isArmed, isRunning, isPaused, phase }}
248+
variant="immersive"
249+
hint={isRunning ? '点击暂停 · 长按放弃' : '点击继续'}
250+
className="absolute bottom-32 left-0 right-0"
262251
/>
263-
) : (
264-
<ImmersiveRing progress={progress} timeStr={timeStr} label={label} breatheAnimation={breatheAnimation} />
265252
)}
266253

267254
{/* T2 记忆锚点提醒浮层 — work 阶段每 12 分钟一句话要点,15 秒自动消失 */}

0 commit comments

Comments
 (0)