@@ -20,12 +20,12 @@ const FLOOR_RADIUS = 2.7
2020const FLOOR_SEGMENTS = 64
2121const FLOOR_OPACITY = 0.14
2222const FLOOR_Y = - 2.25
23- const DRAG_SENSITIVITY = 0.012
2423const ROTATION_DAMPING = 0.1
2524const WAVE_SPEED = 0.6
2625const WAVE_INTENSITY = 0.05
2726const FLOAT_SPEED = 0.9
2827const FLOAT_INTENSITY = 0.06
28+ const AUTO_ROTATION_INTERVAL_SECONDS = 3.8
2929
3030function normalizeAngle ( value : number ) : number {
3131 let angle = value
@@ -101,6 +101,13 @@ function buildFaceTexture(card: PhilosophyCardData): THREE.CanvasTexture {
101101 ctx . lineWidth = 6
102102 ctx . strokeRect ( 24 , 24 , canvas . width - 48 , canvas . height - 48 )
103103
104+ ctx . strokeStyle = 'rgba(56, 189, 248, 0.28)'
105+ ctx . lineWidth = 3
106+ ctx . beginPath ( )
107+ ctx . moveTo ( 74 , 148 )
108+ ctx . lineTo ( 300 , 148 )
109+ ctx . stroke ( )
110+
104111 ctx . fillStyle = 'rgba(125, 211, 252, 0.9)'
105112 ctx . font = '600 36px Inter, ui-sans-serif, system-ui'
106113 ctx . fillText ( 'PHILOSOPHY' , 74 , 110 )
@@ -113,6 +120,33 @@ function buildFaceTexture(card: PhilosophyCardData): THREE.CanvasTexture {
113120 ctx . font = '500 40px Inter, ui-sans-serif, system-ui'
114121 wrapText ( ctx , card . body , 74 , 420 , canvas . width - 148 , 56 , 8 )
115122
123+ const iconX = 824
124+ const iconY = 188
125+ const iconSize = 98
126+ const half = iconSize / 2
127+ ctx . strokeStyle = 'rgba(56, 189, 248, 0.78)'
128+ ctx . lineWidth = 4
129+ ctx . beginPath ( )
130+ ctx . moveTo ( iconX , iconY - half )
131+ ctx . lineTo ( iconX + half , iconY - half / 2 )
132+ ctx . lineTo ( iconX + half , iconY + half / 2 )
133+ ctx . lineTo ( iconX , iconY + half )
134+ ctx . lineTo ( iconX - half , iconY + half / 2 )
135+ ctx . lineTo ( iconX - half , iconY - half / 2 )
136+ ctx . closePath ( )
137+ ctx . stroke ( )
138+ ctx . beginPath ( )
139+ ctx . moveTo ( iconX , iconY - half )
140+ ctx . lineTo ( iconX , iconY )
141+ ctx . lineTo ( iconX + half , iconY + half / 2 )
142+ ctx . moveTo ( iconX , iconY )
143+ ctx . lineTo ( iconX - half , iconY + half / 2 )
144+ ctx . stroke ( )
145+ ctx . fillStyle = 'rgba(56, 189, 248, 0.85)'
146+ ctx . beginPath ( )
147+ ctx . arc ( iconX , iconY , 6 , 0 , Math . PI * 2 )
148+ ctx . fill ( )
149+
116150 const texture = new THREE . CanvasTexture ( canvas )
117151 texture . colorSpace = THREE . SRGBColorSpace
118152 texture . needsUpdate = true
@@ -124,6 +158,9 @@ export default function PhilosophyScene({ cards, activeIndex, onActiveIndexChang
124158 const cubeRef = useRef < THREE . Group | null > ( null )
125159 const targetYRef = useRef ( 0 )
126160 const onActiveChangeRef = useRef ( onActiveIndexChange )
161+ const currentAutoFaceRef = useRef ( 0 )
162+ const nextAutoSwitchAtRef = useRef ( AUTO_ROTATION_INTERVAL_SECONDS )
163+ const elapsedTimeRef = useRef ( 0 )
127164
128165 useEffect ( ( ) => {
129166 onActiveChangeRef . current = onActiveIndexChange
@@ -154,6 +191,8 @@ export default function PhilosophyScene({ cards, activeIndex, onActiveIndexChang
154191 const cube = new THREE . Group ( )
155192 scene . add ( cube )
156193 cubeRef . current = cube
194+ currentAutoFaceRef . current = 0
195+ nextAutoSwitchAtRef . current = AUTO_ROTATION_INTERVAL_SECONDS
157196
158197 const size = CUBE_SIZE
159198 const body = new THREE . Mesh (
@@ -187,7 +226,7 @@ export default function PhilosophyScene({ cards, activeIndex, onActiveIndexChang
187226 new THREE . MeshBasicMaterial ( {
188227 map : buildFaceTexture ( card ) ,
189228 transparent : false ,
190- side : THREE . DoubleSide ,
229+ side : THREE . FrontSide ,
191230 } )
192231 ) )
193232 const maxAnisotropy = renderer . capabilities . getMaxAnisotropy ( )
@@ -220,48 +259,6 @@ export default function PhilosophyScene({ cards, activeIndex, onActiveIndexChang
220259 floor . position . y = FLOOR_Y
221260 scene . add ( floor )
222261
223- let dragging = false
224- let prevX = 0
225-
226- const findNearestFace = ( angle : number ) => {
227- let best = 0
228- let bestDistance = Number . POSITIVE_INFINITY
229- FACE_ANGLES . forEach ( ( candidate , idx ) => {
230- const distance = Math . abs ( normalizeAngle ( angle - candidate ) )
231- if ( distance < bestDistance ) {
232- best = idx
233- bestDistance = distance
234- }
235- } )
236- return best
237- }
238-
239- const down = ( event : PointerEvent ) => {
240- dragging = true
241- prevX = event . clientX
242- renderer . domElement . style . cursor = 'grabbing'
243- }
244- const move = ( event : PointerEvent ) => {
245- if ( ! dragging ) return
246- const dx = event . clientX - prevX
247- prevX = event . clientX
248- targetYRef . current += dx * DRAG_SENSITIVITY
249- }
250- const up = ( ) => {
251- if ( ! dragging ) return
252- dragging = false
253- renderer . domElement . style . cursor = 'grab'
254- const nearest = findNearestFace ( targetYRef . current )
255- targetYRef . current = FACE_ANGLES [ nearest ]
256- onActiveChangeRef . current ( nearest )
257- }
258-
259- renderer . domElement . style . cursor = 'grab'
260- renderer . domElement . addEventListener ( 'pointerdown' , down )
261- window . addEventListener ( 'pointermove' , move )
262- window . addEventListener ( 'pointerup' , up )
263- window . addEventListener ( 'pointercancel' , up )
264-
265262 const handleResize = ( ) => {
266263 if ( ! mount ) return
267264 camera . aspect = mount . clientWidth / mount . clientHeight
@@ -275,6 +272,13 @@ export default function PhilosophyScene({ cards, activeIndex, onActiveIndexChang
275272 const animate = ( ) => {
276273 frameId = requestAnimationFrame ( animate )
277274 const elapsed = clock . getElapsedTime ( )
275+ elapsedTimeRef . current = elapsed
276+ if ( elapsed >= nextAutoSwitchAtRef . current ) {
277+ currentAutoFaceRef . current = ( currentAutoFaceRef . current + 1 ) % FACE_ANGLES . length
278+ nextAutoSwitchAtRef . current = elapsed + AUTO_ROTATION_INTERVAL_SECONDS
279+ onActiveChangeRef . current ( currentAutoFaceRef . current )
280+ }
281+ targetYRef . current = FACE_ANGLES [ currentAutoFaceRef . current ]
278282
279283 const cubeGroup = cubeRef . current
280284 if ( cubeGroup ) {
@@ -290,10 +294,6 @@ export default function PhilosophyScene({ cards, activeIndex, onActiveIndexChang
290294 return ( ) => {
291295 cancelAnimationFrame ( frameId )
292296 window . removeEventListener ( 'resize' , handleResize )
293- renderer . domElement . removeEventListener ( 'pointerdown' , down )
294- window . removeEventListener ( 'pointermove' , move )
295- window . removeEventListener ( 'pointerup' , up )
296- window . removeEventListener ( 'pointercancel' , up )
297297
298298 scene . traverse ( ( obj ) => {
299299 const mesh = obj as THREE . Mesh
@@ -318,7 +318,9 @@ export default function PhilosophyScene({ cards, activeIndex, onActiveIndexChang
318318 } , [ cards ] )
319319
320320 useEffect ( ( ) => {
321+ currentAutoFaceRef . current = activeIndex
321322 targetYRef . current = FACE_ANGLES [ activeIndex ] ?? 0
323+ nextAutoSwitchAtRef . current = elapsedTimeRef . current + AUTO_ROTATION_INTERVAL_SECONDS
322324 } , [ activeIndex ] )
323325
324326 return (
0 commit comments