Recipes
Grid and skyline together
One background scene for a whole page, with a camera that follows the sections and contribution bars that rise in the About section.
This is the setup abhiramkrishna.com uses: one grid behind the whole page, a camera that moves as you scroll between sections, and the contribution skyline standing on the grid in the About section. Using one scene for both keeps it to one WebGL context and one animation loop.

1. A fixed background
<div class="background"></div>
<main>
<section id="hero">…</section>
<section id="projects">…</section>
<section id="about">…</section>
</main>.background {
position: fixed;
inset: 0;
z-index: -1;
background: #011627;
}The grid tracks the cursor and clicks on window, so it still reacts behind your content.
2. The grid and its camera poses
import { SpacetimeGrid, scrollCamera } from "@abhiramkrishna/spacetime-grid";
const poses = {
hero: { position: [0, 1.3, 6], lookAt: [0, -1, -3] },
projects: { position: [0.6, 0.75, 4.6], lookAt: [0.5, -1.25, -3.4] },
about: { position: [1.7, 1.15, 5], lookAt: [1.4, -1, -3] },
} as const;
const top = (id: string) => document.getElementById(id)!.offsetTop;
const grid = new SpacetimeGrid(document.querySelector<HTMLElement>(".background")!, {
theme: { fog: "#011627" },
});
grid.use(scrollCamera({
poses,
initial: "hero",
segments: () => [
{ from: "hero", to: "projects", start: 0, end: top("projects") },
{ from: "projects", to: "about", start: top("about") - innerHeight, end: top("about") },
],
}));segments runs every frame, so the offsets stay right when images load or the window resizes.
3. The skyline in the About section
Anchor the bars to the About pose, so they sit in view when the camera arrives there. Then let them rise as the section scrolls in:
import { contributionSkyline } from "@abhiramkrishna/contribution-skyline";
import data from "./data/contributions.json";
const clamp = (value: number) => Math.min(1, Math.max(0, value));
const skyline = contributionSkyline(data, {
anchor: { ndc: [0, -0.45], pose: poses.about },
progress: () => {
const start = top("about") - innerHeight;
const end = top("about") - innerHeight * 0.4;
return clamp((scrollY - start) / (end - start));
},
});
grid.use(skyline);- Before the About section, progress is 0 and the skyline costs almost nothing.
- As the section scrolls in, the bars rise week by week and the days beneath them glow.
- Scrolling back up sinks them again.
To get the data, see Getting the data. The simplest way is to fetch it at build time.
4. Phones
On a narrow screen, the bars are too small to read. Only add the layer from step 3 on wider screens, and put the 2D calendar in the About section on phones:
if (matchMedia("(min-width: 641px)").matches) {
grid.use(skyline);
} else {
const { renderContributionCalendar } = await import("@abhiramkrishna/contribution-skyline/calendar");
renderContributionCalendar(document.querySelector(".about-calendar")!, data);
}The grid itself switches to its compact layout below 1024px of width.