Files
Emil_Shanayev_Resume/components/Gizmo.tsx
T
EmilandClaude Sonnet 5 b5ffc67a82
Deploy to GitHub Pages / build (push) Canceled after 0s
Deploy to GitHub Pages / deploy (push) Canceled after 0s
Redesign the resume site as a 3D-editor viewport
New design: studio backdrop with a perspective floor and X/Z axis lines, an
orientation gizmo that orbits on scroll, Bricolage Grotesque and Instrument
Sans (bundled, no build-time network). Projects are laid out as plates by a
row planner that keeps every row full for any count; a demo image from the
repo root is shown when present, otherwise a generated voxel sculpture.
Text and projects still come live from Gitea.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XtcZtnG4GQA1d6mW5PNrS9
2026-09-20 21:37:35 +03:00

60 lines
2.0 KiB
TypeScript

'use client'
import { useScrollProgress } from '@/lib/useScroll'
import styles from './gizmo.module.css'
const AXES = [
{ label: 'X', vector: [1, 0, 0], color: 'var(--x)' },
{ label: 'Y', vector: [0, 1, 0], color: 'var(--y)' },
{ label: 'Z', vector: [0, 0, 1], color: 'var(--z)' },
] as const
const PITCH = 0.46
const LENGTH = 29
const CENTER = 48
function project([x, y, z]: readonly [number, number, number], yaw: number) {
const x1 = x * Math.cos(yaw) + z * Math.sin(yaw)
const z1 = -x * Math.sin(yaw) + z * Math.cos(yaw)
const y2 = y * Math.cos(PITCH) - z1 * Math.sin(PITCH)
const depth = y * Math.sin(PITCH) + z1 * Math.cos(PITCH)
return { x: CENTER + x1 * LENGTH, y: CENTER - y2 * LENGTH, depth }
}
/** Orientation gizmo of a 3D viewport. It orbits as the page scrolls and returns to the top when pressed. */
export default function Gizmo() {
const progress = useScrollProgress()
const yaw = 0.62 + progress * Math.PI * 1.35
const ends = AXES.flatMap((axis) =>
([1, -1] as const).map((sign) => ({
axis,
positive: sign === 1,
...project([axis.vector[0] * sign, axis.vector[1] * sign, axis.vector[2] * sign], yaw),
})),
).sort((a, b) => a.depth - b.depth)
return (
<button
type="button"
className={styles.gizmo}
aria-label="Back to top"
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
>
<svg viewBox="0 0 96 96" aria-hidden="true">
{ends.map(({ axis, positive, x, y }) =>
positive ? (
<g key={axis.label + '+'}>
<line x1={CENTER} y1={CENTER} x2={x} y2={y} stroke={axis.color} strokeWidth="3.2" strokeLinecap="round" />
<circle cx={x} cy={y} r="9.5" fill={axis.color} />
<text x={x} y={y + 3.6} textAnchor="middle" className={styles.letter}>{axis.label}</text>
</g>
) : (
<circle key={axis.label + '-'} cx={x} cy={y} r="6" fill={axis.color} opacity="0.32" />
),
)}
</svg>
</button>
)
}