Recipes / Grid and skyline together

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.

Bars standing on the animated grid

1. A fixed background

HTML
<div class="background"></div>
<main>
  <section id="hero">…</section>
  <section id="projects">…</section>
  <section id="about">…</section>
</main>
CSS
.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

TypeScript
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:

TypeScript
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:

TypeScript
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.