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
40 lines
1.1 KiB
TypeScript
40 lines
1.1 KiB
TypeScript
export type Span = 2 | 3 | 4 | 6
|
|
|
|
/**
|
|
* Column spans (out of 6) for `count` projects, so that every row is exactly full whatever the count:
|
|
* rows of two or three, a feature row first, and never a lone project in the last row (a single project
|
|
* spans the whole width). Two-project rows alternate 4+2 and 2+4 for rhythm.
|
|
*/
|
|
export function planSpans(count: number): Span[] {
|
|
if (count <= 0) return []
|
|
if (count === 1) return [6]
|
|
|
|
const sizes: number[] = []
|
|
const pattern = [2, 3, 3]
|
|
let remaining = count
|
|
for (let i = 0; remaining > 0; i++) {
|
|
const size = Math.min(pattern[i % pattern.length], remaining)
|
|
sizes.push(size)
|
|
remaining -= size
|
|
}
|
|
|
|
const last = sizes.length - 1
|
|
if (sizes[last] === 1) {
|
|
if (sizes[last - 1] === 3) {
|
|
sizes[last - 1] = 2
|
|
sizes[last] = 2
|
|
} else {
|
|
sizes[last - 1] = 3
|
|
sizes.pop()
|
|
}
|
|
}
|
|
|
|
const spans: Span[] = []
|
|
let pairs = 0
|
|
for (const size of sizes) {
|
|
if (size === 3) spans.push(2, 2, 2)
|
|
else spans.push(...(pairs++ % 2 === 0 ? ([4, 2] as const) : ([2, 4] as const)))
|
|
}
|
|
return spans
|
|
}
|