SugarpineSoftware
devMay 10, 2026

Building the Hero Scene: Canvas, Campfires, and Getting Weird With It

I wanted the opening of this site to feel like stepping into something, not landing on a page. Here's how the animated forest scene came together — and why I chose Canvas over WebGL.

A dark forest at night with scattered light filtering through tall pine treesA dark forest at night with scattered light filtering through tall pine trees

When I started thinking about this portfolio I kept running into the same problem: every developer site I looked at felt like a document. A very well-formatted document, maybe. But still a document.

I wanted something you enter.

The idea came pretty fast — a dark forest at night, campfire light flickering across pine trees, stars drifting overhead. Something that put you in a place before it told you anything. The natural world I actually live in, translated into pixels.

Why Canvas, not WebGL

WebGL was the obvious call for anything animated and performance-sensitive. Three.js would have made the particles and lighting straightforward. But I kept coming back to a few things:

First, the aesthetic I was going for isn't photorealistic — it's impressionistic. Flat gradients, simplified tree silhouettes, painterly glow. Canvas 2D handles that beautifully without the conceptual overhead of a shader pipeline.

Second, I wanted to write it myself. Not as a flex — I've used Three.js plenty — but because the constraints of the 2D API would force me to be deliberate about every element. You can't lean on a PBR material to make something look good. You have to make it look good.

function drawTree(ctx, x, baseY, height, progress) {
  const alpha = lerp(0.85, 0.2, progress);
  ctx.fillStyle = `rgba(14, 22, 14, ${alpha})`;
  // hand-coded polygon silhouette
  ctx.beginPath();
  ctx.moveTo(x, baseY);
  ctx.lineTo(x - height * 0.28, baseY - height);
  ctx.lineTo(x + height * 0.28, baseY - height);
  ctx.closePath();
  ctx.fill();
}

The scroll transition

The part I'm most happy with is the scroll-driven morphing: the forest slowly gives way to a green grid, the campfire dims, a terminal cursor appears. It's a small thing, but it earns the metaphor. This person lives between two worlds, and the site shows you that instead of telling you.

Firelight glow on tree bark at dusk, warm amber tonesFirelight glow on tree bark at dusk, warm amber tones

Technically it's simple — a scrollY listener feeding a progress value into the draw loop, with each element's color, alpha, and scale lerping toward their "digital" state. No scroll library, no GSAP. Just math.

window.addEventListener('scroll', () => {
  const progress = Math.min(window.scrollY / (window.innerHeight * 0.8), 1);
  requestAnimationFrame(() => draw(progress));
});

What I'd do differently

The tree silhouettes are hand-coded polygons. That was fine for 13 trees but I'd reach for an SVG-to-canvas approach if I ever added more variety. And the firefly movement could use a more organic noise function — right now it's sinusoidal, which works, but proper Perlin noise would breathe differently.

Still — I like it. It feels like me.

← All posts