Dev.to WebDev 🛠 Dev 👁 0 📖 3 min read

Canvas sketches with no build step

Canvas is the most fun you can have with forty lines of JavaScript, and it needs nothing: no framework, no bundler, no dev server. Here's a sketch with the three details that make it look right. The page One

Canvas is the most fun you can have with forty lines of JavaScript, and it needs nothing: no framework, no bundler, no dev server. Here's a sketch with the three details that make it look right.

The page

One HTML file. Open it in a browser.

<!doctype html>
<canvas id="c" width="320" height="200"></canvas>
<script type="module">
  // the sketch goes here
</script>

Keep the script inline. From file://, browsers block module scripts loaded with src, so a second file means running a local server.

Detail 1: sharp on Retina

A canvas is a bitmap. On a 2× display, a 320×200 canvas gets stretched and looks soft. Size the backing store in device pixels, then scale the drawing back:

const canvas = document.querySelector("#c");
const ctx = canvas.getContext("2d");

const { width, height } = canvas;
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.scale(dpr, dpr);

Now you draw in CSS pixels and get device-pixel sharpness.

Detail 2: move by time, not by frame

requestAnimationFrame runs at the display's refresh rate: 60, 120, sometimes less. Move things by speed × elapsed time and they move at the same speed everywhere:

const dots = Array.from({ length: 40 }, () => ({
  x: Math.random() * width,
  y: Math.random() * height,
  vx: (Math.random() - 0.5) * 120, // pixels per second
  vy: (Math.random() - 0.5) * 120,
}));

let last = performance.now();

function frame(now) {
  const dt = Math.min((now - last) / 1000, 0.05); // seconds, capped
  last = now;

  ctx.fillStyle = "rgba(15, 23, 42, 0.25)"; // fade the last frame
  ctx.fillRect(0, 0, width, height);

  ctx.fillStyle = "#f59e0b";
  for (const d of dots) {
    d.x += d.vx * dt;
    d.y += d.vy * dt;
    if (d.x < 0 || d.x > width) d.vx *= -1;
    if (d.y < 0 || d.y > height) d.vy *= -1;
    ctx.beginPath();
    ctx.arc(d.x, d.y, 3, 0, Math.PI * 2);
    ctx.fill();
  }

  requestAnimationFrame(frame);
}

requestAnimationFrame(frame);

The cap on dt matters: switch tabs for a minute, come back, and an uncapped dt teleports every dot.

Detail 3: fade, don't clear

Painting a translucent rectangle instead of calling clearRect leaves short trails. Change 0.25 to 0.05 for long ones, or 1 for none. It's the cheapest effect in canvas.

Tweaking it live

The fun part of a sketch is changing numbers. With a plain HTML file, that's edit, switch windows, reload, repeat.

In JSLab, a JavaScript & TypeScript scratchpad for macOS, I switch the tab to the Browser runtime, and it renders into a Web View tile under its output. The page starts empty, apart from a <div id="root">, so the first line makes the canvas:

document.body.innerHTML = '<canvas id="c" width="320" height="200"></canvas>';

The rest of the sketch stays exactly as above. With Auto Run, the sketch reruns a moment after I stop typing, so changing 120 to 400 is the whole loop.

One thing I didn't expect to like: every run reloads the page. In a hot-reload setup, it's easy to end up with two requestAnimationFrame loops running at once after an edit. Here, each run starts from a clean page, so there's only ever one.

canvas.onclick with an AudioContext works too, and while a tab is playing sound, a speaker icon on the tab mutes it.

JSLab launches Nov 15. The Web View docs: https://js-lab.app/docs/web-view

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.