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.
@@ -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. The
17
- // footer is only the count of jobs the team is working on.
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.28s, 0.36s; }
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 { set: function (text, bad) { want = { text: text, bad: Boolean(bad) }; next(); } };
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 sphere, the
309
- // dots spiral in at ACT_PACE, and the next card blooms where they land.
310
- var ACT_LEAVE_MS = 160, ACT_GLIDE_MS = 180, ACT_BEAT_MS = 100, ACT_PACE = 0.24, ACT_GROW_MS = 180, ACT_SETTLE_MS = 260;
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({ 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 });
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
- if (!reduced && mode === 'idle') angle += dt * 0.15;
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
- d.sx = cx + ox * Math.cos(turn) - oy * Math.sin(turn);
441
- d.sy = cy + ox * Math.sin(turn) + oy * Math.cos(turn);
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
- if (mode === 'card' || (reduced && mode === 'idle')) { raf = 0; return; }
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
- function place(s, x, y) { lava.style.transform = 'translate(' + (x - base / 2) + 'px, ' + (y - base / 2) + 'px) scale(' + s + ')'; }
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
- var vx = rect.left + cx, vy = rect.top + cy;
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
- ? BLOOM * Math.min(window.innerWidth, window.innerHeight) / 2
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
- mode = 'idle';
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
- bet.tapAt = performance.now();
860
- document.body.removeAttribute('data-reveal-ms');
861
- return bet.tapAt;
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
- function typeHere(btn, placeholder, maxLength, sent) {
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
- if (reduced) { swap(); orb.melt(then, to, function () {}, fast); return; }
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
- // from the sphere: the dots spiral in and the card blooms where they land
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 (reduced) { orb.settled(); showCard(card, false); bet.busy = false; if (after) after(); return; }
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
- // the time from the tap to the card fully on screen, kept on the page so
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) document.body.setAttribute('data-reveal-ms', String(Math.round(performance.now() - 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
- turnInPlace(b, function () { draw(card); }, function () { bet.busy = false; measured(); if (card.watch) watchWait(); else stopWatch(); });
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
- }, 80 + TURN_IN_MS + 20);
1536
+ }, turnMs);
1114
1537
  }
1115
1538
 
1116
- // While the calm line shows, look every WAIT_LOOK_MS for the idea; once the
1117
- // Mac has one ready, it is asked for the way a tap does and turns in with
1118
- // one crossfade. It waits while the owner types there (the box has the
1119
- // cursor or any words), and on the big orb it waits for their tap.
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 (!quiet()) return;
1555
+ if (bet.busy || bet.noting) return;
1132
1556
  api('/card').then(function (s) {
1133
- if (s.http !== 200 || !quiet() || !bet.shown || !bet.shown.watch || !s.card) return;
1134
- if (s.card.id !== bet.shown.id && isLive(s)) { turnTo(s.card); return; }
1135
- if (s.queue > 0) {
1136
- bet.busy = true;
1137
- api('/act', { orb: true, card_id: bet.shown.id }).catch(offline).then(function (r) {
1138
- if (r.ok && r.card) turnTo(r.card); else bet.busy = false;
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
- bet.tapAt = performance.now();
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
- bet.tapAt = performance.now();
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, unless the owner is typing or a motion is under way.
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.id !== bet.shown.id && !bet.busy && !typingOnCard() && document.body.classList.contains('has-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
- turnTo(s.card);
1718
+ arrive(s.card);
1285
1719
  }
1286
1720
  }).catch(function () { /* the next refresh tries again */ });
1287
1721
  }