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

Flipbook Animation: 9 Canvas Page Flip Scenes

How does a flipbook make motion from a few drawings? 02 overlays faint neighboring pages, 05 measures where the flip rate fuses into one line, and 09 fills the

A Flipbook Is Not Drawing a Picture on Sheet After Sheet but a Calculation That Raises the Page Index With Time and Leaves the Gap to Neighboring Pages to the Eye: Making One Starts by Choosing the Page Count and the Flip Rate

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

One page-array design runs under the nine scenes. The drawings are twelve entries in a PAGES array, a page holds only the horizontal position, vertical position and squash of one ball, and the ball bounces twice across the twelve pages.

    function makePages(n) {
        const out = [];
        for (let i = 0; i < n; i += 1) {
            // Two bounces, so one bounce is half a pass: b is the position inside one bounce (0 to 1), h is the height (a parabola)
            const b = (i * 2 / n) % 1;
            const h = 4 * b * (1 - b);
            const sy = 0.8 + 0.2 * Math.min(1, h * 3);
            out.push({ x: 0.14 + 0.72 * (i / (n - 1)), y: FLOOR_Y - h * HOP_H, sx: 2 - sy, sy: sy });
        }
        return out;
    }
    const PAGES = makePages(FRAME_COUNT);

The page index is the whole part of a fractional phase

In 01, 02, 04 and 07 the function wrapIdx takes the whole part of the phase and returns it as the page index. The other scenes reach their pages another way, and each one says how in its own section. The clock is a single scene clock, and none of the nine sources calls Date or Math.random or loads an outside image.

Fix the page count and the flip rate first

A flipbook is a book of pages that seems to move when you riffle it, and in code it is a page index that climbs with time while it reads an array. The page count is FRAME_COUNT 12, and the rate is written as pages per second, like the FLIP_HZ of 12 in 01.

Flipbook Animation: 9 Scenes — the Rate a Thumb Riffles Pages, an Onion Skin Overlay, a Fan Bending Around the Spine, a Timing Chart With Uneven Spacing, the Rate Where Flicker Fuses, a Dragged Corner That Settles, Thickness and Edge Stripes, Rewind and Round-Trip Indexes and Automatic Middle Pages

The order starts at rate, then moves through overlay, bend and spacing, and ends with the eye's afterimage, a hand drag, the book's thickness, index math and filling middle pages. Scenes 01 to 05 run on a timetable, 06 is dragged by hand, and 07 to 09 change the page count, the index and the drawings themselves.

What the last full render measured for area, frames and intensity

The values in the table come from 측정.json. The largest cumulative areas are 32.8% in 06 and 32.6% in 03, the smallest is 6.8% in 05, and only 01 and 05 moved in 23/23 frames.

Scene Cumulative area Moving frames Mean intensity
01 Flip rate 15.6% 23/23 86.4
02 Onion skin 7.3% 7/23 69.1
03 Spine fan 32.6% 12/23 52.7
04 Spacing chart 25.8% 17/23 80.2
05 Afterimage fusion 6.8% 23/23 68.5
06 Corner drag 32.8% 18/23 58.6
07 Edge stripes 10.6% 20/23 84.5
08 Loop and round trip 17.4% 16/23 99.5
09 Middle pages 12.2% 9/23 85.9

01The Rate a Thumb Riffles the Pages

phase accumulates the rate times the time, its whole part is pageIndex, and rate chases its target exponentially so it does not stop dead. With a 170px page, one pass of 12 pages takes 1.0 second at FLIP_HZ 12 and 4.0 seconds at 3, and the ball moves 33.5px on average (55.9px at most) between pages. The scene covers 15.6% cumulative area and moved in 23/23 frames.

The Rate a Thumb Riffles the Pages
Open in a new tab

01 · The Rate a Thumb Riffles the Pages — Measured area 15.619% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

FLIP_HZpageIndexframeCount
    function update(dt) {
        if (dt <= 0) {
            return;
        }
        rate += (rateTgt - rate) * (1 - Math.exp(-6 * dt));
        if (!manual) {
            phase += rate * dt;
        }
        pageIndex = wrapIdx(phase, frameCount);
    }

02An Onion Skin That Shows the Neighboring Pages

Over the current page, the ball of an earlier page is tinted blue and the ball of a later page red, and the alpha halves with each step of distance: 0.25, 0.125 and 0.0625. A page number outside the range is not drawn, so at the seam of a pass the first page does not show through behind the last, and the panel on the right shows the same page with no overlay. The scene covers 7.3% cumulative area and moved in 7/23 frames.

An Onion Skin That Shows the Neighboring Pages
Open in a new tab

02 · An Onion Skin That Shows the Neighboring Pages — Measured area 7.299% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

GHOST_ALPHAghostSpantint
    // Overlay: the page at distance d (from 1) gets alpha GHOST_ALPHA x 0.5^(d-1). The seam where a pass wraps back to the start is not overlaid
    function ghosts(R, amount) {
        for (let d = ghostSpan; d >= 1; d -= 1) {
            const a = GHOST_ALPHA * Math.pow(0.5, d - 1) * amount;
            for (let s = 0; s < 2; s += 1) {
                const j = s === 0 ? pageIndex - d : pageIndex + d;
                if (j < 0 || j >= FRAME_COUNT) {
                    continue;
                }
                pageBall(R, PAGES[j], tint[s], a, tint[s], false);
            }
        }
    }

03A Fan of Pages Bending Around the Spine

edgeProject joins a page cross-section from SEG 8 pieces and lets each piece trail the page angle by more toward the tip, bendRad times sin(a), while near pages are enlarged above and below by 1/(1 - z × PERSP). The left panel has zero bend and stands flat, and the right panel lets the edge follow by BEND_K times BEND_SPAN so the pages fan out. Here the page number comes from an angle, not from wrapIdx, and the scene covers 32.6% cumulative area, the second largest, with movement in 12/23 frames.

A Fan of Pages Bending Around the Spine
Open in a new tab

03 · A Fan of Pages Bending Around the Spine — Measured area 32.601% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

BEND_KspineXedgeProject
    function edgeProject(spineX, a, len, bendRad) {
        const out = [{ x: spineX, z: 0, s: 1 }];
        let x = spineX;
        let z = 0;
        const dl = len / SEG;
        for (let k = 1; k <= SEG; k += 1) {
            const ph = a - bendRad * Math.sin(a) * (k / SEG);
            x += dl * Math.cos(ph);
            z += dl * Math.sin(ph);
            out.push({ x: x, z: z, s: 1 / (1 - z * PERSP) });
        }
        return out;
    }

04A Timing Chart That Spaces Each Page Differently

frameY(i, p) is (i/11) raised to the power p, and spacing is the height difference from the neighboring page. At an exponent of 2.2 the first gap is 0.0051 and the last 0.1892, 37 times wider, the equal panel (exponent 1) holds 0.0909 everywhere, and an exponent of 0.5 flips it to 0.3015 first and 0.0465 last. The scene covers 25.8% cumulative area and moved in 17/23 frames.

A Timing Chart That Spaces Each Page Differently
Open in a new tab

04 · A Timing Chart That Spaces Each Page Differently — Measured area 25.848% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

EASE_POWspacingframeY
    function update(dt) {
        if (dt <= 0) {
            return;
        }
        powNow += (powTgt - powNow) * (1 - Math.exp(-5 * dt));
        if (!manual) {
            phase += TRACK_HZ * dt;
        }
        pageIndex = wrapIdx(phase, FRAME_COUNT);
    }
    function frameY(i, p) { return Math.pow(i / (FRAME_COUNT - 1), p); }
    function spacing(i, p) { return i <= 0 ? 0 : frameY(i, p) - frameY(i - 1, p); }

05The Rate Where Flicker Fuses Into Motion

The ball moves steadily at 190px a second, and only the spot where it sat at the moment each page changed goes into hist; the distance between flips, stepPx, is BALL_V / h, which is 95px at 2 pages a second and 7.9px at 24. The gap equals the 18px ball diameter at fuseLine, 10.56 pages a second (at 3.61 seconds), and past that rate the dots in the afterimage panel join into one line. Here page changes are stamped with their time, and the scene covers 6.8% cumulative area, the smallest, yet moved in 23/23 frames.

The Rate Where Flicker Fuses Into Motion
Open in a new tab

05 · The Rate Where Flicker Fuses Into Motion — Measured area 6.82% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

HZ_MAXstepPxfuseLine
    function ballX(t, R) { return R.x + 18 + (BALL_V * t) % (R.w - 36); }
    function ballY(x, R) { return R.y + R.h / 2 + 30 * Math.sin((x - R.x) * 0.035); }
    // Distance (screen pixels) the ball moved between two flips
    function stepPx(h) { return BALL_V / h; }
    function hzAt(t) { return lerp(HZ_MIN, HZ_MAX, ease((t - 0.8) / 6.6)); }
    function update(dt) {
        if (dt <= 0) {
            return;
        }
        hz = manHz > 0 ? manHz : hzAt(sceneT);
        phase += hz * dt;
        while (flips < Math.floor(phase)) {
            flips += 1;
            hist.push({ t: sceneT - (phase - flips) / hz, x: 0 });
        }
        while (hist.length > 0 && hist[0].t < sceneT - 0.6) {
            hist.shift();
        }
    }

06Drag a Corner, Release, and a Page Settles

If the angle at release is past SNAP_ANGLE (90 degrees) the target is 180 degrees, otherwise 0, and the spring integrates the acceleration -K(ang - tgtAng) - C × vel in 1/240 second pieces. K 140 and C 16 give a damping ratio of 0.676, so a page released at 2.7 radians reaches 90 percent in 0.217 seconds, overshoots the target by 5.1 percent and settles within 0.5 degrees at 0.496 seconds. The scene covers 32.8% cumulative area, the largest, and moved in 18/23 frames; when you drag it yourself, pointerdown calls setPointerCapture so pointermove keeps arriving after the pointer leaves the canvas.

Drag a Corner, Release, and a Page Settles
Open in a new tab

06 · Drag a Corner, Release, and a Page Settles — Measured area 32.812% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

SNAP_ANGLEKC
    function update(dt) {
        if (dt <= 0) {
            return;
        }
        if (held) {
            ang += (dragTo - ang) * (1 - Math.exp(-7 * dt));
            vel = 0;
            return;
        }
        if (still) {
            ang = tgtAng;
            vel = 0;
            return;
        }
        let left = dt;
        while (left > 1e-9) {
            const h = Math.min(SUB, left);
            vel += (-K * (ang - tgtAng) - C * vel) * h;
            ang += vel * h;
            left -= h;
        }
    }

07A Book Edge Where Page Count Shows as Thickness and Stripes

The page count, pageCount, becomes thickness: one STRIPE_W 3.2px stripe stands per page, and the current page, currentBand, rises bright with read pages to its left and pages remaining to its right. At 12, 24, 36 and 60 pages the band widens in proportion to 38.4, 76.8, 115.2 and 192px, and shownN chases the target count exponentially so the number does not jump. The scene covers 10.6% cumulative area and moved in 20/23 frames.

A Book Edge Where Page Count Shows as Thickness and Stripes
Open in a new tab

07 · A Book Edge Where Page Count Shows as Thickness and Stripes — Measured area 10.568% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

pageCountedgeStripecurrentBand
    function update(dt) {
        if (dt <= 0) {
            return;
        }
        shownN += (pageCount - shownN) * (1 - Math.exp(-5 * dt));
        if (!manual) {
            phase += PAGE_HZ * dt;
        }
        pageNow = wrapIdx(phase, Math.max(1, Math.round(shownN)));
    }
    // Left edge of the side stripe for page i: each page stands STRIPE_W further along
    function edgeStripe(i) { return i * STRIPE_W; }

08Choosing the Page Index for Rewind and Round Trip

The page number comes from a counting step n: when loopMode is loop, indexOf takes the remainder after dividing by 12, and when it is pingpong, triWave folds it. The left panel has period 24, so the last page (lastPage, number 11) and the first page each repeat back to back, 2 places per period, while the right panel has period 22 and has 0. The scene covers 17.4% cumulative area and moved in 16/23 frames, and its mean intensity of 99.5 is the largest of the nine.

Choosing the Page Index for Rewind and Round Trip
Open in a new tab

08 · Choosing the Page Index for Rewind and Round Trip — Measured area 17.37% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

loopModetriWavelastPage
    // Triangle wave: with dup the period is 2N (the end page twice in a row), otherwise 2N - 2
    function triWave(n, count, dup) {
        const per = dup ? 2 * count : 2 * count - 2;
        const m = ((n % per) + per) % per;
        if (m < count) {
            return m;
        }
        return dup ? 2 * count - 1 - m : 2 * count - 2 - m;
    }
    function indexOf(n, dup) {
        if (loopMode === 'loop') {
            return ((n % FRAME_COUNT) + FRAME_COUNT) % FRAME_COUNT;
        }
        return triWave(n, FRAME_COUNT, dup);
    }

09Filling the Pages Between Two Keyframes Automatically

Only the first and last key pages carry a six-point shape, and lerpPoint makes the pages between from points that share a pointId. At MID_COUNT 0 a point jumps up to 97px in one page and at 4 it moves 19px per page, and with mismatched ids the middle page area on a 194×168px page shrinks from 6896 to 5344, 3869, 2470 and 1148 before it jumps to 6857 on the last key page. The page here comes from rounding the travel position, pageK, and the scene covers 12.2% cumulative area with movement in 9/23 frames.

Filling the Pages Between Two Keyframes Automatically
Open in a new tab

09 · Filling the Pages Between Two Keyframes Automatically — Measured area 12.199% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

MID_COUNTlerpPointpointId
    function lerpPoint(a, b, t) {
        return { x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t) };
    }
    // Drawing for page number k (0 = A, MID_COUNT + 1 = B): when match is false, the middle pages pair point i of A with point PAIR[i] of B
    function figAt(k, match) {
        // The end page (B) is the key page itself: mismatched ids only twist the middle pages
        if (k === MID_COUNT + 1) {
            return KEY_B.map(function (p) { return { x: p.x, y: p.y }; });
        }
        const t = k / (MID_COUNT + 1);
        const out = [];
        for (let pointId = 0; pointId < KEY_A.length; pointId += 1) {
            out.push(lerpPoint(KEY_A[pointId], KEY_B[match ? pointId : PAIR[pointId]], t));
        }
        return out;
    }

When You Build a Flipbook in Code, How Many Values Turn Twelve Pages Into One Motion?

Four values tie twelve pages into one motion

The motion is set by the page count, the flip rate, the overlay alpha and the spine bend. With 12 pages at a rate of 12, one pass takes 1.0 second, and lowering the rate to 3 makes it 4.0 seconds, so the same twelve pages become another motion from a single value.

A page is a value with four slots

A page from makePages is an object holding the ball's horizontal position, vertical position and squash, and changing the drawing does not touch the index calculation. That split lets the overlay in 02, the bend in 03 and the middle pages in 09 run on the same kind of array.

Keep Each Page's Dot Position in a Table, Pick the Page Index From Elapsed Time and Paint the Neighboring Pages Faintly: the Order in Which One Flipbook Scene Is Drawn

Get the page index from elapsed time

01 accumulates time into a phase and takes the whole part as the page index. 05 adds a record of the moment each page changed and uses it for the afterimage.

Find the neighboring pages by index difference

02 pulls out pageIndex - d and pageIndex + d for each distance d and halves the alpha at each step. The overlay is painted first and the current ball goes on top of it, opaque.

Add bend and spacing after the index is settled

The bend in 03 and the spacing in 04 change only the drawn shape and height, not the number. That is why you can set a bend of 0 beside a bend of 0.3 at the same moment of the same page.

Putting It on a Real Screen Like a Flipbook Maker: Building Without a Library, Writing Page Drawings as an Array and When the Scene Changes

Where it fits

In hand-drawn animation practice, the overlay in 02 helps line up a new drawing with its neighbors and the spacing chart in 04 sets how far each page should move. For a storyboard you can size a scene from the page count and rate first, then use the fuse line in 05 to judge how many pages a second still read as connected. For a page-turn interface, the bend in 03 and the corner drag in 06 are parts you can lift out.

Swapping in real drawings

A real flipbook should store performance.now() at the start and, on each frame, recompute the page as the whole part of elapsed time times pages per second. The demo accumulates frame gaps into a phase, so moving it over by raising the page one at a time would change the speed on a slow device. Replace the PAGES[i] slot with an array of images and the calculation still works.

How this differs from the page curl post

Paper Page Curl Animation: 9 Scenes covers the shape of one sheet whose corner curls, and this post covers the numbers, spacing and overlay of many pages.

Drawing without a library

Canvas 2D and requestAnimationFrame run the nine scenes, with no WebGL and no CDN. To build a flipbook maker free of dependencies, you can cut out one scene and paste it, because the page array and the index calculation are enough.

Where a Flipbook Breaks: a Trap of Fractional Page Numbers That Smear Between Pages, a Seam Where the Last Page Shows Twice and Hitches and a Flip Rate That Drifts Because It Relies on the Frame Gap

Three traps come down to how the page number becomes an integer and where it is cut.

A fractional page number smears between pages

At a phase of 3.5, mixing pages 3 and 4 half and half would draw two faint balls that look just like the onion skin in 02. These scenes keep the whole part for the page and draw the overlay separately, with the alpha halved at each distance.

A seam where the last page shows twice

The period 24 rule in 08 repeats the same number in 2 places per period, 11 to 11 and 0 to 0, so the end page stays for two beats and hitches. The period 22 rule has 0 such places, and in a looping playback that difference is the seam.

The flip rate drifts when it leans on the frame gap

Code that raises the page one at a time slows down when frames arrive late. The demo cuts the accumulated time step at DT_MAX 0.05 seconds and drops the first frame gap after a hidden tab returns, so a pile of pages does not land in one frame.

If you want to press on the three traps and then touch the source, the card below holds the zip, the password is zyrzaf6v, and opening it gives you the source of the nine scenes.

Change the Flip Rate, the Overlay Alpha and the Spine Bend and How Much Does the Feel Shift? FLIP_HZ, GHOST_ALPHA and BEND_K Compared

Name Changed value Feel
A slower flip 01 FLIP_HZ 12 → 3 One pass stretches from 1 second to 4, each page reads on its own, and the ball seems to jump across 33.5px on average (55.9px at most) between pages
A stronger overlay 02 GHOST_ALPHA 0.25 → 0.6 The alpha for one, two and three steps away grows to 0.6, 0.3 and 0.15 and shows clearly, but the nearest overlay is almost as dark as the current ball, so the page is told apart only by its outline
A stronger bend 03 BEND_K 0.3 → 0.9 At 90 degrees the tip goes from 34.9px to 74.5px right of the spine, and at 135 degrees it folds back to the spine (0.2px) and mushes the page shape

What to check before changing a value

Lowering FLIP_HZ leaves the movement between pages at 33.5px on average, because the page index decides it, and only the time of one pass changes. BEND_K cuts the scale above and below at 90 degrees from 1.246 to 1.088, so a stronger bend leans toward a page that looks smaller.

Lowering GHOST_ALPHA keeps the current page clean but blurs the overlay and weakens its help in lining up neighbors.

One Page of a Flipbook Under Reduced Motion: Accessibility Through a Sentence for the Scene and the Page Number on Show

Reduced motion and a hidden tab

The nine sources read prefers-reduced-motion, and when it is on they draw one still page of the REST_S second scene with no loop, while 03 and 06 put the page straight at its target angle. While the tab is hidden the clock stops, and on return the first frame gap is dropped and the scene carries on.

Keyboard and pointer

The stage box has role="application" and tabindex 0, with ← and ↑ for the previous page, → and ↓ and Space for the next, Enter and P to pause, N for one step and R to restart. 06 turns off click-to-flip and lets pointer events with setPointerCapture drag the corner.

The status line splits when it reads

During the demo the status line is aria-live="off", and touching the stage switches it to polite. While it flows on its own it writes one sentence with no number, and only when paused or flipped by hand does it add a number like "page M of N", and role="status" writes once when a pass ends.

Flipbook FAQ: Building Without a Library, Where the Page Drawings Come From and What You Need Before Putting It on the Web

Do you need a library to make a flipbook

Canvas 2D is enough, and the nine scenes load no library, no WebGL and no CDN. The page array and the index calculation are a few lines of pure function, so you can cut out only the scene you need and paste it.

Where do the page drawings come from

The pages here are an array drawn in code, and if you have your own drawings you swap the PAGES[i] slot for an array of images. If you made only two drawings, fill the middle pages by calculation from two key pages as 09 does.

What else do you need for a flipbook online free of friction

Give the stage role="application" and a descriptive sentence, draw one still page under reduced motion, and let people flip pages and pause from the keyboard. Storing the start time and recomputing the page from elapsed time keeps the speed the same on a slow device.

For reference, the MDN Canvas 2D page covers the drawing calls, requestAnimationFrame covers a loop that receives the time gap, Page Visibility API covers the moment a tab hides, and prefers-reduced-motion covers the reduced motion setting. Another way to turn a surface over is in Card Flip and Fold Effects, numbers that roll to a new value are in Number Rolling Animation, and timing scenes are in Stopwatch in JavaScript.

Enter the archive password

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