Draw My Canvas / studio

Geometric Flow

Seven hexagons gliding along harmonic paths in constant rotation.

Plate 06harmonic

Waiting for the animation above to load…

What lands in the file, and what the width does

One click writes whatever the animation is drawing at that moment to a PNG, with the drawmycanvas.com mark drawn into the picture rather than laid over it. Leave the width box empty and you get the stage exactly as your browser rasterised it — your window’s width times its device pixel ratio, which is about 1,600 px across from a 1280‑px window on a HiDPI laptop and about 353 px from a 390‑px phone.

Type a width instead, or take a preset, and the frame is redrawn into a canvas that wide: the height follows the stage’s own shape and the mark scales with it. The stage is 16:9 on a wide window and 4:3 below 560 px, so a width of 1200 saves 1200×675 on a laptop and 1200×900 on a phone. 1200 px is the width Open Graph and X link cards are cut from — the canonical card is 1200×630, and a card crops the extra height rather than letterboxing it.

Honest limits. Asking for more pixels than the stage was drawn at resamples pixels that were never drawn: a 1920‑px file exported from a 353‑px phone stage is bigger, not sharper. For a big file that is sharp, use Save as wallpaper: it draws the plate again from scratch at exactly 1179×2556 or 1290×2796 (phones), 1920×1080, 2560×1440 or 3840×2160 (4K), so every line is rasterised at that size and the shape is the screen’s, never stretched. Because the plate restarts, a wallpaper is a fresh run of it rather than the exact frame on screen; it is given as long as the stage has been running, up to 20 seconds, to develop. A very large width is a real memory allocation and a browser is allowed to refuse it; when that happens the line above says so plainly and nothing else on the page changes. Stills are PNG only — no JPEG, no WebP. On browsers that can record video, Record a clip saves 5, 10 or 20 seconds of the running animation as MP4 or WebM (whichever this browser can encode) at the same width, with the mark in every frame. A clip is not a seamless loop, it has no audio, and a width bigger than the stage is resampled rather than sharper — only the wallpaper is redrawn at its size. Where a browser cannot record video the button never appears and a PNG is the only export. And nothing is uploaded: the frame or clip is assembled in your own browser, so no frame of this plate ever reaches us.

Live on an HTML canvas · vanilla JavaScript · no dependencies Open fullscreen

About this piece

Seven hexagons drift around the frame, but they don’t move in straight lines. Their position is steered by harmonic functions — one sine of time sets each hexagon’s distance from the centre while its direction turns at half that rate — so all seven ride the same closed, two-lobed loop, and the whole motion repeats every 419 frames, about 7 seconds.

While it travels, every hexagon also spins on its own axis and gently pulses in size. A translucent fade each frame leaves a soft motion trail behind the shapes, tying the seven separate paths into one flowing system.

What to look for

The shapes are both stroked and filled at low opacity, so they read as glass panels rather than solid tiles, and overlaps stay legible.

Because size and distance are driven by different sine frequencies, a hexagon can be large and near the edge, or small and central — the combinations keep cycling.

The path each hexagon follows

The code that places a hexagon is two lines: a distance d = 0.22R + 0.14R·sin t from the centre of the frame, and a direction (cos t/2, sin t/2), where R is the frame’s shorter side and t is the running clock. Write θ = t/2 for the direction angle. Then sin t = sin 2θ, and the two lines collapse into one polar curve:

r(θ) = R · (0.22 + 0.14 · sin 2θ)

That is not a Lissajous figure, which gives each axis a single sine wave of its own. It is one radius that swells and shrinks twice per turn. The radius peaks at 0.36R when θ is 45° and 225°, and bottoms out at 0.08R at 135° and 315°. Because 0.22 is bigger than 0.14 the radius never reaches zero, so the hexagons never cross the centre. The result is a peanut-shaped closed loop: two lobes reaching towards the bottom-right and top-left corners (canvas y points down), pinched to a waist only 0.16R across. On a 600-pixel-tall frame the lobes reach 216 px from the centre and the waist passes 48 px from it. The hexagons travel it clockwise.

All seven hexagons ride the same curve. Hexagon s uses the clock t + 2πs/7, so neighbours sit π/7 (about 26°) apart in direction. The seven together cover 154° of the loop, a convoy that leaves the rest of it empty, and each hexagon arrives where the one ahead of it was 30 frames earlier. Hexagons 0, 3 and 6 are coral (the tail, middle and head of the convoy) and the other four are cobalt.

Two more motions run on their own clocks. Each hexagon spins to the angle t + 0.5s radians, two full turns per lap. The offset between neighbours is 0.5, not 2π/7, so a hexagon reaching a neighbour’s old spot is tilted about 23° differently. Each one also pulses in size, 26 ± 12 pixels (14 to 38) following sin 2t. That has a period of π in t, a quarter of the 4π one lap takes, so every hexagon swells four times per lap, always at the same four places on the curve.

The real repeat period. The clock advances t += 0.03 per frame. The loop closes when θ has turned a full circle, at t = 4π. The spin (a whole number of turns) and the pulse (four cycles) are back in step by then too, so the motion repeats exactly every 4π / 0.03 ≈ 418.9 frames: about 6.98 seconds at 60 frames per second. Halfway, at t = 2π, nothing repeats: θ has turned only 180°, and each hexagon sits on the opposite lobe. Honest limit: the clock counts frames, not seconds, so on a 120 Hz screen the same lap takes about 3.5 seconds.

How a hexagon is drawn

Each hexagon is drawn with plain path calls, not a shape primitive. The canvas origin is moved to the hexagon’s point on the loop with ctx.translate and turned by the spin with ctx.rotate. Then seven points are joined at angles k · 60°, for k = 0…6, on a circle of radius size. The seventh point is the first one again, which closes the outline. Because the hexagon is drawn around its own origin, the rotation turns it about its centre and leaves its position alone. A regular hexagon looks the same after every 60° turn, so the spin you see repeats every 35 frames, even though the angle itself keeps growing. The outline is stroked at 85% opacity and filled at 10%, which is why overlaps read like glass.

The trail. Nothing is cleared between frames. Before drawing, the full-size piece paints the whole canvas with globalCompositeOperation = 'destination-out' at alpha 0.12. That erases 12% of whatever ink is there, so every mark keeps 88% of its strength per frame and fades to half in about five and a half frames. The obvious alternative, a translucent fillRect in the background colour, gives the same decay in theory. On a real 8-bit canvas it does not: compositing a low-alpha colour onto itself, frame after frame, drifts the plate away from the palette. The dark background sinks to pure #000000 and the light one walks towards a dirty pink. Erasing towards transparency settles exactly at alpha 0, so the empty canvas shows the page background behind it, the true palette colour in both themes. (The small gallery tile, which always sits on the dark plate colour, still uses the fillRect fade.)

Draw your own

Here is the whole technique in 25 lines of HTML and plain JavaScript, with no library. It draws seven hexagons on the polar path above, with the same spin, pulse and destination-out trail, in the cobalt #7098FF and coral #FF6E48 that /assets/plates.js paints the gallery tiles with, on the plate colour #0B0D12. Save it as an .html file and open it.

<style>canvas { background: #0B0D12 }</style>
<canvas id="geo" width="480" height="480"></canvas>
<script>
const ctx = document.getElementById('geo').getContext('2d');
const N = 7, R = 480, c = 240, TAU = 2 * Math.PI; let t = 0;
function frame() {
  ctx.globalCompositeOperation = 'destination-out';  // trail: erase 12%
  ctx.fillStyle = 'rgba(0,0,0,0.12)'; ctx.fillRect(0, 0, 480, 480);
  ctx.globalCompositeOperation = 'source-over';
  for (let s = 0; s < N; s++) {
    const st = t + s * TAU / N, th = st / 2;         // theta = t / 2
    const r = R * (0.22 + 0.14 * Math.sin(2 * th));  // the polar path
    const sz = 26 + 12 * Math.sin(2 * st);           // the pulse
    const ink = s % 3 === 0 ? '#FF6E48' : '#7098FF'; // coral, cobalt
    ctx.save(); ctx.translate(c + r * Math.cos(th), c + r * Math.sin(th));
    ctx.rotate(t + s * 0.5); ctx.beginPath();        // the spin
    for (let k = 0; k < 6; k++)
      ctx.lineTo(sz * Math.cos(k * TAU / 6), sz * Math.sin(k * TAU / 6));
    ctx.closePath(); ctx.strokeStyle = ink; ctx.stroke();
    ctx.fillStyle = ink + '1A'; ctx.fill(); ctx.restore(); // 1A = 10%
  }
  t += 0.03; requestAnimationFrame(frame);
}
frame();
</script>
Paste it into an empty .html file.

Where to take it: change 0.14 to a value above 0.22, such as 0.3, and the radius goes negative for part of each turn, so the path dives through the centre of the frame and the waist turns into two small inner loops (the lobes then reach 0.52R, so shrink R to keep them on the canvas). Change sin(2 * th) to sin(3 * th) and you get three lobes instead of two. What the snippet does not do is follow the page theme or honour reduced motion, as the piece above does; it is the motion alone.

Curious how the loop and canvas fit together? Read how it works →

More from the gallery

All 32 animations in the gallery →