1313import { useRef , useMemo , useEffect } from 'react' ;
1414import { useFrame } from '@react-three/fiber' ;
1515import * as THREE from 'three' ;
16+ import { useEffectiveTier } from '@/lib/performance/usePerformanceMode' ;
1617import type { ChronosPhase , ChronosStyle } from './chronosStyles' ;
1718import { CHRONOS_PHASES } from './chronosStyles' ;
1819
20+ /** 长按判定阈值(ms):超过则触发 onLongPress 而非 onTap */
21+ const LONG_PRESS_MS = 800 ;
22+ /** 点击判定阈值(ms):超过则不视为 tap */
23+ const TAP_MAX_MS = 500 ;
24+
1925interface ChronosSphereProps {
2026 /** 生物阶段 */
2127 phase : ChronosPhase ;
@@ -29,6 +35,8 @@ interface ChronosSphereProps {
2935 bloom ?: boolean ;
3036 /** 点击生物回调 */
3137 onTap ?: ( ) => void ;
38+ /** 长按生物回调(进入沉睡) */
39+ onLongPress ?: ( ) => void ;
3240}
3341
3442/** 简易伪随机噪声(确定性,避免重渲染抖动) */
@@ -38,13 +46,24 @@ function noise(x: number, y: number, z: number, t: number): number {
3846 ) ;
3947}
4048
41- export function ChronosSphere ( { phase, style, intensity = 50 , ambientLight = 0.5 , bloom = false , onTap } : ChronosSphereProps ) {
49+ export function ChronosSphere ( { phase, style, intensity = 50 , ambientLight = 0.5 , bloom = false , onTap, onLongPress } : ChronosSphereProps ) {
4250 const meshRef = useRef < THREE . Mesh > ( null ) ;
4351 const pointsRef = useRef < THREE . Points > ( null ) ;
4452 const ringRef = useRef < THREE . Mesh > ( null ) ;
4553 const ringMatRef = useRef < THREE . MeshBasicMaterial > ( null ) ;
4654 const materialRef = useRef < THREE . MeshStandardMaterial > ( null ) ;
4755
56+ // 性能档位(P3-15 同源):low 隔 2 帧更新粒子/顶点
57+ const tier = useEffectiveTier ( ) ;
58+ const frameSkip = tier === 'low' ? 3 : tier === 'medium' ? 2 : 1 ;
59+ const frameRef = useRef ( 0 ) ;
60+
61+ // 当前值(useFrame 中向 targets 收敛,实现平滑过渡)
62+ const current = useRef ( {
63+ color : new THREE . Color ( CHRONOS_PHASES [ phase ] . body ) ,
64+ emissive : CHRONOS_PHASES [ phase ] . emissiveIntensity ,
65+ } ) ;
66+
4867 // 目标值(阶段变化时更新,useFrame 中 lerp)
4968 const targets = useRef ( {
5069 bodyColor : new THREE . Color ( CHRONOS_PHASES [ phase ] . body ) ,
@@ -61,6 +80,39 @@ export function ChronosSphere({ phase, style, intensity = 50, ambientLight = 0.5
6180 targets . current . particleRadius = palette . particleRadius ;
6281 targets . current . spin = palette . spinSpeed ;
6382
83+ // ── 手势:tap(<500ms)与 long-press(800ms)区分 ──
84+ const pointerDownRef = useRef ( 0 ) ;
85+ const longPressTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
86+ const longPressFiredRef = useRef ( false ) ;
87+
88+ const cancelLongPress = ( ) => {
89+ if ( longPressTimerRef . current ) {
90+ clearTimeout ( longPressTimerRef . current ) ;
91+ longPressTimerRef . current = null ;
92+ }
93+ } ;
94+
95+ const handlePointerDown = ( e : { stopPropagation : ( ) => void } ) => {
96+ e . stopPropagation ( ) ;
97+ longPressFiredRef . current = false ;
98+ pointerDownRef . current = Date . now ( ) ;
99+ cancelLongPress ( ) ;
100+ if ( onLongPress ) {
101+ longPressTimerRef . current = setTimeout ( ( ) => {
102+ longPressFiredRef . current = true ;
103+ onLongPress ( ) ;
104+ } , LONG_PRESS_MS ) ;
105+ }
106+ } ;
107+
108+ const handlePointerUp = ( e : { stopPropagation : ( ) => void } ) => {
109+ e . stopPropagation ( ) ;
110+ cancelLongPress ( ) ;
111+ if ( longPressFiredRef . current ) return ;
112+ const elapsed = Date . now ( ) - pointerDownRef . current ;
113+ if ( elapsed < TAP_MAX_MS && onTap ) onTap ( ) ;
114+ } ;
115+
64116 // 有机球体:细分二十面体 + 原始顶点缓存
65117 const geometry = useMemo ( ( ) => {
66118 const geo = new THREE . IcosahedronGeometry ( 1 , 3 ) ;
@@ -111,11 +163,23 @@ export function ChronosSphere({ phase, style, intensity = 50, ambientLight = 0.5
111163 prevBloom . current = bloom ;
112164 } , [ bloom ] ) ;
113165
166+ // 卸载时清理长按定时器
167+ useEffect ( ( ) => {
168+ return ( ) => {
169+ if ( longPressTimerRef . current ) clearTimeout ( longPressTimerRef . current ) ;
170+ } ;
171+ } , [ ] ) ;
172+
114173 // 每帧动画
115174 useFrame ( ( { clock } ) => {
116175 const t = clock . elapsedTime ;
117176 const safeDelta = Math . min ( clock . getDelta ( ) , 0.1 ) ;
118177
178+ // 低档跳帧:非更新帧直接返回(呼吸缩放仍每帧执行,视觉无感)
179+ frameRef . current += 1 ;
180+ const shouldUpdate = frameRef . current % frameSkip === 0 ;
181+ const step = frameSkip ;
182+
119183 // 绽放进度推进(P1 完成动画)
120184 if ( isBlooming . current ) {
121185 bloomRef . current = Math . min ( 1 , bloomRef . current + safeDelta * 1.2 ) ;
@@ -129,50 +193,61 @@ export function ChronosSphere({ phase, style, intensity = 50, ambientLight = 0.5
129193 // 守护灵联动:低分 → 节奏放缓(P2)
130194 const focusFactor = 0.7 + ( intensity / 100 ) * 0.6 ;
131195
196+ // 颜色/发光平滑过渡:向目标 lerp(阶段切换不再瞬跳)
197+ const lerpK = Math . min ( safeDelta * 2.5 , 1 ) ;
198+ current . current . color . lerp ( targets . current . bodyColor , lerpK ) ;
199+ current . current . emissive += ( targets . current . emissive - current . current . emissive ) * lerpK ;
200+
132201 // 环境光补偿:暗环境增强自发光,亮环境减弱(P2)
133202 if ( materialRef . current ) {
134203 const ambientBoost = 1 + ( 0.5 - ambientLight ) * 1.2 ;
135- materialRef . current . emissiveIntensity = targets . current . emissive * Math . max ( 0.4 , ambientBoost ) ;
204+ materialRef . current . emissiveIntensity = current . current . emissive * Math . max ( 0.4 , ambientBoost ) ;
205+ materialRef . current . color . copy ( current . current . color ) ;
136206 }
137207
138208 // 呼吸缩放
139209 if ( meshRef . current ) {
140210 const breath = 1 + Math . sin ( t * 1.2 * focusFactor ) * targets . current . breathe ;
141211 meshRef . current . scale . setScalar ( breath * bloomPulse ) ;
142212 meshRef . current . rotation . y += safeDelta * targets . current . spin * focusFactor ;
213+ }
143214
215+ // 跳帧更新:顶点扰动 + 粒子(low/medium 档降开销)
216+ if ( shouldUpdate ) {
144217 // 顶点扰动(细胞膜)
145- const pos = geometry . attributes . position as THREE . BufferAttribute ;
146- const original = geometry . userData . original as Float32Array ;
147- const amp = style . noiseAmplitude * ( 0.6 + targets . current . breathe * 8 ) ;
148- for ( let i = 0 ; i < pos . count ; i ++ ) {
149- const i3 = i * 3 ;
150- const ox = original [ i3 ] ;
151- const oy = original [ i3 + 1 ] ;
152- const oz = original [ i3 + 2 ] ;
153- const n = noise ( ox * 4 , oy * 4 , oz * 4 , t * focusFactor ) ;
154- pos . setXYZ ( i3 / 3 , ox + n * amp , oy + n * amp , oz + n * amp ) ;
218+ if ( meshRef . current ) {
219+ const pos = geometry . attributes . position as THREE . BufferAttribute ;
220+ const original = geometry . userData . original as Float32Array ;
221+ const amp = style . noiseAmplitude * ( 0.6 + targets . current . breathe * 8 ) ;
222+ for ( let i = 0 ; i < pos . count ; i ++ ) {
223+ const i3 = i * 3 ;
224+ const ox = original [ i3 ] ;
225+ const oy = original [ i3 + 1 ] ;
226+ const oz = original [ i3 + 2 ] ;
227+ const n = noise ( ox * 4 , oy * 4 , oz * 4 , t * focusFactor ) ;
228+ pos . setXYZ ( i3 / 3 , ox + n * amp , oy + n * amp , oz + n * amp ) ;
229+ }
230+ pos . needsUpdate = true ;
155231 }
156- pos . needsUpdate = true ;
157- }
158232
159- // 粒子:向球体聚拢/散开 + 绽放爆发
160- if ( pointsRef . current ) {
161- const posAttr = particleGeo . attributes . position as THREE . BufferAttribute ;
162- const targetR = targets . current . particleRadius ;
163- for ( let i = 0 ; i < posAttr . count ; i ++ ) {
164- const x = posAttr . getX ( i ) ;
165- const y = posAttr . getY ( i ) ;
166- const z = posAttr . getZ ( i ) ;
167- const len = Math . sqrt ( x * x + y * y + z * z ) || 1 ;
168- const targetLen = targetR * ( isBlooming . current ? 1 + bloomRef . current * 2 : 1 ) ;
169- // 向目标半径缓动
170- const newLen = len + ( targetLen - len ) * Math . min ( safeDelta * 1.5 , 1 ) ;
171- const k = newLen / len ;
172- posAttr . setXYZ ( i , x * k , y * k , z * k ) ;
233+ // 粒子:向球体聚拢/散开 + 绽放爆发
234+ if ( pointsRef . current ) {
235+ const posAttr = particleGeo . attributes . position as THREE . BufferAttribute ;
236+ const targetR = targets . current . particleRadius ;
237+ for ( let i = 0 ; i < posAttr . count ; i ++ ) {
238+ const x = posAttr . getX ( i ) ;
239+ const y = posAttr . getY ( i ) ;
240+ const z = posAttr . getZ ( i ) ;
241+ const len = Math . sqrt ( x * x + y * y + z * z ) || 1 ;
242+ const targetLen = targetR * ( isBlooming . current ? 1 + bloomRef . current * 2 : 1 ) ;
243+ // 向目标半径缓动(step 补偿跳帧)
244+ const newLen = len + ( targetLen - len ) * Math . min ( safeDelta * step * 1.5 , 1 ) ;
245+ const k = newLen / len ;
246+ posAttr . setXYZ ( i , x * k , y * k , z * k ) ;
247+ }
248+ posAttr . needsUpdate = true ;
249+ pointsRef . current . rotation . y += safeDelta * 0.15 ;
173250 }
174- posAttr . needsUpdate = true ;
175- pointsRef . current . rotation . y += safeDelta * 0.15 ;
176251 }
177252
178253 // 呼吸波纹(扩散环)
@@ -185,14 +260,18 @@ export function ChronosSphere({ phase, style, intensity = 50, ambientLight = 0.5
185260 } ) ;
186261
187262 return (
188- < group onClick = { ( e ) => { e . stopPropagation ( ) ; onTap ?.( ) ; } } >
263+ < group
264+ onPointerDown = { handlePointerDown }
265+ onPointerUp = { handlePointerUp }
266+ onPointerLeave = { cancelLongPress }
267+ >
189268 { /* 有机球体主体 */ }
190269 < mesh ref = { meshRef } geometry = { geometry } castShadow >
191270 < meshStandardMaterial
192271 ref = { materialRef }
193- color = { CHRONOS_PHASES [ phase ] . body }
272+ color = { current . current . color }
194273 emissive = { style . emissiveColor }
195- emissiveIntensity = { targets . current . emissive }
274+ emissiveIntensity = { current . current . emissive }
196275 roughness = { 0.35 }
197276 metalness = { 0.15 }
198277 flatShading
0 commit comments