Skip to content

Commit deb2135

Browse files
authored
Threejs Philosophy section: 2-column layout, auto-rotating cube, and corrected face rendering (#41)
1 parent c6979dd commit deb2135

File tree

2 files changed

+65
-79
lines changed

2 files changed

+65
-79
lines changed

src/templates/threejs/PhilosophyScene.tsx

Lines changed: 50 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ const FLOOR_RADIUS = 2.7
2020
const FLOOR_SEGMENTS = 64
2121
const FLOOR_OPACITY = 0.14
2222
const FLOOR_Y = -2.25
23-
const DRAG_SENSITIVITY = 0.012
2423
const ROTATION_DAMPING = 0.1
2524
const WAVE_SPEED = 0.6
2625
const WAVE_INTENSITY = 0.05
2726
const FLOAT_SPEED = 0.9
2827
const FLOAT_INTENSITY = 0.06
28+
const AUTO_ROTATION_INTERVAL_SECONDS = 3.8
2929

3030
function 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 (

src/templates/threejs/PhilosophySection.tsx

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { lazy, Suspense, useMemo, useState } from 'react'
22

33
const PhilosophyScene = lazy(() => import('./PhilosophyScene'))
44
const CUBE_FACE_COUNT = 4
5-
const FACE_LABELS = ['Front', 'Back', 'Left', 'Right']
65

76
type PhilosophyCardData = { title: string; body: string }
87
type Philosophy = { title: string; body: string; cards: PhilosophyCardData[] }
@@ -32,37 +31,22 @@ export default function PhilosophySection({ philosophy }: { philosophy: Philosop
3231
aria-labelledby="philosophy-title"
3332
>
3433
<div className="relative z-10 mx-auto max-w-5xl px-6">
35-
<header className="mb-10 max-w-2xl">
36-
<p className="mb-2 text-[10px] font-semibold uppercase tracking-[0.28em] text-blue-400">
37-
{philosophy.title}
38-
</p>
39-
<p className="text-sm leading-relaxed text-slate-400">{philosophy.body}</p>
40-
</header>
34+
<div className="grid gap-10 lg:grid-cols-2 lg:items-center">
35+
<header className="max-w-xl">
36+
<p className="mb-3 text-[11px] font-semibold uppercase tracking-[0.28em] text-blue-400/90">
37+
{philosophy.title}
38+
</p>
39+
<h2 id="philosophy-title" className="text-3xl font-semibold leading-tight text-slate-100 sm:text-4xl">
40+
{philosophy.title}
41+
</h2>
42+
<p className="mt-4 text-lg leading-relaxed text-slate-300">{philosophy.body}</p>
43+
</header>
4144

42-
<div className="mx-auto mb-8 h-[430px] w-full max-w-3xl overflow-hidden rounded-2xl border border-blue-900/35 bg-[#070d1d]/45 backdrop-blur-sm">
43-
<Suspense fallback={null}>
44-
<PhilosophyScene cards={cubeCards} activeIndex={activeFace} onActiveIndexChange={setActiveFace} />
45-
</Suspense>
46-
</div>
47-
48-
<div className="mx-auto grid max-w-4xl gap-3 sm:grid-cols-2">
49-
{cubeCards.map((card, index) => (
50-
<button
51-
key={`${FACE_LABELS[index]}-${card.title}`}
52-
type="button"
53-
onClick={() => setActiveFace(index)}
54-
className={`rounded-xl border px-4 py-3 text-left transition ${
55-
activeFace === index
56-
? 'border-cyan-400/80 bg-cyan-500/10 shadow-[0_0_24px_rgba(6,182,212,0.18)]'
57-
: 'border-blue-900/45 bg-[#060b17]/70 hover:border-blue-500/70 hover:bg-[#0b1428]/75'
58-
}`}
59-
>
60-
<p className="mb-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-cyan-300/85">
61-
{FACE_LABELS[index]}
62-
</p>
63-
<p className="text-sm font-semibold text-slate-100">{card.title}</p>
64-
</button>
65-
))}
45+
<div className="h-[430px] w-full overflow-hidden rounded-2xl border border-blue-900/35 bg-[#070d1d]/45 backdrop-blur-sm">
46+
<Suspense fallback={null}>
47+
<PhilosophyScene cards={cubeCards} activeIndex={activeFace} onActiveIndexChange={setActiveFace} />
48+
</Suspense>
49+
</div>
6650
</div>
6751
</div>
6852
</section>

0 commit comments

Comments
 (0)