Files
Emil_Shanayev_Resume/components/Cover.tsx
T
EmilandClaude Sonnet 5 15d36d6437
Deploy to GitHub Pages / build (push) Canceled after 0s
Deploy to GitHub Pages / deploy (push) Canceled after 0s
Add a dark theme, default, with a switch in the top bar
Colours move into theme tokens (light values in :root, dark in
[data-theme=dark]); the closing contact band inverts on each theme. The
choice is stored in localStorage and applied by a head script before the
first paint. Voxel covers and image shadows follow the theme.

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

103 lines
4.1 KiB
TypeScript

import { useMemo } from 'react'
// Deterministic isometric "sculpture" for a project without a demo image: the repo name seeds a small voxel
// stack, so every project gets its own picture that stays the same between visits.
function hash(text: string): number {
let h = 2166136261
for (let i = 0; i < text.length; i++) {
h ^= text.charCodeAt(i)
h = Math.imul(h, 16777619)
}
return h >>> 0
}
function random(seed: number) {
let a = seed
return () => {
a = (a + 0x6d2b79f5) | 0
let t = Math.imul(a ^ (a >>> 15), 1 | a)
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}
const GRID = 5
const TILE = 54
const RISE = 30
const ACCENTS = ['#e0384b', '#5aa300', '#2c63e0']
const NEUTRAL = { top: 'var(--cube-top)', left: 'var(--cube-left)', right: 'var(--cube-right)' }
function mix(hex: string, other: number, amount: number): string {
const channel = (i: number) => parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16)
const to = (i: number) => Math.round(channel(i) * (1 - amount) + other * amount).toString(16).padStart(2, '0')
return `#${to(0)}${to(1)}${to(2)}`
}
type Cube = { i: number; j: number; k: number; top: string; left: string; right: string }
function build(name: string): Cube[] {
const next = random(hash(name))
const cubes: Cube[] = []
const accent = ACCENTS[Math.floor(next() * ACCENTS.length)]
const second = ACCENTS[Math.floor(next() * ACCENTS.length)]
for (let i = 0; i < GRID; i++) {
for (let j = 0; j < GRID; j++) {
const edge = Math.abs(i - 2) + Math.abs(j - 2)
const height = Math.max(0, Math.round(next() ** 1.3 * 4 - edge * 0.35))
for (let k = 0; k < height; k++) {
const isTop = k === height - 1
const pick = next()
const color = isTop && pick < 0.3 ? accent : isTop && pick < 0.4 ? second : null
cubes.push(
color
? { i, j, k, top: mix(color, 255, 0.22), left: color, right: mix(color, 0, 0.24) }
: { i, j, k, ...NEUTRAL },
)
}
}
}
if (cubes.length < 5) cubes.push({ i: 2, j: 2, k: 0, ...NEUTRAL }, { i: 2, j: 2, k: 1, top: mix(accent, 255, 0.22), left: accent, right: mix(accent, 0, 0.24) })
return cubes.sort((a, b) => a.i + a.j - (b.i + b.j) || a.k - b.k)
}
const face = (points: [number, number][]) => points.map((p) => p.join(',')).join(' ')
export default function Cover({ name }: Readonly<{ name: string }>) {
const cubes = useMemo(() => build(name), [name])
const originX = 220
const originY = 150
const point = (i: number, j: number, z: number, dx: number, dy: number): [number, number] => [
+(originX + (i - j) * (TILE / 2) + dx).toFixed(1),
+(originY + (i + j) * (TILE / 4) + dy - z).toFixed(1),
]
return (
<svg viewBox="0 0 440 320" preserveAspectRatio="xMidYMid meet" role="presentation" aria-hidden="true">
{Array.from({ length: GRID * GRID }, (_, n) => {
const i = Math.floor(n / GRID)
const j = n % GRID
return (
<polygon
key={n}
points={face([point(i, j, 0, 0, 0), point(i, j, 0, TILE / 2, TILE / 4), point(i, j, 0, 0, TILE / 2), point(i, j, 0, -TILE / 2, TILE / 4)])}
fill="none"
style={{ stroke: 'var(--cover-grid)' }}
/>
)
})}
{cubes.map(({ i, j, k, top, left, right }) => {
const z = (k + 1) * RISE
const base = k * RISE
return (
<g key={`${i}-${j}-${k}`} strokeLinejoin="round" strokeWidth="0.75" style={{ stroke: 'var(--cube-edge)' }}>
<polygon points={face([point(i, j, z, -TILE / 2, TILE / 4), point(i, j, z, 0, TILE / 2), point(i, j, base, 0, TILE / 2), point(i, j, base, -TILE / 2, TILE / 4)])} style={{ fill: left }} />
<polygon points={face([point(i, j, z, 0, TILE / 2), point(i, j, z, TILE / 2, TILE / 4), point(i, j, base, TILE / 2, TILE / 4), point(i, j, base, 0, TILE / 2)])} style={{ fill: right }} />
<polygon points={face([point(i, j, z, 0, 0), point(i, j, z, TILE / 2, TILE / 4), point(i, j, z, 0, TILE / 2), point(i, j, z, -TILE / 2, TILE / 4)])} style={{ fill: top }} />
</g>
)
})}
</svg>
)
}