GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

座席予約システム 作り方 — 視界と連席を計算する座席表9種

舞台に近い席が、よく見える席とはかぎりません。そこで02番は前の列の頭の上を視線が通るすき間と距離・角度で席ごとに点数をつけ、03番はその席の目の高さから見た舞台を遠近で描き、04番は一席だけの空きを残さない連席を探します。座席予約システム 作り方を、計算がそのまま画面に出る九つの座席表で確かめます。

最前列の中央がいちばん見やすい席とはかぎりません — 舞台の高さ・客席の段差・前の人の頭が決める視界

自動再生 · タイルを押すとその項目へ · すべて1つのzip

座席表が空席と売約済みの二色で塗り分けた格子で終わると、その格子には席ごとの見え方が載りません。02の客席(10列、列ごとに0.12 mずつ高くなる段差、舞台の高さ0.95 m)で列ごとの平均点を出すと、A列57・B列36・C列41で、いちばん高いのは舞台から7列目にあたるG列の62点でした。客席でいちばん点の低い列は、最前列ではなくそのすぐ後ろの列です。

二列目が最前列より下がる理由

A列は前に人がいないので、すき間を満点の12 cmとして数えます。B列からは、目から舞台の前端へ引いた線がA列の人の目の上を2.93 cmしか通らず、その近さでは舞台の幅が視野のおよそ100°を占めます。点数はすき間0.3・距離0.1・舞台の見える幅0.35・舞台の前端を見下ろしたり見上げたりする角度0.1・正面からのずれ0.15の重みで足した五つの合計で、B列は重みの大きい二つで同時に点を落とします。

舞台を0.50 mまで下げたとき

舞台の高さを0.50 mにすると、B列からK列までのどの列でもすき間がマイナス(−9.84〜−3.01 cm)になり、143席のうち138席で視線が前の人の目より下を通って、平均は53まで下がります。反対に1.40 mまで上げると平均は70、最高はF-06の82点、B列のすき間は15.69 cmです。同じE-07でも、この三つの高さで60点・64点・81点と変わります。

座席予約システム 作り方 9種 — 扇形の客席・視界の点数マップ・席から見た舞台・連席さがし・同時予約の取り合い・慣性つきスタジアムマップ・入口から席までの道案内・飛行機の座席自動割り当て・コンサート予約のファーストビュー

01〜03は一つの扇形の客席を三段階で読みます。席を並べ、席ごとに視界の点数をつけ、最後にその席に座って舞台を眺める順です。04〜08は予約画面が実際にぶつかる問題を一つずつ受け持ちます(連席・同時の取り合い・一万席の地図・入場の道・客室の条件)。09は01〜05の仕事を6.6秒の一つの流れにつないだファーストビューです。九つとも外部ライブラリもWebGLも使わず2Dキャンバス一枚で描き、乱数はシードを固定しているので、開くたびに同じ順で再生されます。

480×300のキャンバスで各場面がどれだけ動いたか

描画を2秒間で24枚撮り、となり合う2枚ずつを比べたので、場面ごとの差分は23個になります。

場面 累積面積 動いたフレーム 強度の平均
01 扇形の客席 20.2% 23/23 45.6
02 視界の点数マップ 17.9% 23/23 88.1
03 席から見た舞台 63.3% 23/23 69.8
04 連席さがし 10.5% 23/23 61.8
05 同時予約 15.5% 22/23 81.7
06 スタジアムマップ 71.4% 23/23 44.7
07 入口からの道案内 31.3% 19/23 62.7
08 客室の自動割り当て 12.5% 22/23 87.6
09 予約のファーストビュー 34.5% 23/23 57.0

舞台の絵がキャンバスいっぱいに広がる03と、地図が拡大していく06は60%を超え、数十個の席の点だけが動く04と08は10〜13%にとどまりました。07の19/23は、歩く点が席に着いてから次の目的地へ移るまでキャンバスが待つ時間から来ていると考えられます(いちばん遠いA-22でも、2.4秒のループのうち2.046秒で着きます)。05と08の22/23は、出来事と出来事のあいだにはさまる短い待ちによるものと考えられます。

座席表 作成方法を調べると出てくる予約画面は、何を計算しているのでしょうか?

コンサートでは一席から舞台がどれだけ見えるか

座席表 作成 ツールで席の並びを描くところまではどの予約画面も同じでも、着席の公演でチケットを買う人が知りたいのは、その席から舞台がどれだけ見えるかです。02が客席を点数の地図に塗り替え、03が選んだ席からの眺めを描くのはそのためです。

映画館では何人が並んで座れるか

映画館の予約でおもに問われるのは別のことで、三、四人の友だちが、売約済みの席とのあいだに一席だけの空きを残さずに並べるかどうかです。そこを受け持つのが04で、05は迷っているあいだにほかの人がその席を取ってしまう場面を加えます。

スタジアムと客室では席が多すぎるか、席に条件がつく

一万席を超えるスタジアムの地図では、何を描かないかを決めることが計算になり(06)、入場したあとは入口から席までの道が要ります(07)。客室の座席指定では、となり合わせ・窓側か通路側かの希望・非常口の列に子どもを座らせないことを一度に満たさなければならず、08は一行を置いてみて、次の一行が入らなければ置いた席を外して別の候補へ移る順番で進みます。期間をドラッグで選ぶカレンダー css デザイン 9選や、ピンを一本落とす地図 ui デザイン 9選とくらべると、座席表は選べるマスの形と、となり合うマスどうしの関係まで計算していることがわかります。

01弧の長さで席を割りふる扇形の客席

列 i は半径62 + i × 18 pxの弧に乗り、下のループはまず扇形から通路二本ぶん(14 pxを半径で割ったラジアン)を切り取ってから、残った区間ごとに弧の長さをSEAT_Wの16 pxで割って切り捨てるので、A列の5席からK列の24席まで、10列で143席になります。席は0.2秒 + 列 × 110 ms + 列の中の順番 × 12 msで出発し、硬さ260・減衰14のばね(減衰比0.434、行き過ぎ21.4%)で跳ね上がりながら、高く上がるほど影が広く薄くなり、最後のK-24は1.466秒に出発して、143席が2.254秒までに落ち着きます。舞台を囲んで曲がる客席は四角い格子では表せないので、この関数が最初の一歩になります。

弧の長さで席を割りふる扇形の客席
新しいタブで開く

01 · 弧の長さで席を割りふる扇形の客席 — 計測 累積面積 20.203% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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);
        }

02前の列の頭にさえぎられるぶん冷めていく視界の点数マップ

列 i の目は i × RAKE_STEP(0.12 m)+ 1.15 mの高さにあって舞台からは2.4 + i × 0.95 m離れており、clearanceは目から舞台の前端へ引いた線が前の列の目の上をどれだけ通るかをメートルで返します。このすき間(12 cmで満点)を距離・舞台の幅の角度・見下ろす角度・正面からのずれと0.3・0.1・0.35・0.1・0.15の重みで合わせ、点数は舞台側から毎秒520 pxで広がって塗られ、そのあいだも舞台の高さが上下するので、マップ全体が水色(0)から杏色(100)のあいだで塗り直されつづけます。ホールの見え方を案内するページなら、前の人の頭の上を視線が何センチ通るかを席ごとに示せます。

前の列の頭にさえぎられるぶん冷めていく視界の点数マップ
新しいタブで開く

02 · 前の列の頭にさえぎられるぶん冷めていく視界の点数マップ — 計測 累積面積 17.908% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RAKE_STEPC_GOODSCORE_WEIGHTS
    // すき間(メートル)— 列 i の目から舞台の前端(高さ stageH)まで引いた直線が前の列の席(距離 rowDist(i−1))を通るときの高さ − 前の列の目の高さ。
    // 直線の高さ = stageH + (自分の目 − stageH) × (前の列の距離 / 自分の距離)。最前列は前の人がいないので 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);
    }

03その席の目の高さから遠近で見た舞台

カメラを席の目の位置に置いて(0, 2.05, −1.6)を向かせ、projectは焦点距離360でカメラからの奥行きで割り、奥の幕・舞台の上面と前面・左右に揺れる三人の役者・前の列の頭と肩を一つのリストにまとめて奥行きの大きい順に塗るので、近い頭が舞台の上に重なります。カメラはA-03→E-07→K-13→G-02の順に席のあいだを0.7秒ですべり、舞台の前端と役者の胸の高さの線のうち頭に隠れる割合は、A-03で0%、E-07で39%、K-13で49%、G-02で32%でした。A-03には頭が一つも映りませんが、舞台の前端の幅が画面の幅の261%に広がり、最前列の得と損が一枚に収まっています。

その席の目の高さから遠近で見た舞台
新しいタブで開く

03 · その席の目の高さから遠近で見た舞台 — 計測 累積面積 63.324% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

FOCALGLIDE_SSTAGE_SAMPLES
        // 舞台照明のにじみ
        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);
        }
        // 奥行きの大きい順に塗る — 近い頭があとから舞台に重なる
        const list = sceneList03(cam);
        list.sort(function (a, b) { return b.z - a.z; });
        for (const d of list) {
            d.paint();
        }
        miniMap03();

04一席だけの空きを残さない連席さがし

この場面は8列×14席の映画館の格子で空席は61、findBlocksは通路で区切られた区間ごとにGROUP_SIZE 4の窓を一席ずつずらし、窓の中がどれも空いていれば候補にして、窓のすぐ外の席が取り残されるときはその席を記録します。候補は6つ見つかり、ばねで追いかける窓が1つあたり160 msとどまって、一席を取り残す候補では窓が珊瑚色に変わり、点数の1位と2位(F-06〜F-09の3.917とF-07〜F-10の3.851)がそろって外れて、E-05〜E-08の3.744が選ばれます。

一席だけの空きを残さない連席さがし
新しいタブで開く

04 · 一席だけの空きを残さない連席さがし — 計測 累積面積 10.482% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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 });
            }
        });

05先に売れた席を揺らして知らせ、近い代わりの席をすすめる同時予約

planBookingは再生全体を前もって計算します。最初に28席が売れていて、ほかの購入者36人が平均70 msに設定した指数分布のランダムな間隔(このシードでは最初の人が0.35秒、最後の人が2.513秒)でやって来ては点数の3乗に比例する確率で空席を押さえ、900 ms後に決済し、デモの購入者は0.3秒にいちばん高い空席E-07を押さえます。ところが1.0秒にほかの窓口の決済が先に終わり(それまでにほかの購入者15人が席を押さえています)、下のループは輪を7 × e^(−t/0.12) × sin(2π·9·t) pxで揺らし、0.45秒後にマスの距離 + 3 × (1 − 点数)がいちばん小さいD-06へ押さえ直します。

先に売れた席を揺らして知らせ、近い代わりの席をすすめる同時予約
新しいタブで開く

05 · 先に売れた席を揺らして知らせ、近い代わりの席をすすめる同時予約 — 計測 累積面積 15.514% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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);

06拡大するほど区画が座席に分かれる慣性つきのスタジアムマップ

地図には24区画に11,720席があり、倍率は1/60秒ごとに対数で×7までの残りの0.06ずつ近づくので、×7に近づくほど遅くなり(0.25秒で×1.46、0.5秒で×3.77、1.0秒で×6.35)、そのあいだzoomAboutが基準点(330, 82)を画面の同じ位置に留めます。×2.2から×4までの帯は196 ms(0.338秒から0.533秒)で通り過ぎ、区画の多角形が薄れるのと入れ替わりに席が濃くなり、1.35秒に(−300, 150) px/秒で放した移動は1/60秒ごとに速さの8%を失って、約69 px進んだ2.375秒に止まります。forVisibleSeatsは席を一つずつ見る前に、区画を外接矩形ごと捨てます。

拡大するほど区画が座席に分かれる慣性つきのスタジアムマップ
新しいタブで開く

06 · 拡大するほど区画が座席に分かれる慣性つきのスタジアムマップ — 計測 累積面積 71.413% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ZOOM_EASEFRICTIONSPLIT_FROM
    // 基準点を留めた拡大 — 新しいオフセット = 基準点 − (基準点 − オフセット) × 新しい倍率 / 古い倍率
    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;
    }

07入口から自分の席まで通路をたどる道案内

床は12 pxのマスが38×18並び、歩けるマスは370で、breadthFirstは二つの入口から同時に距離0で始めてキューから取り出したマスの上下左右に距離 + 1を渡し、席のマスには舞台側のすぐ手前のマスからしか入れません。B-05までは左の入口から23マス・曲がり4回で、角を9 pxの2次曲線で丸めると、マスを足した276 pxではなく265 pxになります。歩く点は毎秒230 px、0.05秒ごとに11.5 px進み、通った道には太いミントの跡が、残りの道には紫の点線が引かれます。

入口から自分の席まで通路をたどる道案内
新しいタブで開く

07 · 入口から自分の席まで通路をたどる道案内 — 計測 累積面積 31.31% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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;
                }
                // 席の列へは、その席のすぐ手前のマス(舞台側)からだけ入る
                if (nid === goal ? d[0] !== 1 : !walkable(rr, cc)) {
                    continue;
                }
                dist[nid] = dist[id] + 1;
                from[nid] = id;
                queue.push(nid);
            }
        }

08窓側・通路側の希望と非常口の条件を合わせる飛行機の座席自動割り当て

A B C | D E Fが8列並ぶ客室に、家族1(子ども連れの三人・窓側)、ふたり(窓側)、家族2(子ども連れの三人・窓側)、親と子(ふたり・通路側)、ひとり(通路側)の五つの一行を順に座らせます。placePartiesは候補に置いてみて、次の一行が入らなければその席を戻して次の候補へ進み、子ども連れの一行は非常口の4列目を飛ばします。結果は置いてみた回数6・戻った回数1(ふたりが6E・6Fを返して7A・7Bへ移ると家族2が入る)・非常口で飛ばした回数2で、1段階は170 msです。

窓側・通路側の希望と非常口の条件を合わせる飛行機の座席自動割り当て
新しいタブで開く

08 · 窓側・通路側の希望と非常口の条件を合わせる飛行機の座席自動割り当て — 計測 累積面積 12.541% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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 });
            }

09席が決まるとチケットが裏返って出てくるコンサート予約のファーストビュー

このファーストビューは前の場面を一本につなぎ、0.1秒から01の客席が組み上がり、0.3秒から02の点数の色が広がり、0.5〜1.0秒に64席が売れて、1.05秒に二人用のfindBlocksが36の候補からG-10・G-11(各68点)を選びます。1.15秒に小さなプレビュー窓がその眺めを見せ、1.35秒にチケットがばねで上がり、1.6秒から0.8秒で一回転するあいだ幅はcosの大きさで決まって、横向きに近いほど面が暗くなります。裏に「座席指定」、表に「予約の確認」と席番号が出るので、座席選択 チケットの流れを一画面で通して見せられます。

席が決まるとチケットが裏返って出てくるコンサート予約のファーストビュー
新しいタブで開く

09 · 席が決まるとチケットが裏返って出てくるコンサート予約のファーストビュー — 計測 累積面積 34.492% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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();
        // 精巧さをもう一層 — 横向きに倒れているあいだ(|cos| が小さいとき)面が暗くなり、光を斜めに受けているように見せる
        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();

席は極座標で、視界は前の列との高さくらべで、連席はずらす窓で求めます — 座席表を支える計算

席を数える前に通路を切り取るわけ

buildHallは先に角度から通路の幅を取り除き、残りを割るので、通路をまたぐ席は生まれません。一つの列の三つの区間はそれぞれ別に切り捨てるので、区間ごとの端数が捨てられ、E列は弧がD列より長いのに同じ12席になります。

すき間を一つ手で計算する

E列(i = 4)を例にとります。目の高さは4 × 0.12 + 1.15 = 1.63 m、舞台からの距離は2.4 + 4 × 0.95 = 6.2 mで、D列は5.25 m先にあり、目の高さは1.51 mです。1.63 mから0.95 mの舞台の前端へ下りる線はD列を通るとき0.95 + 0.68 × (5.25 / 6.2) = 1.5258 mの高さにあり、D列の目より1.58 cm上を通ります。舞台が0.95 mのとき02がE-07に出す値が、この1.58 cmです。

拡大は基準点を、連席はとなりの席を見る

zoomAboutは画面の一点が地図の同じ点の上に残るようなオフセットを解きます。新しいオフセット = 基準点 − (基準点 − オフセット) × 新しい倍率 / 古い倍率なので、拡大のあいだ基準点のずれは0 pxです。findBlocksは窓の内側だけで判断を終えず、両端のすぐ外の席を確かめ、その席が空いていてさらに外が売約済みか区間の端なら、それを窓が残す一席だけの空きとみなします。

座席表が壊れやすい場面 — 一万席で重くなる再描画・二人が同時に押さえた同じ席・一席だけ空きが残る連席割り当て

空いて見える席がもう売れていることがある

05ではE-07が紫の輪で自分の席として描かれていますが、1.0秒にほかの窓口で始まった決済が先に終わり、席はなくなります。その時点で、ほかの購入者15人がすでに席を押さえています。画面は7 pxから始まって0.1秒で3.04 px、0.2秒で1.32 px、0.3秒で0.57 pxへ小さくなる揺れで知らせ、一列前で一席となりのD-06をすすめます。cellDistはMath.hypotで距離を出すので距離は√2 ≈ 1.414席ぶんで、足りない点数の3 × 0.144を足すと費用は1.414 + 0.433 ≈ 1.85です。空席として描かれた席は、画面が最後に受け取った更新の時点で空いていたというだけです。

点数がいちばん高い連席が一席を取り残すとき

04のF列は中央の5席(F-06〜F-10)が空いています。4席の窓はその中に二通り入り、その二つが点数の合計の1位と2位ですが、どちらも一席を取り残すので両方とも外され、6つの候補のうちH-06〜H-09を含む3つが同じ理由で落ちます。04はbestBlockを一席だけの空きを禁じる呼び方と許す呼び方で二回呼び、答えが食い違うときは許した側の勝者を珊瑚色の点線で画面に残します。この比較を自分の手で確かめたいときは、zipをパスワード74ar6jn4で開き、04フォルダのindex.htmlでbestBlockの第2引数を入れ替えてみてください。いちばん上の連席をそのまま取るとF-10が一席だけ残り、そこは一人で来た購入者にしか埋められません。

一万席を毎フレーム描き直す

06の11,720席を低い倍率で一つずつ描くと矩形が11,720個になりますが、その倍率なら区画の多角形24個で足ります。高い倍率ではforVisibleSeatsが外接矩形が画面の外にある区画を飛ばし、×3.77で1,792席、×7では742〜799席(全体の6.3〜6.8%)だけを渡します。いちばん重いのは区画と席を重ねて描く分かれ目の帯で、×2.9で2,567席になるため、06は空席・売約済みの席・斜線をそれぞれ一つのPath2Dにまとめ、三つのパスを塗るだけにしています。

色だけに頼らない座席表 — 模様・席番号の読み上げ・動きを減らす設定のアクセシビリティ

席の色を取り去っても残るもの

01・04・05・07・08・09が呼ぶ共通のdrawSeatでは、空席は薄い地に濃い1.1 pxの枠線、売約済みは灰色に45°の斜線、押さえた席はバター色に2 pxの枠線、自分の席はミントにチェックです。白黒で見ても、枠線・斜線・チェックで四つの状態を見分けられます。drawSeatを使わない06も、売約済みの席に別のパスで斜線を引いて同じ原則に従います。

01・02・03でキーボードから席を選ぶ

扇形の客席を使う三つの場面には、本物の<button>である「前の席」「次の席」がrole="group"の中に並び、矢印キーでも席を動かせます。左右は列の中のとなりの席へ、上下は前後の列で角度がいちばん近い席へ移ります(hallNeighbor)。Enterを押すとE-07に止まり、状態の一行が移った先の席番号を伝えます。03は一時停止中か動きを減らす設定のとき、席を選ぶとすべらずにその席の眺めへそのまま切り替わります。

動きを減らす設定と一時停止ボタン

九つの場面はどれもprefers-reduced-motionをchangeイベントまで聞いていて、設定がオンになるとループを止め、席が決まり終えた最後の一枚を描きます。その状態でも01・02・03は席を選ぶとすぐ描き直します。一時停止ボタンはaria-pressedで状態を伝え、状態の一行は最初aria-live="off"で始まって最初の操作のあとにだけpoliteに切り替わるので、自動で動くデモがページの読み上げにかぶりません。

段差を急に、人数を多く、拡大を速くすると — 数字一つで変わる予約画面

変形 変えた値 変わること
段差を急に 02 RAKE_STEP 0.12 → 0.3 E列のすき間 1.58 → 8.55 cm、平均 56 → 68、最高 G-07(68)→ F-06(81)、最後列の見下ろす角度 6.67° → 14.83°
人数を多く 04 GROUP_SIZE 4 → 6 候補 6 → 1、一席だけの空きによる除外 3 → 0、選択 E-05〜E-08 → H-05〜H-10
拡大を速く 06 ZOOM_EASE 0.06 → 0.3 ×7の95%に届くまで 1.221 → 0.375秒、区画が席に分かれる帯 196 → 33 ms

段差を0.3 mにするとE列の目は2.35 mに上がり、線はD列の上で0.95 + 1.4 × 0.8468 = 2.1355 mを通るので、そこの目の高さ2.05 mを引くと8.55 cmが残ります。03で頭に隠れる割合もE-07で39→24%、K-13で49→34%、G-02で32→27%に減りますが、最後列は舞台をより急な角度で見下ろすことになります。六人の一行では六席続きの空きが一つしか見つからず、選択は合計5.08のH列へ下がります。拡大の割合を0.3にすると分かれ目の帯は2フレームで過ぎ、区画が一気に席へ変わるので、分かれていく様子そのものを見せたいなら0.06のあたりに置いてください。

座席選択の画面を自作するときのよくある質問

視界の点数を自分のホールの寸法に合わせるには、どこを変えますか?

各index.htmlの上のほうにある共通コードに寸法が集まっています。FIRST_ROW_M(最前列まで2.4 m)、ROW_DEPTH_M(1列あたり0.95 m)、RAKE_STEP、EYE_H(1.15 m)、STAGE_H、満点のすき間を決めるC_GOOD、五つの重みを並べたSCORE_WEIGHTSです。席の並びそのものは、buildHallに渡すROW_R0・ROW_GAP・SEAT_W・AISLE_Wで決まります。

座席予約システム 無料 の既製品を使わずに、05を自前の予約サーバーにつなぐには何を変えますか?

planBookingは押さえと販売の出来事を時刻順に並べたリストを前もって作り、上のコードのループが時刻を過ぎた出来事を順に当てはめます。このリストをサーバーからのメッセージで埋めるように変え、取り合いになった席を誰が取るかはサーバーに決めさせてください。画面は結果を見せるだけにして、自分の押さえが断られたときにだけ揺れとD-06のような代わりの提案を動かします。

11,720席の地図はスマートフォンで重くなりませんか?

06は画面にかかる区画だけを回り、その中でも画面外の席を飛ばし、同じ状態の席を一つのPath2Dでまとめて塗ります。キャンバスの画素は端末のピクセル比を最大2まで掛け、描画の座標はsetTransformで480×300のまま保ちます。タブが隠れているあいだはループが止まり、戻ると続きから動きます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。