GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

One Line Drawing Generator: 9 Canvas Scenes

In a one line drawing generator, join each dot to its nearest neighbor and the line keeps crossing itself, so 04 flips crossed segments to shorten it, 03

What Does a Continuous Line Drawing Generator Need to Compute?

Auto-plays · click a tile to jump to its section · all nine in one zip

A one line drawing is a face, a flower or a word made from one stroke with no pen lifts. Moving a picture into one stroke takes three decisions in order: where the line should spend most of its length, in what order it passes those places, and how fast that order gets drawn. Each of the nine scenes digs deep into one of those decisions or chains several of them together.

Decision Scenes What gets computed Value measured here
Where 01 · 02 · 03 Cell darkness from 0 to 1 raises the wobble size, the zigzag turns or the dot count 01 wobble up to 2.43px · 02 10 to 61 turns per row · 03 1,800 dots
In what order 04 · 07 · 08 · 09 Join the nearest dot next and flip the crossings, or count the lines that meet at each corner 04 4,080 → 3,864px · 07 the envelope has 4 odd corners, so it is impossible
How fast 05 · 06 · 08 · 09 A running-length table gives an even speed, or the bends set both speed and width 05 240px per second · 06 70 to 492px per second · 08 620px per second

The source pictures are shapes built in code

The face in 01, the steaming coffee cup in 02, the cat face in 03 and the flower in 09 are functions that layer distances to ellipses, line segments and triangles and hand back a darkness from 0 to 1 for each cell. No outside photo or image file is involved; only 08 paints letters onto an offscreen canvas and reads those pixels back. 01 and 03 weight by darkness rather than brightness, so the blacker the cell, the bigger the wobble and the denser the dots. Using your own picture means swapping that darkness function for one that reads pixels, and the FAQ at the end walks through it.

Pages where it fits

The about-me block on a portfolio suits the spiral face from 01 or the flower from 09, and the signature slot under a brand hero suits the even-speed pen from 05. The double heart in 06 belongs on the first card of a wedding invitation, and the untangling route in 04 fits a page that explains a delivery round to customers. Where a gallery of finished line art files hands over the result, these scenes put the act of drawing itself on screen.

Why a Drawing Made Without Lifting the Pen Holds the Eye: Outlines Made by Dot Order and Line Length

With only one line, the order in which it visits the dots is the picture. The same 420 dots joined in a poor order give a line that cuts across the picture and tangles, while a better order pulls an outline out of the very same dots. A picture that is still being drawn likely holds attention longer than a finished one, because the viewer follows the pen tip and waits to see where the next stroke heads.

Why nearest-first joining crosses itself

04 starts at a depot and joins 420 delivery stops, each time moving to the closest stop not yet visited. That pass compares distances 87,990 times and yields a first route 4,080px long, yet the route holds 34 pairs of segments that cross. Choosing only the closest stop appears to leave skipped stops behind, and the trip back to them later cuts across lines already drawn. After 32 flips the length falls to 3,864px (−5.3%) and the crossings drop to 0. The flower in 09 runs the same math, flipping 61 times to clear 74 crossings and shortening 4,094px to 3,826px (−6.6%).

Line length becomes drawing time

For anyone asking how to animate a line drawing, the move here is to turn its length into time. The spiral in 01 lays down 11,160px in 4.6 seconds and the zigzag in 02 lays down 13,688px in 5 seconds, both above 2,400px per second and quick enough to fill the board. The signature in 05 covers 1,312.6px in 5.47 seconds and the heart in 06 covers 1,742.1px in 7.45 seconds, slow enough for the eye to stay with the tip. A quick pen is for filling a picture; a slow pen is for the feel of handwriting.

One Line Drawing Generator: 9 Scenes — a Brightness Spiral, Zigzag Shading, Evenly Spread Dots, an Untangled Path, a Constant-Speed Pen, Tremor-Driven Width, a Single-Stroke Figure Check, One-Line Cursive and a Hero Drawn in One Stroke

01 and 02 shake a line along a fixed path, a spiral or a set of rows, to build shading. 03 and 04 handle where dots go and the order they are visited in, 05 and 06 handle the pen's speed and width, and 07 and 08 test whether a figure or a word can be drawn in one go. 09 is a hero that chains the math from 03, 04, 05 and 06 into one flower. Each scene draws on a single 2D canvas with no outside library, no WebGL and no image file, and a seeded random generator gives the same picture on every load. The only control is a pause button; the drawing runs by itself.

What the 480×300 board recorded over 2 seconds

The render captured 24 frames, and the table counts how many of the 23 frame-to-frame comparisons showed a change and what share of the board changed at least once. 09, where color washes in, covers the most at 35.0%, while 05 and 08, where only one line and the band beneath it grow behind the pen tip, stay between 4% and 6%.

Scene Changed area Moving frames Mean intensity
01 Brightness spiral 23.1% 23/23 92.9
02 Zigzag shading 21.4% 21/23 123.7
03 Stippling 12.4% 14/23 40.1
04 Delivery route untangle 18.4% 20/23 74.4
05 Constant-speed pen 4.4% 23/23 73.1
06 Ballpoint heart 23.4% 23/23 84.1
07 Single-stroke check 11.5% 23/23 84.9
08 One-line cursive 5.7% 19/23 81.5
09 Flower hero 35.0% 19/23 79.4

The 14/23 for 03 appears to come from dots that move every 0.22 seconds and then rest in their new spots until the next step. The 20/23 for 04 and 19/23 for 08 and 09 likely reflect the short holds where the board sits still between stages.

Where this differs from the neighboring posts

Tracing a logo outline is the subject of logo loading animation, stretching thread by sampled darkness belongs to string art, reading letter pixels as dots is in glyph pixel sampling, and the speed and width of a handwritten signature are in signature canvas. This post builds on those by fixing the order of the dots and turning line length into time. For color that bleeds under the line the way it does in 09, continue to watercolor effect.

01A Single Spiral That Wobbles with Darkness Until a Picture Appears

A spiral wound 6px apart is followed in 1.2px steps, and the darker the spot it passes, the farther it swings across the spiral and the shorter its ripples become (one ripple spans 62.83px in light areas and 5.71px in the darkest), so a face surfaces. The pen covers 24.33 turns, 9,301 vertices and 11,160px at 2,426.1px per second in 4.6 seconds, and in the darkest spots two 2.43px swings plus the 2px stroke overrun the 6px gap by 0.86px, so neighboring turns touch and clump black. It works as a continuous line drawing portrait in the profile photo slot of a portfolio front page.

A Single Spiral That Wobbles with Darkness Until a Picture Appears
Open in a new tab

01 · A Single Spiral That Wobbles with Darkness Until a Picture Appears — Measured area 23.066% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SPIRAL_GAPWOBBLE_AMPWOBBLE_FREQ_DARK
        while (a * theta < SPIRAL_RADIUS) {
            const r = a * theta;
            const c = Math.cos(theta);
            const s = Math.sin(theta);
            const bx = CX + r * c;
            const by = CY + r * s;
            // tangent = d/dθ (r cosθ, r sinθ) = (a cosθ − r sinθ, a sinθ + r cosθ); the normal is the tangent turned 90°
            const tx = a * c - r * s;
            const ty = a * s + r * c;
            const tl = Math.sqrt(tx * tx + ty * ty);
            const nx = -ty / tl;
            const ny = tx / tl;
            dSmooth = lerp(dSmooth, dark(bx, by), SAMPLE_STEP / DARK_SMOOTH);
            phase += lerp(WOBBLE_FREQ_LIGHT, WOBBLE_FREQ_DARK, dSmooth) * SAMPLE_STEP;
            const off = WOBBLE_AMP * dSmooth * Math.sin(phase);
            maxOffset = Math.max(maxOffset, Math.abs(off));
            xs.push(bx + nx * off);
            ys.push(by + ny * off);
            // arc length ds = √(r² + a²)·dθ, so an equal step needs dθ = ds / √(r² + a²)
            theta += SAMPLE_STEP / Math.sqrt(r * r + a * a);
        }
        return { xs: Float32Array.from(xs), ys: Float32Array.from(ys), turns: theta / TAU, maxOffset: maxOffset };

02One Zigzag Line That Packs Tighter in the Shadows

One line runs back and forth across 27 rows set 10px apart, 1px at a time, and darkness raises both the zigzag height (up to 4.4px) and how fast its phase advances, so shadows fill with tight peaks while the row ends link through 14-step half circles. The result is 12,137 vertices and 13,688px drawn at 2,738px per second in 5 seconds, with 10 to 61 turns per row (806 in all), and even an empty row at background darkness 0.03 still turns 10 times, so no row is perfectly flat. It suits a cafe menu header or a product sketch where a hand-shaded cup should fill in.

One Zigzag Line That Packs Tighter in the Shadows
Open in a new tab

02 · One Zigzag Line That Packs Tighter in the Shadows — Measured area 21.386% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

ROW_GAPZIG_AMPZIG_FREQ_DARK
        for (let ry = ROW_TOP; ry <= H - ROW_TOP; ry += ROW_GAP) {
            const dir = rows % 2 === 0 ? 1 : -1;
            let phase = 0;
            for (let k = 0; k <= ROW_RIGHT - ROW_LEFT; k += 1) {
                const x = dir > 0 ? ROW_LEFT + k : ROW_RIGHT - k;
                const d = dark(x, ry);
                phase += lerp(ZIG_FREQ_LIGHT, ZIG_FREQ_DARK, d);
                xs.push(x);
                ys.push(ry + ZIG_AMP * d * triangleWave(phase));
            }
            // turn count — the triangle wave changes direction once every π of phase
            turnsPerRow.push(Math.floor(phase / Math.PI));
            rows += 1;
            if (ry + ROW_GAP > H - ROW_TOP) {
                break;
            }
            // row-end half circle — from this row's end to the next row's end, bulging outward with radius ROW_GAP/2
            const ex = dir > 0 ? ROW_RIGHT : ROW_LEFT;
            const cy = ry + ROW_GAP / 2;
            for (let j = 1; j < TURN_STEPS; j += 1) {
                const a = -Math.PI / 2 + Math.PI * j / TURN_STEPS;
                xs.push(ex + dir * Math.cos(a) * ROW_GAP / 2);
                ys.push(cy + Math.sin(a) * ROW_GAP / 2);
            }
        }

03Stippled Dots That Spread Evenly by Moving to Their Darkness-Weighted Centers

1,800 dots are scattered toward the dark areas, and then 14 times, every 0.22 seconds starting at 0.25 seconds, each dot is sent to the darkness-weighted center of the cells for which it is the nearest dot and glides there, with dots moving faster than 30px per second shown in coral and settled dots in ink. The mean distance to the nearest neighbor goes from 1.88px with a spread of 0.68 right after scattering to 2.88px and 0.5 at the end, the average move per step drops from 1.07px to 0, and 204 dots end up on the two eyes. It fits a pet brand landing page where a portrait should come into focus out of dots.

Stippled Dots That Spread Evenly by Moving to Their Darkness-Weighted Centers
Open in a new tab

03 · Stippled Dots That Spread Evenly by Moving to Their Darkness-Weighted Centers — Measured area 12.4% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

STIPPLE_COUNTRELAX_ROUNDSMAX_DOTS
        let moved = 0;
        for (let i = 0; i < dots.n; i += 1) {
            // a dot with no cells assigned stays put — no division by zero
            toX[i] = sumW[i] > 0 ? sumX[i] / sumW[i] : dots.xs[i];
            toY[i] = sumW[i] > 0 ? sumY[i] / sumW[i] : dots.ys[i];
            moved += dist2(toX[i], toY[i], dots.xs[i], dots.ys[i]);
        }
        return moved / Math.max(1, dots.n);

04A One-Line Path That Untangles by Flipping Crossed Segments

The 420 stops, depot included, are joined nearest-first over 1.4 seconds, and from 1.55 seconds each frame compares 1,200 segment pairs, reverses the order between any two that cross and flashes the flipped stretch as a coral band. Clearing 34 crossed pairs takes 32 flips and 261,459 compared pairs, and at 60Hz, after 218 frames at 5.18 seconds, the start of the status line switches from “Untangling crossings” to “Untangled”, while the “· Total length” readout stays on after the route is clear. It fits a florist or subscription delivery page that shows customers the order of a day's round.

A One-Line Path That Untangles by Flipping Crossed Segments
Open in a new tab

04 · A One-Line Path That Untangles by Flipping Crossed Segments — Measured area 18.442% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

STOP_COUNTTRIES_PER_FRAMEGREEDY_TIME
    function segmentsCross(ax, ay, bx, by, cx, cy, dx, dy) {
        const d1 = cross3(ax, ay, bx, by, cx, cy);
        const d2 = cross3(ax, ay, bx, by, dx, dy);
        const d3 = cross3(cx, cy, dx, dy, ax, ay);
        const d4 = cross3(cx, cy, dx, dy, bx, by);
        return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0));
    }
            if (segmentsCross(xs[a], ys[a], xs[b], ys[b], xs[c], ys[c], xs[d], ys[d])) {
                // reverse the order from i+1 to j — rejoined as a→c and b→d, the crossing disappears and the length shrinks
                let lo = u.i + 1;
                let hi = u.j;
                u.lastLo = lo;
                u.lastHi = hi;
                while (lo < hi) {
                    const tmp = o[lo];
                    o[lo] = o[hi];
                    o[hi] = tmp;
                    lo += 1;
                    hi -= 1;
                }
                u.flips += 1;
                u.flipsThisPass += 1;
                flipped += 1;
            }

05A Pen That Keeps the Same Speed Even Through the Curves

A signature of 39 points, four loops plus a 6-point underline, is joined by a cubic curve whose slope at each point comes from its two neighbors, and a 609-entry running-length table lets the pen hold 240px per second over the whole 1,312.6px for 5.47 seconds. A lavender dot dropped every 0.12 seconds lands 28.8px further along the curve each time (45 dots, though the straight-line gap shrinks to 3.4px at the loop tops), and the band under the line marks where a pen given equal time per piece would have run slow (sky) or fast (coral). It belongs under a brand hero as a founder's signature or as the sign-off on a thank-you page.

A Pen That Keeps the Same Speed Even Through the Curves
Open in a new tab

05 · A Pen That Keeps the Same Speed Even Through the Curves — Measured area 4.36% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PEN_SPEEDTICK_EVERYCURVE_STEPS
    function indexAtLength(c, s) {
        if (s <= 0) {
            return 0;
        }
        if (s >= c.total) {
            return c.lens.length - 1;
        }
        let lo = 0;
        let hi = c.lens.length - 1;
        while (hi - lo > 1) {
            const mid = (lo + hi) >> 1;
            if (c.lens[mid] < s) {
                lo = mid;
            }
            else {
                hi = mid;
            }
        }
        return lo + (s - c.lens[lo]) / Math.max(1e-9, c.lens[hi] - c.lens[lo]);
    }

06A Ballpoint Line That Trembles and Changes Width with Speed

An outer heart of 40 points, one step inward and an inner heart pushed 7px in along the normal are drawn as one line, and at each spot the sharpest bend within 14px on either side sets the speed (70 to 492px per second here), the speed alone sets the width, and a noise tremor scaled by TREMOR_AMP 1.2 nudges the line sideways. Width runs from 2.42 to 4.96px, thin on the quick stretches and thick at the slow corners, the full 1,742.1px takes 7.45 seconds, and the pink felt inside rises from 0 to 0.55 opacity as the line advances. It suits the first card of a wedding invitation or a handwritten note on an anniversary page.

A Ballpoint Line That Trembles and Changes Width with Speed
Open in a new tab

06 · A Ballpoint Line That Trembles and Changes Width with Speed — Measured area 23.39% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

TREMOR_AMPWIDTH_KCORNER_SLOW
            let peak = 0;
            for (let j = lo; j < n && c.lens[j] - c.lens[i] <= CORNER_LOOK; j += 1) {
                peak = Math.max(peak, bend[j]);
            }
            const v = Math.max(PEN_SLOWEST, PEN_FAST / (1 + CORNER_SLOW * peak));
            speeds[i] = v;
            widths[i] = PEN_WIDTH / (1 + WIDTH_K * v);
            if (i > 0) {
                times[i] = times[i - 1] + (c.lens[i] - c.lens[i - 1]) / ((v + speeds[i - 1]) / 2);
            }

07Single-Stroke Figures Solved by Counting Corners with an Odd Number of Lines

Four figures take turns: for 0.45 seconds each corner shows how many lines meet there and odd corners get a coral ring, then a figure with 0 or 2 odd corners is traced in its found order at 520px per second as pastel bands with a step number on each line. The house (2 odd) starts at corner 0 and ends at corner 1 over ten strokes in 2.35 seconds, the bow (0 odd) splices in one loop for six strokes in 1.79 seconds, the star takes ten strokes in 3.27 seconds, and the envelope, with four odd corners, gets an X stamp for 1.8 seconds. It fits a kids' worksheet or a puzzle page that reveals, like an answer key, whether a figure can be drawn and in what order.

Single-Stroke Figures Solved by Counting Corners with an Odd Number of Lines
Open in a new tab

07 · Single-Stroke Figures Solved by Counting Corners with an Odd Number of Lines — Measured area 11.544% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

COUNT_TIMETRACE_SPEEDIMPOSSIBLE_TIME
        const deg = degrees(f);
        const odd = [];
        for (let i = 0; i < deg.length; i += 1) {
            if (deg[i] % 2 === 1) {
                odd.push(i);
            }
        }
        if (odd.length > 2) {
            return { possible: false, odd: odd, order: [], splices: 0 };
        }
            if (next >= 0) {
                // started walking again while backing up = splice in one leftover loop
                if (backing) {
                    splices += 1;
                    backing = false;
                }
                stack.push(next);
            }
            else {
                out.push(stack.pop());
                backing = true;
            }

08A Cursive Name Written in One Line by Joining the Letters

Each letter of the word atelier is painted offscreen in an italic 700 104px serif face and read back, and on a 3px grid its edge cells (535 here, a count that depends on the serif font the device has) are chained nearest-first and joined to the next letter by a stroke through two points 14px below the baseline, one 6px right of the previous letter's end and one 6px left of the next letter's first dot. The 280 points and 6 joining strokes become one 2,299px curve written at 620px per second in 3.71 seconds, with a highlighter band under each letter in its own pastel. It suits a studio or shop name under a logo, or the name line on a personal home page.

A Cursive Name Written in One Line by Joining the Letters
Open in a new tab

08 · A Cursive Name Written in One Line by Joining the Letters — Measured area 5.701% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

GLYPH_CELLBASELINE_DIPWRITE_SPEED
        for (let k = 0; k < WORD.length; k += 1) {
            const x = left + m.measureText(WORD.slice(0, k)).width;
            lefts.push(x);
            const g = glyphOutline(WORD[k], x);
            cells += g.cells;
            const path = glyphPath(g);
            if (path[0].length === 0) {
                continue;
            }
            if (px.length > 0) {
                // end of the previous letter → down below the baseline → under the next letter's first dot → first dot: the cursive joining stroke
                px.push(px[px.length - 1] + 6, path[0][0] - 6);
                py.push(BASE_Y + BASELINE_DIP, BASE_Y + BASELINE_DIP);
                joins += 1;
            }
            starts.push(px.length * CURVE_STEPS);
            for (let i = 0; i < path[0].length; i += 1) {
                px.push(path[0][i]);
                py.push(path[1][i]);
            }
        }

09A Flower Hero Drawn in One Line, Then Washed with Color

700 dots are evened out over 6 steps, joined nearest-first from the foot of the stem and untangled on screen at 14,000 pairs per frame, while the pen draws the fully solved order computed up front as an ink line like the one in 06, with a 1.4 tremor and corner slowdowns, retimed to finish in 3.4 seconds. The first route shrinks from 4,094px to 3,826px as 74 crossings fall to 0 after 61 flips, the on-screen untangle wraps up in 70 frames, by 2.71 seconds at 60Hz and so before the pen sets off at 2.84 seconds, and from 6.24 seconds ten color patches wash in under the line for 1.4 seconds, completing the picture at 7.64 seconds. It works as the landing hero for a flower shop or a portfolio that wants the drawing first and the color after.

A Flower Hero Drawn in One Line, Then Washed with Color
Open in a new tab

09 · A Flower Hero Drawn in One Line, Then Washed with Color — Measured area 35.003% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

FLOWER_DOTSTRIES_09PEN_TIME_09
    const greedy09 = greedyOrder(final09.xs, final09.ys, final09.n, start09);
    const greedy09Length = pathLength(final09.xs, final09.ys, greedy09, final09.n);
    // the fully untangled order — the same computation is run to the end up front. Even if the on-screen untangle step runs out of time, the pen draws this order
    const solved09 = makeUntangle(Int32Array.from(greedy09));
    while (!solved09.done) {
        untangleStep(solved09, final09.xs, final09.ys, TRIES_09);
    }
    const solved09Length = pathLength(final09.xs, final09.ys, solved09.order, final09.n);
    function orderedCurve(o) {
        const px = [];
        const py = [];
        for (let k = 0; k < o.length; k += 1) {
            px.push(final09.xs[o[k]]);
            py.push(final09.ys[o[k]]);
        }
        return curveThrough(px, py);
    }
    const ink09 = inkLine(orderedCurve(solved09.order), TREMOR_09);

Dots from Brightness, Order from Uncrossing, Speed from Running Length — the Math Behind a Single Line

Turning darkness into odds

03 measures darkness once in each of 16,000 cells (3px cells, 160 by 100) to build a weight table, and 09 scatters its flower dots with the same function. It then picks random spots and keeps each one with a probability equal to that cell's weight raised to the power 1.4. A cell at darkness 0.5 keeps a dot with probability 0.5^1.4 ≈ 0.38 and a cell at 0.9 with 0.9^1.4 ≈ 0.86, so a 1.8-to-1 darkness ratio widens to about 2.3 to 1. The higher exponent leaves fewer stray dots on the pale background and crowds more of them onto the dark outline.

Working the crossing test by hand

segmentsCross decides with four cross products. Feed it a(0,0)–b(10,10) and c(0,10)–d(10,0): seen from a–b, c gives +100 and d gives −100, and seen from c–d, a gives −100 and b gives +100, so both pairs change sign and the segments cross. The two segments add up to 14.14 + 14.14 = 28.28, and reversing the order between them rejoins the ends as a→c and b→d for 10 + 10 = 20. Rejoining two crossed segments so they no longer cross does not lengthen the route, so repeated flips can only shrink the total. A cross product of exactly 0, meaning three points on one line, does not count as a crossing, which appears to stop flips that would not shorten the route from bouncing back and forth over the same spot.

Length into time, bends into speed

05's pen turns elapsed time × 240 into a length s, and indexAtLength halves the 609-entry running-length table until it reaches the step that holds s. The 608 steps are cut in half each round and 2^10 = 1,024 is larger than 608, so the step is settled in at most 10 rounds. 06 works the other way and derives speed from the bends: a straight spot with bend 0 gets v = 560 and width 6 ÷ (1 + 0.003 × 560) ≈ 2.24px, while a bend of 0.1 rad per px gets v = 560 ÷ (1 + 60 × 0.1) = 80 and width 6 ÷ 1.24 ≈ 4.84px. On the heart even the straightest stretch only reaches 492px per second, so the real width starts at 2.42px. Because a canvas path is stroked with one lineWidth, 06 sorts its widths into 8 bins and strokes one path per bin.

Where the Line Breaks: a Reordering Pass That Stalls at Ten Thousand Dots, Curves That Kink at the Seams and Figures with Four Odd Corners

Ten thousand dots and the reordering pass holds the screen

One full pass of untangleStep compares (N − 3)(N − 2) ÷ 2 pairs. With 420 dots that is 87,153 pairs, so at 1,200 pairs per frame a pass takes 73 frames, but with 10,000 dots one pass grows to 49,975,003 pairs, or 41,646 frames at the same rate (11.6 minutes at 60 frames per second). Run the whole pass inside one frame with no cap and that single frame holds 50 million pairs, and even the nearest-first join takes 49,995,000 comparisons. A per-frame cap swaps that for a different catch, since the untangle time then follows the display's refresh rate. 04's 218 frames take 3.63 seconds at 60 frames per second and 7.27 seconds at 30, which by this arithmetic likely runs past the 5.2-second drawing phase (DRAW_TIME) before “Untangled” appears, and 1.82 seconds at 120. 03, asked for 10,000 dots, stops at 2,400 through MAX_DOTS, and its 12px buckets cut one relaxation step from 5,731,200 comparisons to 218,776, about 26.2 times fewer.

Pieces with their own tangents kink at the seams

Join the 39 signature points of 05 with pieces that each use their own chord as the tangent at both ends, and the direction one piece ends with stops matching the direction the next one starts with. Measured on the same points, the 37 seams bend by up to 158.8° and by 44.6° on average, with 34 of them past 20°. Tangents taken from each point's two neighbors are shared by the pieces on both sides of a seam, so the kink there is 0°. Drop the length table as well and give each piece the same time, and piece length turns into speed, so the pen swings between 76 and 769px per second, a 10.17-to-1 range. To watch the seams kink yourself, swap the two tangents that buildCurve passes to cubicPiece for that piece's chord in the 05 folder's index.html, after opening the downloaded archive with the password hqaux5m7.

Four corners with three lines each cannot be drawn in one stroke

The envelope in 07 is a rectangle plus four diagonals that meet in the middle, so its degree list is [3, 3, 3, 3, 4] and it has four odd corners. planStroke does not search for an order at all and returns impossible on the spot. The house has degrees [3, 3, 4, 4, 2, 4], two odd corners, so it starts at one and ends at the other, and the bow and the star have no odd corners, so they return to where they began. When you design a figure, adding or removing one line flips the parity of both of its end corners, so a figure with four odd corners becomes drawable once a single extra line joins two of them.

Sparser Dots, Stronger Tremor, a Faster Pen — One Value That Changes the Hand-Drawn Feel

Each row below edits one constant in one file and reruns the same functions to measure the change.

Variant Value changed What changes
Sparser dots 03 STIPPLE_COUNT 1800 → 700 Mean neighbor distance 2.88 → 5.09px and spread 0.50 → 0.42, so the dots sit more evenly, but the two eyes get 73 dots instead of 204 and look thin; comparisons per step fall from 218,776 to 84,284, and the 6.38-second cycle stays the same
Stronger tremor 06 TREMOR_AMP 1.2 → 3 The narrowest gap between the two heart outlines goes from 2.16 to −0.13px, so the lines touch in 2 places instead of 0; length, timing and width are unchanged
Faster pen 05 PEN_SPEED 240 → 520 Drawing time 5.47 → 2.52 seconds, cycle 8.47 → 5.52 seconds, spacing of the 0.12-second dots 28.8 → 62.4px, dot count 45 → 21

Stopping on the Finished Drawing: Reduced Motion, a Pause Button and a Scene Description for Accessibility

What remains under reduced motion

All nine scenes read prefers-reduced-motion: reduce, and when it is on they skip the loop, compute up to the end time in 1/60-second steps and show the finished drawing. Switching the setting midway is caught by the change listener, which jumps to the same finished frame. For 07 that frame is the house fully traced. On first load, 01, 02, 05, 06, 08 and 09 open their first cycle partway in (01 at 1.5, 02 at 2, 05 at 2.6, 06 at 0.8, 08 at 1.4 and 09 at 4.6 seconds) so the board does not open blank, while 03, 04 and 07 start at 0.

The button and screen readers

Each of the nine canvases carries role="img" with an aria-label describing the picture, the pause button at the bottom right reports its state through aria-pressed, and keyboard focus draws a 3px ink outline. 04 and 07, the two scenes with a status line, keep aria-live off during the demo and announce changed text only after the first press of the button. When the tab is hidden the loop stops, and on return the clock is reset so the drawing picks up where it left off. The references are MDN prefers-reduced-motion and MDN requestAnimationFrame.

Questions to Answer Before Adding One Line Art Animation to a Web Page

Where do I change the code to build this from my own photo?

01, 02, 03 and 09 call a darkness function such as faceDark that returns 0 to 1 per cell, so draw your photo onto an offscreen canvas, read it with getImageData and return 1 minus the brightness in its place. A canvas that has drawn an image from another origin throws a security error on that read, so wrap the line in try as 08 does and keep the photo on the same site.

Can a single line drawing animation with thousands of dots run smoothly on a phone?

04 compares at most 1,200 pairs per frame and 09 at most 14,000, and 03 and 09 stop scattering at 2,400 dots. The canvas pixel ratio is capped at 2 as well. Before adding more dots, run the numbers from the section on where the line breaks, because reordering time grows with the square of the dot count.

What do I write to add my own single-stroke figure to 07?

Each entry in 07's FIGURES holds two flat arrays: pts, the corner coordinates listed as alternating x and y, and edges, the pairs of corner numbers that are joined. Add a figure in that shape and the degree count, odd-corner rings, order search and impossible stamp all run unchanged. More than 2 odd corners earns the impossible stamp, and each added line that joins two odd corners removes 2 of them.

Enter the archive password

The password is inside this article. You will find it as you read.