atris 3.74.14 → 3.74.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/gm-phone-page.js +84 -29
- package/package.json +1 -1
package/lib/gm-phone-page.js
CHANGED
|
@@ -90,6 +90,8 @@ button:active:not(:disabled):not(.muted) { opacity: 0.7; }
|
|
|
90
90
|
:focus-visible { outline: 2px solid var(--amber); outline-offset: 4px; }
|
|
91
91
|
/* a link's ring hugs its own tall tap box, round, so it never touches the button above */
|
|
92
92
|
.links button:focus-visible, .quiet:focus-visible, .bet td button:focus-visible { outline-offset: 0; border-radius: 12px; }
|
|
93
|
+
/* a new card holds the focus itself (landFocus): its ring is round like the page's boxes and clear of the small orb above */
|
|
94
|
+
.bet:focus-visible { outline-offset: 6px; border-radius: 14px; }
|
|
93
95
|
[hidden] { display: none !important; }
|
|
94
96
|
/* dark until the page's script has run: a slow link never shows the words without the dots or the card */
|
|
95
97
|
body:not(.on) .screen, body:not(.on) nav { visibility: hidden; }
|
|
@@ -104,9 +106,21 @@ body:not(.on) .screen, body:not(.on) nav { visibility: hidden; }
|
|
|
104
106
|
padding: max(24px, env(safe-area-inset-top)) max(24px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left));
|
|
105
107
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
106
108
|
}
|
|
109
|
+
/* the big orb is 72% of the screen's short side, at most 520px. On a short
|
|
110
|
+
screen (a phone on its side) it is as tall as the room between the top
|
|
111
|
+
margin (--top) and the footer with 8px above it (--foot), once its line
|
|
112
|
+
has its two lines and the 4px above them (55px) */
|
|
107
113
|
.stage { position: relative; width: min(72vmin, 520px); aspect-ratio: 1 / 1; }
|
|
108
|
-
|
|
109
|
-
.
|
|
114
|
+
@supports (height: 100svh) {
|
|
115
|
+
.screen {
|
|
116
|
+
--top: max(24px, env(safe-area-inset-top));
|
|
117
|
+
--foot: calc(max(20px, env(safe-area-inset-bottom)) + 44px + 8px);
|
|
118
|
+
--orb: min(72vmin, 520px, 100svh - var(--top) - var(--foot) - 55px);
|
|
119
|
+
/* the orb and its line stay centred, lifted only as far as the line needs to end above the footer */
|
|
120
|
+
padding-bottom: max(24px, env(safe-area-inset-bottom), var(--top) + var(--orb) + 55px + 2 * var(--foot) - 100svh);
|
|
121
|
+
}
|
|
122
|
+
.stage { width: var(--orb); }
|
|
123
|
+
}
|
|
110
124
|
.stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 50%; touch-action: none; cursor: pointer; }
|
|
111
125
|
.stage canvas:focus-visible { outline-offset: -8%; }
|
|
112
126
|
.line {
|
|
@@ -147,14 +161,21 @@ body.wide .screen { max-width: 64rem; }
|
|
|
147
161
|
/* the team's member header is held with its names, so every header stays over its column */
|
|
148
162
|
.bet table.k-team th.member { position: sticky; left: 0; z-index: 2; background: var(--bg); }
|
|
149
163
|
.bet td.member button { font-size: 18px; font-weight: 700; text-align: left; min-height: 32px; }
|
|
150
|
-
/* a long name never takes over a phone: the held column stops at about fifteen letters and the name breaks at its hyphens
|
|
151
|
-
|
|
164
|
+
/* a long name never takes over a phone: the held column stops at about fifteen letters and the name breaks at its hyphens;
|
|
165
|
+
on the narrowest phones it stops sooner, so the status beside it keeps about 7rem */
|
|
166
|
+
.bet table.k-team td.member button { width: max-content; max-width: min(8.5em, 100vw - 195px); white-space: normal; overflow-wrap: break-word; }
|
|
152
167
|
.bet td.role { min-width: 9rem; color: var(--text-2); }
|
|
153
168
|
.bet td.ovr { white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; }
|
|
154
169
|
.bet th.ovr { text-align: right; }
|
|
155
170
|
.bet td.engine, .bet td.done { white-space: nowrap; }
|
|
156
171
|
.bet td.engine button { text-align: left; text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 0.25em; min-height: 32px; }
|
|
157
172
|
.bet td.status { min-width: 16rem; color: var(--text-2); }
|
|
173
|
+
/* the team's status is its last column: swiped all the way, it fills the room
|
|
174
|
+
beside the held names (--room, measured) and no more, so every line starts
|
|
175
|
+
beside the names, never under them; with room to spare it keeps 16rem, and
|
|
176
|
+
the table is only as wide as its columns, so no spare width widens it */
|
|
177
|
+
.bet table.k-team { min-width: 0; }
|
|
178
|
+
.bet table.k-team td.status { min-width: min(16rem, var(--room, 16rem)); overflow-wrap: anywhere; }
|
|
158
179
|
.bet td.status.working { color: var(--text); }
|
|
159
180
|
.bet td.status.waiting-for-your-yes, .bet td.status.needs-you { color: var(--amber); }
|
|
160
181
|
.bet td.work { min-width: 14rem; }
|
|
@@ -498,8 +519,8 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
498
519
|
var CARRY = 0.82;
|
|
499
520
|
// what the dots did on the last frame, kept on the canvas so the motion can be
|
|
500
521
|
// measured, and ms: how much of the dots' own clock that frame was, which is
|
|
501
|
-
// at most 50ms however long the frame took
|
|
502
|
-
var stats = { frame: 0, mode: 'idle', step: 0, mean: 0, ms: 0 };
|
|
522
|
+
// at most 50ms however long the frame took; dots: how many the sphere has
|
|
523
|
+
var stats = { frame: 0, mode: 'idle', step: 0, mean: 0, ms: 0, dots: 0 };
|
|
503
524
|
canvas.__orb = stats;
|
|
504
525
|
// Two speeds, chosen by truth. A bet already waiting in the queue plays
|
|
505
526
|
// the same curves at FAST of their length (tap to card in about two seconds, unhurried).
|
|
@@ -510,7 +531,17 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
510
531
|
var bridgeAt = 0;
|
|
511
532
|
var REST = 56;
|
|
512
533
|
var dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
513
|
-
|
|
534
|
+
// How many dots, by the size the sphere is drawn at. An upright phone's
|
|
535
|
+
// orb (230 to 320px across) draws 5500 and a laptop's (520px) 11000, as
|
|
536
|
+
// always. A smaller orb (a phone on its side) keeps the spacing of the
|
|
537
|
+
// narrowest phone's, and a bigger one the spacing of the biggest phone's
|
|
538
|
+
// until it reaches 11000, so the sphere is the same light stipple at any
|
|
539
|
+
// size and never a solid ball.
|
|
540
|
+
function dotsFor(size) {
|
|
541
|
+
if (size < 230) return Math.round(5500 * (size / 230) * (size / 230));
|
|
542
|
+
if (size <= 320) return 5500;
|
|
543
|
+
return Math.min(11000, Math.round(5500 * (size / 320) * (size / 320)));
|
|
544
|
+
}
|
|
514
545
|
var w = 0, h = 0, R = 0, cx = 0, cy = 0;
|
|
515
546
|
var dots = [];
|
|
516
547
|
// the dots are drawn by color and brightness, LEVELS of each, so a frame
|
|
@@ -546,27 +577,40 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
546
577
|
n.style.setProperty('--spin', v[0]); n.style.setProperty('--rot0', v[1]);
|
|
547
578
|
});
|
|
548
579
|
|
|
580
|
+
// the sphere's size on the page, and the dots that size holds
|
|
549
581
|
function measure() {
|
|
550
582
|
w = canvas.clientWidth; h = canvas.clientHeight;
|
|
551
583
|
canvas.width = w * dpr; canvas.height = h * dpr;
|
|
552
584
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
553
585
|
R = Math.min(w, h) * 0.42; cx = w / 2; cy = h / 2;
|
|
586
|
+
if (w && h) fit(dotsFor(Math.min(w, h)));
|
|
554
587
|
}
|
|
555
588
|
|
|
556
|
-
|
|
557
|
-
|
|
589
|
+
// Grow or trim the sphere to n dots: random points on it (normalised
|
|
590
|
+
// gaussians), so it reads as stipple, and any n of them still do. Every
|
|
591
|
+
// dot kept keeps its place. The first dots start at the centre, so the
|
|
592
|
+
// sphere grows out of the point; one added later starts on the sphere.
|
|
593
|
+
function fit(n) {
|
|
594
|
+
if (n === dots.length) return;
|
|
558
595
|
var gauss = function () { return Math.sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * Math.PI * Math.random()); };
|
|
559
|
-
dots =
|
|
560
|
-
|
|
561
|
-
|
|
596
|
+
var born = dots.length > 0, cosA = Math.cos(angle), sinA = Math.sin(angle);
|
|
597
|
+
if (n < dots.length) dots.length = n;
|
|
598
|
+
while (dots.length < n) {
|
|
562
599
|
var gx = gauss(), gy = gauss(), gz = gauss();
|
|
563
|
-
var
|
|
564
|
-
|
|
565
|
-
x: gx /
|
|
600
|
+
var len = Math.hypot(gx, gy, gz) || 1;
|
|
601
|
+
var d = {
|
|
602
|
+
x: gx / len, y: gy / len, z: gz / len, sx: cx, sy: cy, vx: 0, vy: 0, r: 0.4 + Math.random() * 0.7, bx: 0, by: 0, dl: 0, kx: 0, ky: 0,
|
|
566
603
|
// its own slow loop in the swarm
|
|
567
604
|
f1: 0.5 + Math.random() * 0.8, f2: 0.5 + Math.random() * 0.8, p1: Math.random() * 6.283, p2: Math.random() * 6.283,
|
|
568
|
-
}
|
|
605
|
+
};
|
|
606
|
+
if (born) { d.sx = cx + (d.x * cosA + d.z * sinA) * R; d.sy = cy + d.y * R; }
|
|
607
|
+
// mid spiral it goes in with the rest, from where it starts
|
|
608
|
+
d.bx = d.sx; d.by = d.sy;
|
|
609
|
+
d.dl = (1 - Math.min(1, Math.hypot(d.sx - cx, d.sy - cy) / (R || 1))) * 0.28 + Math.random() * 0.05;
|
|
610
|
+
dots.push(d);
|
|
569
611
|
}
|
|
612
|
+
px = new Float32Array(n); py = new Float32Array(n); pr = new Float32Array(n); pb = new Int16Array(n); order = new Int32Array(n);
|
|
613
|
+
stats.dots = n;
|
|
570
614
|
}
|
|
571
615
|
|
|
572
616
|
// StippleOrb startBoom: each dot remembers where it starts, and the outer
|
|
@@ -909,7 +953,6 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
909
953
|
}
|
|
910
954
|
|
|
911
955
|
measure();
|
|
912
|
-
seed();
|
|
913
956
|
|
|
914
957
|
// StippleOrb's pointer, ported with trackWindow on, as LetterOrb uses it.
|
|
915
958
|
// Over the sphere the dots part around the pointer and spring back; the
|
|
@@ -950,11 +993,12 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
950
993
|
var ratio = Math.min(window.devicePixelRatio || 1, 2);
|
|
951
994
|
// a phone's bars sliding in and out send resizes too; only a real change redraws
|
|
952
995
|
if (!canvas.clientWidth || (canvas.clientWidth === w && canvas.clientHeight === h && ratio === dpr)) return;
|
|
953
|
-
var was = { cx: cx, cy: cy, R: R };
|
|
996
|
+
var was = { cx: cx, cy: cy, R: R }, kept = dots.length;
|
|
954
997
|
dpr = ratio;
|
|
955
998
|
measure();
|
|
956
999
|
var k = was.R ? R / was.R : 1;
|
|
957
|
-
|
|
1000
|
+
// the dots it had move with the sphere; any it gained already sit on it
|
|
1001
|
+
for (var i = 0; i < Math.min(kept, dots.length); i++) {
|
|
958
1002
|
var d = dots[i];
|
|
959
1003
|
d.sx = cx + (d.sx - was.cx) * k; d.sy = cy + (d.sy - was.cy) * k;
|
|
960
1004
|
d.bx = cx + (d.bx - was.cx) * k; d.by = cy + (d.by - was.cy) * k;
|
|
@@ -1141,9 +1185,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
1141
1185
|
var inCells = card.options.filter(function (o) { return o.cell; });
|
|
1142
1186
|
if (card.table) {
|
|
1143
1187
|
var grid = b.appendChild(drawTable(card, inCells));
|
|
1144
|
-
|
|
1145
|
-
var held = grid.querySelector('tbody td.member');
|
|
1146
|
-
if (held && getComputedStyle(held).position === 'sticky') grid.style.scrollPaddingLeft = held.offsetWidth + 'px';
|
|
1188
|
+
holdNames(grid);
|
|
1147
1189
|
moreRight(grid);
|
|
1148
1190
|
grid.addEventListener('scroll', function () { moreRight(grid); }, { passive: true });
|
|
1149
1191
|
}
|
|
@@ -1203,7 +1245,16 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
1203
1245
|
|
|
1204
1246
|
// the fade on a table's right edge, while some of it is still off to the right
|
|
1205
1247
|
function moreRight(grid) { grid.classList.toggle('more-right', grid.scrollLeft + grid.clientWidth < grid.scrollWidth - 1); }
|
|
1206
|
-
|
|
1248
|
+
// whatever scrolls into view in the table lands beside the held names, never
|
|
1249
|
+
// under them, and the room beside them is kept for the last column's width
|
|
1250
|
+
function holdNames(grid) {
|
|
1251
|
+
var held = grid.querySelector('tbody td.member');
|
|
1252
|
+
if (!held || getComputedStyle(held).position !== 'sticky' || !grid.clientWidth) return;
|
|
1253
|
+
grid.style.scrollPaddingLeft = held.offsetWidth + 'px';
|
|
1254
|
+
grid.style.setProperty('--room', (grid.clientWidth - held.offsetWidth) + 'px');
|
|
1255
|
+
}
|
|
1256
|
+
// a turned phone: the room beside the names changes with the screen
|
|
1257
|
+
window.addEventListener('resize', function () { var g = box().querySelector('.grid'); if (g) { holdNames(g); moreRight(g); } });
|
|
1207
1258
|
// A row of small links that wraps never ends or starts a line with its dot:
|
|
1208
1259
|
// a dot whose two links sit on different lines becomes the line break.
|
|
1209
1260
|
function breakLinks(root) {
|
|
@@ -2291,17 +2342,21 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
2291
2342
|
}, true);
|
|
2292
2343
|
|
|
2293
2344
|
// Where the focus goes. A new card: when the focus left with the old card
|
|
2294
|
-
// (or sits on the orb), it goes to the
|
|
2295
|
-
//
|
|
2296
|
-
//
|
|
2345
|
+
// (or sits on the orb), it goes to the card itself, never to one of its
|
|
2346
|
+
// options or its typing box. An Enter still being pressed (a note just
|
|
2347
|
+
// sent with Enter) then lands on nothing, never on do it or yes, which arm
|
|
2348
|
+
// and then start; a screen reader reads the card from its title, and one
|
|
2349
|
+
// Tab is its first option. Back on the big orb, a focus that was lost (on
|
|
2350
|
+
// a card that has gone) goes to the orb itself.
|
|
2297
2351
|
function focusFree() {
|
|
2298
2352
|
var a = document.activeElement;
|
|
2299
|
-
return !a || a === document.body || !a.isConnected || a.id === 'orb' || a.id === 'lava';
|
|
2353
|
+
return !a || a === document.body || !a.isConnected || a.id === 'orb' || a.id === 'lava' || (a === box() && !document.body.classList.contains('has-card'));
|
|
2300
2354
|
}
|
|
2355
|
+
// the card can hold the focus, though Tab never stops on it
|
|
2356
|
+
box().tabIndex = -1;
|
|
2301
2357
|
function landFocus() {
|
|
2302
2358
|
if (!document.body.classList.contains('has-card') || !focusFree()) return;
|
|
2303
|
-
|
|
2304
|
-
if (first) first.focus({ preventScroll: true });
|
|
2359
|
+
box().focus({ preventScroll: true });
|
|
2305
2360
|
}
|
|
2306
2361
|
new MutationObserver(function () {
|
|
2307
2362
|
var orbNode = document.getElementById('orb');
|
package/package.json
CHANGED