atris 3.74.6 → 3.74.8
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/bin/atris.js +9 -0
- package/commands/idea.js +36 -0
- package/commands/zen.js +4 -2
- package/lib/engine-login.js +105 -0
- package/lib/fleet.js +4 -1
- package/lib/gm-bet.js +706 -161
- package/lib/gm-card.js +45 -4
- package/lib/gm-phone-page.js +450 -59
- package/lib/idea-tick.js +20 -0
- package/lib/values.js +38 -0
- package/lib/zen-apply.js +16 -1
- package/lib/zen.js +88 -14
- package/package.json +1 -1
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,24 @@ 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 measured
|
|
361
|
+
var stats = { frame: 0, mode: 'idle', step: 0, mean: 0 };
|
|
362
|
+
canvas.__orb = stats;
|
|
312
363
|
// Two speeds, chosen by truth. A bet already waiting in the queue plays
|
|
313
364
|
// the same curves at FAST of their length (tap to card in about two seconds, unhurried).
|
|
314
365
|
// A bet still being made plays the full landing length, and the spiral
|
|
@@ -360,7 +411,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
360
411
|
for (var i = 0; i < count; i++) {
|
|
361
412
|
var gx = gauss(), gy = gauss(), gz = gauss();
|
|
362
413
|
var n = Math.hypot(gx, gy, gz) || 1;
|
|
363
|
-
dots.push({
|
|
414
|
+
dots.push({
|
|
415
|
+
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,
|
|
416
|
+
// its own slow loop in the swarm
|
|
417
|
+
f1: 0.5 + Math.random() * 0.8, f2: 0.5 + Math.random() * 0.8, p1: Math.random() * 6.283, p2: Math.random() * 6.283,
|
|
418
|
+
});
|
|
364
419
|
}
|
|
365
420
|
}
|
|
366
421
|
|
|
@@ -372,7 +427,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
372
427
|
var cosA = Math.cos(angle), sinA = Math.sin(angle);
|
|
373
428
|
for (var i = 0; i < dots.length; i++) {
|
|
374
429
|
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; }
|
|
430
|
+
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; }
|
|
431
|
+
d.kx = 0; d.ky = 0;
|
|
376
432
|
var len = Math.hypot(d.bx - cx, d.by - cy);
|
|
377
433
|
d.dl = (1 - Math.min(1, len / R)) * 0.28 + Math.random() * 0.05;
|
|
378
434
|
}
|
|
@@ -384,7 +440,12 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
384
440
|
var dt = Math.min(0.05, (now - (lastT || now)) / 1000);
|
|
385
441
|
lastT = now;
|
|
386
442
|
ctx.clearRect(0, 0, w, h);
|
|
387
|
-
|
|
443
|
+
var swarming = mode === 'swarm' && !reduced;
|
|
444
|
+
var toward = 1 - Math.pow(1 - SWARM_EASE, dt * 60);
|
|
445
|
+
swarmK += ((swarming ? 1 : 0) - swarmK) * toward;
|
|
446
|
+
spin += ((swarming ? SWARM_SPIN : IDLE_SPIN) - spin) * toward;
|
|
447
|
+
clock += dt;
|
|
448
|
+
if (!reduced && (mode === 'idle' || mode === 'swarm')) angle += dt * spin;
|
|
388
449
|
wave.age += dt;
|
|
389
450
|
light.x += (light.tx - light.x) * 0.06;
|
|
390
451
|
light.y += (light.ty - light.y) * 0.06;
|
|
@@ -400,13 +461,19 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
400
461
|
else age += (ageTo - age) * 0.05;
|
|
401
462
|
}
|
|
402
463
|
var cosA = Math.cos(angle), sinA = Math.sin(angle);
|
|
464
|
+
var most = 0, sum = 0, wander = WANDER * R * swarmK;
|
|
403
465
|
for (var i = 0; i < dots.length; i++) {
|
|
404
466
|
var d = dots[i];
|
|
405
467
|
var fade = 1, shrink = 1;
|
|
468
|
+
var x0 = d.sx, y0 = d.sy;
|
|
406
469
|
var rx = d.x * cosA + d.z * sinA;
|
|
407
470
|
var rz = -d.x * sinA + d.z * cosA;
|
|
408
|
-
if (mode === 'idle') {
|
|
471
|
+
if (mode === 'idle' || mode === 'swarm') {
|
|
409
472
|
var homeX = cx + rx * R, homeY = cy + d.y * R;
|
|
473
|
+
if (wander > 0.05) {
|
|
474
|
+
homeX += Math.sin(clock * d.f1 + d.p1) * wander;
|
|
475
|
+
homeY += Math.sin(clock * d.f2 + d.p2) * wander;
|
|
476
|
+
}
|
|
410
477
|
if (waveOn) {
|
|
411
478
|
var wx = homeX - wave.x, wy = homeY - wave.y;
|
|
412
479
|
var wd = Math.sqrt(wx * wx + wy * wy) || 1;
|
|
@@ -437,11 +504,14 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
437
504
|
var e = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
438
505
|
var ox = (d.bx - cx) * (1 - e), oy = (d.by - cy) * (1 - e);
|
|
439
506
|
var turn = e * 1.5;
|
|
440
|
-
|
|
441
|
-
d.
|
|
507
|
+
// the speed it had carries on and fades, then the spiral has it
|
|
508
|
+
d.kx += d.vx; d.ky += d.vy; d.vx *= CARRY; d.vy *= CARRY;
|
|
509
|
+
d.sx = cx + ox * Math.cos(turn) - oy * Math.sin(turn) + d.kx * (1 - e);
|
|
510
|
+
d.sy = cy + ox * Math.sin(turn) + oy * Math.cos(turn) + d.ky * (1 - e);
|
|
442
511
|
fade = 1 - Math.pow(Math.max(0, (e - 0.8) / 0.2), 1.5);
|
|
443
512
|
shrink = 1 - 0.35 * e;
|
|
444
513
|
}
|
|
514
|
+
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
515
|
// front dots are lit; the back half is a dim ghost. amber in shadow, cream where lit.
|
|
446
516
|
var lit = rx * light.x + d.y * light.y + rz * lz;
|
|
447
517
|
var front = rz > 0;
|
|
@@ -453,6 +523,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
453
523
|
ctx.arc(d.sx, d.sy, d.r * (front ? 0.6 + l * 0.7 : 0.6) * shrink, 0, Math.PI * 2);
|
|
454
524
|
ctx.fill();
|
|
455
525
|
}
|
|
526
|
+
stats.frame += 1; stats.mode = mode; stats.step = most; stats.mean = sum / Math.min(64, dots.length || 1);
|
|
456
527
|
if (mode === 'pull') {
|
|
457
528
|
// where the dots land: one bright point that swells until the lava is born
|
|
458
529
|
var arrived = Math.min(1, Math.max(0, (age - 0.3) / 0.7));
|
|
@@ -463,7 +534,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
463
534
|
var left = 1 - (now - bridgeAt) / BRIDGE_MS;
|
|
464
535
|
if (left > 0) point(left, 1); else bridgeAt = 0;
|
|
465
536
|
}
|
|
466
|
-
|
|
537
|
+
// still once a card is up, and under reduced motion the sphere is drawn once and holds
|
|
538
|
+
if (mode === 'card' || (reduced && (mode === 'idle' || mode === 'swarm'))) { raf = 0; return; }
|
|
467
539
|
raf = requestAnimationFrame(frame);
|
|
468
540
|
}
|
|
469
541
|
|
|
@@ -496,13 +568,17 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
496
568
|
// A queued bet (the fast pace) blooms contained: the lava grows to BLOOM
|
|
497
569
|
// of the smaller window side, the page never dims, and it settles.
|
|
498
570
|
function grow() {
|
|
571
|
+
// fixed while it grows past the screen, so the page never scrolls sideways
|
|
572
|
+
window.scrollTo(0, 0);
|
|
499
573
|
var rect = canvas.getBoundingClientRect();
|
|
500
574
|
var contained = pace < 1;
|
|
501
575
|
var growMs = quick ? ACT_GROW_MS : contained ? FAST_GROW_MS : (GROW_MS * pace);
|
|
502
576
|
var settleMs = quick ? ACT_SETTLE_MS : contained ? FAST_SETTLE_MS : (SETTLE_MS * pace);
|
|
503
|
-
|
|
577
|
+
// the point the dots drew, where the sphere is on screen (smaller and higher on the calm line)
|
|
578
|
+
var vx = rect.left + rect.width / 2, vy = rect.top + rect.height / 2;
|
|
579
|
+
var bloom = quick ? ACT_BLOOM : BLOOM;
|
|
504
580
|
var reach = contained
|
|
505
|
-
?
|
|
581
|
+
? bloom * Math.min(window.innerWidth, window.innerHeight) / 2
|
|
506
582
|
: Math.hypot(Math.max(vx, window.innerWidth - vx), Math.max(vy, window.innerHeight - vy)) * 1.08;
|
|
507
583
|
base = Math.round(Math.min(1000, Math.max(280, (2 * reach) / MAX_SCALE)));
|
|
508
584
|
var sMax = (2 * reach) / base;
|
|
@@ -510,14 +586,15 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
510
586
|
var rest = REST / base;
|
|
511
587
|
lava.style.width = base + 'px';
|
|
512
588
|
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
589
|
origin = { x: vx, y: vy };
|
|
516
590
|
lava.style.position = 'fixed';
|
|
591
|
+
lava.style.opacity = '0';
|
|
592
|
+
place(sMin, origin.x, origin.y);
|
|
517
593
|
lava.style.visibility = 'visible';
|
|
518
594
|
lava.classList.remove('still', 'alive', 'tap');
|
|
519
595
|
void lava.offsetWidth;
|
|
520
596
|
lava.classList.add('alive');
|
|
597
|
+
if (contained) { rise(growMs, settleMs, sMin, sMax, rest); return; }
|
|
521
598
|
var t0 = performance.now();
|
|
522
599
|
var step = function (now) {
|
|
523
600
|
var t = Math.min(1, (now - t0) / growMs);
|
|
@@ -545,6 +622,38 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
545
622
|
requestAnimationFrame(step);
|
|
546
623
|
}
|
|
547
624
|
|
|
625
|
+
// A contained bloom is one motion. The lava is born at the point and
|
|
626
|
+
// swells for growMs on a soft curve while it already drifts up, then
|
|
627
|
+
// eases down into the small orb at the top over settleMs. Its size turns at the peak with no
|
|
628
|
+
// stop, and its place moves on one curve the whole way, so nothing snaps.
|
|
629
|
+
// The card fades up under it from the peak.
|
|
630
|
+
function rise(growMs, settleMs, sMin, sMax, rest) {
|
|
631
|
+
var total = growMs + settleMs, k = growMs / total;
|
|
632
|
+
var p = restPoint();
|
|
633
|
+
var covered = false;
|
|
634
|
+
var t0 = performance.now();
|
|
635
|
+
var step = function (now) {
|
|
636
|
+
var u = Math.min(1, (now - t0) / total);
|
|
637
|
+
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));
|
|
638
|
+
var m = ease(u);
|
|
639
|
+
place(s, origin.x + (p.x - origin.x) * m, origin.y + (p.y - origin.y) * m);
|
|
640
|
+
lava.style.opacity = String(smooth(0, 0.1, u));
|
|
641
|
+
core.style.opacity = String(1 - smooth(0.04, 0.5, u));
|
|
642
|
+
dim.style.opacity = '0';
|
|
643
|
+
if (!covered && u >= k) {
|
|
644
|
+
covered = true;
|
|
645
|
+
mode = 'card';
|
|
646
|
+
stop();
|
|
647
|
+
ctx.clearRect(0, 0, w, h);
|
|
648
|
+
canvas.style.visibility = 'hidden';
|
|
649
|
+
if (onBorn) onBorn('cover');
|
|
650
|
+
}
|
|
651
|
+
if (u < 1) { requestAnimationFrame(step); return; }
|
|
652
|
+
settled();
|
|
653
|
+
};
|
|
654
|
+
requestAnimationFrame(step);
|
|
655
|
+
}
|
|
656
|
+
|
|
548
657
|
// the small lava orb at the top, still, and itself the next tap
|
|
549
658
|
function settled() {
|
|
550
659
|
var p = restPoint();
|
|
@@ -587,7 +696,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
587
696
|
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
697
|
pointer.inside = false;
|
|
589
698
|
age = -1; timed = false; fired = false;
|
|
590
|
-
|
|
699
|
+
// after an act the next card is on its way, so the dots come out swarming and never stop
|
|
700
|
+
mode = fast ? 'swarm' : 'idle';
|
|
591
701
|
bridgeAt = reduced ? 0 : performance.now();
|
|
592
702
|
start();
|
|
593
703
|
setTimeout(cb, reduced ? 0 : beatMs);
|
|
@@ -602,7 +712,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
602
712
|
var t0 = performance.now();
|
|
603
713
|
var step = function (now) {
|
|
604
714
|
var k = Math.min(1, (now - t0) / glideMs);
|
|
605
|
-
var t = ease(k);
|
|
715
|
+
var t = fast ? 0.5 - 0.5 * Math.cos(Math.PI * k) : ease(k);
|
|
606
716
|
if (now - t0 >= leaveMs) takeOff();
|
|
607
717
|
place(from.s + (sEnd - from.s) * t, from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t);
|
|
608
718
|
core.style.opacity = String(smooth(0.3, 0.9, k));
|
|
@@ -675,6 +785,24 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
675
785
|
mode = 'idle';
|
|
676
786
|
start();
|
|
677
787
|
},
|
|
788
|
+
// the next card is on its way: the sphere is on the page and its dots keep moving
|
|
789
|
+
swarm: function () {
|
|
790
|
+
if (mode === 'card') {
|
|
791
|
+
canvas.style.visibility = 'visible';
|
|
792
|
+
measure();
|
|
793
|
+
for (var i = 0; i < dots.length; i++) { dots[i].sx = cx; dots[i].sy = cy; dots[i].vx = 0; dots[i].vy = 0; }
|
|
794
|
+
lava.classList.remove('tap', 'still', 'alive');
|
|
795
|
+
lava.style.visibility = 'hidden';
|
|
796
|
+
lava.style.opacity = '0';
|
|
797
|
+
lava.tabIndex = -1;
|
|
798
|
+
age = -1; timed = false; fired = false;
|
|
799
|
+
mode = 'swarm';
|
|
800
|
+
}
|
|
801
|
+
if (mode === 'idle') mode = 'swarm';
|
|
802
|
+
start();
|
|
803
|
+
},
|
|
804
|
+
// nothing is on its way: back to the idle drift under the pointer
|
|
805
|
+
calm: function () { if (mode === 'swarm') mode = 'idle'; start(); },
|
|
678
806
|
melt: melt,
|
|
679
807
|
leaveMs: reduced ? 200 : LEAVE_MS,
|
|
680
808
|
// a bet is already up when the page loads: the settled orb, no motion
|
|
@@ -689,7 +817,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
689
817
|
// card, and nothing from the card before survives a draw. Every tap posts
|
|
690
818
|
// the card's id and the option's id, or the words typed in its box, to
|
|
691
819
|
// /card; the Mac refuses anything else, and its answer is the next card.
|
|
692
|
-
var bet = { shown: null, watching: null, tapAt: 0 };
|
|
820
|
+
var bet = { shown: null, watching: null, tapAt: 0, noting: false, clock: null };
|
|
693
821
|
// while an act or a turn is under way the card takes no taps; the page
|
|
694
822
|
// marks it on the body too, so anyone reading the page can tell
|
|
695
823
|
var busyNow = false;
|
|
@@ -726,13 +854,16 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
726
854
|
if (lines.getClientRects().length > 1) title.classList.add('said');
|
|
727
855
|
}
|
|
728
856
|
if (card.body) b.appendChild(el('p', 'why', card.body));
|
|
857
|
+
var calm = isWait(card);
|
|
858
|
+
// the calm line: what the writer is doing now, how long it has been, and the notes typed here
|
|
859
|
+
if (calm) { b.appendChild(el('p', 'waitstep')); b.appendChild(el('span', 'waittime')); b.appendChild(el('div', 'waitnotes')); }
|
|
729
860
|
// a reply keeps the title's spot; the turns so far sit quietly under it
|
|
730
|
-
if (card.thread && card.thread.length) {
|
|
861
|
+
if (!calm && card.thread && card.thread.length) {
|
|
731
862
|
var thread = b.appendChild(el('div', 'thread'));
|
|
732
863
|
card.thread.forEach(function (t) { thread.appendChild(el('p', 'turn ' + (t.who === 'owner' ? 'you' : 'orb'), t.text)); });
|
|
733
864
|
}
|
|
734
865
|
// 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(' · ')));
|
|
866
|
+
if (card.job && !card.table && !calm) b.appendChild(el('p', 'jobline', [card.job.member, card.job.role].filter(Boolean).join(' · ')));
|
|
736
867
|
var inCells = card.options.filter(function (o) { return o.cell; });
|
|
737
868
|
if (card.table) b.appendChild(drawTable(card, inCells));
|
|
738
869
|
if (card.time) b.appendChild(el('span', 'time', card.time));
|
|
@@ -758,6 +889,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
758
889
|
act.appendChild(el('p', 'links note'));
|
|
759
890
|
b.appendChild(act);
|
|
760
891
|
if (card.input) b.appendChild(inputLine(card.input));
|
|
892
|
+
if (calm) live(card, true);
|
|
761
893
|
}
|
|
762
894
|
|
|
763
895
|
// a table: one row per line, a line between every cell; an option with a
|
|
@@ -828,7 +960,9 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
828
960
|
return btn;
|
|
829
961
|
}
|
|
830
962
|
|
|
831
|
-
// the card's box: a quiet line that turns into the field in its own spot
|
|
963
|
+
// the card's box: a quiet line that turns into the field in its own spot.
|
|
964
|
+
// On the calm line it is open from the start and stays open, without
|
|
965
|
+
// taking the cursor, and the words are a note for the idea being written.
|
|
832
966
|
function inputLine(input) {
|
|
833
967
|
var wrap = el('div', 'links sayline');
|
|
834
968
|
var open = wrap.appendChild(el('button', 'quiet', input.label || 'or say something'));
|
|
@@ -837,6 +971,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
837
971
|
if (bet.busy) return;
|
|
838
972
|
typeHere(open, input.placeholder || 'say something', input.max || 280, function (text, field) { send(field, { text: text }, null); });
|
|
839
973
|
});
|
|
974
|
+
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
975
|
return wrap;
|
|
841
976
|
}
|
|
842
977
|
|
|
@@ -869,10 +1004,12 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
869
1004
|
// The tapped button becomes the field at its own size and spot, with the
|
|
870
1005
|
// cursor already in it, and the other options dim. Enter or send hands
|
|
871
1006
|
// the words to sent(text, field). Left empty, it turns back into the button.
|
|
872
|
-
|
|
1007
|
+
// stay: the calm line's box, open from the start without the cursor, and
|
|
1008
|
+
// open after every send.
|
|
1009
|
+
function typeHere(btn, placeholder, maxLength, sent, stay) {
|
|
873
1010
|
var field = el('div', btn.classList.contains('choice') ? 'choice field' : 'field');
|
|
874
1011
|
// its exact height, to the subpixel, so nothing under it moves
|
|
875
|
-
field.style.height = btn.getBoundingClientRect().height + 'px';
|
|
1012
|
+
if (stay) field.classList.add('stay'); else field.style.height = btn.getBoundingClientRect().height + 'px';
|
|
876
1013
|
var input = el('input');
|
|
877
1014
|
input.type = 'text';
|
|
878
1015
|
input.maxLength = maxLength;
|
|
@@ -883,7 +1020,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
883
1020
|
go.type = 'button';
|
|
884
1021
|
var submit = function () {
|
|
885
1022
|
var text = input.value.trim();
|
|
886
|
-
if (text && !bet.busy) sent(text, field);
|
|
1023
|
+
if (text && !bet.busy && !bet.noting) sent(text, field);
|
|
887
1024
|
};
|
|
888
1025
|
// a press on send keeps the cursor in the field, so it never closes first
|
|
889
1026
|
go.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
|
@@ -892,13 +1029,14 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
892
1029
|
if (e.key === 'Enter') { e.preventDefault(); submit(); }
|
|
893
1030
|
});
|
|
894
1031
|
input.addEventListener('blur', function () {
|
|
895
|
-
if (input.value.trim() || field.classList.contains('chosen') || !field.isConnected) return;
|
|
1032
|
+
if (stay || input.value.trim() || field.classList.contains('chosen') || !field.isConnected) return;
|
|
896
1033
|
field.replaceWith(btn);
|
|
897
1034
|
dimOthers(null);
|
|
898
1035
|
});
|
|
899
1036
|
field.appendChild(input);
|
|
900
1037
|
field.appendChild(go);
|
|
901
1038
|
btn.replaceWith(field);
|
|
1039
|
+
if (stay) return field;
|
|
902
1040
|
dimOthers(field);
|
|
903
1041
|
input.focus();
|
|
904
1042
|
return field;
|
|
@@ -936,9 +1074,10 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
936
1074
|
var heldAt = hold(spot);
|
|
937
1075
|
var answer = api('/act', body).catch(offline);
|
|
938
1076
|
if (option && IN_PLACE.indexOf(option.action) >= 0) {
|
|
1077
|
+
document.body.setAttribute('data-in-place', '');
|
|
939
1078
|
Promise.all([answer, afterHold(heldAt)]).then(function (both) {
|
|
940
1079
|
var r = both[0];
|
|
941
|
-
if (!r.ok || !r.card) { bet.busy = false; unsent(from, r, body.text, option); return; }
|
|
1080
|
+
if (!r.ok || !r.card) { bet.busy = false; document.body.removeAttribute('data-in-place'); unsent(from, r, body.text, option); return; }
|
|
942
1081
|
turnTo(r.card);
|
|
943
1082
|
});
|
|
944
1083
|
return;
|
|
@@ -956,12 +1095,12 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
956
1095
|
landed = null;
|
|
957
1096
|
var wait = r.ack && lineAt ? Math.max(0, AFTER_MS - (performance.now() - lineAt)) : 0;
|
|
958
1097
|
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; }
|
|
1098
|
+
// nothing more to show: the sphere stays, with its line, and its dots settle into the idle drift
|
|
1099
|
+
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
1100
|
// the card the Mac has on screen now is the one that comes back; a
|
|
962
1101
|
// refusal comes back on it with one line why
|
|
963
1102
|
var then = r.card || from;
|
|
964
|
-
if (!then) { bet.busy = false; line.set('that did not go through. tap to try again', true); return; }
|
|
1103
|
+
if (!then) { bet.busy = false; orb.calm(); line.set('that did not go through. tap to try again', true); return; }
|
|
965
1104
|
if (!r.ok) { spiralTo(then, function () { unsent(then, r, typed, then.id === (from && from.id) ? option : null); }); return; }
|
|
966
1105
|
spiralTo(then, null);
|
|
967
1106
|
}, wait);
|
|
@@ -975,27 +1114,49 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
975
1114
|
}
|
|
976
1115
|
|
|
977
1116
|
// 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
|
|
1117
|
+
// sphere; the card leaves the page once it has faded all the way. From the
|
|
1118
|
+
// calm line the sphere is already there: its words fade and it glides home.
|
|
979
1119
|
function leave(then, fast) {
|
|
1120
|
+
if (waiting()) { leaveWait(then, fast); return; }
|
|
980
1121
|
var b = box();
|
|
981
1122
|
var to = sphereSpot();
|
|
982
1123
|
b.classList.remove('quick', 'acted', 'up');
|
|
983
1124
|
b.classList.add('leave');
|
|
1125
|
+
b.classList.toggle('fast', Boolean(fast));
|
|
984
1126
|
var swap = function () {
|
|
985
1127
|
b.style.minHeight = '';
|
|
986
1128
|
b.hidden = true;
|
|
987
|
-
b.classList.remove('leave', 'holding');
|
|
1129
|
+
b.classList.remove('leave', 'fast', 'holding');
|
|
988
1130
|
document.body.classList.remove('has-card', 'wide');
|
|
989
1131
|
window.scrollTo(0, 0);
|
|
990
1132
|
};
|
|
991
|
-
|
|
1133
|
+
// reduced motion: no travel; the card and the small orb fade, then the sphere is there
|
|
1134
|
+
if (reduced) {
|
|
1135
|
+
document.getElementById('lava').style.opacity = '0';
|
|
1136
|
+
setTimeout(function () { swap(); stageFade(); orb.melt(then, to, function () {}, fast); }, 200);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
992
1139
|
orb.melt(then, to, swap, fast);
|
|
993
1140
|
}
|
|
994
1141
|
|
|
995
|
-
//
|
|
1142
|
+
// under reduced motion the sphere never travels: it fades in where it now sits
|
|
1143
|
+
function stageFade() {
|
|
1144
|
+
if (!reduced) return;
|
|
1145
|
+
var stage = document.getElementById('stage');
|
|
1146
|
+
stage.style.transition = 'none';
|
|
1147
|
+
stage.style.opacity = '0';
|
|
1148
|
+
void stage.offsetWidth;
|
|
1149
|
+
stage.style.transition = 'opacity 0.2s linear';
|
|
1150
|
+
stage.style.opacity = '';
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// from the sphere: the dots spiral in and the card grows out of the point.
|
|
1154
|
+
// The calm line is not a card that blooms: the sphere stays and its dots keep moving.
|
|
996
1155
|
function spiralTo(card, after) {
|
|
997
1156
|
line.set('', false);
|
|
998
|
-
if (
|
|
1157
|
+
if (isWait(card)) { enterWait(card); if (after) after(); return; }
|
|
1158
|
+
// reduced motion: no spiral; the card fades in where it goes
|
|
1159
|
+
if (reduced) { orb.settled(); showCard(card, true); bet.busy = false; if (after) after(); return; }
|
|
999
1160
|
orb.play(function (phase) {
|
|
1000
1161
|
// the card blooms where the dots land; what comes with it (a line why, the words back in their box) comes with it
|
|
1001
1162
|
if (phase === 'cover') { showCard(card, true, true, true); if (after) after(); }
|
|
@@ -1012,6 +1173,9 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1012
1173
|
// the Mac's own refusals are plain words; anything else reads as one plain line
|
|
1013
1174
|
if (note) note.textContent = (r && r.http === 409 && r.message) || (r && r.http === 0 && r.message) || 'that did not go through. try again.';
|
|
1014
1175
|
if (!typed) return;
|
|
1176
|
+
// the calm line's box is open already: the words go back into it
|
|
1177
|
+
var openBox = !option && box().querySelector('.sayline .field.stay input');
|
|
1178
|
+
if (openBox) { openBox.value = typed; return; }
|
|
1015
1179
|
// the words go back where they were typed, or to this card's own box when the card is a new one
|
|
1016
1180
|
var spot = option ? box().querySelector('[data-option="' + option.id + '"]') : box().querySelector('.sayline .quiet');
|
|
1017
1181
|
if (!spot) return;
|
|
@@ -1030,15 +1194,17 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1030
1194
|
|
|
1031
1195
|
// fast: the quick fade from the big orb; act: the short one after an act
|
|
1032
1196
|
function showCard(card, animate, fast, act) {
|
|
1197
|
+
if (isWait(card)) { enterWait(card, !animate); return; }
|
|
1033
1198
|
var b = box();
|
|
1034
1199
|
bet.shown = card;
|
|
1035
1200
|
// where it starts from is set first: drawing measures the title, and a
|
|
1036
1201
|
// measure is a frame the card would otherwise start from somewhere else
|
|
1037
|
-
b.classList.remove('leave', 'holding');
|
|
1202
|
+
b.classList.remove('leave', 'fast', 'holding', 'wait-in');
|
|
1038
1203
|
b.classList.toggle('quick', Boolean(fast) && !act);
|
|
1039
1204
|
b.classList.toggle('acted', Boolean(act));
|
|
1040
1205
|
b.classList.toggle('up', !animate);
|
|
1041
1206
|
// on the page before it is drawn, so a long title can tell it runs past one line
|
|
1207
|
+
document.body.classList.remove('waiting');
|
|
1042
1208
|
document.body.classList.add('has-card');
|
|
1043
1209
|
b.hidden = false;
|
|
1044
1210
|
draw(card);
|
|
@@ -1050,6 +1216,221 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1050
1216
|
requestAnimationFrame(function () { b.classList.add('up'); });
|
|
1051
1217
|
}
|
|
1052
1218
|
|
|
1219
|
+
// ---- the calm line: the dots keep moving while the next idea is written -----------------
|
|
1220
|
+
// The sphere stays on the page, a little smaller and higher, and its dots
|
|
1221
|
+
// swarm. The card sits under it with what the writer is doing now, and its
|
|
1222
|
+
// box is open: words typed there are a note for the idea being written,
|
|
1223
|
+
// and the card stays. When the idea lands it grows out of the dots. A tap
|
|
1224
|
+
// on the dots goes back to the big orb, as the small orb does on a card.
|
|
1225
|
+
function isWait(card) { return Boolean(card && card.kind === 'wait'); }
|
|
1226
|
+
function waiting() { return document.body.classList.contains('waiting'); }
|
|
1227
|
+
// the calm line is one card while its words change, under any stamp
|
|
1228
|
+
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]))); }
|
|
1229
|
+
|
|
1230
|
+
// the smaller sphere's place: at most 300px, and high enough that the card
|
|
1231
|
+
// under it has WAIT_ROOM. It is read from the sphere's own place in the
|
|
1232
|
+
// layout, which the calm line keeps and a transform never moves.
|
|
1233
|
+
var WAIT_ROOM = 250;
|
|
1234
|
+
function placeWait(animate) {
|
|
1235
|
+
var stage = document.getElementById('stage');
|
|
1236
|
+
var body = document.body;
|
|
1237
|
+
var hidden = !stage.offsetHeight;
|
|
1238
|
+
if (hidden) body.classList.add('has-card', 'waiting');
|
|
1239
|
+
var screenTop = document.getElementById('screen').getBoundingClientRect().top;
|
|
1240
|
+
var h = stage.offsetHeight || 1, top0 = screenTop + stage.offsetTop;
|
|
1241
|
+
if (hidden) body.classList.remove('waiting');
|
|
1242
|
+
var k = Math.min(1, 300 / h, 0.62 * Math.min(window.innerWidth, window.innerHeight) / h);
|
|
1243
|
+
var top = Math.max(24, (window.innerHeight - (k * h + WAIT_ROOM)) / 2);
|
|
1244
|
+
var s = body.style;
|
|
1245
|
+
s.setProperty('--wait-k', k.toFixed(4));
|
|
1246
|
+
s.setProperty('--wait-dy', Math.round(top - top0) + 'px');
|
|
1247
|
+
s.setProperty('--wait-top', Math.round(top + k * h + 6 - screenTop) + 'px');
|
|
1248
|
+
if (animate) return;
|
|
1249
|
+
stage.style.transition = 'none';
|
|
1250
|
+
requestAnimationFrame(function () { requestAnimationFrame(function () { stage.style.transition = ''; }); });
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// a turned phone: the calm line's sphere and card find their place again
|
|
1254
|
+
window.addEventListener('resize', function () { if (waiting()) placeWait(false); });
|
|
1255
|
+
|
|
1256
|
+
// atOnce: the page opened on it, so it is simply there
|
|
1257
|
+
function enterWait(card, atOnce) {
|
|
1258
|
+
var b = box();
|
|
1259
|
+
var was = waiting() && !b.hidden;
|
|
1260
|
+
bet.shown = card;
|
|
1261
|
+
if (atOnce) line.clear(); else line.set('', false);
|
|
1262
|
+
orb.swarm();
|
|
1263
|
+
if (!was) { placeWait(!atOnce && !reduced); stageFade(); }
|
|
1264
|
+
b.classList.remove('leave', 'fast', 'holding', 'quick', 'acted', 'up', 'wait-in');
|
|
1265
|
+
document.body.classList.add('has-card', 'waiting');
|
|
1266
|
+
document.getElementById('orb').setAttribute('aria-label', 'the atris orb, writing your next idea. tap to go back to the big orb');
|
|
1267
|
+
b.hidden = false;
|
|
1268
|
+
draw(card);
|
|
1269
|
+
watchWait();
|
|
1270
|
+
bet.busy = false;
|
|
1271
|
+
if (was || atOnce) { b.classList.add('up'); return; }
|
|
1272
|
+
b.classList.add('wait-in');
|
|
1273
|
+
void b.offsetWidth;
|
|
1274
|
+
requestAnimationFrame(function () { b.classList.add('up'); });
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// The calm line's live words: what the writer is doing now, how long it
|
|
1278
|
+
// has been, and the newest note typed here with the answer to it. draw()
|
|
1279
|
+
// puts them on the card; while it shows they are kept fresh in place. Its
|
|
1280
|
+
// options and its box never change under one id, so the field, its words,
|
|
1281
|
+
// and the cursor in it are never touched.
|
|
1282
|
+
function live(card, atOnce) {
|
|
1283
|
+
var b = box();
|
|
1284
|
+
var step = b.querySelector('.waitstep');
|
|
1285
|
+
if (!isWait(card) || !step) return;
|
|
1286
|
+
bet.shown = card;
|
|
1287
|
+
b.setAttribute('data-card', card.id);
|
|
1288
|
+
var h = b.querySelector('h1');
|
|
1289
|
+
if (h.textContent !== card.title) h.textContent = card.title;
|
|
1290
|
+
var lines = Array.isArray(card.progress) ? card.progress : [];
|
|
1291
|
+
var newest = lines.length ? lines[lines.length - 1] : null;
|
|
1292
|
+
stepTo(step, newest && typeof newest.line === 'string' ? newest.line : '', atOnce);
|
|
1293
|
+
notesTo(b.querySelector('.waitnotes'), card.thread || [], atOnce);
|
|
1294
|
+
// the Mac's own count of how long it has been, carried on from when it came
|
|
1295
|
+
if (Number.isFinite(card.waited_ms)) waitClock = { key: String(card.id).split('#')[0], ms: card.waited_ms, at: Date.now() };
|
|
1296
|
+
tickWait();
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// one line at a time, crossfaded in its own spot, so nothing under it moves
|
|
1300
|
+
function stepTo(node, text, atOnce) {
|
|
1301
|
+
if (node.getAttribute('data-text') === text) return;
|
|
1302
|
+
node.setAttribute('data-text', text);
|
|
1303
|
+
clearTimeout(node.fading);
|
|
1304
|
+
if (atOnce || reduced || !node.textContent) { node.textContent = text; node.classList.remove('out'); return; }
|
|
1305
|
+
node.classList.add('out');
|
|
1306
|
+
node.fading = setTimeout(function () { node.textContent = node.getAttribute('data-text'); node.classList.remove('out'); }, 260);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// the newest note and the answer to it; a new one fades in, and the room it
|
|
1310
|
+
// takes opens smoothly
|
|
1311
|
+
function notesTo(node, turns, atOnce) {
|
|
1312
|
+
var want = turns.slice(-2);
|
|
1313
|
+
var had = Array.prototype.map.call(node.children, function (n) { return n.textContent; });
|
|
1314
|
+
if (had.join('\\n') === want.map(function (t) { return t.text; }).join('\\n')) return;
|
|
1315
|
+
var from = node.offsetHeight;
|
|
1316
|
+
node.style.height = '';
|
|
1317
|
+
node.textContent = '';
|
|
1318
|
+
want.forEach(function (t, i) {
|
|
1319
|
+
var p = node.appendChild(el('p', 'turn ' + (t.who === 'owner' ? 'you' : 'orb'), t.text));
|
|
1320
|
+
if (!atOnce && !reduced && had[i] !== t.text) p.classList.add('fresh');
|
|
1321
|
+
});
|
|
1322
|
+
if (atOnce || reduced) return;
|
|
1323
|
+
var to = node.offsetHeight;
|
|
1324
|
+
if (from !== to) {
|
|
1325
|
+
node.style.height = from + 'px';
|
|
1326
|
+
void node.offsetHeight;
|
|
1327
|
+
node.style.height = to + 'px';
|
|
1328
|
+
clearTimeout(node.opening);
|
|
1329
|
+
node.opening = setTimeout(function () { node.style.height = ''; }, 340);
|
|
1330
|
+
}
|
|
1331
|
+
requestAnimationFrame(function () { requestAnimationFrame(function () { Array.prototype.forEach.call(node.querySelectorAll('.fresh'), function (p) { p.classList.remove('fresh'); }); }); });
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// how long the idea has been written, in plain words: the Mac's own count
|
|
1335
|
+
// when the card has it, else from when it began on the Mac's clock when
|
|
1336
|
+
// that is sane, else from when the page first saw the calm line
|
|
1337
|
+
var seen = { key: null, at: 0 };
|
|
1338
|
+
var waitClock = { key: null, ms: 0, at: 0 };
|
|
1339
|
+
function sinceWords(ms) {
|
|
1340
|
+
var s = Math.round(ms / 1000);
|
|
1341
|
+
if (!(s >= 5) || s > 3600) return '';
|
|
1342
|
+
if (s < 60) return Math.max(5, Math.round(s / 5) * 5) + ' seconds so far';
|
|
1343
|
+
var m = Math.round(s / 60);
|
|
1344
|
+
return m === 1 ? 'about a minute so far' : 'about ' + m + ' minutes so far';
|
|
1345
|
+
}
|
|
1346
|
+
function tickWait() {
|
|
1347
|
+
var node = box().querySelector('.waittime');
|
|
1348
|
+
var card = bet.shown;
|
|
1349
|
+
if (!node || !isWait(card) || !waiting()) { clearInterval(bet.clock); bet.clock = null; return; }
|
|
1350
|
+
var key = String(card.id).split('#')[0];
|
|
1351
|
+
if (seen.key !== key) seen = { key: key, at: Date.now() };
|
|
1352
|
+
var began = typeof card.started_at === 'number' ? card.started_at : Date.parse(card.started_at || '');
|
|
1353
|
+
var ms = waitClock.key === key ? waitClock.ms + (Date.now() - waitClock.at) : Date.now() - began;
|
|
1354
|
+
if (!(ms >= 0 && ms < 3600000)) ms = Date.now() - seen.at;
|
|
1355
|
+
var words = sinceWords(ms);
|
|
1356
|
+
if (node.textContent !== words) node.textContent = words;
|
|
1357
|
+
if (!bet.clock) bet.clock = setInterval(tickWait, 1000);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// A note on the calm line: the words hold in the box for the moment it
|
|
1361
|
+
// takes, then the box clears and stays open, and the note and the answer
|
|
1362
|
+
// to it show above. Nothing moves on: the idea that comes in is the one
|
|
1363
|
+
// written with it. Words that did not reach the Mac stay in the box.
|
|
1364
|
+
function sendNote(field, text) {
|
|
1365
|
+
var from = bet.shown;
|
|
1366
|
+
var input = field.querySelector('input');
|
|
1367
|
+
var body = { card_id: from.id, text: text, send: sendId([from.id, 'the box', text].join(' | ')) };
|
|
1368
|
+
bet.noting = true;
|
|
1369
|
+
field.classList.add('chosen');
|
|
1370
|
+
input.readOnly = true;
|
|
1371
|
+
var note = box().querySelector('.act .note');
|
|
1372
|
+
if (note) note.textContent = '';
|
|
1373
|
+
api('/act', body).catch(offline).then(function (r) {
|
|
1374
|
+
bet.noting = false;
|
|
1375
|
+
field.classList.remove('chosen');
|
|
1376
|
+
input.readOnly = false;
|
|
1377
|
+
// the owner went back to the big orb meanwhile: the Mac has the note, and nothing here moves
|
|
1378
|
+
if (!waiting()) return;
|
|
1379
|
+
if (r.ok && r.card && sameCard(r.card, from)) { input.value = ''; input.blur(); live(r.card); return; }
|
|
1380
|
+
// the Mac moved on (the idea landed meanwhile): it comes in, and the words go back in its box
|
|
1381
|
+
if (r.card && !sameCard(r.card, from)) { landFromWait(r.card, r.ok ? null : function () { unsent(r.card, r, text, null); }); return; }
|
|
1382
|
+
if (note) note.textContent = (r && r.message) || 'that did not go through. try again.';
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// off the calm line, home or to an act's next card: its words fade, then
|
|
1387
|
+
// the sphere glides back to its own place and size
|
|
1388
|
+
function leaveWait(then, fast) {
|
|
1389
|
+
var b = box();
|
|
1390
|
+
stopWatch();
|
|
1391
|
+
if (!fast) orb.calm();
|
|
1392
|
+
b.classList.remove('wait-in');
|
|
1393
|
+
b.classList.add('leave', 'fast');
|
|
1394
|
+
setTimeout(function () {
|
|
1395
|
+
b.hidden = true;
|
|
1396
|
+
b.classList.remove('leave', 'fast', 'holding', 'up');
|
|
1397
|
+
document.body.classList.remove('has-card', 'wide', 'waiting');
|
|
1398
|
+
stageFade();
|
|
1399
|
+
document.getElementById('orb').setAttribute('aria-label', 'the atris orb. tap to think of one new idea for the business');
|
|
1400
|
+
then();
|
|
1401
|
+
}, 220);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// The idea lands: the calm line's words fade while the dots spiral in
|
|
1405
|
+
// from wherever they are, and the card grows out of the point.
|
|
1406
|
+
function landFromWait(card, after) {
|
|
1407
|
+
var b = box();
|
|
1408
|
+
bet.busy = true;
|
|
1409
|
+
stopWatch();
|
|
1410
|
+
b.classList.remove('wait-in');
|
|
1411
|
+
b.classList.add('leave', 'fast');
|
|
1412
|
+
var off = function () {
|
|
1413
|
+
b.classList.remove('leave', 'fast');
|
|
1414
|
+
document.body.classList.remove('waiting');
|
|
1415
|
+
document.getElementById('orb').setAttribute('aria-label', 'the atris orb. tap to think of one new idea for the business');
|
|
1416
|
+
};
|
|
1417
|
+
if (reduced) {
|
|
1418
|
+
setTimeout(function () { off(); orb.settled(); showCard(card, true); bet.busy = false; if (after) after(); }, 200);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
orb.play(function (phase) {
|
|
1422
|
+
if (phase === 'cover') { off(); showCard(card, true, true, true); if (after) after(); }
|
|
1423
|
+
if (phase === 'settled') bet.busy = false;
|
|
1424
|
+
}, true, true);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// a card the Mac has now that is not the one on screen
|
|
1428
|
+
function arrive(card) {
|
|
1429
|
+
if (waiting()) { landFromWait(card); return; }
|
|
1430
|
+
if (isWait(card)) { leave(function () { enterWait(card); }, true); return; }
|
|
1431
|
+
turnTo(card);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1053
1434
|
// where the sphere sits once the card is gone, read before anything moves,
|
|
1054
1435
|
// in page coordinates (the screen's once the page is back at the top)
|
|
1055
1436
|
function sphereSpot() {
|
|
@@ -1075,7 +1456,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1075
1456
|
var b = box();
|
|
1076
1457
|
b.classList.remove('holding');
|
|
1077
1458
|
bet.shown = card;
|
|
1078
|
-
|
|
1459
|
+
document.body.setAttribute('data-in-place', '');
|
|
1460
|
+
turnInPlace(b, function () { draw(card); }, function () { bet.busy = false; document.body.removeAttribute('data-in-place'); measured(); if (card.watch) watchWait(); else stopWatch(); });
|
|
1079
1461
|
}
|
|
1080
1462
|
|
|
1081
1463
|
function turnInPlace(b, paint, after) {
|
|
@@ -1113,10 +1495,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1113
1495
|
}, 80 + TURN_IN_MS + 20);
|
|
1114
1496
|
}
|
|
1115
1497
|
|
|
1116
|
-
// While the calm line shows, look every WAIT_LOOK_MS
|
|
1117
|
-
//
|
|
1118
|
-
//
|
|
1119
|
-
//
|
|
1498
|
+
// While the calm line shows, look every WAIT_LOOK_MS: its live words are
|
|
1499
|
+
// kept fresh, even while the owner types, and once the Mac has the idea
|
|
1500
|
+
// ready it is asked for the way a tap does and grows out of the dots. It
|
|
1501
|
+
// waits while the owner types there (the box has the cursor or any words)
|
|
1502
|
+
// or a note is on its way, and on the big orb it waits for their tap.
|
|
1120
1503
|
var WAIT_LOOK_MS = 1500;
|
|
1121
1504
|
function typingOnCard() {
|
|
1122
1505
|
var input = box().querySelector('.field input');
|
|
@@ -1125,30 +1508,32 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1125
1508
|
function stopWatch() { if (bet.watching) { clearInterval(bet.watching); bet.watching = null; } }
|
|
1126
1509
|
function watchWait() {
|
|
1127
1510
|
if (bet.watching) return;
|
|
1128
|
-
var quiet = function () { return !bet.busy && document.body.classList.contains('has-card') && !typingOnCard(); };
|
|
1511
|
+
var quiet = function () { return !bet.busy && !bet.noting && document.body.classList.contains('has-card') && !typingOnCard(); };
|
|
1129
1512
|
bet.watching = setInterval(function () {
|
|
1130
1513
|
if (!bet.shown || !bet.shown.watch) { stopWatch(); return; }
|
|
1131
|
-
if (
|
|
1514
|
+
if (bet.busy || bet.noting) return;
|
|
1132
1515
|
api('/card').then(function (s) {
|
|
1133
|
-
if (s.http !== 200 || !
|
|
1134
|
-
if (s.card
|
|
1135
|
-
if (
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1516
|
+
if (s.http !== 200 || !s.card || !bet.shown || !bet.shown.watch || bet.busy || bet.noting) return;
|
|
1517
|
+
if (sameCard(s.card, bet.shown)) live(s.card);
|
|
1518
|
+
else if (quiet()) { bet.busy = true; arrive(s.card); return; }
|
|
1519
|
+
if (!quiet() || !(s.queue > 0)) return;
|
|
1520
|
+
bet.busy = true;
|
|
1521
|
+
api('/act', { orb: true, card_id: bet.shown.id }).catch(offline).then(function (r) {
|
|
1522
|
+
if (r.ok && r.card && !sameCard(r.card, bet.shown)) { arrive(r.card); return; }
|
|
1523
|
+
bet.busy = false;
|
|
1524
|
+
if (r.ok && r.card) live(r.card);
|
|
1525
|
+
});
|
|
1141
1526
|
}).catch(function () { /* the next look tries again */ });
|
|
1142
1527
|
}, WAIT_LOOK_MS);
|
|
1143
1528
|
}
|
|
1144
1529
|
|
|
1145
|
-
function isLive(s) { return Boolean(s && s.card); }
|
|
1146
|
-
|
|
1147
1530
|
// ---- the orb ----------------------------------------------------------------------------
|
|
1148
1531
|
// The big orb: the next card, with the spiral, as on page open. An idea
|
|
1149
1532
|
// that landed behind the calm line is the next one, so it is never passed.
|
|
1150
1533
|
function think() {
|
|
1151
1534
|
if (bet.busy) return;
|
|
1535
|
+
// on the calm line the dots are the orb on the card: a tap goes back to the big one
|
|
1536
|
+
if (waiting()) { goHome(); return; }
|
|
1152
1537
|
buzz();
|
|
1153
1538
|
bet.busy = true;
|
|
1154
1539
|
bet.tapAt = performance.now();
|
|
@@ -1161,6 +1546,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1161
1546
|
api('/act', { orb: true, card_id: bet.shown ? bet.shown.id : null }).then(function (r) {
|
|
1162
1547
|
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
1548
|
line.set('', false);
|
|
1549
|
+
// nothing ready yet: the calm line, with the sphere where it is and its dots moving
|
|
1550
|
+
if (isWait(r.card)) { enterWait(r.card); return; }
|
|
1164
1551
|
orb.play(function (phase) { if (phase === 'settled') { bet.busy = false; showCard(r.card, true, true); } }, true);
|
|
1165
1552
|
}, failed);
|
|
1166
1553
|
}
|
|
@@ -1174,6 +1561,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1174
1561
|
buzz();
|
|
1175
1562
|
bet.busy = true;
|
|
1176
1563
|
stopWatch();
|
|
1564
|
+
// from the calm line the sphere is already there; its line comes once the card's words are gone
|
|
1565
|
+
if (waiting()) { leave(function () { line.set('tap to think', false); bet.busy = false; }); return; }
|
|
1177
1566
|
// the line under the big orb is ready before the sphere is back
|
|
1178
1567
|
line.set('tap to think', false);
|
|
1179
1568
|
leave(function () { bet.busy = false; });
|
|
@@ -1196,6 +1585,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1196
1585
|
function firstLook() {
|
|
1197
1586
|
api('/card').then(function (s) {
|
|
1198
1587
|
if (s.http !== 200) return;
|
|
1588
|
+
if (s.card && isWait(s.card)) { enterWait(s.card, true); return; }
|
|
1199
1589
|
if (s.card) { orb.settled(); showCard(s.card, false); return; }
|
|
1200
1590
|
line.set(s.status === 'skipped' ? 'not now. tap when you want another' : 'tap to think', false);
|
|
1201
1591
|
});
|
|
@@ -1274,14 +1664,15 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1274
1664
|
|
|
1275
1665
|
// Every few seconds: the footer counts, and the card on screen. When the
|
|
1276
1666
|
// Mac's card has changed (a new id: its options are not the ones shown),
|
|
1277
|
-
// it turns in place,
|
|
1667
|
+
// it turns in place, or grows out of the dots on the calm line, unless the
|
|
1668
|
+
// owner is typing or a motion is under way.
|
|
1278
1669
|
function refresh() {
|
|
1279
1670
|
return api('/card').then(function (s) {
|
|
1280
1671
|
if (s.http !== 200) { line.set('this link stopped working. start gm phone again on the mac.', true); return; }
|
|
1281
1672
|
drawWorking(s.team_working || 0);
|
|
1282
|
-
if (s.card && bet.shown && s.card
|
|
1673
|
+
if (s.card && bet.shown && !sameCard(s.card, bet.shown) && !bet.busy && !bet.noting && !typingOnCard() && document.body.classList.contains('has-card')) {
|
|
1283
1674
|
bet.busy = true;
|
|
1284
|
-
|
|
1675
|
+
arrive(s.card);
|
|
1285
1676
|
}
|
|
1286
1677
|
}).catch(function () { /* the next refresh tries again */ });
|
|
1287
1678
|
}
|