Give every seat its own pixels
Six players at a nine-handed table is the most ordinary table this tool draws, and on every phone in the sweep one of them had their stack painted under the player next to them. Measured across nine viewports and six table sizes: seventeen of fifty-four tables hid a name or a stack behind something belonging to another seat. Two causes, and the layout only knew about a third of one of them. The felt decided how big a puck could be from a single number — the straight-line distance between the closest two *neighbouring* seats, judged against a fraction of a puck's width. Distance has one number in it and a puck is half again as wide as it is tall, so the same 84px was generous side by side and nowhere near enough on the diagonal. And neighbours are the wrong pairs to ask about: on a phone held sideways the felt is 528x87, the ring is very nearly a straight line, and the seat that lands on top of you is the one across the table. Ten of the tables in the sweep had a collision that a neighbours-only check cannot see — seats 1 and 7, overlapping by 68px, while every adjacent pair had room. seatsCollide() asks the real question instead: do any two of these boxes want the same pixels. Two equal axis-aligned boxes overlap exactly when their centres are closer than one box in both axes, so it is an answer rather than a proxy, and it looks at every pair. The second cause was the small blind badge, sitting at `left: -6px` — hanging off its own puck's left corner onto whatever was beside it. On an iPhone Pro Max at six seats, with the pucks not touching at all, "SB" was painted across the next player's $1,137. All three badges now sit inside the puck's width, and in the same place as each other: a seat only ever wears one, so two positions bought nothing except making the dealer's eye find which of two spots this seat was using before it could read which blind it is. Then the layout had to be able to act on the answer, and it couldn't. It chose between two widths — 96px or 58px — and if neither fitted it widened the ring and let the seats overlap, on the reasoning that a dealer can see an overhang but cannot see a tap landing on the wrong player. That trade is real; it was also a false choice, because the tables that overlapped did not want either number. At nine seats a Galaxy S8 had room for a 61px puck, an iPhone 14 for 68, a Pixel 7 for 74, an iPhone Pro Max for 79. Three media queries declared three more of these numbers — 78px, 64px, 62px — at three breakpoints, guessing at something a media query cannot see: the same 390px phone has room for a 90px puck at four seats and 61 at nine. So the width is searched for now, not chosen. render.js walks candidates from the top, applies each one, measures what the browser actually drew, and takes the widest that leaves every seat its own box. There was a width that worked on every table in the sweep; the code was never asking for it. Ten candidates is ten forced layouts, so the result is cached against the felt's size, the seat count and the names — measured 12.5ms cold, 0.8ms warm, and renderSeats runs after every tap. Three things this got wrong on the way, all caught by measuring rather than reading: The badge overhang was folded into the space between two seats. That collapsed the ring on a phone held sideways — 87px of height, and 18px of badge is most of the room the ring has to move in — until a six-handed landscape table was unsatisfiable at any width. It didn't belong there: the badge points *away* from the table at both ends, so two seats facing each other across the felt wear them back to back. box-sizing: border-box, which the puck needed so the searched number means the width it is drawn at, quietly turned every min-height from a content height into a drawn one. An empty chair went from a 52px tap target to a 42px one on every phone in the sweep. The audit found it; the four min-heights have each gained the padding and border they used to be quoted without. The new audit check reported a name under a neighbour on a table whose seats were 9px apart. A puck's name is a `-webkit-box` with `line-clamp`, and getClientRects() hands back a rect for every line the text *would* have taken, including the ones the clamp throws away — so the probe was hit-testing 30px below the puck, where nothing is painted. It clips to what is painted now. uiAudit gains "no seat covers another seat's writing", the complement of the check already there for a puck's own decoration on its own name — which is why nothing caught this: seatLabelsClear() only ever looked inside one seat. Fault-injected against the previous version: eighteen configurations, naming both causes. And the sweep itself now runs two table shapes. It had run one for its whole life — five players at a nine-handed table — and how crowded the felt is depends on how many people are sitting down far more than on anything a viewport can report. A full table was never once looked at. 632 tests, 0 failures. 190 audits across thirteen viewports at two table shapes, with a mouse and with a thumb. Every table from two seats to twelve now keeps its own text on every viewport in the sweep. One configuration still reports: a dozen players on a 640x360 phone, where the felt is 87px tall and two rows of 55px pucks cannot both fit and stay on the rail — the ring grows 8px past the edge to keep the seats off each other, which is the trade the old code named and got right. Left reported rather than tuned away. Names on a crowded phone table truncate harder than they did, because the puck is now sized to fit instead of allowed to overlap: "Christopher" is "Christ…" at nine seats on an iPhone 14. The full name is in the DOM, in the seat's form and in the sidebar. The next lever on that is the seat layer's inset, which is still a fixed 44px of "half a puck" for a puck that is no longer 96px wide — worth 54px to 60px where it was measured. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
@@ -619,8 +619,22 @@ button,
|
||||
.seat {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 96px;
|
||||
min-height: 54px;
|
||||
/* Set by render.js, which measures the widest puck that lets every seat keep its
|
||||
own box on this particular felt. The 96px is what a table gets before the first
|
||||
measurement lands, and what it keeps whenever there is room for it.
|
||||
|
||||
border-box so the number means the width the puck is drawn at. As content-box it
|
||||
did not: a 64px puck measured 74 across once its padding and border were counted,
|
||||
and the search that sets this compares it against distances on the felt. */
|
||||
box-sizing: border-box;
|
||||
width: var(--seatPuckW, 96px);
|
||||
/* Every min-height on a seat is a drawn height now, border and padding included,
|
||||
because of the box-sizing above. They were content heights before, so each one
|
||||
here and in the three blocks that override it has gained the 8px of padding and
|
||||
2px of border it used to be quoted without — the puck is the size it always was.
|
||||
Caught by the audit rather than by reading: an empty chair went from a 52px tap
|
||||
target to a 42px one on every phone in the sweep, which is under the floor. */
|
||||
min-height: 64px;
|
||||
background: linear-gradient(160deg, var(--surface-3), #1b1b1b);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--r-lg);
|
||||
@@ -972,14 +986,29 @@ button,
|
||||
is marked anyway and has the side benefit of making the two badges far easier to
|
||||
tell apart at speed — light-on-blue against dark-on-gold, rather than two pale
|
||||
labels differing only in hue. */
|
||||
/* All three sit in the same place, and inside the puck's own width.
|
||||
|
||||
They used to be spread along the top edge — the small blind at -6px, so hanging off
|
||||
the left corner, the big blind at 22px — and that cost twice over. The overhang put
|
||||
the badge onto whatever was to the left of the seat: measured on an iPhone Pro Max at
|
||||
six seats, where the pucks were not touching at all, "SB" was painted across the next
|
||||
player's $1,137. And two badges in two different places means the dealer's eye has to
|
||||
find which of two spots this seat is using before it can read which blind this is,
|
||||
forty times a night. A seat only ever wears one of these — the small and the big
|
||||
blind are different players by construction, and the straddle chain starts left of
|
||||
the big blind and stops short of the small one — so there was never anything for the
|
||||
second position to avoid.
|
||||
|
||||
Left rather than centred because the dealer button's chip owns the top-right corner,
|
||||
and the two are read together: who has the button, and who is posting. */
|
||||
.sbBadge {
|
||||
left: -6px;
|
||||
left: 2px;
|
||||
background: linear-gradient(180deg, #2f6cb5, #22508c);
|
||||
color: #eaf3ff;
|
||||
}
|
||||
|
||||
.bbBadge {
|
||||
left: 22px;
|
||||
left: 2px;
|
||||
background: linear-gradient(180deg, #e6ad4d, #c98a2a);
|
||||
color: #241a05;
|
||||
}
|
||||
@@ -992,7 +1021,7 @@ button,
|
||||
looking for mid-hand is the one telling them where the action comes back to.
|
||||
Dark text on the light end of the ramp, measured 5.76:1 against the darker stop. */
|
||||
.strBadge {
|
||||
left: 22px;
|
||||
left: 2px;
|
||||
background: linear-gradient(180deg, #cbaaf0, #a97fd6);
|
||||
color: #1b1030;
|
||||
}
|
||||
@@ -1004,22 +1033,22 @@ button,
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* Seats close enough together that full-size pucks would overlap. The class is set
|
||||
from a measurement render.js takes off the ring itself — the closest two neighbours,
|
||||
straight line — because no media query can see it: the same 390px phone is roomy at
|
||||
six seats and crowded at nine, and a wide landscape felt crowds at its ends while
|
||||
its rim says there is space. Not scoped to a screen size for the same reason. */
|
||||
/* Seats close enough together that a puck this narrow can't carry the full layout. The
|
||||
class is set from a measurement render.js takes off the ring itself, because no media
|
||||
query can see it: the same 390px phone is roomy at six seats and crowded at nine, and
|
||||
a wide landscape felt crowds at its ends while its rim says there is space. Not scoped
|
||||
to a screen size for the same reason.
|
||||
|
||||
It used to carry the width too — 58px, against the roomy layout's 96 — and those two
|
||||
numbers were the whole vocabulary the felt had. A table that needed 68 got 96 and
|
||||
overlapped, or got 58 and threw away ten pixels of name it could have kept. The width
|
||||
is now searched for and set as a custom property; what's left here is the typography,
|
||||
which does still want to change in one step rather than slide. */
|
||||
.seatsTight .seat {
|
||||
/* 58 was chosen by measurement and 46 was tried against it. On a roomy phone 46
|
||||
cleared every overlap at nine seats where 58 left two — but in the state that
|
||||
actually matters, a hand with a side pot where the bar has grown and the felt has
|
||||
shrunk to suit, 46 overlapped four times and 58 twice. Pucks touching at that
|
||||
density is the felt being small, not the puck being wide, and paying for it with
|
||||
an ellipsis on every long name buys nothing. What has to hold is that each seat
|
||||
keeps its own centre, so a tap lands on the player it was aimed at, and that is
|
||||
measured at every size. */
|
||||
width: 58px;
|
||||
min-height: 38px;
|
||||
/* 38px of content plus 6 of padding and 2 of border, which is what this drew before
|
||||
the box became a border-box — and 46 clears the 44px a chair has to answer a thumb
|
||||
with. A seat is a control: tapping it opens that player's form. */
|
||||
min-height: 46px;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
@@ -3507,9 +3536,12 @@ body.hasActionBar #toastHost {
|
||||
--feltAspect: 1;
|
||||
}
|
||||
|
||||
/* No width here any more. This was 78px, and two blocks below it were 64 and 62 —
|
||||
three guesses, at three breakpoints, at the one number that depends on something
|
||||
a media query cannot see: how many people are sitting down. The same 390px phone
|
||||
has room for a 90px puck at four seats and 61px at nine. render.js measures it. */
|
||||
.seat {
|
||||
width: 78px;
|
||||
min-height: 44px;
|
||||
min-height: 54px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3566,12 +3598,11 @@ body.hasActionBar #toastHost {
|
||||
inset: min(26px, 12%) 44px;
|
||||
}
|
||||
|
||||
/* Narrower than the desktop puck: at a full ten-handed table the ring is
|
||||
flattest at the top and bottom, and wide pucks are what made neighbours
|
||||
overlap there. The puck is only a summary — the real form is the sheet. */
|
||||
/* The puck is only a summary — the real form is the sheet — so a phone gives it
|
||||
tighter padding and a shorter floor. How *wide* it gets is measured, not declared
|
||||
here: see the note on the 900px block above. */
|
||||
.seat {
|
||||
width: 64px;
|
||||
min-height: 42px;
|
||||
min-height: 52px;
|
||||
padding: var(--sp-1) var(--sp-1);
|
||||
}
|
||||
|
||||
@@ -3930,10 +3961,10 @@ body.hasActionBar #toastHost {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Nine seats around a table this small overlap each other, so they shrink to suit. */
|
||||
/* Seats around a table this small overlap each other, so they shrink to suit — by
|
||||
measurement now rather than by this block's old guess of 62px. */
|
||||
.seat {
|
||||
width: 62px;
|
||||
min-height: 40px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
/* The same inset the phone layout uses, which this branch never had because it is
|
||||
|
||||
@@ -169,7 +169,10 @@ const Render = {
|
||||
// finishes inside the table. Falls back to the old pair whenever there is nothing to
|
||||
// measure, which is every call from the unit tests and the first render before the
|
||||
// felt has been laid out.
|
||||
seatRingRadii() {
|
||||
// `puckSize` is the footprint the caller has just measured, badges included. Without
|
||||
// one this falls back to the first chair on the felt, which is what it always did —
|
||||
// and which is wrong whenever some other chair is the taller one.
|
||||
seatRingRadii(puckSize = null) {
|
||||
const fallback = { rx: this.RING_MAX_RX, ry: this.RING_MAX_RY };
|
||||
if (typeof document === "undefined") return fallback;
|
||||
|
||||
@@ -179,7 +182,7 @@ const Render = {
|
||||
if (!box || !box.width || !box.height || !table || !puck) return fallback;
|
||||
|
||||
const tableBox = table.getBoundingClientRect();
|
||||
const puckBox = puck.getBoundingClientRect();
|
||||
const puckBox = puckSize || puck.getBoundingClientRect();
|
||||
if (!tableBox.width || !puckBox.width) return fallback;
|
||||
|
||||
// Worked in absolute pixels rather than from half-widths, because the seat
|
||||
@@ -364,32 +367,35 @@ const Render = {
|
||||
// come down a size rather than the table pretending it has room it hasn't.
|
||||
SEAT_ROOM_PX: 84,
|
||||
|
||||
// Called on every render and again whenever the felt is resized — the action bar
|
||||
// growing mid-hand shortens the table, and the seats have to answer that without
|
||||
// waiting for the next render to come along.
|
||||
// Which layout the felt gets, and how wide its ring is.
|
||||
//
|
||||
// These two answers depend on each other, which is what made the first attempt at
|
||||
// this wobble: the ring's radius is limited by the size of a puck, the puck's size
|
||||
// is set by the `seatsTight` class, and that class was being decided from the gaps
|
||||
// between seats — measured at a radius that hadn't been worked out yet. Fixing the
|
||||
// gap measurement to use the real radius then made a nine-handed phone overhang
|
||||
// that had been fine, because now the two were chasing each other.
|
||||
//
|
||||
// So it is settled by trying the candidates in order of preference rather than by
|
||||
// solving it in one pass: the roomy layout if everything fits, the tight one if it
|
||||
// doesn't, and if neither fits, the tight pucks on a ring wide enough that seats
|
||||
// don't overlap — because two seats fighting over a tap is a worse failure than a
|
||||
// puck sitting slightly over the rail. A dealer can see an overhang; they cannot
|
||||
// see that the seat they meant to press wasn't the one that answered.
|
||||
// The range a puck may be drawn at. The bottom of it is where a name stops being a
|
||||
// name: at 44px the tight layout shows about five characters and an ellipsis, which
|
||||
// is still "Chris…" rather than nothing.
|
||||
SEAT_WIDTH_MAX: 96,
|
||||
SEAT_WIDTH_MIN: 44,
|
||||
// The step the search walks down in. Six pixels is finer than the difference between
|
||||
// two font sizes and coarse enough to settle in ten tries.
|
||||
SEAT_WIDTH_STEP: 6,
|
||||
// Below this a puck cannot hold two lines of name and a row of hole cards, so the
|
||||
// typography drops to the tight set in one go. Width scales continuously; type
|
||||
// changes deliberately, which is the split the modern guidance actually draws —
|
||||
// clamp for the continuum, a query for the things meant to be discrete.
|
||||
SEAT_TIGHT_BELOW_PX: 76,
|
||||
// Daylight between two pucks. They are rounded, so touching reads as overlapping.
|
||||
SEAT_CLEARANCE_PX: 4,
|
||||
// How far a blind badge hangs off the puck it belongs to — the `top: -9px` and
|
||||
// `bottom: -9px` in holdem.css. It points away from the table at both ends, so it
|
||||
// takes no room between two seats; what it must not do is reach *sideways*, which is
|
||||
// what seatFootprint.test.js holds the stylesheet to.
|
||||
SEAT_BADGE_OVERHANG_PX: 9,
|
||||
|
||||
// How much space two neighbouring seats need between their centres.
|
||||
//
|
||||
// This was a flat 84px, which is the figure a 96px-wide puck was judged against —
|
||||
// seats allowed to sit a little closer than their own width, since the pucks are
|
||||
// rounded and a small overlap at the corners reads fine. Asking the *tight* layout
|
||||
// to clear the same 84px is asking a 68px puck to keep a 96px puck's distance: it
|
||||
// could never pass, so every landscape table fell through to the fallback and took
|
||||
// an overhang it didn't need. The ratio is what carries over, not the number.
|
||||
// Kept for closestSeatGapPx's sake and no longer what decides the layout. It was a
|
||||
// proxy — a single distance standing in for "do these two boxes want the same
|
||||
// pixels" — and the answer it gave was wrong in both directions: a 96px puck was
|
||||
// allowed within 84px of its neighbour on the theory that rounded corners forgive a
|
||||
// little overlap, and the overlap it forgave turned out to be 20px of a stack
|
||||
// figure. seatsCollide asks the real question instead.
|
||||
SEAT_ROOM_RATIO: 84 / 96,
|
||||
|
||||
seatRoomNeededPx() {
|
||||
@@ -400,34 +406,198 @@ const Render = {
|
||||
return width ? width * this.SEAT_ROOM_RATIO : this.SEAT_ROOM_PX;
|
||||
},
|
||||
|
||||
// Does any pair of seats want the same pixels?
|
||||
//
|
||||
// Two equal axis-aligned boxes overlap exactly when their centres are closer than
|
||||
// one box in *both* axes, so this answers the question rather than standing in for
|
||||
// it. The distance test it replaces could not: distance has one number in it and
|
||||
// the pucks are half again as wide as they are tall, so the same 84px was generous
|
||||
// sideways and nowhere near enough on the diagonal.
|
||||
//
|
||||
// Every pair, not only neighbours — this is the half that was actually missing.
|
||||
// On a felt 528px wide and 87 tall the ring is very nearly a straight line, and the
|
||||
// seat that lands on top of you is the one across the table rather than the one
|
||||
// beside you. Measured on the running page before this went in: ten of the tables
|
||||
// in the sweep had a collision between two seats that a neighbours-only check
|
||||
// cannot see, five of them at 640x360, where seats 1 and 7 overlapped by 68px.
|
||||
seatsCollide(count, box, radii, width, height, clearance = this.SEAT_CLEARANCE_PX) {
|
||||
if (count < 2 || !box || !box.width || !box.height) return false;
|
||||
const room = { x: width + clearance, y: height + clearance };
|
||||
const points = this.ringPositions(count, radii.rx, radii.ry, box.width / box.height)
|
||||
.map(seat => ({
|
||||
x: (seat.left / 100) * box.width,
|
||||
y: (seat.top / 100) * box.height,
|
||||
}));
|
||||
for (let a = 0; a < points.length; a++) {
|
||||
for (let b = a + 1; b < points.length; b++) {
|
||||
if (Math.abs(points[a].x - points[b].x) < room.x &&
|
||||
Math.abs(points[a].y - points[b].y) < room.y) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// The box a seat actually takes up, at whatever width is applied right now.
|
||||
//
|
||||
// Widest and tallest are taken separately and across every chair, because a
|
||||
// two-line name makes one puck taller than its neighbours and the ring has to have
|
||||
// room for the worst of them, not the first one it measures.
|
||||
measuredSeatBox() {
|
||||
if (typeof document === "undefined") return null;
|
||||
const seats = document.querySelectorAll("#seatsLayer .seat");
|
||||
if (!seats.length) return null;
|
||||
let width = 0, height = 0;
|
||||
seats.forEach(seat => {
|
||||
const box = seat.getBoundingClientRect();
|
||||
width = Math.max(width, box.width);
|
||||
height = Math.max(height, box.height);
|
||||
});
|
||||
return width && height ? { width, height } : null;
|
||||
},
|
||||
|
||||
// What two seats have to clear each other by, which is the puck and nothing else.
|
||||
//
|
||||
// The blind badge was the reason to think otherwise, and it was worth chasing: "SB"
|
||||
// was painted across the next player's $1,137 on an iPhone Pro Max whose pucks were
|
||||
// not touching at all, so something was reaching past a puck's own box.
|
||||
//
|
||||
// It was reaching sideways. The badge sat at `left: -6px`, hanging off the puck's
|
||||
// left corner onto whatever was beside it, and that is fixed where it was caused —
|
||||
// in the stylesheet, by moving the badges inside the puck's own width.
|
||||
//
|
||||
// Its remaining overhang is vertical and costs nothing, because it always points
|
||||
// *away* from the table: `top: -9px` on the upper arc, `bottom: -9px` on the lower
|
||||
// one, so two seats facing each other across the felt wear their badges back to
|
||||
// back. Two seats on the same arc wear them at the same height, offset by exactly
|
||||
// as much as the pucks are.
|
||||
//
|
||||
// Adding it anyway was tried first, and it broke the case it was least expected to:
|
||||
// a phone held sideways gives the ring 87px of height, two rows of 55px pucks leave
|
||||
// 18px between them, and demanding 18px more for badges that point outward made a
|
||||
// six-handed landscape table unsatisfiable at any width. Twelve-seat tables on three
|
||||
// phones went the same way. The clearance below is what keeps rounded corners from
|
||||
// reading as a touch.
|
||||
seatFootprint(puckBox) {
|
||||
return puckBox && { width: puckBox.width, height: puckBox.height };
|
||||
},
|
||||
|
||||
applySeatWidth(layer, width) {
|
||||
layer.style.setProperty("--seatPuckW", `${width}px`);
|
||||
layer.classList.toggle("seatsTight", width < this.SEAT_TIGHT_BELOW_PX);
|
||||
},
|
||||
|
||||
// Widest first, because the widest that fits is the one we want.
|
||||
seatWidthCandidates() {
|
||||
const widths = [];
|
||||
for (let w = this.SEAT_WIDTH_MAX; w > this.SEAT_WIDTH_MIN; w -= this.SEAT_WIDTH_STEP) {
|
||||
widths.push(w);
|
||||
}
|
||||
widths.push(this.SEAT_WIDTH_MIN);
|
||||
return widths;
|
||||
},
|
||||
|
||||
// What the table has changed about the shape of its pucks. Anything that can make a
|
||||
// name wrap to a second line belongs here, because the height that comes back from
|
||||
// measuring is what the ring is sized against.
|
||||
seatShapeSignature() {
|
||||
if (!this.state) return "";
|
||||
return `${this.state.seats.map(p => p.name || "").join("")}|${this.state.dealerSeatIndex}`;
|
||||
},
|
||||
|
||||
// Which width the pucks get, and how wide a ring they sit on.
|
||||
//
|
||||
// This used to choose between two numbers — 96px or 58px — and when neither fitted
|
||||
// it widened the ring and let the seats overlap, on the reasoning that a dealer can
|
||||
// see an overhang but cannot see a tap landing on the wrong player. That trade was
|
||||
// real and it was also a false choice, because the tables that overlapped did not
|
||||
// want either number. Measured across the sweep at nine seats: a Galaxy S8 had room
|
||||
// for a 61px puck, an iPhone 14 for 68, a Pixel 7 for 74, an iPhone Pro Max for 79.
|
||||
// Handing all four the same 58 or the same 96 is how a puck ends up twenty pixels
|
||||
// inside its neighbour with a stack figure underneath it — which is what the running
|
||||
// page did, on every phone in the list, at the most ordinary table there is.
|
||||
//
|
||||
// So the width is searched for rather than chosen. There is a width that works on
|
||||
// every table in the sweep, down to a dozen seats on the smallest phone: the code
|
||||
// was never asking for it.
|
||||
//
|
||||
// Walked from the top rather than bisected. Feasibility is *nearly* monotone — a
|
||||
// narrower puck sits on a wider ring and has a smaller box — but not quite, because
|
||||
// a narrower puck can wrap a name onto a second line and come back taller. Walking
|
||||
// down takes the widest that actually measures clean, which is the honest answer;
|
||||
// bisection would take the widest that a model of the puck says is clean.
|
||||
//
|
||||
// Measuring is the expensive half — each width has to be applied and read back, and
|
||||
// that is a forced layout apiece — so it happens once per width, up front. Which
|
||||
// ring each width then sits on is arithmetic on numbers already in hand.
|
||||
//
|
||||
// The ring is allowed to grow past the one that keeps every seat on the felt, but
|
||||
// only after every width has been tried on the contained one. That order is the
|
||||
// trade the old code named and got right: a dealer can see a puck over the rail;
|
||||
// they cannot see that the seat they pressed wasn't the one that answered. It only
|
||||
// ever comes up on a phone held sideways, where the felt is a 692x117 slot.
|
||||
RING_GROWTH_STEPS: 8,
|
||||
|
||||
resolveSeatLayout(count) {
|
||||
const layer = document.getElementById("seatsLayer");
|
||||
if (!layer) return { radii: this.seatRingRadii() };
|
||||
|
||||
for (const tight of [false, true]) {
|
||||
layer.classList.toggle("seatsTight", tight);
|
||||
const radii = this.seatRingRadii();
|
||||
const roomy = this.closestSeatGapPx(count, this.ringBox(), radii) >= this.seatRoomNeededPx();
|
||||
if (roomy && !this.tooTallForTheTable()) return { radii };
|
||||
const box = this.ringBox();
|
||||
const key = box &&
|
||||
`${Math.round(box.width)}x${Math.round(box.height)}:${count}:${this.seatShapeSignature()}`;
|
||||
// Ten candidate widths mean ten forced layouts, and renderSeats runs after every
|
||||
// tap a dealer makes. Nothing in the key changes between one tap and the next
|
||||
// unless the table itself has, so within a hand this is measured once and read
|
||||
// back afterwards.
|
||||
if (key && this.seatLayoutCache && this.seatLayoutCache.key === key) {
|
||||
this.applySeatWidth(layer, this.seatLayoutCache.width);
|
||||
return this.seatLayoutCache;
|
||||
}
|
||||
|
||||
// Neither fits — a phone held sideways gives the felt about 140px of height and
|
||||
// there is no ring that puts a dozen readable pucks inside that. Keep the small
|
||||
// pucks and widen the ring only as far as it takes to stop the seats colliding,
|
||||
// rather than all the way back out: going straight to the full width bought an
|
||||
// overhang of 14px where 4 would have done.
|
||||
layer.classList.add("seatsTight");
|
||||
const contained = this.seatRingRadii();
|
||||
const box = this.ringBox();
|
||||
for (let step = 1; step <= 8; step++) {
|
||||
const t = step / 8;
|
||||
const radii = {
|
||||
rx: contained.rx + (this.RING_MAX_RX - contained.rx) * t,
|
||||
ry: contained.ry + (this.RING_MAX_RY - contained.ry) * t,
|
||||
};
|
||||
if (this.closestSeatGapPx(count, box, radii) >= this.seatRoomNeededPx()) return { radii };
|
||||
const candidates = [];
|
||||
for (const width of this.seatWidthCandidates()) {
|
||||
this.applySeatWidth(layer, width);
|
||||
const puck = this.measuredSeatBox();
|
||||
if (!puck) {
|
||||
// Nothing laid out yet — the very first render. Take the widest and let
|
||||
// the resize that follows do this properly.
|
||||
const radii = this.seatRingRadii();
|
||||
if (key) this.seatLayoutCache = { key, width, radii };
|
||||
return { width, radii };
|
||||
}
|
||||
return { radii: { rx: this.RING_MAX_RX, ry: this.RING_MAX_RY } };
|
||||
candidates.push({
|
||||
width,
|
||||
contained: this.seatRingRadii(puck),
|
||||
footprint: this.seatFootprint(puck),
|
||||
tooTall: this.tooTallForTheTable(),
|
||||
});
|
||||
}
|
||||
|
||||
const fits = (candidate, radii) => !candidate.tooTall &&
|
||||
!this.seatsCollide(count, box, radii, candidate.footprint.width, candidate.footprint.height);
|
||||
|
||||
let chosen = null;
|
||||
for (let step = 0; step <= this.RING_GROWTH_STEPS && !chosen; step++) {
|
||||
const t = step / this.RING_GROWTH_STEPS;
|
||||
for (const candidate of candidates) {
|
||||
const radii = {
|
||||
rx: candidate.contained.rx + (this.RING_MAX_RX - candidate.contained.rx) * t,
|
||||
ry: candidate.contained.ry + (this.RING_MAX_RY - candidate.contained.ry) * t,
|
||||
};
|
||||
if (fits(candidate, radii)) { chosen = { width: candidate.width, radii }; break; }
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing cleared at any width on any ring. Keep the narrowest puck on the
|
||||
// widest ring, and let the audit's "no seat covers another seat's writing" say
|
||||
// so out loud rather than the layout quietly papering over it.
|
||||
if (!chosen) {
|
||||
const last = candidates[candidates.length - 1];
|
||||
chosen = { width: last.width, radii: { rx: this.RING_MAX_RX, ry: this.RING_MAX_RY } };
|
||||
}
|
||||
|
||||
this.applySeatWidth(layer, chosen.width);
|
||||
if (key) this.seatLayoutCache = { key, ...chosen };
|
||||
return chosen;
|
||||
},
|
||||
|
||||
updateSeatDensity() {
|
||||
|
||||
+12
-4
@@ -33,10 +33,18 @@ node tests/holdem/runUiAudit.js # needs Playwright and the site running
|
||||
|
||||
It checks WCAG AA contrast, touch target sizes, touch-action and tap highlights, that fields stay selectable and are large
|
||||
enough not to trigger iOS zoom, that nothing important is covered or off-screen with
|
||||
no way to scroll to it, that no grid column has collapsed to nothing, and that no
|
||||
text is painted outside its box. It runs over the table, all five overlays and Quick
|
||||
Split, at thirteen viewports from a 360px phone to a 1600px desktop and the same
|
||||
phones on their side — each viewport twice, once with a mouse and once with a thumb.
|
||||
no way to scroll to it, that no grid column has collapsed to nothing, that no
|
||||
text is painted outside its box, and that no seat covers another seat's writing. It
|
||||
runs over the table, all five overlays and Quick Split, at thirteen viewports from a
|
||||
360px phone to a 1600px desktop and the same phones on their side — each viewport
|
||||
twice, once with a mouse and once with a thumb, and at two table shapes.
|
||||
|
||||
Two table shapes is also newer than it sounds. The sweep ran on one — five players at
|
||||
a nine-handed table — for its whole life, and how much room the felt has depends on
|
||||
how many people are sitting down far more than on anything a viewport can report. A
|
||||
full table is where the seats crowd and it had never been looked at: the first sweep
|
||||
that did found one player's name or stack painted under another on eighteen of the
|
||||
configurations, on every phone in the list.
|
||||
|
||||
That last part, and two of those checks, are newer than this paragraph claiming them.
|
||||
`runAll()` did not include the contrast check at all, and touch targets sat behind an
|
||||
|
||||
@@ -104,7 +104,7 @@ describe("the tokens themselves", () => {
|
||||
test("every var() the stylesheet reaches for exists", () => {
|
||||
const used = new Set((css.match(/var\((--[a-zA-Z0-9-]+)/g) || []).map(s => s.slice(4)));
|
||||
// Published from JS at runtime rather than declared here — see actionBar.js.
|
||||
const fromScript = new Set(["--actionBarHeight", "--feltRoomPx"]);
|
||||
const fromScript = new Set(["--actionBarHeight", "--feltRoomPx", "--seatPuckW"]);
|
||||
// Defined by the site's own stylesheet, not this one.
|
||||
const fromSite = new Set(["--font-mono"]);
|
||||
const missing = [...used].filter(t =>
|
||||
|
||||
@@ -34,11 +34,24 @@ const OVERLAYS = [
|
||||
];
|
||||
|
||||
// A table part-way through a real night: long names, uneven stacks, a hand log.
|
||||
function seedTable() {
|
||||
//
|
||||
// Two of them. The sweep ran on one table shape for its whole life — five players at a
|
||||
// nine-handed table — and how much room the felt has depends on how many people are
|
||||
// sitting down more than on anything a viewport can tell you. A full table is where the
|
||||
// seats crowd, and it was never once looked at: measured when it finally was, seventeen
|
||||
// of fifty-four tables had one player's name or stack painted under another.
|
||||
const TABLES = [
|
||||
["half full", 9, [["Christopher", 1200], ["Mike from work", 800], ["Alexandra", 1500],
|
||||
["Bob", 250], ["Dee", 600]]],
|
||||
["packed", 12, [["Christopher", 1200], ["Mike from work", 800], ["Alexandra", 1500],
|
||||
["Bob", 250], ["Dee", 600], ["Sam", 900], ["Priya", 1100], ["Jonathan", 480],
|
||||
["Al", 2000], ["Katarzyna", 340], ["Ted", 760], ["Wes", 1300]]],
|
||||
];
|
||||
|
||||
function seedTable([seats, players]) {
|
||||
const state = HoldemState.createDefault();
|
||||
HoldemState.resizeSeats(state, 9);
|
||||
[["Christopher", 1200], ["Mike from work", 800], ["Alexandra", 1500], ["Bob", 250], ["Dee", 600]]
|
||||
.forEach(([name, cash], i) => {
|
||||
HoldemState.resizeSeats(state, seats);
|
||||
players.forEach(([name, cash], i) => {
|
||||
state.seats[i].name = name;
|
||||
state.seats[i].inHand = true;
|
||||
state.seats[i].buyIns = cash;
|
||||
@@ -49,8 +62,9 @@ function seedTable() {
|
||||
for (let hand = 1; hand <= 4; hand++) {
|
||||
state.handHistory.push({
|
||||
handNumber: hand, timestamp: Date.now(), pot: 120,
|
||||
payouts: [{ id: state.seats[hand % 5].id, label: "P", amount: 120 }],
|
||||
ledger: state.seats.slice(0, 5).map(p => ({ id: p.id, name: p.name, in: 30, won: 0 })),
|
||||
payouts: [{ id: state.seats[hand % players.length].id, label: "P", amount: 120 }],
|
||||
ledger: state.seats.slice(0, players.length)
|
||||
.map(p => ({ id: p.id, name: p.name, in: 30, won: 0 })),
|
||||
});
|
||||
}
|
||||
state.handNumber = 5;
|
||||
@@ -85,11 +99,12 @@ async function main() {
|
||||
const hand = touch ? "touch" : "mouse";
|
||||
|
||||
for (const [width, height, rawLabel] of VIEWPORTS) {
|
||||
const label = `${rawLabel} · ${hand}`;
|
||||
await page.setViewportSize({ width, height });
|
||||
|
||||
for (const [shape, seats, players] of TABLES) {
|
||||
const label = `${rawLabel} · ${shape} · ${hand}`;
|
||||
await page.goto(`${BASE}/Casino/TexasHoldem`);
|
||||
await page.evaluate(seedTable);
|
||||
await page.evaluate(seedTable, [seats, players]);
|
||||
await page.goto(`${BASE}/Casino/TexasHoldem`);
|
||||
await page.click("#nextHandBtn");
|
||||
await page.waitForTimeout(300);
|
||||
@@ -101,6 +116,11 @@ async function main() {
|
||||
if (!table.clean) failures.push({ where: `table · ${label} ${width}x${height}`, offenders: table.failed.flatMap(f => f.offenders) });
|
||||
table.warnings.forEach(w => warnings.push(`${label}: ${w}`));
|
||||
|
||||
// The overlays don't change with the table's shape, so they are only
|
||||
// opened over the half-full one — twice the viewports is enough time
|
||||
// spent already.
|
||||
if (shape !== TABLES[0][0]) continue;
|
||||
|
||||
for (const [name, open] of OVERLAYS) {
|
||||
await page.evaluate(open);
|
||||
await page.waitForTimeout(180);
|
||||
@@ -114,6 +134,7 @@ async function main() {
|
||||
if (result.offenders.length) failures.push({ where: `${name} · ${label} ${width}x${height}`, offenders: result.offenders });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [width, height, rawLabel] of VIEWPORTS.filter((_, i) => i % 4 === 0)) {
|
||||
const label = `${rawLabel} · ${hand}`;
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
// Whether two seats can end up wanting the same pixels.
|
||||
//
|
||||
// The felt used to decide this from one number: the straight-line distance between the
|
||||
// closest two *neighbouring* seats, judged against a fraction of a puck's width. Both
|
||||
// halves of that were wrong, and the running page showed it. Measured across nine
|
||||
// viewports and six table sizes:
|
||||
//
|
||||
// seventeen of fifty-four tables had one seat's name or stack painted under another
|
||||
// ten of them had the collision between two seats that are not neighbours
|
||||
//
|
||||
// The second number is the interesting one. On a phone held sideways the felt is 528px
|
||||
// wide and 87 tall, the ring is very nearly a straight line, and the seat that lands on
|
||||
// top of you is the one across the table — seats 1 and 7, overlapping by 68px, while
|
||||
// every neighbouring pair was fine and the check said the table was clean.
|
||||
//
|
||||
// The first is what a distance test can't fix even looking at every pair: distance has
|
||||
// one number in it, and a puck is half again as wide as it is tall. 84px between centres
|
||||
// is generous side by side and nowhere near enough on the diagonal.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { Render } = require("./load");
|
||||
|
||||
const CSS = fs.readFileSync(
|
||||
path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "css", "casino", "holdem.css"), "utf8");
|
||||
|
||||
describe("two boxes on a ring", () => {
|
||||
// A phone held sideways, once the action bar has taken its share: a felt far wider
|
||||
// than it is tall, on which the ring is very nearly a straight line.
|
||||
const felt = { width: 400, height: 70 };
|
||||
const flat = { rx: 44, ry: 42 };
|
||||
|
||||
test("a pair that is not neighbours still counts", () => {
|
||||
// Eight seats, a 48x60 puck. Every neighbouring pair is clear. Seats 1 and 5 are
|
||||
// on top of each other, and so are 2 and 4, and 6 and 8 — the seat that lands on
|
||||
// you here is the one across the table, because the rim doubles back at the ends
|
||||
// of a shape this flat and a long walk apart is no distance at all.
|
||||
assert.equal(Render.seatsCollide(8, felt, flat, 48, 60), true);
|
||||
|
||||
const neighbours = (count, w, h) => {
|
||||
const points = Render.ringPositions(count, flat.rx, flat.ry, felt.width / felt.height)
|
||||
.map(s => ({ x: (s.left / 100) * felt.width, y: (s.top / 100) * felt.height }));
|
||||
return points.some((point, i) => {
|
||||
const next = points[(i + 1) % points.length];
|
||||
return Math.abs(point.x - next.x) < w && Math.abs(point.y - next.y) < h;
|
||||
});
|
||||
};
|
||||
assert.equal(neighbours(8, 48 + Render.SEAT_CLEARANCE_PX, 60 + Render.SEAT_CLEARANCE_PX), false,
|
||||
"no neighbouring pair overlaps, which is exactly why this had to look at all of them");
|
||||
});
|
||||
|
||||
test("and the answer is the box, not a radius", () => {
|
||||
// Two seats 60px apart across and 35px down, with a 64x40 puck. The distance
|
||||
// between them is 69px, so the test this replaces — "are these two further apart
|
||||
// than a puck is wide?" — called it clear. The boxes overlap by 4px by 5px.
|
||||
const boxes = { width: 200, height: 100 };
|
||||
const twoSeats = (dx, dy) => {
|
||||
const original = Render.ringPositions;
|
||||
Render.ringPositions = () => [
|
||||
{ left: 50 - (dx / 2 / boxes.width) * 100, top: 50 - (dy / 2 / boxes.height) * 100 },
|
||||
{ left: 50 + (dx / 2 / boxes.width) * 100, top: 50 + (dy / 2 / boxes.height) * 100 },
|
||||
];
|
||||
try {
|
||||
return Render.seatsCollide(2, boxes, flat, 64, 40, 0);
|
||||
} finally {
|
||||
Render.ringPositions = original;
|
||||
}
|
||||
};
|
||||
assert.ok(Math.hypot(60, 35) > 64, "and a distance test would have passed it");
|
||||
assert.equal(twoSeats(60, 35), true, "60 across and 35 down, with a 64x40 puck");
|
||||
assert.equal(twoSeats(60, 45), false, "far enough apart vertically");
|
||||
assert.equal(twoSeats(66, 10), false, "far enough apart horizontally");
|
||||
});
|
||||
|
||||
test("with daylight, so rounded corners don't read as touching", () => {
|
||||
assert.ok(Render.SEAT_CLEARANCE_PX > 0);
|
||||
});
|
||||
|
||||
test("a table nobody is sitting at can't collide with itself", () => {
|
||||
assert.equal(Render.seatsCollide(1, felt, flat, 96, 60), false);
|
||||
assert.equal(Render.seatsCollide(9, null, flat, 96, 60), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the widths the felt may choose from", () => {
|
||||
test("run widest first, because the widest that fits is the one wanted", () => {
|
||||
const widths = Render.seatWidthCandidates();
|
||||
widths.slice(1).forEach((w, i) => assert.ok(w < widths[i], `${widths[i]} then ${w}`));
|
||||
});
|
||||
|
||||
test("and span the whole declared range", () => {
|
||||
const widths = Render.seatWidthCandidates();
|
||||
assert.equal(widths[0], Render.SEAT_WIDTH_MAX);
|
||||
assert.equal(widths[widths.length - 1], Render.SEAT_WIDTH_MIN);
|
||||
});
|
||||
|
||||
test("the floor is still a name, not a smudge", () => {
|
||||
// At 44px the tight layout shows about five characters and an ellipsis. Below
|
||||
// that a puck stops telling the dealer who is sitting there, which is the one
|
||||
// thing on the felt it exists to say.
|
||||
assert.ok(Render.SEAT_WIDTH_MIN >= 44);
|
||||
assert.ok(Render.SEAT_TIGHT_BELOW_PX > Render.SEAT_WIDTH_MIN);
|
||||
assert.ok(Render.SEAT_TIGHT_BELOW_PX < Render.SEAT_WIDTH_MAX);
|
||||
});
|
||||
|
||||
test("and there are few enough of them to measure after every tap", () => {
|
||||
// Each one costs a forced layout. Ten is the budget; the cache carries the rest.
|
||||
assert.ok(Render.seatWidthCandidates().length <= 12);
|
||||
});
|
||||
});
|
||||
|
||||
describe("what the stylesheet has to keep holding up", () => {
|
||||
test("the puck's width comes from the property, not from a breakpoint", () => {
|
||||
// Three media queries used to declare it — 78px, 64px and 62px — three guesses
|
||||
// at a number that depends on how many people are sitting down, which no media
|
||||
// query can see. The same 390px phone has room for 90px at four seats and 61 at
|
||||
// nine.
|
||||
const declarations = CSS.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
const seatRules = [...declarations.matchAll(/(^|[,}])\s*(\.seat)\s*\{([^}]*)\}/g)]
|
||||
.map(m => m[3]);
|
||||
const hardWidths = seatRules
|
||||
.flatMap(body => body.match(/(?<![\w-])width:\s*[^;]+/g) || [])
|
||||
.filter(decl => !decl.includes("var(--seatPuckW"));
|
||||
assert.deepEqual(hardWidths, []);
|
||||
});
|
||||
|
||||
test("the puck measures what it says it measures", () => {
|
||||
// As content-box a 64px puck came out 74 across once its padding and border were
|
||||
// counted, and the search compares that number against distances on the felt.
|
||||
const rule = CSS.match(/\n\.seat \{[^}]*\}/);
|
||||
assert.ok(rule, ".seat rule not found");
|
||||
assert.match(rule[0], /box-sizing:\s*border-box/);
|
||||
});
|
||||
|
||||
test("and a blind badge never reaches sideways off its own puck", () => {
|
||||
// This is what makes the footprint the puck and nothing more. The badge's
|
||||
// remaining overhang is vertical, and it points away from the table at both ends
|
||||
// — `top` on the upper arc, `bottom` on the lower — so two seats facing each
|
||||
// other across the felt wear their badges back to back.
|
||||
//
|
||||
// It was not always so: at `left: -6px` the small blind hung off the puck's left
|
||||
// corner, and on an iPhone Pro Max at six seats — pucks not touching at all —
|
||||
// "SB" was painted across the next player's $1,137.
|
||||
const badges = [...CSS.matchAll(/\.(sb|bb|str)Badge\s*\{([^}]*)\}/g)];
|
||||
assert.equal(badges.length, 3, "expected all three blind badges");
|
||||
badges.forEach(([, which, body]) => {
|
||||
const left = body.match(/(?<![\w-])left:\s*(-?[\d.]+)px/);
|
||||
assert.ok(left, `.${which}Badge should place itself horizontally`);
|
||||
assert.ok(Number(left[1]) >= 0,
|
||||
`.${which}Badge sits at ${left[1]}px, off the left of its own puck`);
|
||||
});
|
||||
});
|
||||
|
||||
test("and all three sit in the same place, so there is one spot to look at", () => {
|
||||
// Two badges in two positions means the dealer's eye has to find which of them
|
||||
// this seat is using before it can read which blind it is, forty times a night.
|
||||
// A seat only ever wears one: the blinds are different players by construction,
|
||||
// and the straddle chain starts left of the big blind and stops short of the
|
||||
// small one.
|
||||
const lefts = [...CSS.matchAll(/\.(sb|bb|str)Badge\s*\{([^}]*)\}/g)]
|
||||
.map(([, , body]) => (body.match(/(?<![\w-])left:\s*(-?[\d.]+px)/) || [])[1]);
|
||||
assert.equal(new Set(lefts).size, 1, `badges sit at ${lefts.join(", ")}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with no browser to measure", () => {
|
||||
test("nothing here throws", () => {
|
||||
assert.equal(Render.measuredSeatBox(), null);
|
||||
assert.equal(Render.seatFootprint(null), null);
|
||||
assert.deepEqual(Render.seatFootprint({ width: 64, height: 40 }), { width: 64, height: 40 });
|
||||
});
|
||||
});
|
||||
@@ -777,6 +777,77 @@ const LayoutAudit = {
|
||||
return { name: "seat names aren't painted under their own badges", offenders };
|
||||
},
|
||||
|
||||
// A seat's writing, covered by something belonging to a different seat.
|
||||
//
|
||||
// seatLabelsClear() above asks whether a puck's own decoration sits on its own name.
|
||||
// This asks the other half, and the other half was where the tool was actually
|
||||
// failing: measured across nine viewports and six table sizes before this went in,
|
||||
// seventeen of fifty-four tables had one seat's name or stack painted under a
|
||||
// neighbour. On a Galaxy S8 at the most ordinary table there is — six players at a
|
||||
// nine-handed table — seat 2's stack was under seat 3's puck, and five pairs of
|
||||
// seats overlapped, the worst by 61px.
|
||||
//
|
||||
// Two ways in, and it takes both:
|
||||
// - the pucks themselves overlapping, which the layout used to permit on the
|
||||
// reasoning that rounded corners forgive a little; what they were forgiving was
|
||||
// a four-figure stack.
|
||||
// - a blind badge hanging off its puck onto the seat next door, which no amount
|
||||
// of space between the pucks fixes on its own.
|
||||
//
|
||||
// Hit-tested against the glyph run rather than the element box. A name is centred,
|
||||
// so its box is wider than its ink and testing the box would fail on empty air.
|
||||
//
|
||||
// The glyph run is then clipped back to what is actually painted, which is not the
|
||||
// same thing and cost this check its first honest-looking failure. A puck's name is
|
||||
// a `-webkit-box` with `line-clamp`, and getClientRects() hands back a rect for every
|
||||
// line the text *would* have taken — including the two that the clamp throws away.
|
||||
// Measured at 640x360 with twelve players: "Mike from work" reported a line 30px
|
||||
// below its own puck, which duly hit-tested onto the seat underneath, and the seats
|
||||
// were not touching at all. Nothing is painted there, so nothing can cover it.
|
||||
seatsKeepTheirText() {
|
||||
const name = "no seat covers another seat's writing";
|
||||
if (this.modalScope()) {
|
||||
return { name, offenders: [], skipped: "a sheet is open over the table" };
|
||||
}
|
||||
const seats = [...document.querySelectorAll(".seat")];
|
||||
if (!seats.length) return { name, offenders: [], skipped: "no seats on this page" };
|
||||
|
||||
const offenders = [];
|
||||
seats.forEach((seat, index) => {
|
||||
seat.querySelectorAll(".seatPuckName, .seatPuckStack, .seatPuckTotal").forEach(label => {
|
||||
const text = label.textContent.trim();
|
||||
if (!text) return;
|
||||
const painted = label.getBoundingClientRect();
|
||||
if (!painted.width || !painted.height) return;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(label);
|
||||
[...range.getClientRects()].forEach(raw => {
|
||||
const line = {
|
||||
left: Math.max(raw.left, painted.left),
|
||||
right: Math.min(raw.right, painted.right),
|
||||
top: Math.max(raw.top, painted.top),
|
||||
bottom: Math.min(raw.bottom, painted.bottom),
|
||||
};
|
||||
if (line.right - line.left <= 1 || line.bottom - line.top <= 1) return;
|
||||
// Along the line rather than at its middle: a badge takes the start
|
||||
// of it and a neighbouring puck the end, and a single centre probe
|
||||
// walks past both.
|
||||
for (let across = 0.08; across <= 0.92; across += 0.14) {
|
||||
const x = line.left + (line.right - line.left) * across;
|
||||
const y = (line.top + line.bottom) / 2;
|
||||
const over = document.elementFromPoint(x, y);
|
||||
if (!over || seat.contains(over) || over.contains(seat)) continue;
|
||||
offenders.push(
|
||||
`seat ${index + 1} "${text}" is under ${this.describe(over)}`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return { name, offenders };
|
||||
},
|
||||
|
||||
// ---- Runner ----
|
||||
|
||||
// Every check this file knows how to make, every time.
|
||||
@@ -897,6 +968,7 @@ const LayoutAudit = {
|
||||
this.oneDealerButton(),
|
||||
this.hiddenButPainted(),
|
||||
this.seatLabelsClear(),
|
||||
this.seatsKeepTheirText(),
|
||||
this.contrast(options.contrast),
|
||||
this.touchTargets(options.minimumTarget, options.touchTargets),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user