pixel up close, smooth far away

September 8, 2026 meta designpixelart

There are two kinds of drawing on this site and they are never allowed to touch each other. That rule sounds precious when I write it out like that, but it's the thing that actually made the header and the background stop fighting for attention.

the problem it solves

Early on, everything on the page was drawn the same way: pixel-grid sprites, hard edges, one SVG <rect> per cell. The header mountain, the birds, all of it. It looked fine up close and completely flat as soon as you added anything meant to sit further back, like a background silhouette behind the text column. Chunky pixels reading as a "distant mountain in the haze" doesn't work, distance in real vision is soft, not blocky. But going full smooth everywhere killed the thing I actually liked about the pixel art in the first place, which is that it reads as deliberate and handmade rather than a stock vector illustration.

The fix wasn't picking one style. It was deciding, on purpose, which elements get to be crisp and which get to be soft, and never letting an element straddle both.

near: everything you look at directly

The header and footer horizon strips, the tiny nav glyphs next to each section link, the BMK monogram's mountain mark, the 404 puffin, the moon/sun icons on the theme toggle, and every bird and fish that actually flies or swims across the sky and shoreline. All pixel-grid. All built the same way: I sketch the shape as an ASCII grid in a comment at the top of the component first, one character per cell, before writing a single line of SVG:

//   ......................RRR.MM.....................
//   .....................RRRRRMM.....................
//   .....................RRRRRR.....................
//   ....................RRRRRRRR.....................

That's the actual top of the tall header horizon, R for rock, . for open sky, M for the moon that only shows up in dark mode. Once the grid reads right as ASCII, I convert it to merged <rect> runs (adjacent same-color cells in a row collapse into one wider rect instead of one per cell, which keeps the DOM from bloating on a 48-column grid) with preserveAspectRatio="none" so the whole strip stretches to fill its container edge to edge without ever spilling over on a weird viewport width.

Near: the crisp pixel-grid header horizon

far: everything meant to sit back

The background island silhouette is the opposite of all that. It's one continuous shape, traced with cubic-bezier curves from an actual reference photo I took of Lovundfjellet, not sketched cell by cell:

const MOUNTAIN_PATH =
	'M145,300' +
	'C160,250 175,210 195,180' +
	'C210,158 225,140 240,133' +
	'C258,140 275,155 290,170' +
	'C305,195 320,225 335,260' +
	...

Same real mountain, same true two-peak profile (I actually traced the pixel version first, at the exact same column and row coordinates, then converted those anchor points into smooth curves for this pass rather than starting over from scratch) but rendered as one soft mass instead of a stepped outline. Sitting behind the content column at low opacity, it reads as atmosphere, the way a real mountain does when there's a village and a stretch of water between you and it. The night constellation layer works the same way, real star positions, but drawn as soft filled circles rather than pixel squares, because a "distant" star field made of hard pixel dots would look like a bug, not a sky.

Far: the same mountain as a soft silhouette behind the content column

why the line matters

The rule I hold myself to is that nothing straddles both registers. A bird sprite is either a pixel-cell bird flying across the near layer or a smooth silhouette circling the background island, never half of each. The moment you mix them within one element, your eye can't decide how far away the thing is supposed to be, and the whole depth illusion collapses. It's a cheap trick once you see it: crisp things read close, soft things read far, and you don't need actual parallax or blur filters to sell "distance," you just need consistent treatment per element and to never break your own rule partway through.

I broke it exactly once, early in the redesign, drawing a smooth background bird with a couple of hard pixel accents on the wingtip because I thought it would "ground" the silhouette a little:

<!-- what I tried: smooth silhouette body, pixel-square wingtip accents -->
<path d={SILHOUETTE_PATH} class="soft-fill" />
<rect x="34" y="12" width="2" height="2" class="pixel-accent" />
<rect x="36" y="12" width="2" height="2" class="pixel-accent" />

It just looked like a rendering bug, like an asset that failed to load correctly. Took the accents back out within the same session once I looked at it again the next morning with fresh eyes, back to one continuous soft-fill path and nothing else touching it.

the part I didn't expect

I went into this thinking of it as a purely visual trick, near versus far, foreground versus background. What actually sold me on keeping the rule strict is that it also matches how attention works on the page. The near-register elements are all things I want a reader's eye to actually register as content-adjacent detail, the nav glyphs telling you which section you're in, the birds giving the page life. The far-register stuff is explicitly decorative, aria-hidden, pointer-events: none, meant to be felt rather than looked at. Keeping the two visually distinct turned out to also keep them functionally distinct, which I didn't plan for but I'm glad happened anyway.

contrast doesn't get to ignore the background either

Having a soft silhouette sitting directly behind body copy further down the page created a real accessibility question I hadn't fully thought through until I got to checking it properly: what exactly is the text contrast being measured against. The page background here is a gradient wash near the top fading toward a flat tone, and the island shadow itself sits behind readable text lower down the page, so checking contrast against the flat background color alone would have been checking the wrong thing entirely. I ended up measuring against the actual composited tones text can render over, the wash's lightest point and the island-shadow color specifically, using the real relative-luminance calculation rather than eyeballing it, and holding both to the normal WCAG AA floors, 4.5 to 1 for body text, 3 to 1 for large text and interactive elements. The decorative-only tokens, the month markers, the rock color, the moon, are explicitly exempted from that requirement, since none of them carry information that isn't already sitting in readable text somewhere else on the same page. Distance as a visual trick is one thing. Distance as an excuse to let a background silhouette wreck a reader's ability to actually read the words in front of it is a different thing, and I wasn't willing to let the two-register idea be the reason that happened.

0 comments

Log in to comment.

Log in

No account?