Zooming through a letter
How I made the intro type out "parjanya." and then fly through the hole in the second "a", and why the zoom works in log space.
When I was planning this site, I wanted the first thing you see to be my name, and then I wanted the name itself to become the way in. So the first time you open the site in a browser session, you get a “p” and a blinking full stop. The rest of my name types in, and then the page zooms into the second “a” until the hole in that letter fills the whole screen and the site is sitting right behind it.
The letters are paths, not text
Web fonts take a moment to load, and I wanted the intro on screen from the very first frame. So my letters aren’t text at all. When the site builds, a script I wrote opens the same font file the site uses (Geist Medium) with opentype.js and saves each character as an SVG path, already spaced out with the font’s own advance widths and kerning.1
The script also pulls out one special shape: the counter of the second “a”, the enclosed hole in its lower bowl. In Geist Medium that “a” is made of exactly two outlines, and the inner one is 243 by 179 font units. Everything else in the intro is built around that shape.
A background with a hole in it
The overlay behind the letters is one big path drawn with fill-rule="evenodd": a huge rectangle, plus the counter’s outline as a second subpath. Where the two overlap, the fill cancels out. That leaves a hole shaped exactly like the inside of the “a”, and the real page shows through it.
I didn’t want the hole to give the game away too early, so a second copy of the counter sits on top in the background colour. It fades out as the zoom begins, and the site slowly appears through the letter.
Scaling about a point
To zoom into the counter, I scale every point in the word about the counter’s centre :
In SVG that’s translate(cx cy) scale(s) translate(-cx -cy), and I write it to the group’s transform attribute on every frame. Because that changes the geometry itself, the browser redraws crisp vector edges at any scale. A CSS transform can end up scaling a cached bitmap instead, which goes blurry long before 100×.
How far to zoom
The zoom is done once the hole covers the whole screen, which is exactly when all four corners of the viewport sit inside the counter. Every screen is different, so rather than hard-coding a number, I work out the target scale when the page loads. I double the scale until the hole covers the screen, then binary-search the gap. This is the search from the intro’s code, with the fallback for browsers that can’t do it left out:
function findZoomScale() {
// `corners` holds the four screen corners, already converted to the word's SVG
// coordinates. (centreX, centreY) is the middle of the counter.
const coversScreen = (scale: number) =>
corners.every((corner) =>
counter.isPointInFill(new DOMPoint(centreX + (corner.x - centreX) / scale, centreY + (corner.y - centreY) / scale)),
);
// Double until we overshoot, then halve the gap 32 times.
let tooSmall = 1;
let bigEnough = 2;
while (!coversScreen(bigEnough)) {
tooSmall = bigEnough;
bigEnough *= 2;
}
for (let step = 0; step < 32; step++) {
const middle = (tooSmall + bigEnough) / 2;
if (coversScreen(middle)) bigEnough = middle;
else tooSmall = middle;
}
return bigEnough * 1.1; // a little past "only just covered"
}coversScreen inverts , mapping each screen corner back to the point of the unzoomed word that it lines up with at that scale. isPointInFill then tells me whether that point lands inside the hole. On a 1440×900 laptop screen the answer comes out to about 83×, and on a 375×812 phone it’s about 165×.
Why log space
My first instinct was to animate straight from to . It looks wrong: almost all the visible movement happens in the first few frames, and the rest is a slow crawl. What your eye actually follows is the relative change in size, so the rate that feels steady is the rate of change of .
So instead, I animate the exponent:
where is an easing curve on . With , the push-in feels like moving at a steady speed. I went with a cubic ease-in, so the zoom starts gently and then picks up speed as it goes through the letter.2
The small things
- The letters fade into the background over the first two-thirds of the zoom, so nothing bright goes flying past at the end.
- Any click, tap or key press skips ahead to a quicker version of the zoom.
- If you’ve turned on reduced motion, the intro is just the word for a moment, followed by a short fade.
- It only plays once per browser session. After that, pages open with the shutter transition instead.