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

Seat Selection UI: 9 Scenes That Score Every View

In a seat selection ui, a seat near the stage can still have a poor view, so 02 scores the clearance over heads in front with distance and angle, 03 draws that

Front Row Center Is Not Automatically the Best Seat: Stage Height, Floor Rake and the Heads in Front

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

When a seat picker stops at a grid painted in two shades, open and sold, what that grid leaves out is the view from each seat. In the hall of 02 (ten rows, each one 0.12 m higher than the row before it, stage at 0.95 m) the row averages come out A 57, B 36 and C 41, and the best row is G, seventh from the stage, at 62. The worst row in the room is not the first one but the one right behind it.

What drags the second row below the first

Row A has nobody in front, so its clearance is counted as the full 12 cm. From row B, the line from the eye to the stage edge clears the eyes of row A by only 2.93 cm, and at that distance the stage width fills about 100° of the view. The score is a sum of five parts weighted 0.3 for clearance, 0.1 for distance, 0.35 for how wide the stage looks, 0.1 for how steeply you look down or up at the stage edge and 0.15 for how far you sit off the axis, and row B loses on the two heaviest parts at once.

What happens when the stage drops to half a meter

At a stage height of 0.50 m the clearance turns negative in every row from B to K (from −9.84 to −3.01 cm), so the sight line of 138 of the 143 seats runs into the head in front, and the average falls to 53. Raise the stage to 1.40 m and the average climbs to 70, the best seat becomes F-06 at 82 and row B clears by 15.69 cm. The same seat E-07 scores 60, 64 and 81 at those three heights.

Seat Selection UI: 9 Scenes — a Fan-Shaped Hall, a Sightline Score Map, the Stage Seen from Your Seat, a Group Block Finder, Live Booking Contention, a Stadium Map with Inertia, a Route from the Entrance, Automatic Flight Seat Assignment and a Concert Booking Hero

Scenes 01 to 03 read one fan-shaped hall in three layers: place the seats, give each seat a view score, then sit in that seat and look at the stage. Scenes 04 to 08 each take one problem a booking screen actually runs into (group blocks, two buyers at once, a map of ten thousand seats, the walk in, cabin rules). Scene 09 is a hero that strings the work of 01 to 05 into a single 6.6-second flow. Every scene is drawn on one 2D canvas with no outside library or WebGL, and the random numbers come from fixed seeds, so the demo plays out the same way each time it opens.

How much each scene moved on a 480×300 canvas

The render captured 24 frames over two seconds and compared each neighboring pair, which gives 23 differences per scene.

Scene Cumulative area Frames that moved Mean intensity
01 Fan-shaped hall 20.2% 23/23 45.6
02 Sightline score map 17.9% 23/23 88.1
03 Stage from the seat 63.3% 23/23 69.8
04 Group block finder 10.5% 23/23 61.8
05 Live booking 15.5% 22/23 81.7
06 Stadium map 71.4% 23/23 44.7
07 Entrance route 31.3% 19/23 62.7
08 Cabin auto-assign 12.5% 22/23 87.6
09 Booking hero 34.5% 23/23 57.0

Scene 03, where the stage picture fills the whole canvas, and 06, where the map zooms, pass 60%, while 04 and 08, which move only a few dozen seat dots, stay between 10 and 13%. The 19/23 of 07 likely comes from the stretch where the walker has reached its seat and the canvas waits before the next target (even the farthest seat, A-22, is reached at 2.046 s inside a 2.4-second loop). The 22/23 of 05 and 08 appears to come from short pauses between events.

What Does a Seat Reservation UI Actually Have to Compute?

Concerts: how much of the stage one seat can see

A basic seat map ui marks each seat as open or taken and stops there. For a seated show the question a buyer cannot answer from that map is how much of the stage a seat will show, which is why 02 turns the hall into a score map and 03 renders the view from the chosen seat.

Cinemas: how many people can sit side by side

A cinema seat selection ui deals mostly with a different question: can three or four friends sit together without leaving one empty seat stranded between them and the next sold seat. That is the job of 04, and 05 adds the case where somebody else takes the seat while you are still deciding.

Stadiums and cabins: too many seats, or seats with rules

On a stadium map with more than ten thousand seats, choosing what not to draw is the computation (06), and once inside, the buyer needs a path from the gate to the seat (07). Cabin seating has to satisfy sitting together, window or aisle wishes and the no-children-in-the-exit-row rule all at once, so 08 needs an order that places a party, and steps back and re-places when the next party gets stuck. Compare it with dragging a date range in the 9 calendar month views or dropping one pin in the 9 map location pickers: a seat map also computes the shape of what can be picked and how neighboring cells relate.

01A Fan-Shaped Hall That Spaces Seats by Arc Length

Row i sits on a radius of 62 + i × 18 px, and the loop below first cuts the two aisles (14 px divided by the radius, in radians) out of the fan, then divides each remaining span's arc length by SEAT_W 16 px and rounds down, which gives 5 seats in row A up to 24 in row K and 143 across ten rows. Each seat leaves at 0.2 s + row × 110 ms + its place in the row × 12 ms and pops up on a spring with stiffness 260 and damping 14 (damping ratio 0.434, 21.4% overshoot) while its shadow grows wider and fainter the higher it lifts, so the last seat, K-24, leaves at 1.466 s and all 143 have settled by 2.254 s. Interactive seating charts laid out on a rectangular grid cannot follow a curved hall, so for seats that wrap around a stage this function is the first step.

A Fan-Shaped Hall That Spaces Seats by Arc Length
Open in a new tab

01 · A Fan-Shaped Hall That Spaces Seats by Arc Length — Measured area 20.203% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SEAT_WROW_STAGGER_MSPOP_STIFFNESS
        for (let i = 0; i < rows; i += 1) {
            const r = r0 + i * gap;
            const bounds = [-FAN_HALF];
            for (const a of AISLE_ANGLES) {
                const half = aisleW / 2 / r;
                bounds.push(a - half, a + half);
            }
            bounds.push(FAN_HALF);
            rowStart.push(seats.length);
            let k = 0;
            for (let s = 0; s + 1 < bounds.length; s += 2) {
                const a0 = bounds[s];
                const span = bounds[s + 1] - a0;
                const n = Math.floor(span * r / seatW);
                const seg = [];
                for (let j = 0; j < n; j += 1) {
                    const ang = a0 + (j + 0.5) * span / n;
                    seg.push(seats.length);
                    seats.push({ row: i, k: k, ang: ang, r: r, x: HALL_CX + r * Math.sin(ang), y: HALL_CY + r * Math.cos(ang), label: seatLabel(i, k), seg: segs.length });
                    k += 1;
                }
                segs.push(seg);
            }
            rowCount.push(k);
        }

02A Sightline Score Map That Cools Where Heads in Front Block the View

The eye in row i sits at i × RAKE_STEP (0.12 m) + 1.15 m and the stage is 2.4 + i × 0.95 m away, and clearance returns, in meters, how far the line from that eye to the stage edge passes above the eyes of the row ahead. That clearance (12 cm earns full marks) is blended with distance, stage width angle, downward angle and off-axis angle at weights of 0.3, 0.1, 0.35, 0.1 and 0.15, and the score spreads out from the stage at 520 px per second while the stage height swings, so the whole map keeps recoloring between sky blue (0) and apricot (100). On a venue page that explains the views, it can show seat by seat how many centimeters of sight pass over the head in front.

A Sightline Score Map That Cools Where Heads in Front Block the View
Open in a new tab

02 · A Sightline Score Map That Cools Where Heads in Front Block the View — Measured area 17.908% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

RAKE_STEPC_GOODSCORE_WEIGHTS
    // clearance (meters): height where the line from row i's eye to the stage edge (height stageH) passes the row ahead (distance rowDist(i−1)), minus the eye height of that row.
    // line height = stageH + (my eye − stageH) × (row-ahead distance / my distance). The front row has nobody ahead, so it counts as C_GOOD
    function clearance(i, stageH, rake) {
        if (i <= 0) {
            return C_GOOD;
        }
        const e = eyeY(i, rake);
        const lineAt = stageH + (e - stageH) * (rowDist(i - 1) / rowDist(i));
        return lineAt - eyeY(i - 1, rake);
    }

03The Stage in Perspective from the Seat's Eye Level

The camera sits at the seat's eye and looks at (0, 2.05, −1.6), project divides by camera depth with a focal length of 360, and the back curtain, the stage top and front, three swaying actors and every head and shoulder ahead go into one list that is drawn far to near, so the nearer heads paint over the stage. The tour goes A-03, E-07, K-13 and G-02 with a 0.7-second glide between seats, and the share of the stage line hidden by heads reads 0% at A-03, 39% at E-07, 49% at K-13 and 32% at G-02. A-03 sees no heads at all, but its stage front spans 261% of the screen width, which is the front-row trade-off in a single frame.

The Stage in Perspective from the Seat's Eye Level
Open in a new tab

03 · The Stage in Perspective from the Seat's Eye Level — Measured area 63.324% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

FOCALGLIDE_SSTAGE_SAMPLES
        // stage light glow
        const c = project(cam, [0, STAGE_H + 1.2, LOOK_AT[2]]);
        if (c[2] > 0) {
            const rg = ctx.createRadialGradient(c[0], c[1], 4, c[0], c[1], cam.focal * 4 / c[2]);
            rg.addColorStop(0, rgba(STAGE_LIGHT, 0.95));
            rg.addColorStop(1, rgba(STAGE_LIGHT, 0));
            ctx.fillStyle = rg;
            ctx.fillRect(0, 0, W, H);
        }
        // draw far to near: painting the deepest items first lets the nearer heads cover the stage
        const list = sceneList03(cam);
        list.sort(function (a, b) { return b.z - a.z; });
        for (const d of list) {
            d.paint();
        }
        miniMap03();

04A Group Block Finder That Leaves No Single Empty Seat

This scene is a cinema grid of 8 rows by 14 columns with 61 open seats, and findBlocks slides a window of GROUP_SIZE 4 one seat at a time along each segment between aisles, keeping windows whose seats are all open and flagging the seat just outside when it would be left alone. Six candidates turn up, each held for 160 ms under a spring-driven window that turns coral when it strands a seat, and the top two by score (F-06 to F-09 at 3.917 and F-07 to F-10 at 3.851) both drop out, so E-05 to E-08 at 3.744 wins.

A Group Block Finder That Leaves No Single Empty Seat
Open in a new tab

04 · A Group Block Finder That Leaves No Single Empty Seat — Measured area 10.482% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

GROUP_SIZECAND_MSSOLD_MAP
        segs.forEach(function (seg, si) {
            for (let s = 0; s + size <= seg.length; s += 1) {
                const ids = seg.slice(s, s + size);
                if (!ids.every(function (id) { return free[id]; })) {
                    continue;
                }
                let sum = 0;
                for (const id of ids) {
                    sum += score[id];
                }
                let orphan = -1;
                const L = s - 1;
                if (L >= 0 && free[seg[L]] && (L - 1 < 0 || !free[seg[L - 1]])) {
                    orphan = seg[L];
                }
                const R = s + size;
                if (R < seg.length && free[seg[R]] && (R + 1 >= seg.length || !free[seg[R + 1]])) {
                    orphan = seg[R];
                }
                out.push({ seg: si, start: s, ids: ids, sum: sum, orphan: orphan });
            }
        });

05Live Booking That Shakes a Taken Seat and Suggests the Nearest Alternative

planBooking works out the whole run up front: 28 seats sold at the start, then 36 rival buyers arriving at random exponential gaps set to a 70 ms mean (in this seed the first arrives at 0.35 s and the last at 2.513 s), each grabbing an open seat with a chance proportional to its score cubed and paying 900 ms later. The demo's own buyer holds the best open seat, E-07, at 0.3 s, but at 1.0 s a payment from another counter lands first (15 other buyers have held seats by then), so the loop below shakes the ring by 7 × e^(−t/0.12) × sin(2π·9·t) px and, 0.45 s later, moves the hold to D-06, the seat with the lowest grid distance + 3 × (1 − score).

Live Booking That Shakes a Taken Seat and Suggests the Nearest Alternative
Open in a new tab

05 · Live Booking That Shakes a Taken Seat and Suggests the Nearest Alternative — Measured area 15.514% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

ARRIVAL_MEAN_MSHOLD_MSSHAKE_DECAY_S
        while (evi05 < plan05.events.length && plan05.events[evi05].t <= sceneT) {
            const e = plan05.events[evi05];
            if (e.kind === EV_HOLD) {
                state05[e.id] = SEAT_HELD;
                heldAt05[e.id] = e.t;
            }
            else {
                state05[e.id] = SEAT_SOLD;
            }
            evi05 += 1;
        }
        const u = sceneT - CONFLICT_AT_S;
        myShake05 = u >= 0 && u < SHAKE_S ? SHAKE_PX * Math.exp(-u / SHAKE_DECAY_S) * Math.sin(TAU * SHAKE_HZ * u) : 0;
        altPop05.to = sceneT >= CONFLICT_AT_S + SHAKE_S ? 1 : 0;
        stepSpring(altPop05, 320, 14, dt);

06A Stadium Map with Inertia Whose Sections Split into Seats as It Zooms

The map holds 11,720 seats in 24 sections, and every 1/60 s the zoom moves the log of the scale 0.06 of the way toward ×7, so it slows as it nears ×7 (×1.46 at 0.25 s, ×3.77 at 0.5 s, ×6.35 at 1.0 s), while zoomAbout keeps the anchor at (330, 82) fixed on screen. Between ×2.2 and ×4, crossed in 196 ms (from 0.338 to 0.533 s), the section polygons fade out as the seats fade in, and a fling of (−300, 150) px/s at 1.35 s loses 8% of its speed every 1/60 s and comes to rest at 2.375 s after about 69 px. forVisibleSeats throws out whole sections by their bounding boxes before it looks at single seats.

A Stadium Map with Inertia Whose Sections Split into Seats as It Zooms
Open in a new tab

06 · A Stadium Map with Inertia Whose Sections Split into Seats as It Zooms — Measured area 71.413% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ZOOM_EASEFRICTIONSPLIT_FROM
    // zoom about a fixed anchor: new offset = anchor − (anchor − offset) × new scale / old scale
    function zoomAbout(ax, ay, next) {
        ox06 = ax - (ax - ox06) * next / scale06;
        oy06 = ay - (ay - oy06) * next / scale06;
        scale06 = next;
    }
    function forVisibleSeats(s, ox, oy, cull, cb) {
        let n = 0;
        const pad = SEAT_PITCH * s;
        for (const sec of sections06) {
            if (cull) {
                const x0 = ox + sec.box[0] * s - pad;
                const y0 = oy + sec.box[1] * s - pad;
                const x1 = ox + sec.box[2] * s + pad;
                const y1 = oy + sec.box[3] * s + pad;
                if (x1 < 0 || y1 < 0 || x0 > W || y0 > H) {
                    continue;
                }
            }
            for (let i = sec.first; i < sec.first + sec.count; i += 1) {
                const x = ox + seatX06[i] * s;
                const y = oy + seatY06[i] * s;
                if (cull && (x < -pad || y < -pad || x > W + pad || y > H + pad)) {
                    continue;
                }
                n += 1;
                cb(i, x, y);
            }
        }
        return n;
    }

07A Route That Walks the Aisles from the Entrance to Your Seat

The floor is a 38 by 18 grid of 12 px cells with 370 walkable cells, and breadthFirst starts from both entrances at distance 0, hands each dequeued cell's four neighbors distance + 1 and lets a seat cell be entered only from the cell right in front of it on the stage side. For B-05 the route is 23 cells with 4 turns from the left entrance, and rounding each corner with a 9 px quadratic curve brings it to 265 px instead of the 276 px the cells add up to. The walker covers 230 px per second, 11.5 px every 0.05 s, leaving a thick mint trail behind and a violet dashed line ahead.

A Route That Walks the Aisles from the Entrance to Your Seat
Open in a new tab

07 · A Route That Walks the Aisles from the Entrance to Your Seat — Measured area 31.31% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

WAVE_CELLS_SCORNER_RWALK_PX_S
        while (head < queue.length) {
            const id = queue[head];
            head += 1;
            if (id === goal) {
                continue;
            }
            const r = Math.floor(id / G_COLS);
            const c = id % G_COLS;
            for (const d of DIRS) {
                const rr = r + d[0];
                const cc = c + d[1];
                const nid = rr * G_COLS + cc;
                if (rr < 0 || cc < 0 || rr >= G_ROWS || cc >= G_COLS || dist[nid] >= 0) {
                    continue;
                }
                // a seat row is entered only from the cell right in front of the seat (stage side)
                if (nid === goal ? d[0] !== 1 : !walkable(rr, cc)) {
                    continue;
                }
                dist[nid] = dist[id] + 1;
                from[nid] = id;
                queue.push(nid);
            }
        }

08Automatic Flight Seat Assignment That Weighs Window, Aisle and Exit Row Rules

Five parties need seats in an 8-row cabin laid out A B C | D E F: family 1 (three with a child, window), a pair (window), family 2 (three with a child, window), a parent with a child (two, aisle) and one solo traveler (aisle). placeParties places a candidate, and when the next party cannot fit it takes those seats back and moves on to the next candidate, while any party with a child skips row 4, the exit row. The run ends with 6 placements tried, 1 step back (the pair gives up 6E and 6F and moves to 7A and 7B so family 2 fits) and 2 exit-row skips, at one step every 170 ms.

Automatic Flight Seat Assignment That Weighs Window, Aisle and Exit Row Rules
Open in a new tab

08 · Automatic Flight Seat Assignment That Weighs Window, Aisle and Exit Row Rules — Measured area 12.541% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

EXIT_ROWSTEP_MSCABIN_MAP
            for (const ids of candidates(p, taken)) {
                if (p.child && Math.floor(ids[0] / 6) + 1 === EXIT_ROW) {
                    steps.push({ kind: EV_RULE, party: g, seats: ids });
                    continue;
                }
                steps.push({ kind: EV_TRY, party: g, seats: ids });
                for (const id of ids) {
                    taken[id] = true;
                }
                if (place(g + 1)) {
                    return true;
                }
                for (const id of ids) {
                    taken[id] = false;
                }
                steps.push({ kind: EV_UNDO, party: g, seats: ids });
            }

09A Concert Booking Hero Where a Ticket Flips Out Once a Seat Is Set

The hero chains the earlier scenes: the hall of 01 builds from 0.1 s, the score shades of 02 spread from 0.3 s, 64 seats sell between 0.5 and 1.0 s, and at 1.05 s findBlocks for a party of two picks G-10 and G-11 (68 points each) out of 36 candidates. A small preview window shows that view at 1.15 s, the ticket rises on a spring at 1.35 s, and from 1.6 s it turns a full 2π in 0.8 s with its width set by |cos| and its face darkening while it lies edge-on, with Seat assigned on the back and Booking confirmed on the front.

A Concert Booking Hero Where a Ticket Flips Out Once a Seat Is Set
Open in a new tab

09 · A Concert Booking Hero Where a Ticket Flips Out Once a Seat Is Set — Measured area 34.492% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

FILL_POWERPARTY_SIZEFLIP_S
        const c = Math.cos(flip09);
        const w = TICKET_W * Math.max(0.02, Math.abs(c));
        ctx.save();
        ctx.translate(TICKET_CX, y);
        ctx.fillStyle = rgba(INK, 0.2);
        ctx.beginPath();
        ctx.ellipse(0, TICKET_H / 2 + 10, w * 0.5, 6, 0, 0, TAU);
        ctx.fill();
        // one layer of finesse: while the ticket lies edge-on (small |cos|) its face darkens as if catching light at a slant
        const face = c >= 0 ? CREAM : LAVENDER;
        fillRound(-w / 2, -TICKET_H / 2, w, TICKET_H, 10, mix(face, SOLD, 0.45 * (1 - Math.abs(c))), 1);
        roundRectPath(-w / 2, -TICKET_H / 2, w, TICKET_H, 10);
        ctx.lineWidth = 2;
        ctx.strokeStyle = rgba(INK, 0.85);
        ctx.stroke();

Seats in Polar Coordinates, Views Measured Against the Row Ahead, Groups Found with a Sliding Window — the Math Behind a Seat Map

Why the aisles are cut out before the seats are counted

buildHall removes the aisle width from the angle first and only then divides what is left, so no seat can straddle an aisle. Each of the three spans in a row is floored on its own, which is why row E ends up with 12 seats like row D even though its arc is longer: the remainders of the three spans are dropped separately.

One clearance worked out by hand

Take row E (i = 4). Its eye is 4 × 0.12 + 1.15 = 1.63 m high and 2.4 + 4 × 0.95 = 6.2 m from the stage, and row D sits 5.25 m away with eyes at 1.51 m. The line from 1.63 m down to the 0.95 m stage edge is at 0.95 + 0.68 × (5.25 / 6.2) = 1.5258 m when it passes row D, so it clears those eyes by 1.58 cm, the figure 02 reports for E-07 when the stage stands at 0.95 m.

Zoom watches the anchor, a block watches the seat next to it

zoomAbout solves for the offset that keeps one screen point over the same map point: new offset = anchor − (anchor − offset) × new scale / old scale, which is why the anchor drifts 0 px during the whole zoom. findBlocks does not stop at the inside of the window; it checks the seat right outside each end, and a seat that is open while the one beyond it is sold or missing is the lone gap the window would leave behind.

Where a Seat Map Breaks: Redraws That Stall at Ten Thousand Seats, Two Buyers Holding the Same Seat and Group Picks That Strand One Empty Seat

A seat that looks open may already be sold

In 05, E-07 is drawn as the buyer's own seat with a violet ring, yet at 1.0 s a payment that started at another counter finishes first and the seat is gone; by that moment 15 other buyers have held seats. The screen reports it with a shake that starts at 7 px and decays to 3.04 px at 0.1 s, 1.32 px at 0.2 s and 0.57 px at 0.3 s, then offers D-06, one row ahead and one seat over, whose cost of 1.85 is its distance of √2 ≈ 1.414 seats plus 3 × 0.144 for the missing score. A seat drawn as open only means it was open at the last update the screen received.

When the highest-scoring block strands one seat

Row F in 04 has five open seats in the middle (F-06 to F-10). A four-seat window fits inside twice and those two windows hold the top two sums, but either one strands a single seat, so both are thrown out, and three of the six candidates, H-06 to H-09 among them, drop for the same reason. 04 calls bestBlock twice, once forbidding lone gaps and once allowing them, and when the two answers differ it leaves the allowed winner on screen as a coral dashed outline. To run that comparison yourself by flipping the second argument of bestBlock in the index.html of folder 04, unlock the zip with the password 74ar6jn4 and open that file. Taking the top block as it stands leaves F-10 on its own, a seat only a solo buyer can fill.

Redrawing ten thousand seats every frame

Drawing all 11,720 seats of 06 one by one at low zoom means 11,720 rectangles, while the 24 section polygons are enough at that scale. At high zoom forVisibleSeats skips every section whose bounding box is off screen, handing over 1,792 seats at ×3.77 and only 742 to 799 at ×7 (6.3 to 6.8% of the total). The heaviest moment is the split band where sections and seats are drawn together, 2,567 seats at ×2.9, so 06 collects open seats, sold seats and hatching into one Path2D each and fills just three paths.

Not by Color Alone: Seat Patterns, Spoken Seat Numbers and Reduced Motion for Accessibility

What is left when the seat colors are removed

In the shared drawSeat that 01, 04, 05, 07, 08 and 09 call, an open seat has a light fill and a dark 1.1 px border, a sold seat is gray with 45° hatching, a held seat is butter yellow with a 2 px border and your own seat is mint with a check mark. Seen in grayscale, the border, the hatching and the check still tell the four states apart, and 06, which does not use drawSeat, follows the same rule by hatching its sold seats with a separate path.

Choosing a seat from the keyboard in 01, 02 and 03

The three fan-hall scenes put real <button> elements, Prev seat and Next seat, in a role="group", and the arrow keys move as well: left and right go to the next seat in the row, up and down jump to the seat in the row ahead or behind whose angle is closest (hallNeighbor). Pressing Enter lands on E-07, and the status line names the seat it moved to. When 03 is paused or reduced motion is on, picking a seat jumps the view straight there instead of gliding.

Reduced motion and the pause button

All nine scenes listen to prefers-reduced-motion, including its change event, and when it turns on they stop the loop and draw one final frame with every seat settled. Scenes 01, 02 and 03 redraw at once when a seat is picked in that state. The pause button reports its state through aria-pressed, and the status line starts as aria-live="off" and switches to polite only after the first input, so the self-running demo does not talk over the page.

A Steeper Rake, a Larger Party, a Faster Zoom — One Number That Changes the Booking Screen

Variant Value changed What changes
Steeper rake 02 RAKE_STEP 0.12 → 0.3 Row E clearance 1.58 → 8.55 cm, average 56 → 68, best seat G-07 (68) → F-06 (81), back row looking down at 6.67° → 14.83°
Larger party 04 GROUP_SIZE 4 → 6 Candidates 6 → 1, lone-gap rejections 3 → 0, pick E-05 to E-08 → H-05 to H-10
Faster zoom 06 ZOOM_EASE 0.06 → 0.3 Reaching 95% of ×7 takes 1.221 → 0.375 s, the band where sections split into seats lasts 196 → 33 ms

With a 0.3 m rake the row E eye rises to 2.35 m, the line reaches 0.95 + 1.4 × 0.8468 = 2.1355 m over row D, and subtracting the 2.05 m eyes there leaves 8.55 cm. In 03 the share hidden by heads drops from 39 to 24% at E-07, 49 to 34% at K-13 and 32 to 27% at G-02, but the back row now looks down at the stage at a sharper angle. A party of six finds only one run of six open seats, which pushes the pick back to row H with a sum of 5.08. At a zoom rate of 0.3 the split band passes in two frames and the sections snap into seats, so keep it near 0.06 if the split itself is the point.

Frequently Asked Questions About Building a Seat Selection Screen

How do I fit the sightline scores in this seat booking ui design to my own hall?

The shared code near the top of each index.html holds the measurements: FIRST_ROW_M (2.4 m to the first row), ROW_DEPTH_M (0.95 m per row), RAKE_STEP, EYE_H (1.15 m), STAGE_H, C_GOOD for the clearance that earns full marks and SCORE_WEIGHTS for the five weights. The layout itself comes from ROW_R0, ROW_GAP, SEAT_W and AISLE_W passed to buildHall.

What would I change to connect 05 to a real booking server?

planBooking builds a time-sorted list of hold and sale events in advance, and the loop in the code above applies every event whose time has passed. Feed that list from server messages instead, and let the server decide who gets a contested seat; the screen only shows the outcome and runs the shake and the D-06 style suggestion when its own hold is refused.

Does the 11,720-seat map stall on a phone?

06 visits only the sections that touch the screen, skips off-screen seats inside them and fills all seats of one state with a single Path2D. The canvas multiplies its pixels by the device pixel ratio up to 2 at most, and drawing coordinates stay at 480×300 through setTransform. When the tab is hidden the loop stops, and it starts again when the tab comes back.

Enter the archive password

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