Skip to content

Commit 8da3e83

Browse files
authored
Merge pull request #2878 from stakwork/feature/highlights-group
Feature/highlights group
2 parents a4f8a0d + fa294a1 commit 8da3e83

File tree

4 files changed

+417
-4
lines changed

4 files changed

+417
-4
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { Billboard, Edges, Html } from '@react-three/drei'
2+
import { useState } from 'react'
3+
import * as THREE from 'three'
4+
import { useControlStore } from '~/stores/useControlStore'
5+
6+
interface Shape3D {
7+
id: string
8+
label: string
9+
position: [number, number, number]
10+
geometry: 'box' | 'sphere' | 'cylinder'
11+
color: string
12+
size: [number, number, number]
13+
}
14+
15+
export const Groups = () => {
16+
const [selectedShapeId, setSelectedShapeId] = useState<string | null>(null)
17+
const cameraControlsRef = useControlStore((s) => s.cameraControlsRef)
18+
19+
const shapes: Shape3D[] = [
20+
{
21+
id: 'main-highlights',
22+
label: 'Main highlights',
23+
position: [-800, 200, 0],
24+
geometry: 'box',
25+
color: '#4a90e2',
26+
size: [100, 100, 100],
27+
},
28+
{
29+
id: 'controversial-views',
30+
label: 'Controversial views',
31+
position: [0, -400, 200],
32+
geometry: 'box',
33+
color: '#e24a4a',
34+
size: [100, 100, 100],
35+
},
36+
{
37+
id: 'people',
38+
label: 'People',
39+
position: [800, 100, -200],
40+
geometry: 'box',
41+
color: '#50c878',
42+
size: [100, 100, 100],
43+
},
44+
]
45+
46+
const handleClick = (shape: Shape3D) => {
47+
const center = new THREE.Vector3(...shape.position)
48+
const distance = Math.max(...shape.size) * 2
49+
const direction = new THREE.Vector3(1, 1, 1).normalize()
50+
const cameraPosition = new THREE.Vector3().copy(center).addScaledVector(direction, distance)
51+
52+
cameraControlsRef?.setLookAt(
53+
cameraPosition.x,
54+
cameraPosition.y,
55+
cameraPosition.z,
56+
center.x,
57+
center.y,
58+
center.z,
59+
true,
60+
)
61+
62+
setSelectedShapeId(shape.id)
63+
}
64+
65+
const renderGeometry = (shape: Shape3D) => {
66+
switch (shape.geometry) {
67+
case 'box':
68+
return <boxGeometry args={shape.size} />
69+
case 'sphere':
70+
return <sphereGeometry args={[shape.size[0] / 2, 16, 16]} />
71+
case 'cylinder':
72+
return <cylinderGeometry args={[shape.size[0] / 2, shape.size[0] / 2, shape.size[1], 16]} />
73+
default:
74+
return <boxGeometry args={shape.size} />
75+
}
76+
}
77+
78+
return (
79+
<group>
80+
{shapes.map((shape) => (
81+
<Billboard key={shape.id} position={shape.position}>
82+
<mesh>
83+
{renderGeometry(shape)}
84+
<meshBasicMaterial color={shape.color} opacity={selectedShapeId === shape.id ? 0.3 : 0.15} transparent />
85+
<Edges color="#8c6a97" />
86+
<Html center>
87+
<div
88+
onClick={() => handleClick(shape)}
89+
onKeyDown={(e) => {
90+
if (e.key === 'Enter' || e.key === ' ') {
91+
handleClick(shape)
92+
}
93+
}}
94+
role="button"
95+
style={{
96+
color: 'white',
97+
background: 'rgba(0, 0, 0, 0.8)',
98+
borderRadius: '8px',
99+
boxShadow: '0 0 12px rgba(0,0,0,0.6)',
100+
fontWeight: '600',
101+
fontSize: '12px',
102+
border: `2px solid ${shape.color}`,
103+
width: '120px',
104+
padding: '8px',
105+
textAlign: 'center',
106+
cursor: 'pointer',
107+
transition: 'all 0.2s ease',
108+
transform: selectedShapeId === shape.id ? 'scale(1.1)' : 'scale(1)',
109+
}}
110+
tabIndex={0}
111+
>
112+
{shape.label}
113+
</div>
114+
</Html>
115+
</mesh>
116+
</Billboard>
117+
))}
118+
</group>
119+
)
120+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Billboard, Edges, Html, Line } from '@react-three/drei'
2+
import { useCallback, useMemo, useState } from 'react'
3+
import * as THREE from 'three'
4+
import { useControlStore } from '~/stores/useControlStore'
5+
import { useMindsetStore } from '~/stores/useMindsetStore'
6+
import { usePlayerStore } from '~/stores/usePlayerStore'
7+
8+
export const Highlights = () => {
9+
const [selectedId, setSelectedId] = useState<string | null>(null)
10+
const cameraControlsRef = useControlStore((s) => s.cameraControlsRef)
11+
const { playerRef } = usePlayerStore((s) => s)
12+
const highlights = useMindsetStore((s) => s.highlights)
13+
14+
const positions = useMemo(
15+
() =>
16+
highlights.map((_, i) => {
17+
const angle = (i / highlights.length) * Math.PI * 2
18+
const radius = 1000
19+
20+
return [Math.cos(angle) * radius, (i % 2 === 0 ? 1 : -1) * 150, Math.sin(angle) * radius] as [
21+
number,
22+
number,
23+
number,
24+
]
25+
}),
26+
[highlights],
27+
)
28+
29+
const handleProgressChange = useCallback(
30+
(value: number | number[]) => {
31+
const newValue = Array.isArray(value) ? value[0] : value
32+
33+
if (playerRef) {
34+
playerRef.seekTo(newValue, 'seconds')
35+
}
36+
},
37+
[playerRef],
38+
)
39+
40+
const handleClick = (index: number) => {
41+
const position = new THREE.Vector3(...positions[index])
42+
const cameraOffset = new THREE.Vector3(1, 1, 1).normalize().multiplyScalar(300)
43+
44+
cameraControlsRef?.setLookAt(
45+
position.x + cameraOffset.x,
46+
position.y + cameraOffset.y,
47+
position.z + cameraOffset.z,
48+
position.x,
49+
position.y,
50+
position.z,
51+
true,
52+
)
53+
54+
setSelectedId(highlights[index].title)
55+
handleProgressChange(highlights[index].startTime)
56+
}
57+
58+
return (
59+
<group>
60+
{/* Curved lines between sequential nodes */}
61+
{positions.map((pos, i) => {
62+
if (i === 0) {
63+
return null
64+
}
65+
66+
const start = new THREE.Vector3(...positions[i - 1])
67+
const end = new THREE.Vector3(...positions[i])
68+
const mid = start.clone().lerp(end, 0.5).add(new THREE.Vector3(0, 200, 0)) // raise the curve
69+
const curve = new THREE.QuadraticBezierCurve3(start, mid, end)
70+
const points = curve.getPoints(20)
71+
72+
// eslint-disable-next-line react/no-array-index-key
73+
return <Line key={`curve-${i}`} color="#00bfff" dashed={false} lineWidth={1.5} points={points} />
74+
})}
75+
76+
{/* Segment nodes */}
77+
{highlights.map((highlight, i) => (
78+
<Billboard key={highlight.title} position={positions[i]}>
79+
<mesh>
80+
<circleGeometry args={[50, 64, 64]} />
81+
<meshBasicMaterial color={selectedId === highlight.title ? '#2e93b3' : '#8c6a97'} transparent />
82+
<Edges color="#ffffff" />
83+
<Html center>
84+
<button
85+
onClick={() => handleClick(i)}
86+
onPointerDown={(e) => e.stopPropagation()}
87+
onPointerOut={(e) => e.stopPropagation()}
88+
onPointerOver={(e) => e.stopPropagation()}
89+
onPointerUp={(e) => e.stopPropagation()}
90+
style={{
91+
cursor: 'pointer',
92+
background: 'rgba(0, 0, 0, 0.8)',
93+
borderRadius: '50%',
94+
padding: '6px',
95+
width: '150px',
96+
height: '150px',
97+
color: 'white',
98+
border: selectedId === highlight.title ? '2px solid #ffd700' : '1px solid #2e93b3',
99+
textAlign: 'center',
100+
fontWeight: 'bold',
101+
fontSize: '12px',
102+
}}
103+
type="button"
104+
>
105+
{highlight.title}
106+
</button>
107+
</Html>
108+
</mesh>
109+
</Billboard>
110+
))}
111+
</group>
112+
)
113+
}

0 commit comments

Comments
 (0)