Give a phone on its side the table shape it needs #105
@@ -848,8 +848,13 @@ button,
|
|||||||
font-size: 0.55rem;
|
font-size: 0.55rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The hole cards give up their line here. A puck this close to its neighbours is 64px
|
||||||
|
tall and the seats at the ends of a landscape felt are 55px apart, so the line has to
|
||||||
|
come from somewhere — and of the four things on a puck, the cards are the one the
|
||||||
|
dealer is holding in their other hand. Name, bet and stack stay. The seat editor
|
||||||
|
still shows the cards in full. */
|
||||||
.seatsTight .seatPuckCards {
|
.seatsTight .seatPuckCards {
|
||||||
font-size: 0.58rem;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Seat editor panel (sidebar form for the selected seat) ---------- */
|
/* ---------- Seat editor panel (sidebar form for the selected seat) ---------- */
|
||||||
@@ -3038,10 +3043,18 @@ body.hasActionBar #toastHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Keeps every puck inside the viewport: the ring is drawn within this box,
|
/* Keeps every puck inside the viewport: the ring is drawn within this box,
|
||||||
so half a puck of padding is exactly enough to stop the overhang. */
|
so half a puck of padding is exactly enough to stop the overhang.
|
||||||
|
|
||||||
|
Half a puck is the right answer when the felt is tall enough to spare it. On a
|
||||||
|
phone held sideways it isn't: the felt is 96px tall there once the action bar has
|
||||||
|
grown, and 26px off the top and bottom left the ring 44px of height to work with
|
||||||
|
— less than half the table. That is what kept the seats at the ends stacked no
|
||||||
|
matter what shape the rim was.
|
||||||
|
|
||||||
|
Above about 216px of felt the flat 26px still wins, so nothing upright changes. */
|
||||||
#seatsLayer,
|
#seatsLayer,
|
||||||
#betsLayer {
|
#betsLayer {
|
||||||
inset: 26px 44px;
|
inset: min(26px, 12%) 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Narrower than the desktop puck: at a full ten-handed table the ring is
|
/* Narrower than the desktop puck: at a full ten-handed table the ring is
|
||||||
@@ -3410,6 +3423,16 @@ body.hasActionBar #toastHost {
|
|||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The same inset the phone layout uses, which this branch never had because it is
|
||||||
|
matched on height rather than width — so an 844px-wide phone on its side drew its
|
||||||
|
ring against the felt's full box. With the rim a stadium, the top rail is flat,
|
||||||
|
which put the button seat's dealer chip over the Resolve Pot button in the header
|
||||||
|
above. Percentages of a short felt, so the ring keeps the height it needs. */
|
||||||
|
#seatsLayer,
|
||||||
|
#betsLayer {
|
||||||
|
inset: min(26px, 12%) 44px;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Short phones, upright ----------
|
/* ---------- Short phones, upright ----------
|
||||||
|
|||||||
@@ -117,8 +117,11 @@ const Render = {
|
|||||||
// anything visible — it is just the honest way to say "on that player's spoke",
|
// anything visible — it is just the honest way to say "on that player's spoke",
|
||||||
// and it saves the second walk.
|
// and it saves the second walk.
|
||||||
betPosition(index, count, aspect = this.feltAspect()) {
|
betPosition(index, count, aspect = this.feltAspect()) {
|
||||||
const angle = this.ringAngles(count, 44, 42, aspect)[index];
|
const seat = this.seatPosition(index, count, aspect);
|
||||||
return { left: 50 + 27 * Math.cos(angle), top: 50 + 25 * Math.sin(angle) };
|
return {
|
||||||
|
left: 50 + (seat.left - 50) * (27 / 44),
|
||||||
|
top: 50 + (seat.top - 50) * (25 / 42),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// Seats spaced evenly around the rim — by the distance between them, not by the
|
// Seats spaced evenly around the rim — by the distance between them, not by the
|
||||||
@@ -136,11 +139,71 @@ const Render = {
|
|||||||
// an ellipse's has no closed form — 720 steps is far more than enough for a rim a
|
// an ellipse's has no closed form — 720 steps is far more than enough for a rim a
|
||||||
// few hundred pixels long, and it runs once per render.
|
// few hundred pixels long, and it runs once per render.
|
||||||
positionOnRing(index, count, rx, ry, aspect = 1) {
|
positionOnRing(index, count, rx, ry, aspect = 1) {
|
||||||
const target = this.ringAngles(count, rx, ry, aspect)[index];
|
return this.ringPositions(count, rx, ry, aspect)[index];
|
||||||
return {
|
},
|
||||||
left: 50 + rx * Math.cos(target),
|
|
||||||
top: 50 + ry * Math.sin(target),
|
// Past this much wider than tall, the rim stops being an ellipse and becomes a
|
||||||
};
|
// stadium — two straight rails with a semicircular cap at each end.
|
||||||
|
//
|
||||||
|
// An ellipse that flat has nowhere to put the seats near its ends. At 640x360 with
|
||||||
|
// the bar grown by a side pot the felt is 616x96, and six seats came out with two
|
||||||
|
// of them stacked 25px apart at each end, pucks 64px tall. No puck size fixes 25px:
|
||||||
|
// the shape is wrong. A stadium's cap is a half-circle of the felt's own height, so
|
||||||
|
// an end holds one seat and the rest of the table is rail — which is how a real
|
||||||
|
// poker table is built, and why it is shaped that way.
|
||||||
|
//
|
||||||
|
// Only where it is needed. Anything rounder than this keeps the ellipse it has
|
||||||
|
// always had: measured, the cut-off lands between a landscape phone (4.4 and up)
|
||||||
|
// and every other layout the tool has (1.45 at 1280x900, 1.0 to 1.4 on a phone
|
||||||
|
// upright), so no currently-working table changes shape.
|
||||||
|
STADIUM_RATIO: 2.5,
|
||||||
|
|
||||||
|
// Seats spaced evenly around whichever rim this table has, as percentages.
|
||||||
|
ringPositions(count, rx, ry, aspect = 1) {
|
||||||
|
if (count <= 0) return [];
|
||||||
|
const halfWidth = rx * aspect;
|
||||||
|
const halfHeight = ry;
|
||||||
|
if (halfWidth <= halfHeight * this.STADIUM_RATIO) {
|
||||||
|
return this.ringAngles(count, rx, ry, aspect).map(angle => ({
|
||||||
|
left: 50 + rx * Math.cos(angle),
|
||||||
|
top: 50 + ry * Math.sin(angle),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return this.stadiumPositions(count, halfWidth, halfHeight, aspect);
|
||||||
|
},
|
||||||
|
|
||||||
|
// The same walk, around a stadium instead. Its perimeter has a closed form, so
|
||||||
|
// this needs no sampling: two rails of 2(A-B) and two caps of piB.
|
||||||
|
stadiumPositions(count, halfWidth, halfHeight, aspect) {
|
||||||
|
const rail = halfWidth - halfHeight;
|
||||||
|
const cap = Math.PI * halfHeight;
|
||||||
|
const perimeter = 4 * rail + 2 * cap;
|
||||||
|
|
||||||
|
return Array.from({ length: count }, (_, seat) => {
|
||||||
|
// Clockwise from the top centre, so seat one sits where it always has.
|
||||||
|
let walked = (perimeter * seat) / count;
|
||||||
|
let x;
|
||||||
|
let y;
|
||||||
|
if (walked <= rail) { // top rail, going right
|
||||||
|
x = walked;
|
||||||
|
y = -halfHeight;
|
||||||
|
} else if (walked <= rail + cap) { // right cap
|
||||||
|
const turned = (walked - rail) / halfHeight;
|
||||||
|
x = rail + halfHeight * Math.sin(turned);
|
||||||
|
y = -halfHeight * Math.cos(turned);
|
||||||
|
} else if (walked <= 3 * rail + cap) { // bottom rail, going left
|
||||||
|
x = rail - (walked - rail - cap);
|
||||||
|
y = halfHeight;
|
||||||
|
} else if (walked <= 3 * rail + 2 * cap) { // left cap
|
||||||
|
const turned = (walked - 3 * rail - cap) / halfHeight;
|
||||||
|
x = -rail - halfHeight * Math.sin(turned);
|
||||||
|
y = halfHeight * Math.cos(turned);
|
||||||
|
} else { // top rail, back to the start
|
||||||
|
x = -rail + (walked - 3 * rail - 2 * cap);
|
||||||
|
y = -halfHeight;
|
||||||
|
}
|
||||||
|
return { left: 50 + x / aspect, top: 50 + y };
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
RING_STEPS: 720,
|
RING_STEPS: 720,
|
||||||
@@ -156,9 +219,9 @@ const Render = {
|
|||||||
closestSeatGapPx(count, box = this.ringBox()) {
|
closestSeatGapPx(count, box = this.ringBox()) {
|
||||||
if (count < 2 || !box || !box.width || !box.height) return Infinity;
|
if (count < 2 || !box || !box.width || !box.height) return Infinity;
|
||||||
|
|
||||||
const points = this.ringAngles(count, 44, 42, box.width / box.height).map(angle => ({
|
const points = this.ringPositions(count, 44, 42, box.width / box.height).map(seat => ({
|
||||||
x: ((50 + 44 * Math.cos(angle)) / 100) * box.width,
|
x: (seat.left / 100) * box.width,
|
||||||
y: ((50 + 42 * Math.sin(angle)) / 100) * box.height,
|
y: (seat.top / 100) * box.height,
|
||||||
}));
|
}));
|
||||||
return points.reduce((closest, point, i) => {
|
return points.reduce((closest, point, i) => {
|
||||||
const next = points[(i + 1) % points.length];
|
const next = points[(i + 1) % points.length];
|
||||||
|
|||||||
@@ -99,10 +99,27 @@ describe("how close the closest two seats come", () => {
|
|||||||
// apart end up stacked one above the other.
|
// apart end up stacked one above the other.
|
||||||
const gap = (count, width, height) => Render.closestSeatGapPx(count, { width, height });
|
const gap = (count, width, height) => Render.closestSeatGapPx(count, { width, height });
|
||||||
|
|
||||||
test("a flat table's ends are far closer than its rim suggests", () => {
|
test("a flat table no longer stacks two seats at each end", () => {
|
||||||
// 616x96: the landscape felt on a 640x360 phone once the bar has grown.
|
// 616x96: the landscape felt on a 640x360 phone once the bar has grown. On an
|
||||||
const rim = Math.PI * (3 * (271 + 40) - Math.sqrt((3 * 271 + 40) * (271 + 3 * 40))) / 6;
|
// ellipse that flat the end pair came out 25px apart with 64px pucks. The rim
|
||||||
assert.ok(gap(6, 616, 96) < rim / 3, `${gap(6, 616, 96).toFixed(0)}px apart, ${rim.toFixed(0)}px of rim each`);
|
// is a stadium at this shape, so an end holds one seat.
|
||||||
|
assert.ok(gap(6, 616, 96) > 70, `${gap(6, 616, 96).toFixed(0)}px apart`);
|
||||||
|
assert.ok(gap(12, 616, 96) > 40, `${gap(12, 616, 96).toFixed(0)}px apart at twelve seats`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an ellipse's ends are still closer than its rim suggests, which is why the shape changes", () => {
|
||||||
|
// Same felt, forced through the ellipse the tool uses for rounder tables.
|
||||||
|
const asEllipse = Render.ringAngles(6, 44, 42, 616 / 96).map(angle => ({
|
||||||
|
x: ((50 + 44 * Math.cos(angle)) / 100) * 616,
|
||||||
|
y: ((50 + 42 * Math.sin(angle)) / 100) * 96,
|
||||||
|
}));
|
||||||
|
const closest = asEllipse.reduce((least, point, i) => {
|
||||||
|
const next = asEllipse[(i + 1) % asEllipse.length];
|
||||||
|
return Math.min(least, Math.hypot(point.x - next.x, point.y - next.y));
|
||||||
|
}, Infinity);
|
||||||
|
assert.ok(closest < 70, `${closest.toFixed(0)}px`);
|
||||||
|
assert.ok(gap(6, 616, 96) > closest * 1.3,
|
||||||
|
`stadium ${gap(6, 616, 96).toFixed(0)}px against ellipse ${closest.toFixed(0)}px`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a round table's neighbours are about as far apart as the rim says", () => {
|
test("a round table's neighbours are about as far apart as the rim says", () => {
|
||||||
@@ -122,6 +139,53 @@ describe("how close the closest two seats come", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("which rim a table gets", () => {
|
||||||
|
// An ellipse until the table is much wider than tall, then a stadium: two rails
|
||||||
|
// and a half-round cap at each end, which is how a real poker table is built.
|
||||||
|
const shape = (width, height) => {
|
||||||
|
const aspect = width / height;
|
||||||
|
return 44 * aspect > 42 * Render.STADIUM_RATIO ? "stadium" : "ellipse";
|
||||||
|
};
|
||||||
|
|
||||||
|
test("every layout the tool had keeps the ellipse", () => {
|
||||||
|
assert.equal(shape(844, 608), "ellipse", "desktop");
|
||||||
|
assert.equal(shape(600, 600), "ellipse", "tablet");
|
||||||
|
assert.equal(shape(278, 314), "ellipse", "phone upright");
|
||||||
|
assert.equal(shape(336, 240), "ellipse", "short phone upright");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a phone on its side gets the stadium", () => {
|
||||||
|
assert.equal(shape(528, 73), "stadium");
|
||||||
|
assert.equal(shape(780, 113), "stadium");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the cap holds one seat instead of stacking two", () => {
|
||||||
|
// Six seats on a 616x96 rim: the walk between neighbours has to be longer than
|
||||||
|
// the cap, or two of them end up on the same half-circle.
|
||||||
|
const halfWidth = 44 * (616 / 96), halfHeight = 42;
|
||||||
|
const perimeter = 4 * (halfWidth - halfHeight) + 2 * Math.PI * halfHeight;
|
||||||
|
assert.ok(perimeter / 6 > Math.PI * halfHeight, "one seat per cap at six-handed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("seat one is still at the top centre, and the ring still mirrors", () => {
|
||||||
|
const seats = Array.from({ length: 8 }, (_, i) => Render.positionOnRing(i, 8, 44, 42, 616 / 96));
|
||||||
|
assert.ok(Math.abs(seats[0].left - 50) < 0.01);
|
||||||
|
assert.ok(seats[0].top < 50);
|
||||||
|
for (let seat = 1; seat < 4; seat++) {
|
||||||
|
const mirrored = seats[8 - seat];
|
||||||
|
assert.ok(Math.abs((seats[seat].left - 50) + (mirrored.left - 50)) < 0.01, `seat ${seat} across`);
|
||||||
|
assert.ok(Math.abs(seats[seat].top - mirrored.top) < 0.01, `seat ${seat} down`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nobody is drawn off the felt", () => {
|
||||||
|
Array.from({ length: 12 }, (_, i) => Render.positionOnRing(i, 12, 44, 42, 616 / 96)).forEach((seat, i) => {
|
||||||
|
assert.ok(seat.left >= 5 && seat.left <= 95, `seat ${i + 1} across at ${seat.left.toFixed(1)}%`);
|
||||||
|
assert.ok(seat.top >= 7 && seat.top <= 93, `seat ${i + 1} down at ${seat.top.toFixed(1)}%`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("the shape of the ring", () => {
|
describe("the shape of the ring", () => {
|
||||||
test("is a mirror image left to right", () => {
|
test("is a mirror image left to right", () => {
|
||||||
const seats = ringPixels(10, 601, 126);
|
const seats = ringPixels(10, 601, 126);
|
||||||
|
|||||||
Reference in New Issue
Block a user