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

Coin Flip: 9 Canvas Toss Scenes

How does a coin flip pick a side? 03 multiplies airtime by spin rate and reads the parity of half turns, 05 draws a ratio narrowing toward half, and 08 bounces

A Coin Flip Is Reading an Angle, Not Picking a Side: a Coin Flip Simulator Starts by Letting One Rotation Angle Decide Both the Shape and the Face

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

The nine scenes follow one rule. How flat the coin looks comes from the cosine of an angle θ, the visible face comes from the sign of that cosine, and a toss result comes only from the parity of the half turn count. The one random source is a linear congruential generator, seeded(), so the same seed gives the same result, and the source has 0 calls to Math.random, Date or fetch. That makes this coin flip simulator a calculation you can watch, and it is not a tool that judges whether a real coin is fair.

The order runs from one coin to a tally. 01 and 02 cover the shape and the rim light, 03 and 04 cover the toss and the edge landing, 05, 06 and 07 cover the ratio, the offset and the runs that thrown results build up, and 08 and 09 cover a bounce on a stack and a decision between two choices.

Coin Flip: 9 Scenes — a 3D Face Swap That Squashes Flat, a Metal Rim Painted by Light, a Parabolic Toss Read From Half Turns, a Narrow Edge Landing, a Tally Band That Settles to Half, a Weighted Coin, a Run Strip That Grows, a Bounce on a Stack and a Two Choice Decision Hero

What nine renders measured for area and motion

Scene Cumulative area Moving frames Mean intensity
01 Axis spin 24.5% 23/23 63.7
02 Rim light 13.8% 23/23 65.5
03 Toss 17.1% 23/23 48.5
04 Edge stand 18.1% 23/23 85.6
05 Converging band 19.1% 23/23 78.3
06 Weighted coin 11.4% 23/23 93.9
07 Run strip 18.3% 23/23 59.7
08 Bounce 15.8% 23/23 69.1
09 Decision hero 20.3% 23/23 64.0

01 covers the most area at 24.5% because its coin is the largest, and 06 covers the least at 11.4% yet has the highest mean intensity at 93.9. All nine scenes moved in 23/23 frames, so no stretch of the render sat still.

01A Coin That Squashes Flat and Flips Faces Around a Horizontal Axis

A coin is a disc whose width stays fixed, so at the angle θ from ANGLE() only the vertical diameter shrinks, by a factor of COS_SQUASH, and once the angle passes 90 degrees and the cosine turns negative the front and back marks swap. The small coin on the right squashes the same way but keeps drawing the front mark, which makes the swap stand out by contrast. One turn takes 1.50 seconds at OMEGA 4.2 radians per second, and the first swap lands near 0.30 seconds.

A Coin That Squashes Flat and Flips Faces Around a Horizontal Axis
Open in a new tab

01 · A Coin That Squashes Flat and Flips Faces Around a Horizontal Axis — Measured area 24.478% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

OMEGAANGLECOS_SQUASH
    function ANGLE() { return still ? 0 : angle0 + omegaNow * sceneT; }
    function faceName(n) { return n === 0 ? 'Heads' : 'Tails'; }
    function draw() {
        skyFill(BG_TOP, BG_BOT);
        tableBack(230);
        const th = ANGLE();
        const COS_SQUASH = Math.cos(th);
        const cx = 168;
        const cy = 154;
        // halo around the coin - spreads in proportion to how far it is opened vertically (|cos|)
        disc(cx, cy, COIN_R * (1.15 + 0.35 * Math.abs(COS_SQUASH)), COS_SQUASH >= 0 ? GOLD : SKY, 0.34 * Math.abs(COS_SQUASH));
        oval(cx, 240, COIN_R * (0.8 + 0.3 * Math.abs(COS_SQUASH)), COIN_R * 0.26, INK, 0.2);
        drawCoin(cx, cy, COIN_R, th, THICK_PX, 0.36, -0.46, 0.81, 1, 0);

02A Metal Rim Painted From Edge Thickness and Light Direction

Tilt the coin and a rim band shows between the two discs; the rim is split into RIM_N 40 pieces, and each piece takes its brightness from dotLight, the dot product of the piece normal and the light direction. As the coin turns the bright pieces slide to one side, and the 40 cell scale below paints the values of the same function. The coin on the right has thickness 0, so it has no rim pieces and looks paper thin.

A Metal Rim Painted From Edge Thickness and Light Direction
Open in a new tab

02 · A Metal Rim Painted From Edge Thickness and Light Direction — Measured area 13.775% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

THICK_RIMRIM_NdotLight
    function lightLen() { return Math.hypot(LIGHT[0], LIGHT[1], LIGHT[2]); }
    // brightness of one piece - the dot product of its normal and the light direction, mapped from 0 to 1
    function dotLight(pm, th) {
        const n = lightLen();
        const d = (Math.cos(pm) * LIGHT[0] + Math.sin(pm) * Math.cos(th) * LIGHT[1] + Math.sin(pm) * Math.sin(th) * LIGHT[2]) / n;
        return clamp(0.5 + 0.5 * d, 0, 1);
    }

03A Parabolic Toss That Reads the Face From Airtime Times Spin Rate

The coin leaves the hand on a parabola ruled only by gravity, and AIR_T 0.9 seconds times hr, the half turns per second (2 * spinOf(k)), gives halfTurns. On landing Math.round rounds it, and an even count leaves the starting face up while an odd count shows the other. The first toss makes 12.6 half turns, rounds to 13 and lands tails, the second makes 13.68, rounds to 14 and lands heads, and the third makes 14.76, rounds to 15 and lands tails.

A Parabolic Toss That Reads the Face From Airtime Times Spin Rate
Open in a new tab

03 · A Parabolic Toss That Reads the Face From Airtime Times Spin Rate — Measured area 17.137% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

AIR_TSPIN_RATEhalfTurns
        // half turn readout - the number climbs during the toss, and on landing it is rounded and names the face
        const e = tosses[k];
        const live = tossHalf(e, sceneT);
        const landed = sceneT >= e.t0 + e.T;
        box(286, 66, 184, 74, 12, PAPER, 0.9);
        text('Half turns', 296, 82, INK, 1, 11, 700, 'left');
        text(live.toFixed(1), 462, 84, INK, 1, 22, 800, 'right');
        if (landed) {
            const r = Math.round(landHalf(e));
            const verdict = 'Half turns {r} - {p} to {f}'.replace('{r}', String(r)).replace('{p}', r % 2 === 0 ? 'even' : 'odd').replace('{f}', faceName(upFace(r)));
            tag(verdict, 462, 124, upFace(r) === 0 ? GOLD : SKY, INK, 12, 'right');
        }
        else {
            text('Turns per second {s}'.replace('{s}', spinOf(k).toFixed(1)), 296, 124, INK, 1, 11, 700, 'left');
        }

04An Edge Landing Through the Very Narrow Window Thickness Opens

To stand on its rim, the coin has to land inside the narrow window EDGE_WIN(thick), which is the atan of thickness over diameter. Over N_THROWS 100 throws from one fixed sequence a coin of thickness 8 stands 8 times, thickness 16 stands 14 times and thickness 0 does not stand once. At thickness 8 the whole window is 7.9% of a half circle, which is why standing is rare.

An Edge Landing Through the Very Narrow Window Thickness Opens
Open in a new tab

04 · An Edge Landing Through the Very Narrow Window Thickness Opens — Measured area 18.149% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

EDGE_WINtiltAtLandN_THROWS
    function EDGE_WIN(thick) { return Math.atan(thick / DIAM_E); }
    // landing tilt of each throw (radians, 0 to π) - the same fixed sequence
    function tiltAtLand(seed, n) {
        const rng = seeded(seed);
        const out = [];
        for (let i = 0; i < n; i += 1) {
            out.push(rng() * PI);
        }
        return out;
    }
    // 0 heads - 1 tails - 2 rim
    function landKind(ang, thick) {
        if (thick > 0 && Math.abs(ang - PI / 2) < EDGE_WIN(thick)) {
            return 2;
        }
        return ang < PI / 2 ? 0 : 1;
    }

05A Narrowing Band as the Heads Ratio Settles Toward Half With More Tosses

Results from a fixed sequence stack one at a time, ratioLine joins the heads ratio into a line, and a band of half width bandHalf(n) = 1/(2√n) is laid around it. The ratio is 0.600 at toss 10, 0.560 at 25, 0.520 at 50 and 0.560 at 100, and outsideCount finds 7 points outside the band within 100 tosses. The horizontal axis is ln n, so the first few tosses spread wide.

A Narrowing Band as the Heads Ratio Settles Toward Half With More Tosses
Open in a new tab

05 · A Narrowing Band as the Heads Ratio Settles Toward Half With More Tosses — Measured area 19.085% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

TOSS_NbandHalfratioLine
    function bandHalf(n) { return 1 / (2 * Math.sqrt(n)); }
    // heads ratio up to the n-th toss (starts at n = 1)
    function ratioLine(seq) {
        const out = [];
        let heads = 0;
        for (let i = 0; i < seq.length; i += 1) {
            if (seq[i] === 0) {
                heads += 1;
            }
            out.push(heads / (i + 1));
        }
        return out;
    }
    // count of tosses outside the band - counted when the n-th ratio is farther from 0.5 than bandHalf(n)
    function outsideCount(seq) {
        const r = ratioLine(seq);
        let c = 0;
        for (let i = 0; i < r.length; i += 1) {
            if (Math.abs(r[i] - 0.5) > bandHalf(i + 1) + 1e-9) {
                c += 1;
            }
        }
        return c;
    }

06A Weighted Coin Whose Ratio Leans Toward One Face

Putting extra weight on one face widens the range of landing angles where the heavy face ends up down, and the source's headsP(bias) adds half the offset to 0.5. pairSeq splits a fair coin (u < 0.5) and a weighted coin (u < headsP) from the same number u, so the two coins part ways on the same toss. Over 100 tosses the fair coin lands heads 51 times, and the coin with BIAS_OFFSET 0.10 lands heads 58 times.

A Weighted Coin Whose Ratio Leans Toward One Face
Open in a new tab

06 · A Weighted Coin Whose Ratio Leans Toward One Face — Measured area 11.394% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

BIAS_OFFSETheadsPpairSeq
    // probability that the weighted coin lands heads up - the share of angles where the heavy tails side settles down, so half the offset (relative to the diameter) is added to 0.5
    function headsP(bias) { return 0.5 + bias * 0.5; }
    function pairSeq(seed, n, bias) {
        const rng = seeded(seed);
        const fair = [];
        const heavy = [];
        const p = headsP(bias);
        for (let i = 0; i < n; i += 1) {
            const u = rng();
            fair.push(u < 0.5 ? 0 : 1);
            heavy.push(u < p ? 0 : 1);
        }
        return [fair, heavy];
    }

07A Run Strip Whose Length Grows While the Same Face Repeats

Each toss paints one cell of a horizontal strip, cellRun counts the run length that ends at every cell, and the longest run gets a thick outline. In this 200 toss sequence the longest run is 8 cells, while the handMade sequence repeats a face with 30% probability and flips it at the third repeat, so it stays at 3 cells or fewer. Heads counts are 95 for the thrown sequence and 92 for the handmade one, so counting heads alone does not tell them apart.

A Run Strip Whose Length Grows While the Same Face Repeats
Open in a new tab

07 · A Run Strip Whose Length Grows While the Same Face Repeats — Measured area 18.313% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

STRIP_NcellRunhandMade
    // run length that ends at each cell
    function cellRun(seq) {
        const out = [];
        for (let i = 0; i < seq.length; i += 1) {
            out.push(i > 0 && seq[i] === seq[i - 1] ? out[i - 1] + 1 : 1);
        }
        return out;
    }
    // index of the cell where the longest run ends (earlier one wins)
    function runEnd(seq, upto) {
        const r = cellRun(seq);
        let best = -1;
        let bl = 0;
        for (let i = 0; i < upto; i += 1) {
            if (r[i] > bl) {
                bl = r[i];
                best = i;
            }
        }
        return best;
    }

08A Chain of Bounces as a Coin Drops Onto a Stack

A coin dropped on a stack gets its hop heights from hopPeaks, which takes the first landing speed from the fall distance and multiplies it by REST_E 0.45 on each hop until the speed falls under HOP_MIN 30. With 3, 4 and 5 coins stacked the fall distances are 127.7, 116.7 and 105.7 px, and the first hop heights are 25.8, 23.6 and 21.4 px. The coin hops 3 times (25.8, 5.2 and 1.1 px), and at 3 coins the hops add up to 0.614 seconds.

A Chain of Bounces as a Coin Drops Onto a Stack
Open in a new tab

08 · A Chain of Bounces as a Coin Drops Onto a Stack — Measured area 15.761% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

REST_EhopPeaksSTACK0
    // one drop - the first landing speed from the fall distance d, and the hop heights
    function hopPeaks(d, e) {
        const out = [];
        let v = Math.sqrt(2 * GRAV * d) * e;
        while (v >= HOP_MIN) {
            out.push(v * v / (2 * GRAV));
            v *= e;
        }
        return out;
    }

09A Table Hero That Tosses to Decide Between Two Choices

Two choices sit on the left of the table, and when the coin lands, the parity of the half turn count made by halfFor sets the face and the matching choice lights up. The first round's four tosses are tails, tails, heads and tails, because 17.15, 13.11, 15.98 and 17.09 half turns round to 17, 13, 16 and 17, which piles up 1 for reading and 3 for walking. tally(face) counts 0.25 seconds after landing.

A Table Hero That Tosses to Decide Between Two Choices
Open in a new tab

09 · A Table Hero That Tosses to Decide Between Two Choices — Measured area 20.299% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

TOSS_COUNTtallyhalfFor
    function build() {
        const rng = seeded(SEED + lap * 31);
        tosses = [];
        faces = [];
        for (let k = 0; k < TOSS_COUNT; k += 1) {
            const f = rng() < 0.5 ? 0 : 1;
            faces.push(f);
            const h = halfFor(f, rng, 6);
            tosses.push({ t0: 0.2 + TOSS_GAP * k, T: AIR_T, x0: 262, y0: 214, x1: 344 + (k % 2) * 36, y1: GROUND_Y, peak: 120, hr: h / AIR_T, h0: 0 });
        }
    }

When You Write Coin Flip Code, How Many Values Move the Coin?

Four inputs draw one coin

drawCoin has ten parameters, and four of them, the rotation angle th, the thickness thick, the light direction lx·ly·lz and the start face startFace, decide the shape and the face. The top disc is an ellipse with vertical radius R·|cos th|, and a positive cos th draws the start face while a negative one draws the opposite face. The rim paints only those of its 40 pieces where sin(pm) * s is positive.

    function drawCoin(cx, cy, R, th, thick, lx, ly, lz, a, startFace) {
        const c = Math.cos(th);
        const s = Math.sin(th);
        const sg = c >= 0 ? 1 : -1;
        const ac = Math.max(Math.abs(c), 0.012);
        const half = thick / 2;
        const near = -sg * s * half;
        const far = -near;
        const nearFace = sg > 0 ? startFace : 1 - startFace;
        if (thick > 0) {
Value Meaning
th Rotation angle. The shape and the face both come from it
thick Rim thickness. At 0 there are no rim pieces
lx·ly·lz Light direction, used in each piece's dot product
startFace The face shown at angle 0, where 0 is heads

Putting it on a first screen

As a javascript coin flip animation on a first screen, the two choice cards of 09 turn into a decision helper. Swap the card text and it can pick between lunch menus or a speaking order, and the face calculation sits in two functions, halfFor and upFace. Here the parity of half turns splits two faces, whereas the dice roll article reads the top face of a cube with a rotation matrix.

Using it in a probability lesson

In a lesson the band of 05 comes first. Change the toss count from 100 to 400 and the band narrows to half its width, and 06 puts two coins side by side on the same sequence so students can see how much the offset shows in the ratio. 07 can become a task where students tell a handmade sequence from a thrown one by finding the longest run.

Set the Throw Strength, Find the Airtime, Read the Landing Angle From the Rotation Count and Decide the Face That Ends Up On Top: the Order in Which One Coin Flip Is Computed

The order of one toss

First a Toss object gets the start point, the end point, the airtime T, the peak height peak and the half turns per second hr. tossXY places the coin on a parabola, tossHalf grows the angle in proportion to time, and after landing halfAt lays the coin down with a damped oscillation toward the nearest whole half turn. Last, upFace reads the parity of the rounded half turn count to pick the face on top.

    function tossXY(k, t) {
        const u = clamp((t - k.t0) / k.T, 0, 1);
        return [lerp(k.x0, k.x1, u), lerp(k.y0, k.y1, u) - 4 * k.peak * u * (1 - u)];
    }
    // after landing - settles with a damped oscillation to the nearest flat angle (the rounded half turn count)
    function halfAt(k, t) {
        const q = t - (k.t0 + k.T);
        if (q <= 0) {
            return tossHalf(k, t);
        }
        const hl = landHalf(k);
        const hf = Math.round(hl);
        return hf + (hl - hf) * Math.exp(-9 * q) * Math.cos(26 * q);
    }
    function hopLift(k, t) {
        const q = t - (k.t0 + k.T);
        return q <= 0 ? 0 : 12 * Math.exp(-7 * q) * Math.abs(Math.sin(15 * q));
    }
    // face on top - round the half turn count, even keeps the face before the toss (0), odd gives the opposite face (1)
    function upFace(h) { return Math.abs(Math.round(h)) % 2; }

The seed changes when a round repeats

After LAP_S seconds lap goes up by 1 and build() makes new tosses and a new sequence, and 04, 05, 06 and 07 switch to the seed SEED + lap. That seed is why the demo does not repeat one result, and a person who tosses with Enter, Space or a click makes restart() raise lap and open a new round. The first round starts START_S seconds ahead, so 04, 06 and 07 already look filled when they open.

Where a Coin Flip Breaks: a Coin Whose Mark Stays the Same After the Face Changes, a Verdict That Wobbles at the Half Turn Boundary and a Zero Thickness Coin That Cannot Stand on Its Edge

A coin whose mark stays the same after the face changes

In 01 the right coin applies only scale(1, |cos θ|) and draws faceArt(0, sr, 1), the front mark, on every frame, so even at its narrowest no back appears and it reads as a flat sticker. Without the one line that ties the swap to the sign of cos θ, the coin turns into a folding picture.

        // contrast version on the right - does not swap faces and draws only the front mark, so even at the narrowest point the mark stays the same and looks wrong
        const sx = 372;
        const sy = 160;
        const sr = 42;
        coinShadow(sx, 240, sr, 0, 1);
        ctx.save();
        ctx.translate(sx, sy);
        ctx.scale(1, Math.max(Math.abs(COS_SQUASH), 0.012));
        faceArt(0, sr, 1);
        ctx.restore();

A verdict that wobbles at the half turn boundary

The rounding boundary is where the fractional part of the half turn count is 0.5. The first toss of 03 is 12.6, only 0.1 above the boundary 12.5, so raising SPIN_RATE from 7 to 8 alone gives 14.4 and changes the face. In 09, halfFor shakes a whole number of half turns by only ±0.2, which keeps each value at least 0.3 from the boundary, so that verdict does not wobble.

A zero thickness coin cannot stand on its edge

landKind in 04 judges the rim only when thick > 0, and EDGE_WIN(0) is also 0, so there is no window. That is why the thickness 0 coin on the right grid does not stand in 100 throws, while thickness 8 stands 8 times on the same sequence. To make it stand more often, shrink the diameter or thicken the coin to widen the window.

The finished source of all nine coin scenes and the React files are in the zip below, and the password for unzipping it is jfztqwgh.

Change the Toss Count, the Weight Offset and the Spin Rate and How Much Does the Result Ratio Shift? Count, Offset and Speed Compared

Name Changed value Feel
More tosses 05 TOSS_N 100 → 400 The band half width 1/(2√n) halves from 0.050 to 0.025, and the heads ratio of the seed 5055 sequence settles from 0.560 at toss 100 (0.060 off half, outside the band) to 0.4925 at toss 400 (0.0075 off, inside it). Points outside the band are counted cumulatively, so they grow from 7 to 25
More off-center weight 06 BIAS_OFFSET 0.10 → 0.25 headsP rises from 0.55 to 0.625, so the heads in the 100 tosses of the seed 6056 sequence grow from 58 to 66 while the fair coin stays at 51
Faster spin 03 SPIN_RATE 7 → 8 Over 0.9 seconds of airtime the first toss goes from 12.6 to 14.4 half turns, the rounded count changes from 13 (odd) to 14 (even), and the face on top flips from tails to heads

Before you change a value

The three values live in different scenes, so changing one value in one scene keeps the comparison clean. 05 and 06 have fixed seeds, so the same value gives the same numbers, and the SPIN_RATE of 03 climbs by SPIN_STEP 0.6 on each toss, so only the first toss is a fair baseline. The 400 toss figures come from running the same seed in Node, and to see them on screen LAP_S, RESULT_S and REST_S have to grow too, because one round draws only up to toss 136 and toss 400 would arrive at 28.03 seconds.

One Result Frame Under Reduced Motion: Accessibility Through the Outcome Read as a Sentence and a Keyboard Toss

Enter and Space toss, and P, N and R drive the stage

Every stage box is role="application", and its aria-label lists the controls. Enter, Space and a click toss again with a new seed, P pauses and R restarts from the beginning. N is not a one frame step in these scenes: the timetable script is empty, so stepOnce falls through to restart(), which opens the first scene of the next toss and holds it paused. The canvas is role="img" and its aria-label describes the scene in a sentence.

    pad.addEventListener('keydown', function (e) {
        if (e.altKey || e.ctrlKey || e.metaKey) {
            return;
        }
        const k = e.key;
        if (k === 'Enter' || k === ' ') {
            e.preventDefault();
            wake();
            restart();
        }
        else if (k === 'p' || k === 'P') {
            togglePause();
        }
        else if (k === 'n' || k === 'N') {
            stepOnce();
        }
        else if (k === 'r' || k === 'R') {
            wake();
            restart();
        }
    }, { signal: off.signal });

The status line stays quiet during the demo

The status line is aria-live="off" while the demo runs, so no sentences pour out while the coin spins, and the moment a person first touches the stage wake() switches it to polite. The result sentence of a round goes into the data-role="done" paragraph of a role="status" region once, at the RESULT_S second each scene sets, and restart() clears doneSaid so the next round announces its result again.

    // tell the screen reader once that a round has ended - it is not read again when the round repeats
    let doneSaid = false;
    function checkDone() {
        if (!doneSaid && sceneT >= RESULT_S) {
            doneSaid = true;
            doneP.textContent = statusText();
        }
    }

Reduced motion and a hidden tab

With prefers-reduced-motion on there is no loop: showRest runs the timetable up to REST_S seconds and draws one result frame. 01 and 02 fix the angle without rotation (01 shows the front face head on, 02 uses θ = 1.15), 03 and 09 stop on the scene where the first toss has landed, and 04, 05, 06, 07 and 08 stop on the piled up result. REST_S is 1.9 seconds for 03 and 09, 3.6 for 08, 5.6 for 07, 6.0 for 04, 8.6 for 05 and 11.0 for 06, and when the tab hides the loop stops and the first frame after return drops its time step.

    function showRest() {
        stopLoop();
        sceneT = 0;
        evAt = 0;
        script = [];
        build();
        runTo(REST_S);
        doneSaid = false;
        showNow();
    }

Text is dark ink (40, 36, 54), and its contrast against the pill backgrounds is 11.7 for gold, 9.6 for sky, 9.1 for mint, 6.9 for pink and 14.0 for paper, each to 1.

Coin Flip FAQ: Building Without a Library, Whether the Coin Flip Odds Are Really Half and What You Need When You Build It Yourself Instead of Using a Coin Flip Generator

Does this prove the coin flip odds are really half?

No. 05 shows one fixed sequence from seed 5055 closing in from 0.560 at toss 100 to 0.4925 at toss 400, which explains the shape of convergence, and it runs no test of whether a real coin is fair.

Do I need a library to build a coin flip?

No. Everything is drawn with canvas 2D, the sources of all nine scenes have 0 libraries, 0 WebGL and 0 CDN, and with 0 calls to Math.random the same seed gives the same result.

What do I add to use this as a coin flip generator for a draw?

The part that takes a fresh seed every time. This demo hard codes its seeds so the show repeats, so a real draw needs code that sets the seed to a value users cannot edit, and this article does not build that part.

For references, the ellipse page, the requestAnimationFrame page, the basic animations tutorial and the prefers-reduced-motion page cover the canvas and the setting. For a turning body see the spinning top article, and for a wheel that spins to a stop see the roulette article.

Enter the archive password

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