Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,14 +269,15 @@ export default function App() {
</header>
<div
className="galaxy-canvas"
role="img"
aria-label={`${selectedPeriod.metricLabel} GitHub 활동 3D 지도. ${sceneBuckets.length}개 궤도와 ${sceneActivityCount}개 노드 표시 중.`}
role="group"
aria-label={`${selectedPeriod.metricLabel} GitHub 활동 3D 지도. ${sceneBuckets.length}개 궤도와 ${sceneActivityCount}개 노드 표시 중. 궤도 라벨을 선택하면 해당 활동 상세를 볼 수 있습니다.`}
>
<GalaxyScene
buckets={sceneBuckets}
selectedKind={selectedKind}
isRotating={isRotating}
resetRevision={resetRevision}
onSelectKind={setSelectedKind}
/>
</div>
<footer className="stage-status" aria-live="polite">
Expand Down
48 changes: 42 additions & 6 deletions src/components/GalaxyScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type GalaxyProps = {
buckets: readonly ActivityBucket[]
selectedKind: ActivityKind
isRotating: boolean
onSelectKind: (kind: ActivityKind) => void
}

type GalaxySceneProps = GalaxyProps & {
Expand All @@ -33,6 +34,27 @@ const INITIAL_CAMERA = {
}
const CANVAS_PIXEL_RATIO: [number, number] = [1, 1.75]
const CANVAS_BACKGROUND: [string] = ['#f8fbff']
const ORBIT_LABEL_WIDTH = 72
const ORBIT_LABEL_HEIGHT = 24
const ORBIT_LABEL_PADDING = 12
const orbitLabelPosition = new THREE.Vector3()

const calculateOrbitLabelPosition = (
object: THREE.Object3D,
camera: THREE.Camera,
size: { width: number; height: number },
) => {
object.getWorldPosition(orbitLabelPosition)
orbitLabelPosition.project(camera)

const projectedX = (orbitLabelPosition.x * 0.5 + 0.5) * size.width
const projectedY = (-orbitLabelPosition.y * 0.5 + 0.5) * size.height

return [
THREE.MathUtils.clamp(projectedX, ORBIT_LABEL_WIDTH + ORBIT_LABEL_PADDING, size.width - ORBIT_LABEL_PADDING),
THREE.MathUtils.clamp(projectedY, ORBIT_LABEL_PADDING, size.height - ORBIT_LABEL_HEIGHT - ORBIT_LABEL_PADDING),
]
}

const createActivityPointPosition = (
bucket: ActivityBucket,
Expand All @@ -57,7 +79,7 @@ const createActivityOrbits = (buckets: readonly ActivityBucket[]): ActivityOrbit
),
}))

function Galaxy({ buckets, selectedKind, isRotating }: GalaxyProps) {
function Galaxy({ buckets, selectedKind, isRotating, onSelectKind }: GalaxyProps) {
const groupRef = useRef<THREE.Group>(null)
const activityOrbits = useMemo(() => createActivityOrbits(buckets), [buckets])

Expand Down Expand Up @@ -94,13 +116,22 @@ function Galaxy({ buckets, selectedKind, isRotating }: GalaxyProps) {
))
})}
{activityOrbits.map(({ bucket }, index) => (
<Html key={bucket.kind} position={[bucket.radius + 0.2, 0.48 + index * 0.18, 0]}>
<span
<Html
key={bucket.kind}
position={[bucket.radius + 0.2, 0.48 + index * 0.18, 0]}
calculatePosition={calculateOrbitLabelPosition}
style={{ pointerEvents: 'none' }}
>
<button
type="button"
className={`orbit-label${bucket.kind === selectedKind ? ' selected' : ''}`}
style={{ color: bucket.color }}
aria-label={`${bucket.label} 궤도 선택, ${bucket.count}개 활동`}
aria-pressed={bucket.kind === selectedKind}
onClick={() => onSelectKind(bucket.kind)}
>
{bucket.shortLabel} {bucket.count}
</span>
</button>
</Html>
))}
</group>
Expand All @@ -123,7 +154,7 @@ function CameraRig({ controlsRef, resetRevision }: CameraRigProps) {
return null
}

export function GalaxyScene({ buckets, selectedKind, isRotating, resetRevision }: GalaxySceneProps) {
export function GalaxyScene({ buckets, selectedKind, isRotating, resetRevision, onSelectKind }: GalaxySceneProps) {
const controlsRef = useRef<ComponentRef<typeof OrbitControls>>(null)

return (
Expand All @@ -132,7 +163,12 @@ export function GalaxyScene({ buckets, selectedKind, isRotating, resetRevision }
<ambientLight intensity={1.2} />
<directionalLight position={[0, 4, 4]} intensity={2.2} color="#ffffff" />
<pointLight position={[0, 4, 2]} intensity={10} color="#7ed8ee" />
<Galaxy buckets={buckets} selectedKind={selectedKind} isRotating={isRotating} />
<Galaxy
buckets={buckets}
selectedKind={selectedKind}
isRotating={isRotating}
onSelectKind={onSelectKind}
/>
<OrbitControls ref={controlsRef} enablePan={false} minDistance={4} maxDistance={9} />
<CameraRig controlsRef={controlsRef} resetRevision={resetRevision} />
<EffectComposer>
Expand Down
17 changes: 16 additions & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ input:focus-visible {
}

.orbit-label {
appearance: none;
display: block;
min-width: max-content;
padding: 3px 7px;
Expand All @@ -559,11 +560,25 @@ input:focus-visible {
font-weight: 800;
line-height: 1.2;
opacity: 0.68;
pointer-events: none;
cursor: pointer;
pointer-events: auto;
transform: translateX(-100%);
transition:
border-color var(--motion-standard),
box-shadow var(--motion-standard),
opacity var(--motion-standard),
transform var(--motion-standard);
white-space: nowrap;
}

.orbit-label:hover,
.orbit-label:focus-visible {
border-color: currentColor;
box-shadow: 0 8px 20px rgb(54 82 116 / 16%);
opacity: 1;
transform: translateX(-100%) translateY(-1px);
}

.orbit-label.selected {
border-color: currentColor;
opacity: 1;
Expand Down
Loading