@bongos/core 1.19.1012 → 1.19.1013

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.
Files changed (34) hide show
  1. package/.bongos-core.json +51 -41
  2. package/docs/adr/0331-the-sky-is-the-ideators-home.md +6 -1
  3. package/docs/copy-inventory.md +99 -56
  4. package/docs/copy-registry.json +830 -465
  5. package/docs/file-map.md +1 -1
  6. package/docs/module-api-changelog.md +2 -0
  7. package/modules/hall-ui/public/idea-objects.js +4 -1
  8. package/modules/hall-ui/public/sky-composer.js +155 -0
  9. package/modules/hall-ui/public/sky-data.js +287 -0
  10. package/modules/hall-ui/public/sky-draw.js +244 -0
  11. package/modules/hall-ui/public/sky-interaction.js +137 -0
  12. package/modules/hall-ui/public/sky-layout.js +81 -0
  13. package/modules/hall-ui/public/sky-panel.js +141 -0
  14. package/modules/hall-ui/public/sky-tour.js +80 -0
  15. package/modules/hall-ui/public/thinking.css +203 -267
  16. package/modules/hall-ui/public/thinking.html +119 -58
  17. package/modules/hall-ui/public/thinking.states.json +33 -9
  18. package/package-lock.json +2 -2
  19. package/package.json +1 -1
  20. package/src/bongos/routes/me.js +4 -1
  21. package/src/bongos/serve-internal.js +20 -6
  22. package/src/module-api.js +1 -1
  23. package/tests/hall_audit.mjs +27 -0
  24. package/tests/hall_page_gate_map.mjs +11 -5
  25. package/tests/hall_sky.mjs +393 -405
  26. package/tests/hall_thinking_room.mjs +274 -296
  27. package/tests/helpers.mjs +185 -217
  28. package/tests/ideator_large_dead_thought_proof.mjs +33 -25
  29. package/tests/ideator_traceable_proof.mjs +61 -39
  30. package/modules/hall-ui/public/sky.css +0 -365
  31. package/modules/hall-ui/public/sky.html +0 -79
  32. package/modules/hall-ui/public/sky.js +0 -580
  33. package/modules/hall-ui/public/sky.states.json +0 -20
  34. package/modules/hall-ui/public/thinking.js +0 -418
@@ -0,0 +1,244 @@
1
+ // modules/hall-ui/public/sky-draw.js — THE SKY, part 3 of 7: the drawing and
2
+ // the hit test (task 1004231 / sky part 3 of 5; ADR 0331).
3
+ //
4
+ // The prototype's "drawing" and "hit testing" sections, verbatim but for the
5
+ // shared state on `SKY.S`. Everything drawn is a fact from the ledger: bands
6
+ // are versions, wells are goals, rings are criteria (dashed open, solid met,
7
+ // thicker with credits), bodies are tasks past the zoom threshold and dust
8
+ // below it, and a thought is one of the five objects — its SIZE the credits
9
+ // its descent was paid (never smaller for being dead), its BRIGHTNESS how
10
+ // much of that work still stands. Born-of marks and the "filed by a session"
11
+ // accent read facts part 5 ships; until then they are simply never true.
12
+
13
+ (() => {
14
+ 'use strict';
15
+
16
+ const SKY = window.OTBSky; const S = SKY.S;
17
+ const { $, TAU, reduce, NOW, hash, clamp, lerp, smooth, ease, short, ringR, taskAt, diameter, shown, seen, present, toScreen, toWorld, curve, pointOn, reveal, bandAt, cv } = SKY;
18
+ const ctx = cv.getContext('2d');
19
+
20
+ const ACC = [251, 149, 105], CHART = [242, 210, 122];
21
+ const rgba = (c, a) => `rgba(${c[0]},${c[1]},${c[2]},${a})`;
22
+ function goalAlpha(g, now) {
23
+ if (!present(g)) return 0;
24
+ let a = 1;
25
+ if (!seen(g)) a = 0.07; else if (g.revealAt) a = 0.07 + 0.93 * smooth(now - g.revealAt, 0, 900);
26
+ if (S.focusSet && !S.focusSet.has(g.id)) a = Math.min(a, 0.1);
27
+ if (S.lens === 'mine' && S.influence && !SKY.touched(g)) a = Math.min(a, 0.12);
28
+ return a;
29
+ }
30
+ function shuttle(x, y, ang, s, glow) {
31
+ ctx.save(); ctx.translate(x, y); ctx.rotate(ang);
32
+ if (glow) { ctx.shadowColor = rgba(CHART, .7); ctx.shadowBlur = 10; }
33
+ ctx.fillStyle = glow ? '#fff' : 'rgba(255,255,255,.85)'; ctx.strokeStyle = glow ? rgba(CHART, 1) : 'rgba(255,255,255,.5)'; ctx.lineWidth = 1;
34
+ ctx.beginPath(); ctx.moveTo(8 * s, 0); ctx.lineTo(2 * s, -2.2 * s); ctx.lineTo(-3 * s, -2.2 * s); ctx.lineTo(-5 * s, -6 * s); ctx.lineTo(-7 * s, -6 * s); ctx.lineTo(-6 * s, -1.6 * s); ctx.lineTo(-7 * s, 0); ctx.lineTo(-6 * s, 1.6 * s); ctx.lineTo(-7 * s, 6 * s); ctx.lineTo(-5 * s, 6 * s); ctx.lineTo(-3 * s, 2.2 * s); ctx.lineTo(2 * s, 2.2 * s); ctx.closePath(); ctx.fill(); ctx.stroke();
35
+ ctx.restore();
36
+ }
37
+
38
+ function draw(now) {
39
+ const { W, H, DPR, cam, lens, hover, picked, gravityGoal, tourSpot, charted, goals, edges, ideas, BANDS } = S;
40
+ const tt = (now - S.t0) / 1000;
41
+ ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
42
+ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, W, H);
43
+ const k = cam.k;
44
+ const visible = (x, y, r) => { const [sx, sy] = toScreen(x, y); const sr = r * k; return sx + sr > -40 && sx - sr < W + 40 && sy + sr > -40 && sy - sr < H + 40; };
45
+
46
+ // the bands: ground, not words. each version is a slightly different dark. (none in the simple view)
47
+ const [cx0, cy0] = toScreen(0, 0);
48
+ for (let bi = lens === 'simple' ? -1 : BANDS.length - 1; bi >= 0; bi--) {
49
+ const b = BANDS[bi]; const hot = hover && hover.kind === 'band' && hover.ref === b;
50
+ ctx.fillStyle = `rgba(255,255,255,${(hot ? b.tone * 1.6 : b.tone).toFixed(4)})`;
51
+ ctx.beginPath(); ctx.ellipse(cx0, cy0, b.r1 * k * S.ASPECT, b.r1 * k, 0, 0, TAU); if (b.r0) ctx.ellipse(cx0, cy0, b.r0 * k * S.ASPECT, b.r0 * k, 0, 0, TAU, true); ctx.fill('evenodd');
52
+ ctx.strokeStyle = hot ? 'rgba(255,255,255,.14)' : 'rgba(255,255,255,.05)'; ctx.lineWidth = 1; ctx.setLineDash([]);
53
+ ctx.beginPath(); ctx.ellipse(cx0, cy0, b.r1 * k * S.ASPECT, b.r1 * k, 0, 0, TAU); ctx.stroke();
54
+ }
55
+
56
+ // the routes: a whisper until charted
57
+ const shuttles = [];
58
+ for (const e of (lens === 'simple' ? [] : edges)) {
59
+ if (!present(e.a) || !present(e.b)) continue;
60
+ const c = curve(e); const job = charted.get(e.id);
61
+ if (!job) { if (lens === 'all' && k > 0.03 && seen(e.a) && seen(e.b)) { ctx.strokeStyle = 'rgba(255,255,255,.02)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(c.ax, c.ay); ctx.quadraticCurveTo(c.mx, c.my, c.bx, c.by); ctx.stroke(); } continue; }
62
+ const rev = job.from === e.b; const P = (t) => pointOn(c, rev ? 1 - t : t);
63
+ let p = job.done ? 1 : clamp((now - job.t0) / job.ms, 0, 1);
64
+ if (p >= 1 && !job.done) { job.done = true; reveal(rev ? e.a : e.b); }
65
+ if (p <= 0) continue;
66
+ const steps = 40; const upto = Math.round(steps * ease(p));
67
+ const warm = job.done ? clamp((now - job.t0 - job.ms) / 3000, 0, 1) : 0;
68
+ const col = [lerp(CHART[0], 255, warm), lerp(CHART[1], 250, warm), lerp(CHART[2], 235, warm)];
69
+ ctx.lineWidth = job.done ? 1.1 : 1.3; ctx.strokeStyle = rgba(col, job.done ? lerp(0.6, 0.34, warm) : 0.6);
70
+ if (!job.done) { ctx.shadowColor = rgba(CHART, .5); ctx.shadowBlur = 6; }
71
+ ctx.beginPath(); const [x0, y0] = P(0); ctx.moveTo(x0, y0); for (let s = 1; s <= upto; s++) { const [x, y] = P(s / steps); ctx.lineTo(x, y); } ctx.stroke(); ctx.shadowBlur = 0;
72
+ const [hx, hy] = P(ease(p)); const [px, py] = P(Math.max(0, ease(p) - 0.02)); const ang = Math.atan2(hy - py, hx - px);
73
+ shuttles.push({ e, x: hx, y: hy, ang, live: !job.done });
74
+ }
75
+
76
+ const labels = []; const hoverRing = hover && hover.kind === 'ring' ? hover : null;
77
+ for (const g of goals) {
78
+ if (!visible(g.x, g.y, g.r * 1.7) || !present(g)) continue;
79
+ const [sx, sy] = toScreen(g.x, g.y); const sr = g.r * k; const al = goalAlpha(g, now);
80
+ const isPicked = picked && picked.kind === 'goal' && picked.ref === g;
81
+ const isHover = (hover && hover.kind === 'goal' && hover.ref === g) || (hoverRing && hoverRing.goal === g);
82
+ const isGrav = gravityGoal === g; const isSpot = tourSpot && tourSpot.kind === 'goal' && tourSpot.ref === g;
83
+ const n = Math.min(g.crit.length, 8); const ringWidth = (c) => 1 + Math.min(4, Math.log2(1 + (c.credits || 0) / 60));
84
+ ctx.globalAlpha = al;
85
+ if (lens === 'simple') { // the first mock's sky: a well is two faint rings and a name, nothing more
86
+ const has = g.thoughts.some(shown);
87
+ if (g.band !== BANDS[0] && !has) { g._stubs = []; ctx.globalAlpha = 1; continue; }
88
+ ctx.globalAlpha = has ? 1 : 0.55; ctx.lineWidth = 1; ctx.setLineDash([]);
89
+ ctx.strokeStyle = isPicked || isHover ? 'rgba(255,255,255,.22)' : 'rgba(255,255,255,.085)'; ctx.beginPath(); ctx.arc(sx, sy, sr, 0, TAU); ctx.stroke();
90
+ ctx.strokeStyle = 'rgba(255,255,255,.05)'; ctx.beginPath(); ctx.arc(sx, sy, sr * 0.5, 0, TAU); ctx.stroke();
91
+ if (sr > 26 && (has || g.status === 'open')) labels.push({ x: sx, y: sy - sr - 6, text: short(g.title).toLowerCase(), sub: '', big: false, small: true, hot: isPicked || isHover, alpha: 1 });
92
+ g._stubs = []; ctx.globalAlpha = 1; continue;
93
+ }
94
+ if (!seen(g)) { // a silhouette: something is there, and you have not been
95
+ ctx.strokeStyle = 'rgba(255,255,255,.5)'; ctx.lineWidth = 1; ctx.setLineDash([2, 6]); ctx.beginPath(); ctx.arc(sx, sy, Math.max(3, sr), 0, TAU); ctx.stroke(); ctx.setLineDash([]);
96
+ if (isHover) labels.push({ x: sx, y: sy - sr - 8, text: 'unexplored', sub: 'follow a route to reach it', big: false, hot: true, alpha: 1 });
97
+ g._stubs = []; ctx.globalAlpha = 1; continue;
98
+ }
99
+ if (g.status === 'open') {
100
+ // alive: a builder holding a task now, or work landing this week — the one accent a well can wear
101
+ if (g.live > 0) { const br = reduce ? 1 : 0.85 + 0.15 * Math.sin(tt * 0.9 + hash(g.id) * 6); const hg = ctx.createRadialGradient(sx, sy, sr * 0.92, sx, sy, sr * 1.35); hg.addColorStop(0, rgba(ACC, 0.04 + 0.14 * g.live * br)); hg.addColorStop(1, rgba(ACC, 0)); ctx.fillStyle = hg; ctx.beginPath(); ctx.arc(sx, sy, sr * 1.35, 0, TAU); ctx.fill(); }
102
+ const grd = ctx.createRadialGradient(sx, sy, sr * 0.2, sx, sy, sr); grd.addColorStop(0, 'rgba(255,255,255,.05)'); grd.addColorStop(1, 'rgba(255,255,255,.012)');
103
+ ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(sx, sy, sr, 0, TAU); ctx.fill();
104
+ ctx.lineWidth = 1; ctx.setLineDash([]);
105
+ ctx.strokeStyle = isGrav || isSpot ? rgba(ACC, .8) : (isPicked || isHover) ? 'rgba(255,255,255,.4)' : g.live > 0 ? rgba(ACC, 0.18 + 0.3 * g.live) : 'rgba(255,255,255,.14)';
106
+ if (isGrav || isSpot) { ctx.shadowColor = rgba(ACC, .9); ctx.shadowBlur = 22; }
107
+ ctx.beginPath(); ctx.arc(sx, sy, sr, 0, TAU); ctx.stroke(); ctx.shadowBlur = 0;
108
+ if (sr > 22) for (let i = 0; i < n; i++) {
109
+ const c = g.crit[i]; const rr = ringR(g, i, n) * k;
110
+ const hot = (hoverRing && hoverRing.goal === g && hoverRing.index === i) || (tourSpot && tourSpot.kind === 'ring' && tourSpot.goal === g && tourSpot.index === i);
111
+ ctx.beginPath(); ctx.arc(sx, sy, rr, 0, TAU);
112
+ if (hot) { ctx.setLineDash([]); ctx.strokeStyle = rgba(ACC, .9); ctx.lineWidth = ringWidth(c) + 0.5; ctx.shadowColor = rgba(ACC, .8); ctx.shadowBlur = 14; }
113
+ else if (c.satisfied) { ctx.setLineDash([]); ctx.strokeStyle = 'rgba(255,255,255,.42)'; ctx.lineWidth = ringWidth(c); }
114
+ else { ctx.setLineDash([3, 5]); ctx.strokeStyle = 'rgba(255,255,255,.13)'; ctx.lineWidth = ringWidth(c); }
115
+ ctx.stroke(); ctx.shadowBlur = 0;
116
+ if (c.bornOf && sr > 40) { const a = hash(c.bornOf) * TAU; ctx.setLineDash([]); ctx.strokeStyle = rgba(ACC, .9); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(sx + Math.cos(a) * (rr - 7), sy + Math.sin(a) * (rr - 7)); ctx.lineTo(sx + Math.cos(a) * (rr + 7), sy + Math.sin(a) * (rr + 7)); ctx.stroke(); }
117
+ }
118
+ ctx.setLineDash([]); ctx.lineWidth = 1;
119
+ const tasksNow = g.tasks.filter(present);
120
+ if (tasksNow.length && sr > 60) {
121
+ const bodies = smooth(sr, 170, 330);
122
+ if (bodies < 1) { ctx.globalAlpha = al * (1 - bodies); for (let i = -1; i < n; i++) { const rr = (i < 0 ? g.r * 0.99 : ringR(g, i, n)) * k; const cnt = tasksNow.filter((t) => (i < 0 ? t.ci < 0 || t.ci >= n : t.ci === i)).length; if (!cnt) continue; ctx.strokeStyle = `rgba(255,255,255,${Math.min(.3, 0.03 + cnt * 0.004)})`; ctx.lineWidth = Math.min(7, 1.5 + cnt * 0.03); ctx.beginPath(); ctx.arc(sx, sy, rr, 0, TAU); ctx.stroke(); } ctx.lineWidth = 1; ctx.globalAlpha = al; }
123
+ if (bodies > 0) { ctx.globalAlpha = al * bodies; for (const t of tasksNow) { const [ox, oy] = taskAt(t, tt); const [tx, ty] = toScreen(g.x + ox, g.y + oy); t._sx = tx; t._sy = ty; drawTask(t, tx, ty, tt, picked && picked.kind === 'task' && picked.ref === t, hover && hover.kind === 'task' && hover.ref === t); } ctx.globalAlpha = al; }
124
+ }
125
+ const done = tasksNow.filter((t) => ['shipped', 'confirmed', 'completed'].includes(t.status)).length;
126
+ const speaks = sr > 200 || (sr > 34 && g.mass >= 1500) || isPicked || isHover || isGrav || isSpot;
127
+ if (speaks) labels.push({ x: sx, y: sr > 200 ? sy - sr + 44 : sy - sr - 8, text: g.title, sub: `${S.clock < NOW - 3600000 ? done : g.taskDone}/${S.clock < NOW - 3600000 ? tasksNow.length : g.taskTotal} tasks · ${g.credits ? g.credits + ' credits · ' : ''}${g.crit.filter((c) => c.satisfied).length} of ${g.crit.length} rings closed${g.live > 0 ? ' · alive' : ''}`, big: sr > 120, hot: isPicked || isHover || isGrav || isSpot, alpha: al });
128
+ } else if (g.status === 'achieved') {
129
+ const grd = ctx.createRadialGradient(sx, sy, 0, sx, sy, Math.max(4, sr)); grd.addColorStop(0, 'rgba(255,255,255,.16)'); grd.addColorStop(0.5, 'rgba(255,255,255,.04)'); grd.addColorStop(1, 'rgba(255,255,255,0)');
130
+ ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(sx, sy, Math.max(4, sr), 0, TAU); ctx.fill();
131
+ ctx.lineWidth = 1; ctx.setLineDash([]); ctx.strokeStyle = (isPicked || isHover || isSpot) ? 'rgba(255,255,255,.6)' : 'rgba(255,255,255,.22)';
132
+ ctx.beginPath(); ctx.arc(sx, sy, Math.max(3, sr), 0, TAU); ctx.stroke();
133
+ if (sr > 22) for (let i = 0; i < n; i++) { const c = g.crit[i]; const rr = ringR(g, i, n) * k; const hot = hoverRing && hoverRing.goal === g && hoverRing.index === i; ctx.strokeStyle = hot ? rgba(ACC, .9) : 'rgba(255,255,255,.3)'; ctx.lineWidth = ringWidth(c); ctx.beginPath(); ctx.arc(sx, sy, rr, 0, TAU); ctx.stroke(); if (c.bornOf && sr > 40) { const a = hash(c.bornOf) * TAU; ctx.strokeStyle = rgba(ACC, .9); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(sx + Math.cos(a) * (rr - 7), sy + Math.sin(a) * (rr - 7)); ctx.lineTo(sx + Math.cos(a) * (rr + 7), sy + Math.sin(a) * (rr + 7)); ctx.stroke(); } }
134
+ ctx.lineWidth = 1;
135
+ if (g.tasks.length && sr > 170) { const bodies = smooth(sr, 170, 330); ctx.globalAlpha = al * bodies; for (const t of g.tasks.filter(present)) { const [ox, oy] = taskAt(t, tt); const [tx, ty] = toScreen(g.x + ox, g.y + oy); t._sx = tx; t._sy = ty; drawTask(t, tx, ty, tt, false, hover && hover.kind === 'task' && hover.ref === t); } ctx.globalAlpha = al; }
136
+ if (sr > 8) { ctx.strokeStyle = 'rgba(255,255,255,.05)'; ctx.beginPath(); ctx.arc(sx, sy, sr * 1.35, 0, TAU); ctx.stroke(); ctx.strokeStyle = 'rgba(255,255,255,.03)'; ctx.beginPath(); ctx.arc(sx, sy, sr * 1.7, 0, TAU); ctx.stroke(); }
137
+ const core = Math.max(2.4, Math.min(sr * 0.14, 9)); const cg = ctx.createRadialGradient(sx, sy, 0, sx, sy, core * 3); cg.addColorStop(0, 'rgba(255,255,255,.5)'); cg.addColorStop(1, 'rgba(255,255,255,0)');
138
+ ctx.fillStyle = cg; ctx.beginPath(); ctx.arc(sx, sy, core * 3, 0, TAU); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,.92)'; ctx.beginPath(); ctx.arc(sx, sy, core, 0, TAU); ctx.fill();
139
+ const speaks = sr > 200 || (sr > 40 && g.mass >= 1500) || isPicked || isHover || isSpot;
140
+ if (speaks) labels.push({ x: sx, y: sr > 200 ? sy - sr + 44 : sy - sr - 8, text: g.title, sub: `settled · ${g.taskDone} tasks${g.credits ? ' · ' + g.credits + ' credits' : ''} · ${g.crit.length} rings closed`, big: sr > 120, hot: isPicked || isHover || isSpot, small: sr < 60, alpha: al });
141
+ } else {
142
+ ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(sx, sy, Math.max(2, sr), 0, TAU); ctx.fill();
143
+ ctx.strokeStyle = (isPicked || isHover || isSpot) ? 'rgba(255,255,255,.6)' : 'rgba(255,255,255,.22)'; ctx.lineWidth = 1; ctx.setLineDash([]); ctx.beginPath(); ctx.arc(sx, sy, Math.max(2, sr), 0, TAU); ctx.stroke();
144
+ if (sr > 5) { ctx.strokeStyle = 'rgba(255,255,255,.06)'; ctx.beginPath(); ctx.arc(sx, sy, sr * 1.5, 0, TAU); ctx.stroke(); }
145
+ if (sr > 30) { const grd = ctx.createRadialGradient(sx, sy, sr * 0.6, sx, sy, sr); grd.addColorStop(0, 'rgba(255,255,255,0)'); grd.addColorStop(1, 'rgba(255,255,255,.05)'); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(sx, sy, sr, 0, TAU); ctx.fill(); }
146
+ const speaks = sr > 160 || isPicked || isHover || isSpot;
147
+ if (speaks) labels.push({ x: sx, y: sr > 160 ? sy - sr + 40 : sy - sr - 8, text: g.title, sub: `collapsed · ${g.taskDone} of ${g.taskTotal} tasks`, big: false, hot: isPicked || isHover || isSpot, small: sr < 60, alpha: al });
148
+ }
149
+ if (g.bornOf && sr > 14) { ctx.setLineDash([]); ctx.strokeStyle = rgba(ACC, .8); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(sx, sy, Math.min(9, Math.max(4, sr * 0.06)), 0, TAU); ctx.stroke(); ctx.fillStyle = rgba(ACC, .9); ctx.beginPath(); ctx.arc(sx, sy, 2, 0, TAU); ctx.fill(); }
150
+ // rim arrows: one per route, pointing the way the dependency runs
151
+ if (sr > 40 && (lens === 'all' || g.status === 'open')) {
152
+ g._stubs = [];
153
+ for (const e of SKY.edgesOf(g)) {
154
+ const o = e.a === g ? e.b : e.a; if (!present(o)) continue; const a = Math.atan2(o.y - g.y, o.x - g.x);
155
+ const r0 = sr + 5, r1 = sr + 19; const x0 = sx + Math.cos(a) * r0, y0 = sy + Math.sin(a) * r0, x1 = sx + Math.cos(a) * r1, y1 = sy + Math.sin(a) * r1;
156
+ const hot = hover && hover.kind === 'stub' && hover.edge === e && hover.goal === g; const done = charted.has(e.id);
157
+ ctx.strokeStyle = hot ? rgba(CHART, 1) : done ? rgba(CHART, .5) : 'rgba(255,255,255,.3)'; ctx.lineWidth = hot ? 1.6 : 1; ctx.setLineDash([]);
158
+ ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke();
159
+ const out = e.a === g; const hx = out ? x1 : x0, hy = out ? y1 : y0, ha = out ? a : a + Math.PI;
160
+ ctx.beginPath(); ctx.moveTo(hx, hy); ctx.lineTo(hx - Math.cos(ha - 0.5) * 5, hy - Math.sin(ha - 0.5) * 5); ctx.moveTo(hx, hy); ctx.lineTo(hx - Math.cos(ha + 0.5) * 5, hy - Math.sin(ha + 0.5) * 5); ctx.stroke();
161
+ g._stubs.push({ e, x: (x0 + x1) / 2, y: (y0 + y1) / 2, goal: g, other: o });
162
+ }
163
+ } else g._stubs = [];
164
+ ctx.globalAlpha = 1;
165
+ }
166
+
167
+ // thoughts
168
+ for (const i of ideas) {
169
+ if (i.x == null || !visible(i.x, i.y, 40) || !shown(i)) continue;
170
+ const [sx, sy] = toScreen(i.x, i.y);
171
+ const isP = picked && picked.kind === 'idea' && picked.ref === i, isH = hover && hover.kind === 'idea' && hover.ref === i, isS = tourSpot && tourSpot.kind === 'idea' && tourSpot.ref === i;
172
+ const g = i.well && S.goalById.get(i.well); ctx.globalAlpha = g ? Math.max(0.4, goalAlpha(g, now)) : 1;
173
+ drawThought(i, sx, sy, k, tt, isP || isS, isH); ctx.globalAlpha = 1;
174
+ if (k > 2.4 && (i.st === 'star' || i.st === 'remnant' || isP || i.shaped)) labels.push({ x: sx, y: sy - diameter(i) * clamp(k, .55, 1.6) / 2 - 6, text: i.title, sub: '', big: false, small: true, hot: isP, thought: true, alpha: 1 });
175
+ // reach: where its descent lives on beyond the well that caught it
176
+ if (isP || isH) for (const t of [...i.lineage, ...i.descentTasks]) { const tk = S.taskById.get(t.id); if (!tk || tk.goal.id === i.well || !present(tk)) continue; const [ox, oy] = taskAt(tk, tt); const [tx, ty] = toScreen(tk.goal.x + ox, tk.goal.y + oy); ctx.strokeStyle = rgba(ACC, .35); ctx.lineWidth = 1; ctx.setLineDash([2, 4]); ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(tx, ty); ctx.stroke(); ctx.setLineDash([]); ctx.strokeStyle = rgba(ACC, .8); ctx.beginPath(); ctx.arc(tx, ty, 6, 0, TAU); ctx.stroke(); }
177
+ }
178
+ if (S.flying) { const f = S.flying; const p = clamp((now - f.t0) / f.ms, 0, 1); const e = ease(p); const [sx0, sy0] = [W / 2, 150]; const [sx1, sy1] = toScreen(f.idea.x, f.idea.y); const sx = lerp(sx0, sx1, e), sy = lerp(sy0, sy1, e) - Math.sin(p * Math.PI) * 40; drawThought(f.idea, sx, sy, k, tt, false, false); if (p >= 1) { f.idea.landed = true; S.flying = null; } }
179
+ if (picked && picked.kind === 'idea' && picked.ref.lineage.length && picked.ref.x != null) {
180
+ const i = picked.ref; const [sx, sy] = toScreen(i.x, i.y); const base = Math.max(22, diameter(i) * Math.min(1.6, k) + 10);
181
+ i.lineage.forEach((t, n) => { const orb = base + n * 14; const a = hash(t.id) * TAU + tt * (reduce ? 0 : 0.05) * (n % 2 ? -1 : 1); ctx.strokeStyle = 'rgba(255,255,255,.08)'; ctx.lineWidth = 1; ctx.setLineDash([]); ctx.beginPath(); ctx.arc(sx, sy, orb, 0, TAU); ctx.stroke(); const tx = sx + Math.cos(a) * orb, ty = sy + Math.sin(a) * orb; drawTask({ status: t.status }, tx, ty, tt, false, hover && hover.kind === 'ltask' && hover.ref === t); t._sx = tx; t._sy = ty; });
182
+ }
183
+ for (const s of shuttles) shuttle(s.x, s.y, s.ang, 1.1, s.live || (hover && hover.kind === 'shuttle' && hover.ref === s.e));
184
+ S.shuttles = shuttles;
185
+
186
+ ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
187
+ labels.sort((a, b) => (b.hot - a.hot) || ((b.thought ? 0 : 1) - (a.thought ? 0 : 1)) || (b.big - a.big) || ((b.small ? 0 : 1) - (a.small ? 0 : 1)));
188
+ const placed = [{ x0: 0, x1: W, y0: 0, y1: 70 }, { x0: W / 2 - 330, x1: W / 2 + 330, y0: 70, y1: W > 900 ? 230 : 300 }];
189
+ const collides = (r) => placed.some((p) => r.x0 < p.x1 && r.x1 > p.x0 && r.y0 < p.y1 && r.y1 > p.y0);
190
+ for (const l of labels) {
191
+ if (l.alpha != null && l.alpha < 0.5 && !l.hot) continue;
192
+ ctx.font = (l.big ? '300 15px' : l.small ? '400 11px' : '400 12px') + ' Manrope, "Segoe UI", system-ui, sans-serif';
193
+ const text = l.text.length > 46 && !l.big ? l.text.slice(0, 44).trimEnd() + '…' : l.text;
194
+ const w = ctx.measureText(text).width + 12, h = l.small ? 14 : 30;
195
+ let y = l.y; let rect = { x0: l.x - w / 2, x1: l.x + w / 2, y0: y - h, y1: y };
196
+ if (collides(rect)) { y = l.y + h + 6; rect = { x0: l.x - w / 2, x1: l.x + w / 2, y0: y - h, y1: y }; if (collides(rect) && !l.hot) continue; }
197
+ placed.push(rect);
198
+ ctx.fillStyle = l.hot ? 'rgba(255,255,255,.95)' : l.small ? 'rgba(255,255,255,.4)' : 'rgba(255,255,255,.6)';
199
+ ctx.fillText(text, l.x, y - (l.small ? 0 : 14));
200
+ if (!l.small && l.sub) { ctx.font = '400 10px "JetBrains Mono", monospace'; ctx.fillStyle = l.hot ? 'rgba(255,255,255,.6)' : 'rgba(255,255,255,.3)'; ctx.fillText(l.sub.toUpperCase(), l.x, y); }
201
+ }
202
+ }
203
+
204
+ function drawThought(i, sx, sy, k, tt, isPicked, isHover) {
205
+ const d = diameter(i) * clamp(k, 0.55, 1.6); const breathe = reduce ? 1 : 0.9 + 0.1 * Math.sin(tt * 1.3 + hash(i.id) * 6); ctx.setLineDash([]);
206
+ if (i.shaped) { ctx.strokeStyle = rgba(ACC, .7); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(sx, sy, Math.max(d / 2 + 6, 10), 0, TAU); ctx.stroke(); }
207
+ if (i.st === 'star') { const a = 0.32 + 0.68 * i.lit; const glow = d * (0.8 + 1.6 * i.lit) * breathe; const grd = ctx.createRadialGradient(sx, sy, 0, sx, sy, glow); grd.addColorStop(0, rgba(ACC, 0.55 * i.lit)); grd.addColorStop(1, rgba(ACC, 0)); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(sx, sy, glow, 0, TAU); ctx.fill(); ctx.fillStyle = rgba(ACC, a); ctx.beginPath(); ctx.arc(sx, sy, d / 2, 0, TAU); ctx.fill(); }
208
+ else if (i.st === 'remnant') { ctx.strokeStyle = 'rgba(255,255,255,.4)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(sx, sy, d / 2 * 1.15, 0, TAU); ctx.stroke(); }
209
+ else if (i.st === 'protostar') { const aw = i.awaiting || (i.state && i.state.awaitingNod); const pulse = aw && !reduce ? 0.5 + 0.5 * Math.sin(tt * 2 + hash(i.id) * 6) : 0; const c = SKY.temp(i); ctx.fillStyle = rgba(c, .06); ctx.beginPath(); ctx.arc(sx, sy, 7 + pulse * 6, 0, TAU); ctx.fill(); ctx.fillStyle = aw ? rgba(ACC, 0.55 + 0.45 * pulse) : rgba(c, i.well ? .6 : .5); ctx.beginPath(); ctx.arc(sx, sy, 3.2, 0, TAU); ctx.fill(); }
210
+ else if (i.st === 'satellite') { ctx.fillStyle = 'rgba(255,255,255,.4)'; ctx.beginPath(); ctx.arc(sx, sy, 2.6, 0, TAU); ctx.fill(); ctx.strokeStyle = 'rgba(255,255,255,.16)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(sx, sy, 6.5, 0, TAU); ctx.stroke(); if (i.host && i.host.x != null) { const [hx, hy] = toScreen(i.host.x, i.host.y); ctx.strokeStyle = 'rgba(255,255,255,.1)'; ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(hx, hy); ctx.stroke(); } }
211
+ else { ctx.fillStyle = '#141418'; ctx.beginPath(); ctx.arc(sx, sy, 3, 0, TAU); ctx.fill(); ctx.strokeStyle = 'rgba(255,255,255,.18)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(sx, sy, 3, 0, TAU); ctx.stroke(); }
212
+ if (isPicked || isHover) { ctx.strokeStyle = isPicked ? rgba(ACC, .7) : 'rgba(255,255,255,.35)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(sx, sy, Math.max(d / 2 + 8, 12), 0, TAU); ctx.stroke(); }
213
+ }
214
+ function drawTask(t, sx, sy, tt, isPicked, isHover) {
215
+ ctx.setLineDash([]); ctx.lineWidth = 1; const s = t.status;
216
+ if (s === 'active') { const g = ctx.createRadialGradient(sx, sy, 0, sx, sy, 9); g.addColorStop(0, rgba(ACC, .6)); g.addColorStop(1, rgba(ACC, 0)); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(sx, sy, 9, 0, TAU); ctx.fill(); ctx.fillStyle = rgba(ACC, 1); ctx.beginPath(); ctx.arc(sx, sy, 2.6, 0, TAU); ctx.fill(); }
217
+ else if (s === 'shipped' || s === 'confirmed' || s === 'completed') { ctx.fillStyle = 'rgba(255,255,255,.55)'; ctx.beginPath(); ctx.arc(sx, sy, 1.8, 0, TAU); ctx.fill(); }
218
+ else if (s === 'ready') { ctx.strokeStyle = 'rgba(255,255,255,.7)'; ctx.beginPath(); ctx.arc(sx, sy, 2.6, 0, TAU); ctx.stroke(); }
219
+ else if (s === 'backlog') { ctx.strokeStyle = 'rgba(255,255,255,.28)'; ctx.beginPath(); ctx.arc(sx, sy, 2.4, 0, TAU); ctx.stroke(); }
220
+ else if (s === 'blocked') { ctx.strokeStyle = 'rgba(255,255,255,.5)'; ctx.beginPath(); ctx.arc(sx, sy, 2.6, 0, TAU); ctx.stroke(); ctx.beginPath(); ctx.moveTo(sx - 4, sy + 4); ctx.lineTo(sx + 4, sy - 4); ctx.stroke(); }
221
+ else { ctx.strokeStyle = 'rgba(255,255,255,.22)'; ctx.beginPath(); ctx.moveTo(sx - 3, sy); ctx.lineTo(sx + 3, sy); ctx.stroke(); }
222
+ if (isPicked || isHover) { ctx.strokeStyle = isPicked ? rgba(ACC, .7) : 'rgba(255,255,255,.4)'; ctx.beginPath(); ctx.arc(sx, sy, 9, 0, TAU); ctx.stroke(); }
223
+ }
224
+
225
+ // ---------------------------------------------------------------- hit testing
226
+ function hit(sx, sy) {
227
+ const { cam, goals, ideas, picked } = S; const k = cam.k;
228
+ for (const s of S.shuttles) if (!s.live && Math.hypot(s.x - sx, s.y - sy) < 13) return { kind: 'shuttle', ref: s.e, x: s.x, y: s.y };
229
+ for (const g of goals) for (const st of (g._stubs || [])) if (Math.hypot(st.x - sx, st.y - sy) < 10) return { kind: 'stub', edge: st.e, goal: g, other: st.other, ref: st.e };
230
+ let best = null, bd = 14;
231
+ for (const i of ideas) { if (i.x == null || !shown(i)) continue; const [x, y] = toScreen(i.x, i.y); const d = Math.hypot(x - sx, y - sy) - diameter(i) * clamp(k, .55, 1.6) / 2; if (d < bd) { bd = d; best = i; } }
232
+ if (best) return { kind: 'idea', ref: best };
233
+ if (picked && picked.kind === 'idea') for (const t of picked.ref.lineage) if (t._sx != null && Math.hypot(t._sx - sx, t._sy - sy) < 9) return { kind: 'ltask', ref: t };
234
+ for (const g of goals) { if (g.status === 'archived' || g.r * k <= 170 || !seen(g) || !present(g)) continue; for (const t of g.tasks) { if (t._sx == null || !present(t)) continue; if (Math.hypot(t._sx - sx, t._sy - sy) < 8) return { kind: 'task', ref: t }; } }
235
+ for (const g of goals) { const sr = g.r * k; if (sr < 90 || g.status === 'archived' || !seen(g) || !present(g)) continue; const [x, y] = toScreen(g.x, g.y); const d = Math.hypot(x - sx, y - sy); if (d > sr) continue; const n = Math.min(g.crit.length, 8); let bi = -1, bd2 = 7; for (let i = 0; i < n; i++) { const dd = Math.abs(d - ringR(g, i, n) * k); if (dd < bd2) { bd2 = dd; bi = i; } } if (bi >= 0) return { kind: 'ring', goal: g, index: bi, ref: g.crit[bi] }; }
236
+ let bestG = null, bdG = Infinity;
237
+ for (const g of goals) { if (!present(g)) continue; const [x, y] = toScreen(g.x, g.y); const d = Math.hypot(x - sx, y - sy); const rr = Math.max(g.r * k, 9); if (d < rr && d < bdG) { bdG = d; bestG = g; } }
238
+ if (bestG) return { kind: 'goal', ref: bestG };
239
+ const [wx, wy] = toWorld(sx, sy); const b = bandAt(wx, wy); if (b && S.lens !== 'simple') return { kind: 'band', ref: b };
240
+ return null;
241
+ }
242
+
243
+ Object.assign(SKY, { ctx, ACC, CHART, rgba, goalAlpha, draw, drawThought, drawTask, hit });
244
+ })();
@@ -0,0 +1,137 @@
1
+ // modules/hall-ui/public/sky-interaction.js — THE SKY, part 4 of 7: the hover
2
+ // card, the shuttle menu, pointer and keyboard, picking, the lenses and
3
+ // toggles, the stats line, the alt text, jump and the legend (task 1004231 /
4
+ // sky part 3 of 5; ADR 0331).
5
+ //
6
+ // The prototype's "hover card + shuttle menu", "interaction" and "lenses and
7
+ // toggles" sections, verbatim but for the shared state on `SKY.S`. The one
8
+ // behaviour this part CHANGES on purpose: the third lens button, "the whole
9
+ // project", leaves for the project sky at /goal-map (part 4 builds that page)
10
+ // instead of switching this one — the internal 'all' lens survives because the
11
+ // tour walks it. And the "claude layer" pill only appears once part 5's
12
+ // `filed_via` exists on at least one thought; until then it is hidden and
13
+ // every thought shows (the README's "must not ship" row).
14
+ //
15
+ // Keys: L the list, ? the tour, 0 fit, / the composer, Escape closes, arrows
16
+ // pan, + and - zoom.
17
+
18
+ (() => {
19
+ 'use strict';
20
+
21
+ const SKY = window.OTBSky; const S = SKY.S;
22
+ const { $, TAU, hash, clamp, esc, fmtDay, short, hallBase, OBJ, diameter, reach, became, inLayer, shown, seen, present, toWorld, bandAt, cv, hit, edgesOf } = SKY;
23
+
24
+ // ---------------------------------------------------------------- hover card + shuttle menu
25
+ const card = $('card'); const menu = $('menu');
26
+ function glyphHtml(i) { const cls = i.st === 'cold-body' ? 'cold' : i.st; const dim = i.st === 'star' && i.lit < 1 ? ' dim' : ''; const d = Math.round(diameter(i) * 1.1); return `<span class="mk ${cls}${dim}" style="${i.st === 'star' || i.st === 'remnant' ? `width:${d}px;height:${d}px` : ''}"></span>`; }
27
+ function ideaTitle(id) { const i = S.baseIdeas.find((x) => x.id === id); return i ? i.title : 'idea ' + id; }
28
+ function sizeWords(i) { const s = i.size; if (!s) return 'unknown'; const paid = s.paid ?? s.credits; const est = s.estimated ?? s.credits; return `${paid} paid${est !== paid ? ` · ${est} owed by its descent` : ''}${s.payments ? ` · ${s.payments} payment${s.payments === 1 ? '' : 's'}` : ''}`; }
29
+ function showCard(h, sx, sy) {
30
+ if (!h || h.kind === 'shuttle') { card.classList.remove('on'); card.setAttribute('aria-hidden', 'true'); return; }
31
+ const { goalById } = S; let html = '';
32
+ if (h.kind === 'idea') { const i = h.ref; const o = OBJ[i.st] || { noun: i.st, word: i.st }; const wellG = i.well && goalById.get(i.well); const r = reach(i).length; html = `<div class="k">${glyphHtml(i)}<span class="lbl${i.st === 'star' ? ' hot' : ''}">${o.noun} · ${o.word}${i.origin === 'claude' ? ' · filed by a session' : ''}</span></div><div class="t">${esc(i.title)}</div><div class="m">${i.shaped ? `<b>${i.shaped.kind === 'goal' ? 'became this goal' : 'became a ring'}</b> · ` : ''}${wellG ? `caught by <b>${esc(short(wellG.title))}</b>` : `open space · ${Math.round(i.age)} days adrift`} · ${esc(became(i))}${i.size && i.size.credits ? ` · size <b>${i.size.credits}</b>` : ''}${i.lit < 1 ? ` · <b>${Math.round(i.lit * 100)}%</b> alive` : ''}${r ? ' · its reach is drawn' : ''}</div>`; }
33
+ else if (h.kind === 'ring') { const c = h.ref; const g = h.goal; html = `<div class="k"><span class="mk well" style="${c.satisfied ? 'border-style:solid;border-color:rgba(255,255,255,.7)' : ''}"></span><span class="lbl">ring ${h.index + 1} of ${g.crit.length} · ${c.satisfied ? 'closed' : 'open'}</span></div><div class="t">${esc(SKY.plain(c.text).slice(0, 180))}${SKY.plain(c.text).length > 180 ? '…' : ''}</div><div class="m">${c.tasks.length ? `<b>${c.tasks.length}</b> task${c.tasks.length === 1 ? '' : 's'} on this ring · <b>${c.credits}</b> credits` : 'no task gates it yet'}${c.pendingReview ? ' · awaiting review' : ''}${c.bornOf ? ` · <b style="color:var(--accent)">born of a thought</b>: ${esc(ideaTitle(c.bornOf))}` : ''}</div>`; }
34
+ else if (h.kind === 'goal') { const g = h.ref; if (!seen(g)) html = `<div class="k"><span class="mk unseen"></span><span class="lbl">unexplored</span></div><div class="t">Something is here.</div><div class="m">Follow a route from a goal you know to reach it — or turn explore off to see everything.</div>`; else { const met = g.crit.filter((c) => c.satisfied).length; const state = g.status === 'open' ? (g.live > 0 ? 'gravity well · alive' : 'gravity well') : g.status === 'achieved' ? 'settled system' : 'collapsed'; html = `<div class="k"><span class="mk ${g.status === 'open' ? (g.live > 0 ? 'alive' : 'well') : g.status === 'achieved' ? 'settled' : 'collapsed'}"></span><span class="lbl${g.live > 0 ? ' hot' : ''}">${state} · ${esc(g.band.label)}</span></div><div class="t">${esc(g.title)}</div><div class="m"><b>${g.taskDone}</b> of <b>${g.taskTotal}</b> tasks${g.credits ? ` · <b>${g.credits}</b> credits` : ''} · ${g.crit.length ? `<b>${met}</b> of <b>${g.crit.length}</b> rings closed` : 'no rings written'}${g.live > 0 ? ` · <b>${g.active}</b> held now, <b>${g.recent}</b> landed this week` : ''}${g.thoughts.filter(inLayer).length ? ` · <b>${g.thoughts.filter(inLayer).length}</b> of your thoughts` : ''}${g.bornOf ? ` · <b style="color:var(--accent)">born of a thought</b>` : ''}${edgesOf(g).length ? ` · <b>${edgesOf(g).length}</b> route${edgesOf(g).length === 1 ? '' : 's'}` : ''}</div>`; } }
35
+ else if (h.kind === 'task' || h.kind === 'ltask') { const t = h.ref; const kin = t.goal && t.kin ? t.goal.tasks.filter((x) => x.kin === t.kin && x !== t).length : 0; html = `<div class="k"><span class="mk task${t.status === 'active' ? ' live' : ''}"></span><span class="lbl${t.status === 'active' ? ' hot' : ''}">task · ${esc(t.status)}</span></div><div class="t">${esc(t.title)}</div><div class="m">${t.credits != null ? `<b>${t.credits}</b> credits · ` : ''}${t.ci >= 0 ? `ring ${t.ci + 1}` : 'outer orbit'}${t.sourceRef ? ' · born of a thought' : ''}${kin ? ` · lines up with <b>${kin}</b> kin` : ''}${t.drift ? ' · in motion' : ''}</div>`; }
36
+ else if (h.kind === 'stub') { const e = h.edge; const waits = e.a === h.goal; html = `<div class="k"><span class="mk route"></span><span class="lbl chart">route · ${waits ? 'waits on' : 'waited on by'}</span></div><div class="t">${seen(h.other) ? esc(h.other.title) : 'an unexplored goal'}</div><div class="m"><b>${e.count}</b> task dependenc${e.count === 1 ? 'y' : 'ies'} · click to chart it · double-click to chart every route from here</div>`; }
37
+ else if (h.kind === 'band') { const b = h.ref; html = `<div class="k"><span class="lbl">version · ${esc(b.state)}</span></div><div class="t">${esc(b.label)}</div><div class="m"><b>${b.count}</b> goals · <b>${b.tasks}</b> tasks · ${b.r0 === 0 ? 'the present, at the centre' : 'history, further out'} · click to fill the screen with it</div>`; }
38
+ card.innerHTML = html;
39
+ const cw = card.offsetWidth || 260, ch = card.offsetHeight || 80; let x = sx + 18, y = sy + 18; if (x + cw > S.W - 12) x = sx - cw - 18; if (y + ch > S.H - 12) y = sy - ch - 18;
40
+ card.style.left = Math.max(8, x) + 'px'; card.style.top = Math.max(8, y) + 'px'; card.classList.add('on'); card.setAttribute('aria-hidden', 'false');
41
+ }
42
+ function showMenu(e, x, y) {
43
+ S.menuEdge = e; clearTimeout(S.menuTimer); const seed = S.charted.get(e.id) ? S.charted.get(e.id).from : e.a;
44
+ menu.innerHTML = `<div class="h">shuttle · ${esc(short(seed.title))}</div>
45
+ <button type="button" data-act="goal">chart every route from ${esc(short(seed.title))}</button>
46
+ <button type="button" data-act="all">map every route in the sky</button>
47
+ <button type="button" data-act="latest">trace only the latest</button>
48
+ <button type="button" data-act="hide">${S.focusSet ? 'show the unrelated again' : 'hide unrelated systems'}</button>
49
+ <button type="button" data-act="clear">land every shuttle</button>`;
50
+ menu.querySelectorAll('button').forEach((b) => b.addEventListener('click', () => { const act = b.dataset.act; if (act === 'goal') SKY.chartAllOf(seed); else if (act === 'all') SKY.chartEverything(); else if (act === 'latest') SKY.chartLatest(seed); else if (act === 'hide') S.focusSet = S.focusSet ? null : SKY.chartedGoals(); else if (act === 'clear') SKY.clearCharts(); hideMenu(); S.dirty = true; }));
51
+ menu.hidden = false; const mw = menu.offsetWidth || 220, mh = menu.offsetHeight || 150; menu.style.left = Math.min(S.W - mw - 8, x + 14) + 'px'; menu.style.top = Math.min(S.H - mh - 8, y + 14) + 'px';
52
+ }
53
+ function hideMenu() { menu.hidden = true; S.menuEdge = null; }
54
+ menu.addEventListener('pointerenter', () => clearTimeout(S.menuTimer));
55
+ menu.addEventListener('pointerleave', () => { S.menuTimer = setTimeout(hideMenu, 300); });
56
+
57
+ // ---------------------------------------------------------------- interaction
58
+ cv.addEventListener('pointerdown', (e) => { S.drag = { x: e.clientX, y: e.clientY, cx: S.cam.x, cy: S.cam.y, moved: false }; if (cv.setPointerCapture) cv.setPointerCapture(e.pointerId); cv.classList.add('dragging'); S.flight = null; showCard(null); });
59
+ cv.addEventListener('pointermove', (e) => {
60
+ const { cam } = S;
61
+ if (S.drag) { if (S.lens === 'simple') return; const dx = e.clientX - S.drag.x, dy = e.clientY - S.drag.y; if (Math.hypot(dx, dy) > 3) S.drag.moved = true; cam.x = S.drag.cx - dx / cam.k; cam.y = S.drag.cy - dy / cam.k; S.dirty = true; return; }
62
+ const h = hit(e.clientX, e.clientY); const hover = S.hover; const same = h && hover && h.kind === hover.kind && h.ref === hover.ref && h.index === hover.index && h.goal === hover.goal;
63
+ if (!same) { S.hover = h; S.dirty = true; cv.style.cursor = h && h.kind !== 'band' ? 'pointer' : 'grab'; }
64
+ showCard(h, e.clientX, e.clientY);
65
+ if (h && h.kind === 'shuttle') { if (S.menuEdge !== h.ref) showMenu(h.ref, h.x, h.y); } else if (S.menuEdge && menu.hidden === false) { clearTimeout(S.menuTimer); S.menuTimer = setTimeout(hideMenu, 350); }
66
+ });
67
+ cv.addEventListener('pointerleave', () => { S.hover = null; showCard(null); S.dirty = true; });
68
+ cv.addEventListener('pointerup', (e) => { cv.classList.remove('dragging'); if (S.drag && !S.drag.moved) { const h = hit(e.clientX, e.clientY); if (h && h.kind === 'stub') SKY.chartRoute(h.edge, h.goal); else if (h && h.kind === 'shuttle') showMenu(h.ref, h.x, h.y); else if (h && h.kind === 'ring') pick({ kind: 'goal', ref: h.goal }, false); else if (h && h.kind === 'goal' && !seen(h.ref)) { /* not reached yet */ } else if (h && h.kind === 'band') { pick(null); SKY.fitBand(h.ref); } else pick(h, false); } S.drag = null; });
69
+ cv.addEventListener('dblclick', (e) => { const h = hit(e.clientX, e.clientY); if (h && h.kind === 'stub') SKY.chartAllOf(h.goal); else if (h && (h.kind === 'goal' || h.kind === 'ring') && seen(h.kind === 'goal' ? h.ref : h.goal)) SKY.fitGoal(h.kind === 'goal' ? h.ref : h.goal); else { const [wx, wy] = toWorld(e.clientX, e.clientY); SKY.flyTo(wx, wy, S.cam.k * 2.2, 700); } });
70
+ cv.addEventListener('wheel', (e) => { e.preventDefault(); if (S.lens === 'simple') return; const { cam } = S; const f = Math.exp(-e.deltaY * 0.0016); const nk = clamp(cam.k * f, 0.015, 9); const [wx, wy] = toWorld(e.clientX, e.clientY); cam.x = wx - (e.clientX - S.W / 2) / nk; cam.y = wy - (e.clientY - S.H / 2) / nk; cam.k = nk; S.flight = null; S.dirty = true; showCard(null); }, { passive: false });
71
+ addEventListener('keydown', (e) => {
72
+ if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) { if (e.key === 'Escape') e.target.blur(); return; }
73
+ const { cam } = S; const step = 60 / cam.k;
74
+ if (e.key === 'ArrowLeft') { cam.x -= step; S.dirty = true; } else if (e.key === 'ArrowRight') { cam.x += step; S.dirty = true; } else if (e.key === 'ArrowUp') { cam.y -= step; S.dirty = true; } else if (e.key === 'ArrowDown') { cam.y += step; S.dirty = true; }
75
+ else if (e.key === '+' || e.key === '=') { cam.k = clamp(cam.k * 1.3, .015, 9); S.dirty = true; } else if (e.key === '-') { cam.k = clamp(cam.k / 1.3, .015, 9); S.dirty = true; }
76
+ else if (e.key === '0') SKY.fitAll(); else if (e.key === 'Escape') { if (!$('tour').hidden) SKY.endTour(); else if (!menu.hidden) hideMenu(); else pick(null); } else if (e.key === '/') { e.preventDefault(); $('thought').focus(); }
77
+ else if (e.key === '?') SKY.startTour(); else if (e.key.toLowerCase() === 'l') SKY.toggleRoom(); else return;
78
+ });
79
+ function pick(h, fly) { if (h && h.kind === 'ltask') { SKY.renderPanel({ kind: 'ltask', ref: h.ref }); S.dirty = true; return; } S.picked = h; SKY.renderPanel(S.picked); SKY.renderRoom(); S.dirty = true; if (fly && S.picked && S.lens !== 'simple') { if (S.picked.kind === 'goal') SKY.fitGoal(S.picked.ref); else if (S.picked.kind === 'idea') SKY.fitThought(S.picked.ref); } }
80
+
81
+ // ---------------------------------------------------------------- lenses and toggles
82
+ function setLens(l, noFly) {
83
+ S.lens = l;
84
+ for (const [id, v] of [['lensSimple', 'simple'], ['lensMine', 'mine'], ['lensAll', 'all']]) $(id).setAttribute('aria-pressed', String(l === v));
85
+ $('lensWord').textContent = l === 'simple' ? '— simply' : l === 'mine' ? '— your thinking' : '— the whole project'; document.querySelector('.brand h1').firstChild.textContent = l === 'all' ? 'the sky ' : 'your sky ';
86
+ $('claudeBtn').hidden = l !== 'mine' || !S.hasFiledVia; $('influenceBtn').hidden = l !== 'mine'; $('ideasBtn').hidden = l !== 'all'; $('exploreBtn').hidden = l !== 'all'; $('rehearse').hidden = !(l === 'mine' || (l === 'all' && S.ideasOnMap));
87
+ $('recentBtn').hidden = l !== 'simple'; $('roomBtn').hidden = l === 'simple'; $('clockBtn').hidden = l === 'simple'; $('time').hidden = S.clockHidden || l === 'simple'; document.querySelector('.jump').hidden = l === 'simple'; $('legend').hidden = l === 'simple';
88
+ if (l !== 'all') SKY.clearCharts();
89
+ if (l === 'simple') { SKY.toggleRoom(false); pick(null); }
90
+ if (!noFly) { if (l === 'all') SKY.fitAll(); else SKY.fitPresent(); }
91
+ SKY.maybeStart(); SKY.renderRoom(); updateStats(); S.dirty = true;
92
+ }
93
+ $('lensSimple').addEventListener('click', () => setLens('simple')); $('lensMine').addEventListener('click', () => setLens('mine'));
94
+ // "the whole project" is the project sky — its own page (part 4).
95
+ $('lensAll').addEventListener('click', () => { location.assign(`${hallBase()}/goal-map`); });
96
+ $('fitBtn').addEventListener('click', () => (S.lens === 'all' ? SKY.fitAll() : SKY.fitPresent()));
97
+ function togglePill(id, get, set) { $(id).addEventListener('click', () => { set(!get()); $(id).setAttribute('aria-pressed', String(get())); SKY.renderRoom(); updateStats(); S.dirty = true; }); }
98
+ togglePill('recentBtn', () => S.recentOnly, (v) => { S.recentOnly = v; });
99
+ togglePill('claudeBtn', () => S.claudeLayer, (v) => { S.claudeLayer = v; });
100
+ togglePill('influenceBtn', () => S.influence, (v) => { S.influence = v; });
101
+ togglePill('ideasBtn', () => S.ideasOnMap, (v) => { S.ideasOnMap = v; $('rehearse').hidden = !(S.lens === 'mine' || S.ideasOnMap); });
102
+ togglePill('exploreBtn', () => S.explore.on, (v) => { S.explore.on = v; SKY.saveExplore(); SKY.maybeStart(); });
103
+ $('exploreBtn').setAttribute('aria-pressed', String(S.explore.on));
104
+ $('rehearse').addEventListener('click', () => { S.rehearsal = !S.rehearsal; $('rehearse').setAttribute('aria-pressed', String(S.rehearsal)); SKY.applyRehearsal(); SKY.placeIdeas(); if (S.picked && S.picked.kind === 'idea') { S.picked = { kind: 'idea', ref: S.ideas.find((i) => i.id === S.picked.ref.id) }; SKY.renderPanel(S.picked); } SKY.renderRoom(); updateStats(); S.dirty = true; });
105
+
106
+ function updateStats() {
107
+ const { ideas, goals, lens, clock } = S; const NOW = SKY.NOW;
108
+ const pool = ideas.filter(inLayer); const c = pool.reduce((a, i) => { a[i.st] = (a[i.st] || 0) + 1; return a; }, {});
109
+ const gs = goals.filter(present); const openN = gs.filter((g) => g.status === 'open').length, ach = gs.filter((g) => g.status === 'achieved').length, arc = gs.length - openN - ach, live = gs.filter((g) => g.live > 0).length;
110
+ const hiddenN = ideas.length - pool.length; const asOf = clock < NOW - 3600000 ? ` · <span style="color:var(--chart)">as of ${fmtDay(clock)}</span>` : '';
111
+ // GET /sky/mine caps at a thousand rows and says so — the cap is spoken, never a silent slice.
112
+ const capped = S.truncated ? ` · <span class="hint">the newest ${ideas.length} only</span>` : '';
113
+ $('stats').innerHTML = lens === 'simple'
114
+ ? `<b>${pool.filter(shown).length}</b> thoughts${S.recentOnly ? ' · the last thirty days' : ' · everything you filed'}${hiddenN ? ` · <span class="hint">+${hiddenN} hidden</span>` : ''}${capped}`
115
+ : lens === 'mine'
116
+ ? `<b>${pool.filter(present).length}</b> thoughts · <b>${c.star || 0}</b> burning · <b>${c.protostar || 0}</b> waiting · <b>${c.satellite || 0}</b> folded in · <b>${c.remnant || 0}</b> burned out · <b>${c['cold-body'] || 0}</b> declined${hiddenN ? ` · <span class="hint">+${hiddenN} hidden</span>` : ''}${S.rehearsal ? ' · <span style="color:var(--accent)">what if</span>' : ''}${asOf}${capped}`
117
+ : `<b>${gs.length}</b> goals · <b>${live}</b> alive · <b>${openN}</b> wells · <b>${ach}</b> settled · <b>${arc}</b> collapsed · <b>${S.edges.length}</b> routes · <b>${[...S.taskById.values()].filter((t) => present(t) && present(t.goal)).length}</b> tasks${S.explore.on ? ` · <span style="color:var(--chart)">explored ${S.explore.seen.size} of ${goals.length}</span>` : ''}${asOf}`;
118
+ }
119
+ function altText() { const { cam, charted, lens } = S; const k = cam.k; const alt = k < 0.06 ? 'the whole sky' : k < 0.4 ? 'the present' : k < 2 ? 'a well' : 'a thought'; const b = bandAt(cam.x, cam.y); return `<b>${alt}</b>${b ? ` · ${esc(b.label)}` : ''} · zoom ${k.toFixed(2)}${charted.size ? ` · <span style="color:var(--chart)">${charted.size} route${charted.size === 1 ? '' : 's'} charted</span>` : ''}<br><span class="hint">${lens === 'simple' ? 'hover to read · click to open · / file · ? tour' : 'drag · scroll · click a band to fill the screen · double-click a well · hover to read · click a rim arrow to chart · L list · / file · ? tour · 0 fit'}</span>`; }
120
+
121
+ $('jump').addEventListener('input', () => {
122
+ const q = $('jump').value.trim().toLowerCase(); const ul = $('jumpList'); if (!q) { ul.hidden = true; return; }
123
+ const gs = S.goals.filter((g) => seen(g) && g.title.toLowerCase().includes(q)).slice(0, 6).map((g) => ({ kind: 'goal', ref: g, w: g.status === 'open' ? 'well' : g.status }));
124
+ const is = S.ideas.filter((i) => inLayer(i) && i.title.toLowerCase().includes(q)).slice(0, 8).map((i) => ({ kind: 'idea', ref: i, w: (OBJ[i.st] || { word: i.st }).word }));
125
+ const all = [...gs, ...is]; ul.hidden = false;
126
+ ul.innerHTML = all.length ? all.map((h, n) => `<li><button type="button" data-n="${n}"><span class="lbl">${esc(h.w)}</span><span>${esc(h.ref.title)}</span></button></li>`).join('') : '<li><button type="button" disabled>nothing by that name — or not explored yet</button></li>';
127
+ ul.querySelectorAll('button[data-n]').forEach((b) => b.addEventListener('click', () => { const h = all[+b.dataset.n]; if (h.kind === 'goal' && S.lens !== 'all' && h.ref.status !== 'open') setLens('all', true); if (h.kind === 'idea' && S.lens === 'all') { S.ideasOnMap = true; $('ideasBtn').setAttribute('aria-pressed', 'true'); } pick({ kind: h.kind, ref: h.ref }, true); ul.hidden = true; $('jump').value = ''; }));
128
+ });
129
+ document.addEventListener('click', (e) => { if (!(e.target && e.target.closest && e.target.closest('.jump'))) $('jumpList').hidden = true; });
130
+
131
+ $('legend').innerHTML = [
132
+ ['star', 'star', 'burning'], ['protostar', 'protostar', 'waiting'], ['satellite', 'satellite', 'folded in'], ['remnant', 'remnant', 'burned out'], ['cold', 'cold body', 'declined'],
133
+ ['well', 'well', 'an open goal'], ['alive', 'alive', 'a well with work in motion'], ['settled', 'settled', 'achieved'], ['collapsed', 'collapsed', 'archived'], ['unseen', 'unexplored', 'not yet reached'], ['task live', 'body', 'a task'], ['born', 'born of a thought', 'a goal or ring an idea became'], ['route', 'route', 'a dependency, charted'],
134
+ ].map(([cls, name, what]) => `<button type="button" class="obj" title="${esc(what)}"><span class="g"><span class="mk ${cls}"></span></span>${esc(name)}</button>`).join('');
135
+
136
+ Object.assign(SKY, { card, menu, glyphHtml, ideaTitle, sizeWords, showCard, showMenu, hideMenu, pick, setLens, togglePill, updateStats, altText, TAU, hash });
137
+ })();
@@ -0,0 +1,81 @@
1
+ // modules/hall-ui/public/sky-layout.js — THE SKY, part 2 of 7: the clock, the
2
+ // routes, explore, and the camera (task 1004231 / sky part 3 of 5; ADR 0331).
3
+ //
4
+ // The prototype's "the clock", "the routes", "explore" and "the camera"
5
+ // sections, verbatim but for the shared state living on `SKY.S` (see
6
+ // sky-data.js for the split). The clock draws the sky as it stood on a day —
7
+ // nothing moves position, only what exists changes. Routes are the goal
8
+ // graph's own edges, charted by a shuttle when asked. Explore is the project
9
+ // sky's rule (part 4 owns that page; the tour walks it here). The camera is a
10
+ // pan/zoom over world coordinates laid out in sky-data.js.
11
+
12
+ (() => {
13
+ 'use strict';
14
+
15
+ const SKY = window.OTBSky; const S = SKY.S;
16
+ const { $, TAU, reduce, NOW, clamp, smooth, ease, esc, fmtDay, short, store } = SKY;
17
+
18
+ // ---------------------------------------------------------------- the clock
19
+ const slider = $('clock');
20
+ function setClock(t) { S.clock = clamp(t, S.T0, NOW); slider.value = String(Math.round((S.clock - S.T0) / Math.max(1, NOW - S.T0) * 1000)); const past = S.clock < NOW - 3600000; $('when').textContent = past ? fmtDay(S.clock) : 'today'; $('when').classList.toggle('past', past); SKY.updateStats(); S.dirty = true; }
21
+ slider.addEventListener('input', () => { S.playing = false; $('play').setAttribute('aria-pressed', 'false'); setClock(S.T0 + (NOW - S.T0) * (Number(slider.value) / 1000)); });
22
+ $('timeHide').addEventListener('click', () => { S.clockHidden = true; store.set('sky-clock', 'off'); $('time').hidden = true; });
23
+ $('clockBtn').addEventListener('click', () => { S.clockHidden = !S.clockHidden; store.set('sky-clock', S.clockHidden ? 'off' : 'on'); $('time').hidden = S.clockHidden || S.lens === 'simple'; });
24
+ $('play').addEventListener('click', () => { S.playing = !S.playing; $('play').setAttribute('aria-pressed', String(S.playing)); if (S.playing && S.clock >= NOW - 3600000) setClock(S.T0); });
25
+ function present(o) { return (o.born || 0) <= S.clock; }
26
+
27
+ // ---------------------------------------------------------------- the routes
28
+ function curve(e) { const [ax, ay] = toScreen(e.a.x, e.a.y), [bx, by] = toScreen(e.b.x, e.b.y); return { ax, ay, bx, by, mx: (ax + bx) / 2 + (by - ay) * 0.12, my: (ay + by) / 2 - (bx - ax) * 0.12 }; }
29
+ function pointOn(c, t) { const u = 1 - t; return [u * u * c.ax + 2 * u * t * c.mx + t * t * c.bx, u * u * c.ay + 2 * u * t * c.my + t * t * c.by]; }
30
+ function chart(e, from, delay) { if (S.charted.has(e.id) && S.charted.get(e.id).done) return; const len = Math.hypot(e.a.x - e.b.x, e.a.y - e.b.y); S.charted.set(e.id, { done: reduce, t0: performance.now() + (delay || 0), ms: reduce ? 1 : clamp(len * 0.3, 900, 2400), from: from || e.a }); if (reduce) reveal(e.a === (from || e.a) ? e.b : e.a); S.dirty = true; }
31
+ // the camera pulls back to hold both ends of every route being charted — never closer than it was
32
+ function frameAll(pts, pad) { const { cam, W, H } = S; const xs = pts.map((p) => p[0]), ys = pts.map((p) => p[1]); const x0 = Math.min(...xs), x1 = Math.max(...xs), y0 = Math.min(...ys), y1 = Math.max(...ys); const k = Math.min(cam.k, W / Math.max(1, x1 - x0 + pad * 2), H / Math.max(1, y1 - y0 + pad * 2)); const inView = pts.every(([x, y]) => { const [sx, sy] = toScreen(x, y); return sx > 60 && sx < W - 60 && sy > 120 && sy < H - 120; }); if (!inView || k < cam.k * 0.98) flyTo((x0 + x1) / 2 + (panelOpen() ? 170 / k : 0), (y0 + y1) / 2, k, 900); }
33
+ function chartRoute(e, from) { chart(e, from); frameAll([[e.a.x, e.a.y], [e.b.x, e.b.y]], Math.max(e.a.r, e.b.r) * 2.4 + 500); }
34
+ function chartAllOf(g) { const es = SKY.edgesOf(g); es.forEach((e, n) => chart(e, g, n * 160)); frameAll([[g.x, g.y], ...es.map((e) => { const o = e.a === g ? e.b : e.a; return [o.x, o.y]; })], 900); }
35
+ function chartEverything() { S.edges.forEach((e, n) => chart(e, e.a, n * 90)); fitAll(); }
36
+ // "only the latest": routes with both ends in the present band — the
37
+ // prototype named the building version; the bands know which that is.
38
+ function chartLatest(seed) { const now = S.BANDS[0].versions; SKY.edgesOf(seed).filter((e) => now.includes(e.a.version) && now.includes(e.b.version)).forEach((e, n) => chart(e, seed, n * 160)); }
39
+ function chartedGoals() { const s = new Set(); for (const [id] of S.charted) { const e = S.edges.find((x) => x.id === id); s.add(e.a.id); s.add(e.b.id); } return s; }
40
+ function clearCharts() { S.charted.clear(); S.focusSet = null; S.dirty = true; }
41
+
42
+ // ---------------------------------------------------------------- explore: the sky reveals itself
43
+ const explore = S.explore;
44
+ function seen(g) { return !g || S.lens !== 'all' || !explore.on || explore.seen.has(g.id); }
45
+ function reveal(g) { if (!g || !explore.on || S.lens !== 'all' || explore.seen.has(g.id)) return; explore.seen.add(g.id); g.revealAt = performance.now(); store.set('sky-seen', JSON.stringify([...explore.seen])); SKY.renderRoom(); SKY.updateStats(); S.dirty = true; }
46
+ function saveExplore() { store.set('sky-explore', explore.on ? 'on' : 'off'); }
47
+ function startOptions() {
48
+ const el = $('startOpts'); const wells = S.goals.filter((g) => g.status === 'open').sort((a, b) => b.mass - a.mass);
49
+ el.innerHTML = `<button type="button" class="door primary" data-start="mine">where my thoughts are</button>` + wells.map((g) => `<button type="button" class="door" data-start="${esc(g.id)}">${esc(short(g.title))}</button>`).join('') + `<button type="button" class="door" data-start="all">show me everything</button>`;
50
+ el.querySelectorAll('button').forEach((b) => b.addEventListener('click', () => {
51
+ const v = b.dataset.start; $('start').hidden = true;
52
+ if (v === 'all') { explore.on = false; $('exploreBtn').setAttribute('aria-pressed', 'false'); saveExplore(); fitAll(); }
53
+ else if (v === 'mine') { const t = S.goals.filter(SKY.touched); (t.length ? t : wells.slice(0, 1)).forEach(reveal); fitPresent(); }
54
+ else { const g = S.goalById.get(v); reveal(g); SKY.pick({ kind: 'goal', ref: g }, true); }
55
+ SKY.renderRoom(); SKY.updateStats(); S.dirty = true;
56
+ }));
57
+ }
58
+ function maybeStart() { const need = S.lens === 'all' && explore.on && explore.seen.size === 0 && $('tour').hidden; $('start').hidden = !need; if (need) startOptions(); }
59
+
60
+ // ---------------------------------------------------------------- the camera
61
+ const cv = $('sky'); const cam = S.cam;
62
+ function resize() { S.DPR = Math.min(2, devicePixelRatio || 1); S.W = innerWidth; S.H = innerHeight; cv.width = Math.round(S.W * S.DPR); cv.height = Math.round(S.H * S.DPR); cv.style.width = S.W + 'px'; cv.style.height = S.H + 'px'; S.dirty = true; }
63
+ addEventListener('resize', resize);
64
+ const toScreen = (x, y) => [(x - cam.x) * cam.k + S.W / 2, (y - cam.y) * cam.k + S.H / 2];
65
+ const toWorld = (sx, sy) => [(sx - S.W / 2) / cam.k + cam.x, (sy - S.H / 2) / cam.k + cam.y];
66
+ function flyTo(x, y, k, dur) { if (reduce) { cam.x = x; cam.y = y; cam.k = k; S.dirty = true; return; } S.flight = { x0: cam.x, y0: cam.y, k0: cam.k, x, y, k, t0: performance.now(), ms: dur || 900 }; }
67
+ function fitRad(rad) { return Math.min(S.W / (rad * S.ASPECT * 2), S.H / (rad * 2)) * 0.94; }
68
+ function fitAll() { flyTo(0, 0, fitRad(S.BANDS[S.BANDS.length - 1].r1 + 100), 1100); }
69
+ function fitPresent() { flyTo(0, 0, fitRad(S.BANDS[0].r1 + 120), 1100); }
70
+ function fitBand(b) { flyTo(0, 0, fitRad(b.r1 + 80), 1000); }
71
+ function fitGoal(g) { const k = Math.min(S.W, S.H) / (g.r * 2.9); flyTo(g.x + (panelOpen() ? 170 / k : 0), g.y, k, 900); }
72
+ function fitThought(i) { const k = Math.max(cam.k, 1.6); flyTo(i.x + (panelOpen() ? 170 / k : 0), i.y, k, 900); }
73
+ function panelOpen() { return !$('panel').hidden && S.W > 900; }
74
+ function bandAt(x, y) { const d = Math.hypot(x / S.ASPECT, y); return S.BANDS.find((b) => d >= b.r0 - 1 && d <= b.r1 + 1) || null; }
75
+
76
+ Object.assign(SKY, {
77
+ setClock, present, curve, pointOn, chart, frameAll, chartRoute, chartAllOf, chartEverything, chartLatest, chartedGoals, clearCharts,
78
+ seen, reveal, saveExplore, startOptions, maybeStart, cv, resize, toScreen, toWorld, flyTo, fitRad, fitAll, fitPresent, fitBand, fitGoal, fitThought, panelOpen, bandAt,
79
+ smooth, ease, TAU,
80
+ });
81
+ })();