atris 3.74.7 → 3.74.9
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/commands/gm.js +41 -14
- package/commands/member.js +41 -7
- package/lib/gm-bet.js +768 -188
- package/lib/gm-card.js +45 -4
- package/lib/gm-phone-page.js +508 -74
- package/lib/text-table.js +138 -12
- package/lib/zen-apply.js +1 -1
- package/lib/zen.js +15 -2
- package/package.json +1 -1
- package/scripts/det/ytnotes +9 -3
package/lib/gm-phone-page.js
CHANGED
|
@@ -13,8 +13,12 @@
|
|
|
13
13
|
// then settles small at the top and the card fades up. Tap again and the
|
|
14
14
|
// card dissolves, the lava melts back into the sphere, and the next bet plays.
|
|
15
15
|
// Bets wait in a queue on the Mac, so a tap rarely waits; when it does, the
|
|
16
|
-
// spiral follows the real progress. Once a card is up nothing moves
|
|
17
|
-
//
|
|
16
|
+
// spiral follows the real progress. Once a card is up nothing moves, but
|
|
17
|
+
// while the next card is on its way the dots never stop: after an answer
|
|
18
|
+
// they swarm in the sphere until it lands, and on the calm line (writing
|
|
19
|
+
// your next idea) the sphere stays above the card, swarming, its box open
|
|
20
|
+
// for a note, until the idea grows out of the dots. The footer is only the
|
|
21
|
+
// count of jobs the team is working on.
|
|
18
22
|
//
|
|
19
23
|
// Every card is big type in two parts: what is going on, and why it matters
|
|
20
24
|
// to the business. Nothing about how work is run or checked reaches the page.
|
|
@@ -70,7 +74,10 @@ button { font: inherit; color: inherit; background: none; border: 0; padding: 0;
|
|
|
70
74
|
}
|
|
71
75
|
.line.out { opacity: 0; }
|
|
72
76
|
.line.bad { color: var(--text-2); }
|
|
73
|
-
.foot { position: absolute; left: 0; right: 0; bottom: max(20px, env(safe-area-inset-bottom)); text-align: center; }
|
|
77
|
+
.foot { position: absolute; left: 0; right: 0; bottom: max(20px, env(safe-area-inset-bottom)); text-align: center; transition: opacity 0.3s ease-out 0.1s; }
|
|
78
|
+
/* while a card moves the footer steps aside, so it never jumps between the
|
|
79
|
+
bottom of the screen and the end of a card; it comes back at rest */
|
|
80
|
+
body[data-busy]:not([data-in-place]) .foot { opacity: 0; transition: opacity 0.15s ease-in; }
|
|
74
81
|
/* the yeses still waiting: a small line beside the team, never a card on its own */
|
|
75
82
|
#needs { margin-right: 20px; color: var(--amber); }
|
|
76
83
|
.quiet { color: var(--text-2); font-size: 17px; text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 0.25em; min-height: 44px; }
|
|
@@ -150,7 +157,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
150
157
|
.bet.quick { transition-duration: 0.4s, 0.5s; }
|
|
151
158
|
/* after an act the next card fades up as the sphere settles, so the whole motion is about a
|
|
152
159
|
second, and its title drifts only a few pixels into the spot the last title had */
|
|
153
|
-
.bet.acted { transform: translateY(4px); transition-duration: 0.
|
|
160
|
+
.bet.acted { transform: translateY(4px); transition-duration: 0.3s, 0.38s; }
|
|
154
161
|
.bet.acted.up { transform: none; }
|
|
155
162
|
/* card to card: a crossfade in place. the old card is a still copy that
|
|
156
163
|
fades over the box while the new card fades in under it from a small
|
|
@@ -201,11 +208,37 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
201
208
|
.bet h1.said { font-size: 20px; line-height: 1.4; font-weight: 400; letter-spacing: 0; }
|
|
202
209
|
.bet.holding { pointer-events: none; }
|
|
203
210
|
.muted { opacity: 0.35; transition: opacity 0.2s ease; }
|
|
211
|
+
/* after an act the card fades all the way before it leaves the page, never cut mid fade */
|
|
212
|
+
.bet.leave.fast { transition: opacity 0.2s ease-in-out; }
|
|
213
|
+
|
|
214
|
+
/* ---- the calm line: the sphere stays, a little smaller and higher, its dots
|
|
215
|
+
moving the whole time. The card under it takes no room of its own, so the
|
|
216
|
+
sphere never jumps when the card comes or goes; it glides. ---- */
|
|
217
|
+
.stage { transform-origin: 50% 0; transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1); }
|
|
218
|
+
body.has-card.waiting .screen { justify-content: center; padding-top: max(24px, env(safe-area-inset-top)); }
|
|
219
|
+
body.has-card.waiting .stage { display: block; transform: translateY(var(--wait-dy, 0px)) scale(var(--wait-k, 1)); }
|
|
220
|
+
body.has-card.waiting .line { display: block; }
|
|
221
|
+
body.has-card.waiting .foot { position: absolute; margin-top: 0; }
|
|
222
|
+
body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; top: var(--wait-top, 62%); padding-bottom: 76px; }
|
|
223
|
+
.bet.wait-in { transform: translateY(8px); transition: opacity 0.4s ease-out 0.28s, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.28s; }
|
|
224
|
+
.bet.wait-in.up { transform: none; }
|
|
225
|
+
.bet[data-kind="wait"] h1 { text-align: center; }
|
|
226
|
+
.waitstep { margin: 10px 0 0; min-height: 1.5em; text-align: center; color: var(--text-2); font-size: 17px; transition: opacity 0.26s ease; }
|
|
227
|
+
.waitstep.out { opacity: 0; }
|
|
228
|
+
.waittime { display: block; min-height: 1.5em; text-align: center; color: var(--text-3); font-size: 17px; }
|
|
229
|
+
.waitnotes { margin: 6px 0 0; overflow: hidden; transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
|
|
230
|
+
.waitnotes .turn { text-align: center; transition: opacity 0.3s ease-out; }
|
|
231
|
+
.waitnotes .turn.fresh { opacity: 0; }
|
|
232
|
+
.field.stay { height: 52px; }
|
|
204
233
|
|
|
205
234
|
@media (prefers-reduced-motion: reduce) {
|
|
206
235
|
.lava .tx, .lava .ty, .lava .blob { animation: none !important; }
|
|
207
236
|
.bet { transform: none; transition: opacity 0.2s linear; }
|
|
208
237
|
.line { transition: none; }
|
|
238
|
+
/* no travel and no swarm: every card change is a plain fade */
|
|
239
|
+
.stage, .waitstep, .waitnotes, .waitnotes .turn { transition: none; }
|
|
240
|
+
.bet.quick, .bet.acted, .bet.leave, .bet.leave.fast, .bet.wait-in { transform: none; transition: opacity 0.2s linear; }
|
|
241
|
+
.lava { transition: opacity 0.2s linear; }
|
|
209
242
|
}
|
|
210
243
|
</style>
|
|
211
244
|
</head>
|
|
@@ -275,7 +308,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
275
308
|
setTimeout(function () { busy = false; next(); }, 650);
|
|
276
309
|
}, 280);
|
|
277
310
|
}
|
|
278
|
-
return {
|
|
311
|
+
return {
|
|
312
|
+
set: function (text, bad) { want = { text: text, bad: Boolean(bad) }; next(); },
|
|
313
|
+
// the page just opened: no fade to wait out, so the next line comes at once
|
|
314
|
+
clear: function () { if (busy) { line.set('', false); return; } want = null; node.textContent = ''; node.classList.remove('bad', 'out'); },
|
|
315
|
+
};
|
|
279
316
|
})();
|
|
280
317
|
|
|
281
318
|
// ---- the orb: StippleOrb (tone dark) and LetterOrb's tap, ported 1:1 -----------
|
|
@@ -305,10 +342,26 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
305
342
|
// window side and never washes the page
|
|
306
343
|
var FAST_GROW_MS = 460, FAST_SETTLE_MS = 440, BLOOM = 0.72;
|
|
307
344
|
// the owner acted on a card: one motion of about a second when the answer
|
|
308
|
-
// is in. The card fades while the small orb glides into the
|
|
309
|
-
// dots spiral in at ACT_PACE, and the next card
|
|
310
|
-
|
|
345
|
+
// is in. The card fades all the way while the small orb glides into the
|
|
346
|
+
// sphere, the dots spiral in at ACT_PACE, and the next card grows out of
|
|
347
|
+
// the point: the lava swells to ACT_BLOOM and is already drifting up as
|
|
348
|
+
// it does, so its size and place never stop or snap on the way.
|
|
349
|
+
var ACT_LEAVE_MS = 220, ACT_GLIDE_MS = 260, ACT_BEAT_MS = 100, ACT_PACE = 0.28, ACT_GROW_MS = 140, ACT_SETTLE_MS = 420, ACT_BLOOM = 0.56;
|
|
311
350
|
var quick = false;
|
|
351
|
+
// the swarm: while the next card is on its way the dots never stop. The
|
|
352
|
+
// sphere turns at SWARM_SPIN and every dot wanders a slow loop of its
|
|
353
|
+
// own, WANDER of the radius, springing after it. In and out of it the
|
|
354
|
+
// motion eases over about a second, and it ends the moment a card lands.
|
|
355
|
+
var IDLE_SPIN = 0.15, SWARM_SPIN = 0.5, WANDER = 0.1, SWARM_EASE = 0.04;
|
|
356
|
+
var swarmK = 0, spin = IDLE_SPIN, clock = 0;
|
|
357
|
+
// when a spiral starts, each dot keeps the speed it had and lets it fade
|
|
358
|
+
// at CARRY a frame, so the dots turn inward without a stop
|
|
359
|
+
var CARRY = 0.82;
|
|
360
|
+
// what the dots did on the last frame, kept on the canvas so the motion can be
|
|
361
|
+
// measured, and ms: how much of the dots' own clock that frame was, which is
|
|
362
|
+
// at most 50ms however long the frame took
|
|
363
|
+
var stats = { frame: 0, mode: 'idle', step: 0, mean: 0, ms: 0 };
|
|
364
|
+
canvas.__orb = stats;
|
|
312
365
|
// Two speeds, chosen by truth. A bet already waiting in the queue plays
|
|
313
366
|
// the same curves at FAST of their length (tap to card in about two seconds, unhurried).
|
|
314
367
|
// A bet still being made plays the full landing length, and the spiral
|
|
@@ -360,7 +413,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
360
413
|
for (var i = 0; i < count; i++) {
|
|
361
414
|
var gx = gauss(), gy = gauss(), gz = gauss();
|
|
362
415
|
var n = Math.hypot(gx, gy, gz) || 1;
|
|
363
|
-
dots.push({
|
|
416
|
+
dots.push({
|
|
417
|
+
x: gx / n, y: gy / n, z: gz / n, 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,
|
|
418
|
+
// its own slow loop in the swarm
|
|
419
|
+
f1: 0.5 + Math.random() * 0.8, f2: 0.5 + Math.random() * 0.8, p1: Math.random() * 6.283, p2: Math.random() * 6.283,
|
|
420
|
+
});
|
|
364
421
|
}
|
|
365
422
|
}
|
|
366
423
|
|
|
@@ -372,7 +429,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
372
429
|
var cosA = Math.cos(angle), sinA = Math.sin(angle);
|
|
373
430
|
for (var i = 0; i < dots.length; i++) {
|
|
374
431
|
var d = dots[i];
|
|
375
|
-
if (fromHere) { d.bx = d.sx; d.by = d.sy; } else { d.bx = cx + (d.x * cosA + d.z * sinA) * R; d.by = cy + d.y * R; }
|
|
432
|
+
if (fromHere) { d.bx = d.sx; d.by = d.sy; } else { d.bx = cx + (d.x * cosA + d.z * sinA) * R; d.by = cy + d.y * R; d.vx = 0; d.vy = 0; }
|
|
433
|
+
d.kx = 0; d.ky = 0;
|
|
376
434
|
var len = Math.hypot(d.bx - cx, d.by - cy);
|
|
377
435
|
d.dl = (1 - Math.min(1, len / R)) * 0.28 + Math.random() * 0.05;
|
|
378
436
|
}
|
|
@@ -384,7 +442,12 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
384
442
|
var dt = Math.min(0.05, (now - (lastT || now)) / 1000);
|
|
385
443
|
lastT = now;
|
|
386
444
|
ctx.clearRect(0, 0, w, h);
|
|
387
|
-
|
|
445
|
+
var swarming = mode === 'swarm' && !reduced;
|
|
446
|
+
var toward = 1 - Math.pow(1 - SWARM_EASE, dt * 60);
|
|
447
|
+
swarmK += ((swarming ? 1 : 0) - swarmK) * toward;
|
|
448
|
+
spin += ((swarming ? SWARM_SPIN : IDLE_SPIN) - spin) * toward;
|
|
449
|
+
clock += dt;
|
|
450
|
+
if (!reduced && (mode === 'idle' || mode === 'swarm')) angle += dt * spin;
|
|
388
451
|
wave.age += dt;
|
|
389
452
|
light.x += (light.tx - light.x) * 0.06;
|
|
390
453
|
light.y += (light.ty - light.y) * 0.06;
|
|
@@ -400,13 +463,19 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
400
463
|
else age += (ageTo - age) * 0.05;
|
|
401
464
|
}
|
|
402
465
|
var cosA = Math.cos(angle), sinA = Math.sin(angle);
|
|
466
|
+
var most = 0, sum = 0, wander = WANDER * R * swarmK;
|
|
403
467
|
for (var i = 0; i < dots.length; i++) {
|
|
404
468
|
var d = dots[i];
|
|
405
469
|
var fade = 1, shrink = 1;
|
|
470
|
+
var x0 = d.sx, y0 = d.sy;
|
|
406
471
|
var rx = d.x * cosA + d.z * sinA;
|
|
407
472
|
var rz = -d.x * sinA + d.z * cosA;
|
|
408
|
-
if (mode === 'idle') {
|
|
473
|
+
if (mode === 'idle' || mode === 'swarm') {
|
|
409
474
|
var homeX = cx + rx * R, homeY = cy + d.y * R;
|
|
475
|
+
if (wander > 0.05) {
|
|
476
|
+
homeX += Math.sin(clock * d.f1 + d.p1) * wander;
|
|
477
|
+
homeY += Math.sin(clock * d.f2 + d.p2) * wander;
|
|
478
|
+
}
|
|
410
479
|
if (waveOn) {
|
|
411
480
|
var wx = homeX - wave.x, wy = homeY - wave.y;
|
|
412
481
|
var wd = Math.sqrt(wx * wx + wy * wy) || 1;
|
|
@@ -437,11 +506,14 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
437
506
|
var e = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
438
507
|
var ox = (d.bx - cx) * (1 - e), oy = (d.by - cy) * (1 - e);
|
|
439
508
|
var turn = e * 1.5;
|
|
440
|
-
|
|
441
|
-
d.
|
|
509
|
+
// the speed it had carries on and fades, then the spiral has it
|
|
510
|
+
d.kx += d.vx; d.ky += d.vy; d.vx *= CARRY; d.vy *= CARRY;
|
|
511
|
+
d.sx = cx + ox * Math.cos(turn) - oy * Math.sin(turn) + d.kx * (1 - e);
|
|
512
|
+
d.sy = cy + ox * Math.sin(turn) + oy * Math.cos(turn) + d.ky * (1 - e);
|
|
442
513
|
fade = 1 - Math.pow(Math.max(0, (e - 0.8) / 0.2), 1.5);
|
|
443
514
|
shrink = 1 - 0.35 * e;
|
|
444
515
|
}
|
|
516
|
+
if (i < 64) { var stepped = Math.max(Math.abs(d.sx - x0), Math.abs(d.sy - y0)); if (stepped > most) most = stepped; sum += stepped; }
|
|
445
517
|
// front dots are lit; the back half is a dim ghost. amber in shadow, cream where lit.
|
|
446
518
|
var lit = rx * light.x + d.y * light.y + rz * lz;
|
|
447
519
|
var front = rz > 0;
|
|
@@ -453,6 +525,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
453
525
|
ctx.arc(d.sx, d.sy, d.r * (front ? 0.6 + l * 0.7 : 0.6) * shrink, 0, Math.PI * 2);
|
|
454
526
|
ctx.fill();
|
|
455
527
|
}
|
|
528
|
+
stats.frame += 1; stats.mode = mode; stats.step = most; stats.mean = sum / Math.min(64, dots.length || 1); stats.ms = dt * 1000;
|
|
456
529
|
if (mode === 'pull') {
|
|
457
530
|
// where the dots land: one bright point that swells until the lava is born
|
|
458
531
|
var arrived = Math.min(1, Math.max(0, (age - 0.3) / 0.7));
|
|
@@ -463,7 +536,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
463
536
|
var left = 1 - (now - bridgeAt) / BRIDGE_MS;
|
|
464
537
|
if (left > 0) point(left, 1); else bridgeAt = 0;
|
|
465
538
|
}
|
|
466
|
-
|
|
539
|
+
// still once a card is up, and under reduced motion the sphere is drawn once and holds
|
|
540
|
+
if (mode === 'card' || (reduced && (mode === 'idle' || mode === 'swarm'))) { raf = 0; return; }
|
|
467
541
|
raf = requestAnimationFrame(frame);
|
|
468
542
|
}
|
|
469
543
|
|
|
@@ -484,7 +558,9 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
484
558
|
function start() { if (!raf) { lastT = 0; raf = requestAnimationFrame(frame); } }
|
|
485
559
|
function stop() { if (raf) cancelAnimationFrame(raf); raf = 0; }
|
|
486
560
|
|
|
487
|
-
|
|
561
|
+
// at: the frame time this place is for, when it is a step of a motion, kept on
|
|
562
|
+
// the lava so the motion can be measured on the clock it runs on
|
|
563
|
+
function place(s, x, y, at) { lava.style.transform = 'translate(' + (x - base / 2) + 'px, ' + (y - base / 2) + 'px) scale(' + s + ')'; lava.__at = at; }
|
|
488
564
|
function restPoint() {
|
|
489
565
|
var top = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--safe-top')) || 0;
|
|
490
566
|
return { x: document.documentElement.clientWidth / 2, y: window.scrollY + Math.max(16, top) + 8 + REST / 2 };
|
|
@@ -496,13 +572,20 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
496
572
|
// A queued bet (the fast pace) blooms contained: the lava grows to BLOOM
|
|
497
573
|
// of the smaller window side, the page never dims, and it settles.
|
|
498
574
|
function grow() {
|
|
575
|
+
// fixed while it grows past the screen, so the page never scrolls sideways
|
|
576
|
+
window.scrollTo(0, 0);
|
|
499
577
|
var rect = canvas.getBoundingClientRect();
|
|
500
578
|
var contained = pace < 1;
|
|
501
579
|
var growMs = quick ? ACT_GROW_MS : contained ? FAST_GROW_MS : (GROW_MS * pace);
|
|
502
580
|
var settleMs = quick ? ACT_SETTLE_MS : contained ? FAST_SETTLE_MS : (SETTLE_MS * pace);
|
|
503
|
-
|
|
581
|
+
// after an act the card comes up at the bloom's peak; any other card once the lava has settled
|
|
582
|
+
planned('bloom', growMs);
|
|
583
|
+
if (!quick) planned('settle', settleMs);
|
|
584
|
+
// the point the dots drew, where the sphere is on screen (smaller and higher on the calm line)
|
|
585
|
+
var vx = rect.left + rect.width / 2, vy = rect.top + rect.height / 2;
|
|
586
|
+
var bloom = quick ? ACT_BLOOM : BLOOM;
|
|
504
587
|
var reach = contained
|
|
505
|
-
?
|
|
588
|
+
? bloom * Math.min(window.innerWidth, window.innerHeight) / 2
|
|
506
589
|
: Math.hypot(Math.max(vx, window.innerWidth - vx), Math.max(vy, window.innerHeight - vy)) * 1.08;
|
|
507
590
|
base = Math.round(Math.min(1000, Math.max(280, (2 * reach) / MAX_SCALE)));
|
|
508
591
|
var sMax = (2 * reach) / base;
|
|
@@ -510,18 +593,19 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
510
593
|
var rest = REST / base;
|
|
511
594
|
lava.style.width = base + 'px';
|
|
512
595
|
lava.style.height = base + 'px';
|
|
513
|
-
// fixed while it grows past the screen, so the page never scrolls sideways
|
|
514
|
-
window.scrollTo(0, 0);
|
|
515
596
|
origin = { x: vx, y: vy };
|
|
516
597
|
lava.style.position = 'fixed';
|
|
598
|
+
lava.style.opacity = '0';
|
|
599
|
+
place(sMin, origin.x, origin.y);
|
|
517
600
|
lava.style.visibility = 'visible';
|
|
518
601
|
lava.classList.remove('still', 'alive', 'tap');
|
|
519
602
|
void lava.offsetWidth;
|
|
520
603
|
lava.classList.add('alive');
|
|
604
|
+
if (contained) { rise(growMs, settleMs, sMin, sMax, rest); return; }
|
|
521
605
|
var t0 = performance.now();
|
|
522
606
|
var step = function (now) {
|
|
523
607
|
var t = Math.min(1, (now - t0) / growMs);
|
|
524
|
-
place(sMin + (sMax - sMin) * ease(t), origin.x, origin.y);
|
|
608
|
+
place(sMin + (sMax - sMin) * ease(t), origin.x, origin.y, now);
|
|
525
609
|
lava.style.opacity = String(smooth(0.02, 0.22, t));
|
|
526
610
|
core.style.opacity = String(1 - smooth(0.05, 0.4, t));
|
|
527
611
|
dim.style.opacity = contained ? '0' : String(smooth(0.88, 1, t));
|
|
@@ -535,7 +619,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
535
619
|
var s0 = performance.now();
|
|
536
620
|
var settle = function (n2) {
|
|
537
621
|
var u = ease(Math.min(1, (n2 - s0) / settleMs));
|
|
538
|
-
place(sMax + (rest - sMax) * u, origin.x + (p.x - origin.x) * u, origin.y + (p.y - origin.y) * u);
|
|
622
|
+
place(sMax + (rest - sMax) * u, origin.x + (p.x - origin.x) * u, origin.y + (p.y - origin.y) * u, n2);
|
|
539
623
|
dim.style.opacity = contained ? '0' : String(1 - smooth(0.35, 0.85, u));
|
|
540
624
|
if (u < 1) { requestAnimationFrame(settle); return; }
|
|
541
625
|
settled();
|
|
@@ -545,6 +629,38 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
545
629
|
requestAnimationFrame(step);
|
|
546
630
|
}
|
|
547
631
|
|
|
632
|
+
// A contained bloom is one motion. The lava is born at the point and
|
|
633
|
+
// swells for growMs on a soft curve while it already drifts up, then
|
|
634
|
+
// eases down into the small orb at the top over settleMs. Its size turns at the peak with no
|
|
635
|
+
// stop, and its place moves on one curve the whole way, so nothing snaps.
|
|
636
|
+
// The card fades up under it from the peak.
|
|
637
|
+
function rise(growMs, settleMs, sMin, sMax, rest) {
|
|
638
|
+
var total = growMs + settleMs, k = growMs / total;
|
|
639
|
+
var p = restPoint();
|
|
640
|
+
var covered = false;
|
|
641
|
+
var t0 = performance.now();
|
|
642
|
+
var step = function (now) {
|
|
643
|
+
var u = Math.min(1, (now - t0) / total);
|
|
644
|
+
var s = u < k ? sMin + (sMax - sMin) * (0.5 - 0.5 * Math.cos(Math.PI * u / k)) : sMax + (rest - sMax) * ease((u - k) / (1 - k));
|
|
645
|
+
var m = ease(u);
|
|
646
|
+
place(s, origin.x + (p.x - origin.x) * m, origin.y + (p.y - origin.y) * m, now);
|
|
647
|
+
lava.style.opacity = String(smooth(0, 0.1, u));
|
|
648
|
+
core.style.opacity = String(1 - smooth(0.04, 0.5, u));
|
|
649
|
+
dim.style.opacity = '0';
|
|
650
|
+
if (!covered && u >= k) {
|
|
651
|
+
covered = true;
|
|
652
|
+
mode = 'card';
|
|
653
|
+
stop();
|
|
654
|
+
ctx.clearRect(0, 0, w, h);
|
|
655
|
+
canvas.style.visibility = 'hidden';
|
|
656
|
+
if (onBorn) onBorn('cover');
|
|
657
|
+
}
|
|
658
|
+
if (u < 1) { requestAnimationFrame(step); return; }
|
|
659
|
+
settled();
|
|
660
|
+
};
|
|
661
|
+
requestAnimationFrame(step);
|
|
662
|
+
}
|
|
663
|
+
|
|
548
664
|
// the small lava orb at the top, still, and itself the next tap
|
|
549
665
|
function settled() {
|
|
550
666
|
var p = restPoint();
|
|
@@ -587,9 +703,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
587
703
|
for (var i = 0; i < dots.length; i++) { dots[i].sx = cx; dots[i].sy = cy; dots[i].vx = 0; dots[i].vy = 0; }
|
|
588
704
|
pointer.inside = false;
|
|
589
705
|
age = -1; timed = false; fired = false;
|
|
590
|
-
|
|
706
|
+
// after an act the next card is on its way, so the dots come out swarming and never stop
|
|
707
|
+
mode = fast ? 'swarm' : 'idle';
|
|
591
708
|
bridgeAt = reduced ? 0 : performance.now();
|
|
592
709
|
start();
|
|
710
|
+
if (!reduced) planned('beat', beatMs);
|
|
593
711
|
setTimeout(cb, reduced ? 0 : beatMs);
|
|
594
712
|
};
|
|
595
713
|
if (reduced || lava.style.visibility !== 'visible') { reform(); return; }
|
|
@@ -599,12 +717,13 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
599
717
|
lava.style.position = 'fixed';
|
|
600
718
|
place(from.s, from.x, from.y);
|
|
601
719
|
var sEnd = 30 / base;
|
|
720
|
+
planned('glide', glideMs);
|
|
602
721
|
var t0 = performance.now();
|
|
603
722
|
var step = function (now) {
|
|
604
723
|
var k = Math.min(1, (now - t0) / glideMs);
|
|
605
|
-
var t = ease(k);
|
|
724
|
+
var t = fast ? 0.5 - 0.5 * Math.cos(Math.PI * k) : ease(k);
|
|
606
725
|
if (now - t0 >= leaveMs) takeOff();
|
|
607
|
-
place(from.s + (sEnd - from.s) * t, from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t);
|
|
726
|
+
place(from.s + (sEnd - from.s) * t, from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t, now);
|
|
608
727
|
core.style.opacity = String(smooth(0.3, 0.9, k));
|
|
609
728
|
lava.style.opacity = String(1 - smooth(0.7, 1, k));
|
|
610
729
|
if (k < 1) { requestAnimationFrame(step); return; }
|
|
@@ -657,6 +776,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
657
776
|
pace = act ? ACT_PACE : fast ? FAST : 1;
|
|
658
777
|
// after an act the dots spiral in from where the re-formed sphere is
|
|
659
778
|
if (mode !== 'pull') startPull(fast || Boolean(act));
|
|
779
|
+
// the lava is born at 1.05 on the spiral's own clock, which runs at pace
|
|
780
|
+
planned('spiral', Math.max(0, 1.05 - age) * pace * 1000);
|
|
660
781
|
timed = true;
|
|
661
782
|
start();
|
|
662
783
|
},
|
|
@@ -675,6 +796,24 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
675
796
|
mode = 'idle';
|
|
676
797
|
start();
|
|
677
798
|
},
|
|
799
|
+
// the next card is on its way: the sphere is on the page and its dots keep moving
|
|
800
|
+
swarm: function () {
|
|
801
|
+
if (mode === 'card') {
|
|
802
|
+
canvas.style.visibility = 'visible';
|
|
803
|
+
measure();
|
|
804
|
+
for (var i = 0; i < dots.length; i++) { dots[i].sx = cx; dots[i].sy = cy; dots[i].vx = 0; dots[i].vy = 0; }
|
|
805
|
+
lava.classList.remove('tap', 'still', 'alive');
|
|
806
|
+
lava.style.visibility = 'hidden';
|
|
807
|
+
lava.style.opacity = '0';
|
|
808
|
+
lava.tabIndex = -1;
|
|
809
|
+
age = -1; timed = false; fired = false;
|
|
810
|
+
mode = 'swarm';
|
|
811
|
+
}
|
|
812
|
+
if (mode === 'idle') mode = 'swarm';
|
|
813
|
+
start();
|
|
814
|
+
},
|
|
815
|
+
// nothing is on its way: back to the idle drift under the pointer
|
|
816
|
+
calm: function () { if (mode === 'swarm') mode = 'idle'; start(); },
|
|
678
817
|
melt: melt,
|
|
679
818
|
leaveMs: reduced ? 200 : LEAVE_MS,
|
|
680
819
|
// a bet is already up when the page loads: the settled orb, no motion
|
|
@@ -689,7 +828,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
689
828
|
// card, and nothing from the card before survives a draw. Every tap posts
|
|
690
829
|
// the card's id and the option's id, or the words typed in its box, to
|
|
691
830
|
// /card; the Mac refuses anything else, and its answer is the next card.
|
|
692
|
-
var bet = { shown: null, watching: null, tapAt: 0 };
|
|
831
|
+
var bet = { shown: null, watching: null, tapAt: 0, plan: [], waited: 0, noting: false, clock: null };
|
|
693
832
|
// while an act or a turn is under way the card takes no taps; the page
|
|
694
833
|
// marks it on the body too, so anyone reading the page can tell
|
|
695
834
|
var busyNow = false;
|
|
@@ -726,13 +865,16 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
726
865
|
if (lines.getClientRects().length > 1) title.classList.add('said');
|
|
727
866
|
}
|
|
728
867
|
if (card.body) b.appendChild(el('p', 'why', card.body));
|
|
868
|
+
var calm = isWait(card);
|
|
869
|
+
// the calm line: what the writer is doing now, how long it has been, and the notes typed here
|
|
870
|
+
if (calm) { b.appendChild(el('p', 'waitstep')); b.appendChild(el('span', 'waittime')); b.appendChild(el('div', 'waitnotes')); }
|
|
729
871
|
// a reply keeps the title's spot; the turns so far sit quietly under it
|
|
730
|
-
if (card.thread && card.thread.length) {
|
|
872
|
+
if (!calm && card.thread && card.thread.length) {
|
|
731
873
|
var thread = b.appendChild(el('div', 'thread'));
|
|
732
874
|
card.thread.forEach(function (t) { thread.appendChild(el('p', 'turn ' + (t.who === 'owner' ? 'you' : 'orb'), t.text)); });
|
|
733
875
|
}
|
|
734
876
|
// who it is and what they do; the rating and the model live on the team table
|
|
735
|
-
if (card.job && !card.table) b.appendChild(el('p', 'jobline', [card.job.member, card.job.role].filter(Boolean).join(' · ')));
|
|
877
|
+
if (card.job && !card.table && !calm) b.appendChild(el('p', 'jobline', [card.job.member, card.job.role].filter(Boolean).join(' · ')));
|
|
736
878
|
var inCells = card.options.filter(function (o) { return o.cell; });
|
|
737
879
|
if (card.table) b.appendChild(drawTable(card, inCells));
|
|
738
880
|
if (card.time) b.appendChild(el('span', 'time', card.time));
|
|
@@ -758,6 +900,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
758
900
|
act.appendChild(el('p', 'links note'));
|
|
759
901
|
b.appendChild(act);
|
|
760
902
|
if (card.input) b.appendChild(inputLine(card.input));
|
|
903
|
+
if (calm) live(card, true);
|
|
761
904
|
}
|
|
762
905
|
|
|
763
906
|
// a table: one row per line, a line between every cell; an option with a
|
|
@@ -828,7 +971,9 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
828
971
|
return btn;
|
|
829
972
|
}
|
|
830
973
|
|
|
831
|
-
// the card's box: a quiet line that turns into the field in its own spot
|
|
974
|
+
// the card's box: a quiet line that turns into the field in its own spot.
|
|
975
|
+
// On the calm line it is open from the start and stays open, without
|
|
976
|
+
// taking the cursor, and the words are a note for the idea being written.
|
|
832
977
|
function inputLine(input) {
|
|
833
978
|
var wrap = el('div', 'links sayline');
|
|
834
979
|
var open = wrap.appendChild(el('button', 'quiet', input.label || 'or say something'));
|
|
@@ -837,6 +982,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
837
982
|
if (bet.busy) return;
|
|
838
983
|
typeHere(open, input.placeholder || 'say something', input.max || 280, function (text, field) { send(field, { text: text }, null); });
|
|
839
984
|
});
|
|
985
|
+
if (box().getAttribute('data-kind') === 'wait') typeHere(open, input.placeholder || 'add a note for this idea', input.max || 280, function (text, field) { sendNote(field, text); }, true);
|
|
840
986
|
return wrap;
|
|
841
987
|
}
|
|
842
988
|
|
|
@@ -856,9 +1002,9 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
856
1002
|
var input = spot.querySelector('input');
|
|
857
1003
|
if (input) { input.readOnly = true; input.blur(); }
|
|
858
1004
|
bet.busy = true;
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
return
|
|
1005
|
+
var at = tapped();
|
|
1006
|
+
planned('hold', HOLD_MS);
|
|
1007
|
+
return at;
|
|
862
1008
|
}
|
|
863
1009
|
|
|
864
1010
|
function afterHold(heldAt) {
|
|
@@ -869,10 +1015,12 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
869
1015
|
// The tapped button becomes the field at its own size and spot, with the
|
|
870
1016
|
// cursor already in it, and the other options dim. Enter or send hands
|
|
871
1017
|
// the words to sent(text, field). Left empty, it turns back into the button.
|
|
872
|
-
|
|
1018
|
+
// stay: the calm line's box, open from the start without the cursor, and
|
|
1019
|
+
// open after every send.
|
|
1020
|
+
function typeHere(btn, placeholder, maxLength, sent, stay) {
|
|
873
1021
|
var field = el('div', btn.classList.contains('choice') ? 'choice field' : 'field');
|
|
874
1022
|
// its exact height, to the subpixel, so nothing under it moves
|
|
875
|
-
field.style.height = btn.getBoundingClientRect().height + 'px';
|
|
1023
|
+
if (stay) field.classList.add('stay'); else field.style.height = btn.getBoundingClientRect().height + 'px';
|
|
876
1024
|
var input = el('input');
|
|
877
1025
|
input.type = 'text';
|
|
878
1026
|
input.maxLength = maxLength;
|
|
@@ -883,7 +1031,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
883
1031
|
go.type = 'button';
|
|
884
1032
|
var submit = function () {
|
|
885
1033
|
var text = input.value.trim();
|
|
886
|
-
if (text && !bet.busy) sent(text, field);
|
|
1034
|
+
if (text && !bet.busy && !bet.noting) sent(text, field);
|
|
887
1035
|
};
|
|
888
1036
|
// a press on send keeps the cursor in the field, so it never closes first
|
|
889
1037
|
go.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
|
@@ -892,13 +1040,14 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
892
1040
|
if (e.key === 'Enter') { e.preventDefault(); submit(); }
|
|
893
1041
|
});
|
|
894
1042
|
input.addEventListener('blur', function () {
|
|
895
|
-
if (input.value.trim() || field.classList.contains('chosen') || !field.isConnected) return;
|
|
1043
|
+
if (stay || input.value.trim() || field.classList.contains('chosen') || !field.isConnected) return;
|
|
896
1044
|
field.replaceWith(btn);
|
|
897
1045
|
dimOthers(null);
|
|
898
1046
|
});
|
|
899
1047
|
field.appendChild(input);
|
|
900
1048
|
field.appendChild(go);
|
|
901
1049
|
btn.replaceWith(field);
|
|
1050
|
+
if (stay) return field;
|
|
902
1051
|
dimOthers(field);
|
|
903
1052
|
input.focus();
|
|
904
1053
|
return field;
|
|
@@ -936,9 +1085,10 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
936
1085
|
var heldAt = hold(spot);
|
|
937
1086
|
var answer = api('/act', body).catch(offline);
|
|
938
1087
|
if (option && IN_PLACE.indexOf(option.action) >= 0) {
|
|
1088
|
+
document.body.setAttribute('data-in-place', '');
|
|
939
1089
|
Promise.all([answer, afterHold(heldAt)]).then(function (both) {
|
|
940
1090
|
var r = both[0];
|
|
941
|
-
if (!r.ok || !r.card) { bet.busy = false; unsent(from, r, body.text, option); return; }
|
|
1091
|
+
if (!r.ok || !r.card) { bet.busy = false; document.body.removeAttribute('data-in-place'); unsent(from, r, body.text, option); return; }
|
|
942
1092
|
turnTo(r.card);
|
|
943
1093
|
});
|
|
944
1094
|
return;
|
|
@@ -949,19 +1099,23 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
949
1099
|
function flow(answer, from, typed, option) {
|
|
950
1100
|
var landed = null;
|
|
951
1101
|
var sphere = false;
|
|
952
|
-
var lineAt = 0;
|
|
1102
|
+
var lineAt = 0, waitFrom = 0;
|
|
953
1103
|
var land = function () {
|
|
1104
|
+
// the sphere is ready before the answer is in: it waits from now
|
|
1105
|
+
if (sphere && !landed) waitFrom = performance.now();
|
|
954
1106
|
if (!landed || !sphere) return;
|
|
1107
|
+
if (waitFrom) bet.waited += performance.now() - waitFrom;
|
|
955
1108
|
var r = landed;
|
|
956
1109
|
landed = null;
|
|
957
1110
|
var wait = r.ack && lineAt ? Math.max(0, AFTER_MS - (performance.now() - lineAt)) : 0;
|
|
1111
|
+
if (wait) planned('line', wait);
|
|
958
1112
|
setTimeout(function () {
|
|
959
|
-
// nothing more to show: the sphere stays, with its line
|
|
960
|
-
if (r.ok && (r.home || !r.card)) { bet.busy = false; bet.shown = null; line.set(r.home ? 'not now. tap when you want another' : 'tap to think', false); return; }
|
|
1113
|
+
// nothing more to show: the sphere stays, with its line, and its dots settle into the idle drift
|
|
1114
|
+
if (r.ok && (r.home || !r.card)) { bet.busy = false; bet.shown = null; orb.calm(); line.set(r.home ? 'not now. tap when you want another' : 'tap to think', false); return; }
|
|
961
1115
|
// the card the Mac has on screen now is the one that comes back; a
|
|
962
1116
|
// refusal comes back on it with one line why
|
|
963
1117
|
var then = r.card || from;
|
|
964
|
-
if (!then) { bet.busy = false; line.set('that did not go through. tap to try again', true); return; }
|
|
1118
|
+
if (!then) { bet.busy = false; orb.calm(); line.set('that did not go through. tap to try again', true); return; }
|
|
965
1119
|
if (!r.ok) { spiralTo(then, function () { unsent(then, r, typed, then.id === (from && from.id) ? option : null); }); return; }
|
|
966
1120
|
spiralTo(then, null);
|
|
967
1121
|
}, wait);
|
|
@@ -975,27 +1129,50 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
975
1129
|
}
|
|
976
1130
|
|
|
977
1131
|
// the card fades in place while the small orb glides down and becomes the
|
|
978
|
-
// sphere; the card leaves the page once it has faded
|
|
1132
|
+
// sphere; the card leaves the page once it has faded all the way. From the
|
|
1133
|
+
// calm line the sphere is already there: its words fade and it glides home.
|
|
979
1134
|
function leave(then, fast) {
|
|
1135
|
+
if (waiting()) { leaveWait(then, fast); return; }
|
|
980
1136
|
var b = box();
|
|
981
1137
|
var to = sphereSpot();
|
|
982
1138
|
b.classList.remove('quick', 'acted', 'up');
|
|
983
1139
|
b.classList.add('leave');
|
|
1140
|
+
b.classList.toggle('fast', Boolean(fast));
|
|
984
1141
|
var swap = function () {
|
|
985
1142
|
b.style.minHeight = '';
|
|
986
1143
|
b.hidden = true;
|
|
987
|
-
b.classList.remove('leave', 'holding');
|
|
1144
|
+
b.classList.remove('leave', 'fast', 'holding');
|
|
988
1145
|
document.body.classList.remove('has-card', 'wide');
|
|
989
1146
|
window.scrollTo(0, 0);
|
|
990
1147
|
};
|
|
991
|
-
|
|
1148
|
+
// reduced motion: no travel; the card and the small orb fade, then the sphere is there
|
|
1149
|
+
if (reduced) {
|
|
1150
|
+
document.getElementById('lava').style.opacity = '0';
|
|
1151
|
+
planned('leave', 200);
|
|
1152
|
+
setTimeout(function () { swap(); stageFade(); orb.melt(then, to, function () {}, fast); }, 200);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
992
1155
|
orb.melt(then, to, swap, fast);
|
|
993
1156
|
}
|
|
994
1157
|
|
|
995
|
-
//
|
|
1158
|
+
// under reduced motion the sphere never travels: it fades in where it now sits
|
|
1159
|
+
function stageFade() {
|
|
1160
|
+
if (!reduced) return;
|
|
1161
|
+
var stage = document.getElementById('stage');
|
|
1162
|
+
stage.style.transition = 'none';
|
|
1163
|
+
stage.style.opacity = '0';
|
|
1164
|
+
void stage.offsetWidth;
|
|
1165
|
+
stage.style.transition = 'opacity 0.2s linear';
|
|
1166
|
+
stage.style.opacity = '';
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// from the sphere: the dots spiral in and the card grows out of the point.
|
|
1170
|
+
// The calm line is not a card that blooms: the sphere stays and its dots keep moving.
|
|
996
1171
|
function spiralTo(card, after) {
|
|
997
1172
|
line.set('', false);
|
|
998
|
-
if (
|
|
1173
|
+
if (isWait(card)) { enterWait(card); if (after) after(); return; }
|
|
1174
|
+
// reduced motion: no spiral; the card fades in where it goes
|
|
1175
|
+
if (reduced) { orb.settled(); showCard(card, true); bet.busy = false; if (after) after(); return; }
|
|
999
1176
|
orb.play(function (phase) {
|
|
1000
1177
|
// the card blooms where the dots land; what comes with it (a line why, the words back in their box) comes with it
|
|
1001
1178
|
if (phase === 'cover') { showCard(card, true, true, true); if (after) after(); }
|
|
@@ -1012,6 +1189,9 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1012
1189
|
// the Mac's own refusals are plain words; anything else reads as one plain line
|
|
1013
1190
|
if (note) note.textContent = (r && r.http === 409 && r.message) || (r && r.http === 0 && r.message) || 'that did not go through. try again.';
|
|
1014
1191
|
if (!typed) return;
|
|
1192
|
+
// the calm line's box is open already: the words go back into it
|
|
1193
|
+
var openBox = !option && box().querySelector('.sayline .field.stay input');
|
|
1194
|
+
if (openBox) { openBox.value = typed; return; }
|
|
1015
1195
|
// the words go back where they were typed, or to this card's own box when the card is a new one
|
|
1016
1196
|
var spot = option ? box().querySelector('[data-option="' + option.id + '"]') : box().querySelector('.sayline .quiet');
|
|
1017
1197
|
if (!spot) return;
|
|
@@ -1021,24 +1201,49 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1021
1201
|
}
|
|
1022
1202
|
|
|
1023
1203
|
// ---- showing a card ---------------------------------------------------------------------
|
|
1024
|
-
//
|
|
1025
|
-
// the speed can be measured
|
|
1204
|
+
// The time from the tap to the card fully on screen, kept on the page so
|
|
1205
|
+
// the speed can be measured. data-reveal-ms is the clock. data-reveal-plan
|
|
1206
|
+
// is each step of the motion in order with the length it is set to take,
|
|
1207
|
+
// and data-reveal-waited-ms is how long the sphere waited on the Mac's
|
|
1208
|
+
// answer, so late frames on a busy machine are told apart from the motion.
|
|
1209
|
+
var REVEAL = ['data-reveal-ms', 'data-reveal-plan', 'data-reveal-waited-ms'];
|
|
1210
|
+
function tapped() {
|
|
1211
|
+
bet.tapAt = performance.now();
|
|
1212
|
+
bet.plan = [];
|
|
1213
|
+
bet.waited = 0;
|
|
1214
|
+
REVEAL.forEach(function (a) { document.body.removeAttribute(a); });
|
|
1215
|
+
return bet.tapAt;
|
|
1216
|
+
}
|
|
1217
|
+
function planned(name, ms) { if (bet.tapAt) bet.plan.push(name + ' ' + Math.round(ms)); }
|
|
1026
1218
|
function measured() {
|
|
1027
|
-
if (bet.tapAt)
|
|
1219
|
+
if (bet.tapAt) {
|
|
1220
|
+
document.body.setAttribute('data-reveal-plan', bet.plan.join(', '));
|
|
1221
|
+
document.body.setAttribute('data-reveal-waited-ms', String(Math.round(bet.waited)));
|
|
1222
|
+
document.body.setAttribute('data-reveal-ms', String(Math.round(performance.now() - bet.tapAt)));
|
|
1223
|
+
}
|
|
1028
1224
|
bet.tapAt = 0;
|
|
1029
1225
|
}
|
|
1226
|
+
// how long a card's own fade up is set to take, read from its style
|
|
1227
|
+
function fadeMs(n) {
|
|
1228
|
+
var s = getComputedStyle(n);
|
|
1229
|
+
var i = Math.max(0, s.transitionProperty.split(', ').indexOf('opacity'));
|
|
1230
|
+
var at = function (list) { var v = list.split(', '); return (parseFloat(v[i % v.length]) || 0) * 1000; };
|
|
1231
|
+
return at(s.transitionDelay) + at(s.transitionDuration);
|
|
1232
|
+
}
|
|
1030
1233
|
|
|
1031
1234
|
// fast: the quick fade from the big orb; act: the short one after an act
|
|
1032
1235
|
function showCard(card, animate, fast, act) {
|
|
1236
|
+
if (isWait(card)) { enterWait(card, !animate); return; }
|
|
1033
1237
|
var b = box();
|
|
1034
1238
|
bet.shown = card;
|
|
1035
1239
|
// where it starts from is set first: drawing measures the title, and a
|
|
1036
1240
|
// measure is a frame the card would otherwise start from somewhere else
|
|
1037
|
-
b.classList.remove('leave', 'holding');
|
|
1241
|
+
b.classList.remove('leave', 'fast', 'holding', 'wait-in');
|
|
1038
1242
|
b.classList.toggle('quick', Boolean(fast) && !act);
|
|
1039
1243
|
b.classList.toggle('acted', Boolean(act));
|
|
1040
1244
|
b.classList.toggle('up', !animate);
|
|
1041
1245
|
// on the page before it is drawn, so a long title can tell it runs past one line
|
|
1246
|
+
document.body.classList.remove('waiting');
|
|
1042
1247
|
document.body.classList.add('has-card');
|
|
1043
1248
|
b.hidden = false;
|
|
1044
1249
|
draw(card);
|
|
@@ -1047,9 +1252,224 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1047
1252
|
var done = function (e) { if (e.propertyName === 'opacity') { b.removeEventListener('transitionend', done); measured(); } };
|
|
1048
1253
|
b.addEventListener('transitionend', done);
|
|
1049
1254
|
void b.offsetWidth;
|
|
1255
|
+
requestAnimationFrame(function () { b.classList.add('up'); planned('fade', fadeMs(b)); });
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// ---- the calm line: the dots keep moving while the next idea is written -----------------
|
|
1259
|
+
// The sphere stays on the page, a little smaller and higher, and its dots
|
|
1260
|
+
// swarm. The card sits under it with what the writer is doing now, and its
|
|
1261
|
+
// box is open: words typed there are a note for the idea being written,
|
|
1262
|
+
// and the card stays. When the idea lands it grows out of the dots. A tap
|
|
1263
|
+
// on the dots goes back to the big orb, as the small orb does on a card.
|
|
1264
|
+
function isWait(card) { return Boolean(card && card.kind === 'wait'); }
|
|
1265
|
+
function waiting() { return document.body.classList.contains('waiting'); }
|
|
1266
|
+
// the calm line is one card while its words change, under any stamp
|
|
1267
|
+
function sameCard(a, b) { return Boolean(a && b && (a.id === b.id || (isWait(a) && isWait(b) && String(a.id).split('#')[0] === String(b.id).split('#')[0]))); }
|
|
1268
|
+
|
|
1269
|
+
// the smaller sphere's place: at most 300px, and high enough that the card
|
|
1270
|
+
// under it has WAIT_ROOM. It is read from the sphere's own place in the
|
|
1271
|
+
// layout, which the calm line keeps and a transform never moves.
|
|
1272
|
+
var WAIT_ROOM = 250;
|
|
1273
|
+
function placeWait(animate) {
|
|
1274
|
+
var stage = document.getElementById('stage');
|
|
1275
|
+
var body = document.body;
|
|
1276
|
+
var hidden = !stage.offsetHeight;
|
|
1277
|
+
if (hidden) body.classList.add('has-card', 'waiting');
|
|
1278
|
+
var screenTop = document.getElementById('screen').getBoundingClientRect().top;
|
|
1279
|
+
var h = stage.offsetHeight || 1, top0 = screenTop + stage.offsetTop;
|
|
1280
|
+
if (hidden) body.classList.remove('waiting');
|
|
1281
|
+
var k = Math.min(1, 300 / h, 0.62 * Math.min(window.innerWidth, window.innerHeight) / h);
|
|
1282
|
+
var top = Math.max(24, (window.innerHeight - (k * h + WAIT_ROOM)) / 2);
|
|
1283
|
+
var s = body.style;
|
|
1284
|
+
s.setProperty('--wait-k', k.toFixed(4));
|
|
1285
|
+
s.setProperty('--wait-dy', Math.round(top - top0) + 'px');
|
|
1286
|
+
s.setProperty('--wait-top', Math.round(top + k * h + 6 - screenTop) + 'px');
|
|
1287
|
+
if (animate) return;
|
|
1288
|
+
stage.style.transition = 'none';
|
|
1289
|
+
requestAnimationFrame(function () { requestAnimationFrame(function () { stage.style.transition = ''; }); });
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// a turned phone: the calm line's sphere and card find their place again
|
|
1293
|
+
window.addEventListener('resize', function () { if (waiting()) placeWait(false); });
|
|
1294
|
+
|
|
1295
|
+
// atOnce: the page opened on it, so it is simply there
|
|
1296
|
+
function enterWait(card, atOnce) {
|
|
1297
|
+
var b = box();
|
|
1298
|
+
var was = waiting() && !b.hidden;
|
|
1299
|
+
bet.shown = card;
|
|
1300
|
+
if (atOnce) line.clear(); else line.set('', false);
|
|
1301
|
+
orb.swarm();
|
|
1302
|
+
if (!was) { placeWait(!atOnce && !reduced); stageFade(); }
|
|
1303
|
+
b.classList.remove('leave', 'fast', 'holding', 'quick', 'acted', 'up', 'wait-in');
|
|
1304
|
+
document.body.classList.add('has-card', 'waiting');
|
|
1305
|
+
document.getElementById('orb').setAttribute('aria-label', 'the atris orb, writing your next idea. tap to go back to the big orb');
|
|
1306
|
+
b.hidden = false;
|
|
1307
|
+
draw(card);
|
|
1308
|
+
watchWait();
|
|
1309
|
+
bet.busy = false;
|
|
1310
|
+
if (was || atOnce) { b.classList.add('up'); return; }
|
|
1311
|
+
b.classList.add('wait-in');
|
|
1312
|
+
void b.offsetWidth;
|
|
1050
1313
|
requestAnimationFrame(function () { b.classList.add('up'); });
|
|
1051
1314
|
}
|
|
1052
1315
|
|
|
1316
|
+
// The calm line's live words: what the writer is doing now, how long it
|
|
1317
|
+
// has been, and the newest note typed here with the answer to it. draw()
|
|
1318
|
+
// puts them on the card; while it shows they are kept fresh in place. Its
|
|
1319
|
+
// options and its box never change under one id, so the field, its words,
|
|
1320
|
+
// and the cursor in it are never touched.
|
|
1321
|
+
function live(card, atOnce) {
|
|
1322
|
+
var b = box();
|
|
1323
|
+
var step = b.querySelector('.waitstep');
|
|
1324
|
+
if (!isWait(card) || !step) return;
|
|
1325
|
+
bet.shown = card;
|
|
1326
|
+
b.setAttribute('data-card', card.id);
|
|
1327
|
+
var h = b.querySelector('h1');
|
|
1328
|
+
if (h.textContent !== card.title) h.textContent = card.title;
|
|
1329
|
+
var lines = Array.isArray(card.progress) ? card.progress : [];
|
|
1330
|
+
var newest = lines.length ? lines[lines.length - 1] : null;
|
|
1331
|
+
stepTo(step, newest && typeof newest.line === 'string' ? newest.line : '', atOnce);
|
|
1332
|
+
notesTo(b.querySelector('.waitnotes'), card.thread || [], atOnce);
|
|
1333
|
+
// the Mac's own count of how long it has been, carried on from when it came
|
|
1334
|
+
if (Number.isFinite(card.waited_ms)) waitClock = { key: String(card.id).split('#')[0], ms: card.waited_ms, at: Date.now() };
|
|
1335
|
+
tickWait();
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// one line at a time, crossfaded in its own spot, so nothing under it moves
|
|
1339
|
+
function stepTo(node, text, atOnce) {
|
|
1340
|
+
if (node.getAttribute('data-text') === text) return;
|
|
1341
|
+
node.setAttribute('data-text', text);
|
|
1342
|
+
clearTimeout(node.fading);
|
|
1343
|
+
if (atOnce || reduced || !node.textContent) { node.textContent = text; node.classList.remove('out'); return; }
|
|
1344
|
+
node.classList.add('out');
|
|
1345
|
+
node.fading = setTimeout(function () { node.textContent = node.getAttribute('data-text'); node.classList.remove('out'); }, 260);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// the newest note and the answer to it; a new one fades in, and the room it
|
|
1349
|
+
// takes opens smoothly
|
|
1350
|
+
function notesTo(node, turns, atOnce) {
|
|
1351
|
+
var want = turns.slice(-2);
|
|
1352
|
+
var had = Array.prototype.map.call(node.children, function (n) { return n.textContent; });
|
|
1353
|
+
if (had.join('\\n') === want.map(function (t) { return t.text; }).join('\\n')) return;
|
|
1354
|
+
var from = node.offsetHeight;
|
|
1355
|
+
node.style.height = '';
|
|
1356
|
+
node.textContent = '';
|
|
1357
|
+
want.forEach(function (t, i) {
|
|
1358
|
+
var p = node.appendChild(el('p', 'turn ' + (t.who === 'owner' ? 'you' : 'orb'), t.text));
|
|
1359
|
+
if (!atOnce && !reduced && had[i] !== t.text) p.classList.add('fresh');
|
|
1360
|
+
});
|
|
1361
|
+
if (atOnce || reduced) return;
|
|
1362
|
+
var to = node.offsetHeight;
|
|
1363
|
+
if (from !== to) {
|
|
1364
|
+
node.style.height = from + 'px';
|
|
1365
|
+
void node.offsetHeight;
|
|
1366
|
+
node.style.height = to + 'px';
|
|
1367
|
+
clearTimeout(node.opening);
|
|
1368
|
+
node.opening = setTimeout(function () { node.style.height = ''; }, 340);
|
|
1369
|
+
}
|
|
1370
|
+
requestAnimationFrame(function () { requestAnimationFrame(function () { Array.prototype.forEach.call(node.querySelectorAll('.fresh'), function (p) { p.classList.remove('fresh'); }); }); });
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// how long the idea has been written, in plain words: the Mac's own count
|
|
1374
|
+
// when the card has it, else from when it began on the Mac's clock when
|
|
1375
|
+
// that is sane, else from when the page first saw the calm line
|
|
1376
|
+
var seen = { key: null, at: 0 };
|
|
1377
|
+
var waitClock = { key: null, ms: 0, at: 0 };
|
|
1378
|
+
function sinceWords(ms) {
|
|
1379
|
+
var s = Math.round(ms / 1000);
|
|
1380
|
+
if (!(s >= 5) || s > 3600) return '';
|
|
1381
|
+
if (s < 60) return Math.max(5, Math.round(s / 5) * 5) + ' seconds so far';
|
|
1382
|
+
var m = Math.round(s / 60);
|
|
1383
|
+
return m === 1 ? 'about a minute so far' : 'about ' + m + ' minutes so far';
|
|
1384
|
+
}
|
|
1385
|
+
function tickWait() {
|
|
1386
|
+
var node = box().querySelector('.waittime');
|
|
1387
|
+
var card = bet.shown;
|
|
1388
|
+
if (!node || !isWait(card) || !waiting()) { clearInterval(bet.clock); bet.clock = null; return; }
|
|
1389
|
+
var key = String(card.id).split('#')[0];
|
|
1390
|
+
if (seen.key !== key) seen = { key: key, at: Date.now() };
|
|
1391
|
+
var began = typeof card.started_at === 'number' ? card.started_at : Date.parse(card.started_at || '');
|
|
1392
|
+
var ms = waitClock.key === key ? waitClock.ms + (Date.now() - waitClock.at) : Date.now() - began;
|
|
1393
|
+
if (!(ms >= 0 && ms < 3600000)) ms = Date.now() - seen.at;
|
|
1394
|
+
var words = sinceWords(ms);
|
|
1395
|
+
if (node.textContent !== words) node.textContent = words;
|
|
1396
|
+
if (!bet.clock) bet.clock = setInterval(tickWait, 1000);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// A note on the calm line: the words hold in the box for the moment it
|
|
1400
|
+
// takes, then the box clears and stays open, and the note and the answer
|
|
1401
|
+
// to it show above. Nothing moves on: the idea that comes in is the one
|
|
1402
|
+
// written with it. Words that did not reach the Mac stay in the box.
|
|
1403
|
+
function sendNote(field, text) {
|
|
1404
|
+
var from = bet.shown;
|
|
1405
|
+
var input = field.querySelector('input');
|
|
1406
|
+
var body = { card_id: from.id, text: text, send: sendId([from.id, 'the box', text].join(' | ')) };
|
|
1407
|
+
bet.noting = true;
|
|
1408
|
+
field.classList.add('chosen');
|
|
1409
|
+
input.readOnly = true;
|
|
1410
|
+
var note = box().querySelector('.act .note');
|
|
1411
|
+
if (note) note.textContent = '';
|
|
1412
|
+
api('/act', body).catch(offline).then(function (r) {
|
|
1413
|
+
bet.noting = false;
|
|
1414
|
+
field.classList.remove('chosen');
|
|
1415
|
+
input.readOnly = false;
|
|
1416
|
+
// the owner went back to the big orb meanwhile: the Mac has the note, and nothing here moves
|
|
1417
|
+
if (!waiting()) return;
|
|
1418
|
+
if (r.ok && r.card && sameCard(r.card, from)) { input.value = ''; input.blur(); live(r.card); return; }
|
|
1419
|
+
// the Mac moved on (the idea landed meanwhile): it comes in, and the words go back in its box
|
|
1420
|
+
if (r.card && !sameCard(r.card, from)) { landFromWait(r.card, r.ok ? null : function () { unsent(r.card, r, text, null); }); return; }
|
|
1421
|
+
if (note) note.textContent = (r && r.message) || 'that did not go through. try again.';
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// off the calm line, home or to an act's next card: its words fade, then
|
|
1426
|
+
// the sphere glides back to its own place and size
|
|
1427
|
+
function leaveWait(then, fast) {
|
|
1428
|
+
var b = box();
|
|
1429
|
+
stopWatch();
|
|
1430
|
+
if (!fast) orb.calm();
|
|
1431
|
+
b.classList.remove('wait-in');
|
|
1432
|
+
b.classList.add('leave', 'fast');
|
|
1433
|
+
setTimeout(function () {
|
|
1434
|
+
b.hidden = true;
|
|
1435
|
+
b.classList.remove('leave', 'fast', 'holding', 'up');
|
|
1436
|
+
document.body.classList.remove('has-card', 'wide', 'waiting');
|
|
1437
|
+
stageFade();
|
|
1438
|
+
document.getElementById('orb').setAttribute('aria-label', 'the atris orb. tap to think of one new idea for the business');
|
|
1439
|
+
then();
|
|
1440
|
+
}, 220);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// The idea lands: the calm line's words fade while the dots spiral in
|
|
1444
|
+
// from wherever they are, and the card grows out of the point.
|
|
1445
|
+
function landFromWait(card, after) {
|
|
1446
|
+
var b = box();
|
|
1447
|
+
bet.busy = true;
|
|
1448
|
+
stopWatch();
|
|
1449
|
+
b.classList.remove('wait-in');
|
|
1450
|
+
b.classList.add('leave', 'fast');
|
|
1451
|
+
var off = function () {
|
|
1452
|
+
b.classList.remove('leave', 'fast');
|
|
1453
|
+
document.body.classList.remove('waiting');
|
|
1454
|
+
document.getElementById('orb').setAttribute('aria-label', 'the atris orb. tap to think of one new idea for the business');
|
|
1455
|
+
};
|
|
1456
|
+
if (reduced) {
|
|
1457
|
+
setTimeout(function () { off(); orb.settled(); showCard(card, true); bet.busy = false; if (after) after(); }, 200);
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
orb.play(function (phase) {
|
|
1461
|
+
if (phase === 'cover') { off(); showCard(card, true, true, true); if (after) after(); }
|
|
1462
|
+
if (phase === 'settled') bet.busy = false;
|
|
1463
|
+
}, true, true);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// a card the Mac has now that is not the one on screen
|
|
1467
|
+
function arrive(card) {
|
|
1468
|
+
if (waiting()) { landFromWait(card); return; }
|
|
1469
|
+
if (isWait(card)) { leave(function () { enterWait(card); }, true); return; }
|
|
1470
|
+
turnTo(card);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1053
1473
|
// where the sphere sits once the card is gone, read before anything moves,
|
|
1054
1474
|
// in page coordinates (the screen's once the page is back at the top)
|
|
1055
1475
|
function sphereSpot() {
|
|
@@ -1075,7 +1495,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1075
1495
|
var b = box();
|
|
1076
1496
|
b.classList.remove('holding');
|
|
1077
1497
|
bet.shown = card;
|
|
1078
|
-
|
|
1498
|
+
document.body.setAttribute('data-in-place', '');
|
|
1499
|
+
turnInPlace(b, function () { draw(card); }, function () { bet.busy = false; document.body.removeAttribute('data-in-place'); measured(); if (card.watch) watchWait(); else stopWatch(); });
|
|
1079
1500
|
}
|
|
1080
1501
|
|
|
1081
1502
|
function turnInPlace(b, paint, after) {
|
|
@@ -1105,18 +1526,21 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1105
1526
|
b.classList.remove('turn-in');
|
|
1106
1527
|
b.style.height = to + 'px';
|
|
1107
1528
|
ghost.classList.add('gone');
|
|
1529
|
+
var turnMs = 80 + TURN_IN_MS + 20;
|
|
1530
|
+
planned('turn', turnMs);
|
|
1108
1531
|
setTimeout(function () {
|
|
1109
1532
|
ghost.remove();
|
|
1110
1533
|
b.classList.remove('turning');
|
|
1111
1534
|
b.style.height = '';
|
|
1112
1535
|
after();
|
|
1113
|
-
},
|
|
1536
|
+
}, turnMs);
|
|
1114
1537
|
}
|
|
1115
1538
|
|
|
1116
|
-
// While the calm line shows, look every WAIT_LOOK_MS
|
|
1117
|
-
//
|
|
1118
|
-
//
|
|
1119
|
-
//
|
|
1539
|
+
// While the calm line shows, look every WAIT_LOOK_MS: its live words are
|
|
1540
|
+
// kept fresh, even while the owner types, and once the Mac has the idea
|
|
1541
|
+
// ready it is asked for the way a tap does and grows out of the dots. It
|
|
1542
|
+
// waits while the owner types there (the box has the cursor or any words)
|
|
1543
|
+
// or a note is on its way, and on the big orb it waits for their tap.
|
|
1120
1544
|
var WAIT_LOOK_MS = 1500;
|
|
1121
1545
|
function typingOnCard() {
|
|
1122
1546
|
var input = box().querySelector('.field input');
|
|
@@ -1125,34 +1549,35 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1125
1549
|
function stopWatch() { if (bet.watching) { clearInterval(bet.watching); bet.watching = null; } }
|
|
1126
1550
|
function watchWait() {
|
|
1127
1551
|
if (bet.watching) return;
|
|
1128
|
-
var quiet = function () { return !bet.busy && document.body.classList.contains('has-card') && !typingOnCard(); };
|
|
1552
|
+
var quiet = function () { return !bet.busy && !bet.noting && document.body.classList.contains('has-card') && !typingOnCard(); };
|
|
1129
1553
|
bet.watching = setInterval(function () {
|
|
1130
1554
|
if (!bet.shown || !bet.shown.watch) { stopWatch(); return; }
|
|
1131
|
-
if (
|
|
1555
|
+
if (bet.busy || bet.noting) return;
|
|
1132
1556
|
api('/card').then(function (s) {
|
|
1133
|
-
if (s.http !== 200 || !
|
|
1134
|
-
if (s.card
|
|
1135
|
-
if (
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1557
|
+
if (s.http !== 200 || !s.card || !bet.shown || !bet.shown.watch || bet.busy || bet.noting) return;
|
|
1558
|
+
if (sameCard(s.card, bet.shown)) live(s.card);
|
|
1559
|
+
else if (quiet()) { bet.busy = true; arrive(s.card); return; }
|
|
1560
|
+
if (!quiet() || !(s.queue > 0)) return;
|
|
1561
|
+
bet.busy = true;
|
|
1562
|
+
api('/act', { orb: true, card_id: bet.shown.id }).catch(offline).then(function (r) {
|
|
1563
|
+
if (r.ok && r.card && !sameCard(r.card, bet.shown)) { arrive(r.card); return; }
|
|
1564
|
+
bet.busy = false;
|
|
1565
|
+
if (r.ok && r.card) live(r.card);
|
|
1566
|
+
});
|
|
1141
1567
|
}).catch(function () { /* the next look tries again */ });
|
|
1142
1568
|
}, WAIT_LOOK_MS);
|
|
1143
1569
|
}
|
|
1144
1570
|
|
|
1145
|
-
function isLive(s) { return Boolean(s && s.card); }
|
|
1146
|
-
|
|
1147
1571
|
// ---- the orb ----------------------------------------------------------------------------
|
|
1148
1572
|
// The big orb: the next card, with the spiral, as on page open. An idea
|
|
1149
1573
|
// that landed behind the calm line is the next one, so it is never passed.
|
|
1150
1574
|
function think() {
|
|
1151
1575
|
if (bet.busy) return;
|
|
1576
|
+
// on the calm line the dots are the orb on the card: a tap goes back to the big one
|
|
1577
|
+
if (waiting()) { goHome(); return; }
|
|
1152
1578
|
buzz();
|
|
1153
1579
|
bet.busy = true;
|
|
1154
|
-
|
|
1155
|
-
document.body.removeAttribute('data-reveal-ms');
|
|
1580
|
+
tapped();
|
|
1156
1581
|
var failed = function () {
|
|
1157
1582
|
bet.busy = false;
|
|
1158
1583
|
orb.relax();
|
|
@@ -1161,6 +1586,10 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1161
1586
|
api('/act', { orb: true, card_id: bet.shown ? bet.shown.id : null }).then(function (r) {
|
|
1162
1587
|
if (r.http !== 200 || !r.card) { if (r.http === 200) { bet.busy = false; orb.relax(); line.set('tap to think', false); return; } failed(); return; }
|
|
1163
1588
|
line.set('', false);
|
|
1589
|
+
// nothing ready yet: the calm line, with the sphere where it is and its dots moving
|
|
1590
|
+
if (isWait(r.card)) { enterWait(r.card); return; }
|
|
1591
|
+
// the big orb moves once the answer is in
|
|
1592
|
+
bet.waited = performance.now() - bet.tapAt;
|
|
1164
1593
|
orb.play(function (phase) { if (phase === 'settled') { bet.busy = false; showCard(r.card, true, true); } }, true);
|
|
1165
1594
|
}, failed);
|
|
1166
1595
|
}
|
|
@@ -1174,6 +1603,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1174
1603
|
buzz();
|
|
1175
1604
|
bet.busy = true;
|
|
1176
1605
|
stopWatch();
|
|
1606
|
+
// from the calm line the sphere is already there; its line comes once the card's words are gone
|
|
1607
|
+
if (waiting()) { leave(function () { line.set('tap to think', false); bet.busy = false; }); return; }
|
|
1177
1608
|
// the line under the big orb is ready before the sphere is back
|
|
1178
1609
|
line.set('tap to think', false);
|
|
1179
1610
|
leave(function () { bet.busy = false; });
|
|
@@ -1196,6 +1627,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1196
1627
|
function firstLook() {
|
|
1197
1628
|
api('/card').then(function (s) {
|
|
1198
1629
|
if (s.http !== 200) return;
|
|
1630
|
+
if (s.card && isWait(s.card)) { enterWait(s.card, true); return; }
|
|
1199
1631
|
if (s.card) { orb.settled(); showCard(s.card, false); return; }
|
|
1200
1632
|
line.set(s.status === 'skipped' ? 'not now. tap when you want another' : 'tap to think', false);
|
|
1201
1633
|
});
|
|
@@ -1208,11 +1640,12 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1208
1640
|
if (bet.busy) return;
|
|
1209
1641
|
buzz();
|
|
1210
1642
|
bet.busy = true;
|
|
1211
|
-
|
|
1643
|
+
tapped();
|
|
1212
1644
|
var answer = api('/act', { open: which, card_id: bet.shown ? bet.shown.id : null }).catch(offline);
|
|
1213
1645
|
if (!document.body.classList.contains('has-card')) {
|
|
1214
1646
|
answer.then(function (r) {
|
|
1215
1647
|
if (!r.ok || !r.card) { bet.busy = false; line.set((r && r.message) || 'that did not go through. tap to try again', true); return; }
|
|
1648
|
+
bet.waited = performance.now() - bet.tapAt;
|
|
1216
1649
|
spiralTo(r.card, null);
|
|
1217
1650
|
});
|
|
1218
1651
|
return;
|
|
@@ -1274,14 +1707,15 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1274
1707
|
|
|
1275
1708
|
// Every few seconds: the footer counts, and the card on screen. When the
|
|
1276
1709
|
// Mac's card has changed (a new id: its options are not the ones shown),
|
|
1277
|
-
// it turns in place,
|
|
1710
|
+
// it turns in place, or grows out of the dots on the calm line, unless the
|
|
1711
|
+
// owner is typing or a motion is under way.
|
|
1278
1712
|
function refresh() {
|
|
1279
1713
|
return api('/card').then(function (s) {
|
|
1280
1714
|
if (s.http !== 200) { line.set('this link stopped working. start gm phone again on the mac.', true); return; }
|
|
1281
1715
|
drawWorking(s.team_working || 0);
|
|
1282
|
-
if (s.card && bet.shown && s.card
|
|
1716
|
+
if (s.card && bet.shown && !sameCard(s.card, bet.shown) && !bet.busy && !bet.noting && !typingOnCard() && document.body.classList.contains('has-card')) {
|
|
1283
1717
|
bet.busy = true;
|
|
1284
|
-
|
|
1718
|
+
arrive(s.card);
|
|
1285
1719
|
}
|
|
1286
1720
|
}).catch(function () { /* the next refresh tries again */ });
|
|
1287
1721
|
}
|