@odyn-app/api 0.0.0-stage → 0.2.0

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 (42) hide show
  1. package/README.md +35 -2
  2. package/cli/build.mjs +146 -0
  3. package/cli/github.mjs +190 -0
  4. package/cli/odyn-extension.mjs +310 -0
  5. package/dist/index.d.ts +516 -0
  6. package/dist/index.js +571 -0
  7. package/dist/ui/button.d.ts +10 -0
  8. package/dist/ui/button.js +31 -0
  9. package/dist/ui/cn.d.ts +3 -0
  10. package/dist/ui/cn.js +6 -0
  11. package/dist/ui/index.d.ts +10 -0
  12. package/dist/ui/index.js +10 -0
  13. package/dist/ui/orb/orb-base.module.css +21 -0
  14. package/dist/ui/orb/orb-frame-clock.d.ts +34 -0
  15. package/dist/ui/orb/orb-frame-clock.js +110 -0
  16. package/dist/ui/orb/orb-geometry-cache.d.ts +1 -0
  17. package/dist/ui/orb/orb-geometry-cache.js +11 -0
  18. package/dist/ui/orb/orb-helix-geometry.d.ts +7 -0
  19. package/dist/ui/orb/orb-helix-geometry.js +130 -0
  20. package/dist/ui/orb/orb-helix.module.css +155 -0
  21. package/dist/ui/orb/orb-lattice-geometry.d.ts +35 -0
  22. package/dist/ui/orb/orb-lattice-geometry.js +148 -0
  23. package/dist/ui/orb/orb-lattice.module.css +165 -0
  24. package/dist/ui/orb/orb-lens-primary.module.css +195 -0
  25. package/dist/ui/orb/orb-lens-secondary.module.css +143 -0
  26. package/dist/ui/orb/orb-lens.module.css +44 -0
  27. package/dist/ui/orb/orb-morph-geometry.d.ts +7 -0
  28. package/dist/ui/orb/orb-morph-geometry.js +88 -0
  29. package/dist/ui/orb/orb-morph.module.css +62 -0
  30. package/dist/ui/orb/orb-ring-geometry.d.ts +8 -0
  31. package/dist/ui/orb/orb-ring-geometry.js +42 -0
  32. package/dist/ui/orb/orb-ring.module.css +113 -0
  33. package/dist/ui/orb/orb-types.d.ts +23 -0
  34. package/dist/ui/orb/orb-types.js +48 -0
  35. package/dist/ui/orb/orb.d.ts +17 -0
  36. package/dist/ui/orb/orb.js +58 -0
  37. package/dist/ui/setting-row.d.ts +9 -0
  38. package/dist/ui/setting-row.js +9 -0
  39. package/dist/ui/skeleton.d.ts +3 -0
  40. package/dist/ui/skeleton.js +6 -0
  41. package/dist/ui/theme.css +232 -0
  42. package/package.json +63 -4
@@ -0,0 +1,34 @@
1
+ /**
2
+ * One shared clock that moves every spinning orb forward a few times a second.
3
+ *
4
+ * Left to itself, a CSS animation makes the browser redraw the whole window 60
5
+ * times a second for as long as it runs. While a reply is streaming, that
6
+ * redraw cost about 40% of a processor core in testing — and one 3px orb cost
7
+ * as much as four, because the price is the redraw itself, not the dots.
8
+ *
9
+ * So the orbs' CSS animations are paused as soon as they start, and this clock
10
+ * moves them to the right moment ORB_FRAMES_PER_SECOND times a second. Each
11
+ * style keeps its exact shape and easing; only the number of redraws drops.
12
+ * Between ticks nothing changes, and the browser skips the redraw.
13
+ */
14
+ export declare const ORB_FRAMES_PER_SECOND = 12;
15
+ /** Marks an orb's outer element, so a starting animation can be traced to one. */
16
+ export declare const ORB_ROOT_ATTRIBUTE = "data-orb";
17
+ /** Present on an orb held on its opening frame; the clock leaves those alone. */
18
+ export declare const ORB_PAUSED_ATTRIBUTE = "data-orb-paused";
19
+ /** Moves every clocked animation to where it should be at `now`. */
20
+ export declare function tickOrbClock(now: number): void;
21
+ /**
22
+ * Hands these animations to the shared clock, carrying on from wherever each
23
+ * one already is. Handing over one that is already clocked does nothing.
24
+ */
25
+ export declare function clockOrbAnimations(animations: readonly Animation[], now?: number): void;
26
+ /** Takes these animations off the clock. They stay paused where they are. */
27
+ export declare function releaseOrbAnimations(animations: readonly Animation[]): void;
28
+ /**
29
+ * Called by each orb while it is on screen. The first one starts listening for
30
+ * orb animations; the last one to leave stops. Returns the matching "leave".
31
+ */
32
+ export declare function watchOrbAnimations(): () => void;
33
+ /** How many animations the clock is driving — for tests. */
34
+ export declare function clockedOrbAnimationCount(): number;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * One shared clock that moves every spinning orb forward a few times a second.
3
+ *
4
+ * Left to itself, a CSS animation makes the browser redraw the whole window 60
5
+ * times a second for as long as it runs. While a reply is streaming, that
6
+ * redraw cost about 40% of a processor core in testing — and one 3px orb cost
7
+ * as much as four, because the price is the redraw itself, not the dots.
8
+ *
9
+ * So the orbs' CSS animations are paused as soon as they start, and this clock
10
+ * moves them to the right moment ORB_FRAMES_PER_SECOND times a second. Each
11
+ * style keeps its exact shape and easing; only the number of redraws drops.
12
+ * Between ticks nothing changes, and the browser skips the redraw.
13
+ */
14
+ export const ORB_FRAMES_PER_SECOND = 12;
15
+ const ORB_FRAME_INTERVAL_MS = 1000 / ORB_FRAMES_PER_SECOND;
16
+ /** Marks an orb's outer element, so a starting animation can be traced to one. */
17
+ export const ORB_ROOT_ATTRIBUTE = 'data-orb';
18
+ /** Present on an orb held on its opening frame; the clock leaves those alone. */
19
+ export const ORB_PAUSED_ATTRIBUTE = 'data-orb-paused';
20
+ /** Each clocked animation, with when its own timeline started (`performance.now()` scale). */
21
+ const clockedAnimations = new Map();
22
+ let clockTimer = null;
23
+ let mountedOrbCount = 0;
24
+ function readCurrentTime(animation) {
25
+ const { currentTime } = animation;
26
+ return typeof currentTime === 'number' ? currentTime : 0;
27
+ }
28
+ function stopClockIfIdle() {
29
+ if (clockedAnimations.size > 0 || clockTimer === null)
30
+ return;
31
+ clearInterval(clockTimer);
32
+ clockTimer = null;
33
+ }
34
+ /** Moves every clocked animation to where it should be at `now`. */
35
+ export function tickOrbClock(now) {
36
+ for (const [animation, origin] of clockedAnimations) {
37
+ // The stylesheet removed it (the orb left the page, or reduce-motion was
38
+ // switched on), so stop driving it — setting its time would revive it.
39
+ if (animation.playState === 'idle') {
40
+ clockedAnimations.delete(animation);
41
+ continue;
42
+ }
43
+ animation.currentTime = now - origin;
44
+ }
45
+ stopClockIfIdle();
46
+ }
47
+ function tickWhenVisible() {
48
+ // A hidden window has nothing to show; the next visible tick jumps each
49
+ // animation straight to the right moment.
50
+ if (typeof document !== 'undefined' && document.hidden)
51
+ return;
52
+ tickOrbClock(performance.now());
53
+ }
54
+ /**
55
+ * Hands these animations to the shared clock, carrying on from wherever each
56
+ * one already is. Handing over one that is already clocked does nothing.
57
+ */
58
+ export function clockOrbAnimations(animations, now = performance.now()) {
59
+ for (const animation of animations) {
60
+ if (animation.playState === 'idle' || clockedAnimations.has(animation))
61
+ continue;
62
+ clockedAnimations.set(animation, now - readCurrentTime(animation));
63
+ animation.pause();
64
+ }
65
+ if (clockedAnimations.size > 0)
66
+ clockTimer ??= setInterval(tickWhenVisible, ORB_FRAME_INTERVAL_MS);
67
+ }
68
+ /** Takes these animations off the clock. They stay paused where they are. */
69
+ export function releaseOrbAnimations(animations) {
70
+ for (const animation of animations)
71
+ clockedAnimations.delete(animation);
72
+ stopClockIfIdle();
73
+ }
74
+ /**
75
+ * Some orb animations start after the orb first appears — a card opening, a
76
+ * row scrolling back into view. Catch each one the moment it starts.
77
+ */
78
+ function handleAnimationStart(event) {
79
+ const { target } = event;
80
+ if (!(target instanceof Element))
81
+ return;
82
+ if (target.closest(`[${ORB_ROOT_ATTRIBUTE}]`) === null)
83
+ return;
84
+ if (target.closest(`[${ORB_PAUSED_ATTRIBUTE}]`) !== null)
85
+ return;
86
+ clockOrbAnimations(target
87
+ .getAnimations()
88
+ .filter((animation) => animation instanceof CSSAnimation && animation.animationName === event.animationName));
89
+ }
90
+ /**
91
+ * Called by each orb while it is on screen. The first one starts listening for
92
+ * orb animations; the last one to leave stops. Returns the matching "leave".
93
+ */
94
+ export function watchOrbAnimations() {
95
+ if (typeof document === 'undefined')
96
+ return () => { };
97
+ mountedOrbCount += 1;
98
+ if (mountedOrbCount === 1)
99
+ document.addEventListener('animationstart', handleAnimationStart, true);
100
+ return () => {
101
+ mountedOrbCount -= 1;
102
+ if (mountedOrbCount === 0) {
103
+ document.removeEventListener('animationstart', handleAnimationStart, true);
104
+ }
105
+ };
106
+ }
107
+ /** How many animations the clock is driving — for tests. */
108
+ export function clockedOrbAnimationCount() {
109
+ return clockedAnimations.size;
110
+ }
@@ -0,0 +1 @@
1
+ export declare function memoiseByVariant<Variant, Value>(build: (variant: Variant) => Value): (variant: Variant) => Value;
@@ -0,0 +1,11 @@
1
+ export function memoiseByVariant(build) {
2
+ const cache = new Map();
3
+ return (variant) => {
4
+ const cached = cache.get(variant);
5
+ if (cached !== undefined)
6
+ return cached;
7
+ const built = build(variant);
8
+ cache.set(variant, built);
9
+ return built;
10
+ };
11
+ }
@@ -0,0 +1,7 @@
1
+ import type { CSSProperties } from 'react';
2
+ import type { HelixVariant } from './orb-types';
3
+ export interface GlobeDot {
4
+ readonly key: number;
5
+ readonly style: CSSProperties;
6
+ }
7
+ export declare const globeDots: (variant: HelixVariant) => readonly GlobeDot[];
@@ -0,0 +1,130 @@
1
+ import { memoiseByVariant } from './orb-geometry-cache';
2
+ const RADIUS = 8.5;
3
+ const TILT = (14 * Math.PI) / 180;
4
+ const SPIN_STEPS = 8;
5
+ const RING_ARC_STEPS = 3;
6
+ const HALF_TURN = Math.PI;
7
+ const RINGS = [
8
+ { latitude: 52, count: 8 },
9
+ { latitude: 26, count: 8 },
10
+ { latitude: 0, count: 8 },
11
+ { latitude: -26, count: 8 },
12
+ { latitude: -52, count: 8 },
13
+ ];
14
+ function projectGlobe(x, y, z, spin) {
15
+ const spinCosine = Math.cos(spin);
16
+ const spinSine = Math.sin(spin);
17
+ const spunX = x * spinCosine - z * spinSine;
18
+ const spunZ = x * spinSine + z * spinCosine;
19
+ const tiltCosine = Math.cos(TILT);
20
+ const tiltSine = Math.sin(TILT);
21
+ return {
22
+ x: spunX,
23
+ y: y * tiltCosine - spunZ * tiltSine,
24
+ z: y * tiltSine + spunZ * tiltCosine,
25
+ };
26
+ }
27
+ function globeOpacity(z) {
28
+ const depth = Math.max(0, Math.min(1, (z / RADIUS + 0.15) / 1.15));
29
+ return 0.12 + 0.88 * depth * depth;
30
+ }
31
+ function ringDirection(ring) {
32
+ return ring % 2 === 0 ? -1 : 1;
33
+ }
34
+ const SEQUENTIAL_MOVES = (() => {
35
+ const moves = [];
36
+ for (let pass = 0; pass < 2; pass++) {
37
+ for (let ring = 0; ring < RINGS.length; ring++) {
38
+ moves.push({ ring, angle: ringDirection(ring) * HALF_TURN });
39
+ }
40
+ }
41
+ return moves;
42
+ })();
43
+ const SCRAMBLED_MOVES = [2, 1, 3, 0, 4, 2, 1, 3, 0, 4].map((ring) => ({ ring, angle: ringDirection(ring) * HALF_TURN }));
44
+ function ringTurnPoses(initialX, initialY, initialZ, ringIndex, moves) {
45
+ let x = initialX;
46
+ let y = initialY;
47
+ let z = initialZ;
48
+ const poses = [[x, y, z]];
49
+ for (const move of moves) {
50
+ const startX = x;
51
+ const startZ = z;
52
+ for (let step = 1; step <= RING_ARC_STEPS; step++) {
53
+ if (ringIndex === move.ring) {
54
+ const angle = move.angle * (step / RING_ARC_STEPS);
55
+ const cosine = Math.cos(angle);
56
+ const sine = Math.sin(angle);
57
+ x = startX * cosine - startZ * sine;
58
+ y = initialY;
59
+ z = startX * sine + startZ * cosine;
60
+ }
61
+ poses.push([x, y, z]);
62
+ }
63
+ }
64
+ return poses;
65
+ }
66
+ const SLOW_SPIN = 0.4;
67
+ const BURST_SPIN = (Math.PI * 2 - SLOW_SPIN * 4) / 4;
68
+ const BREATH_POSES = (() => {
69
+ const poses = [{ scale: 1, spin: 0 }];
70
+ let spin = 0;
71
+ const steps = [
72
+ { scale: 1, kind: 'slow' },
73
+ { scale: 0.9, kind: 'burst' },
74
+ { scale: 0.9, kind: 'slow' },
75
+ { scale: 0.8, kind: 'burst' },
76
+ { scale: 0.8, kind: 'slow' },
77
+ { scale: 0.9, kind: 'burst' },
78
+ { scale: 0.9, kind: 'slow' },
79
+ { scale: 1, kind: 'burst' },
80
+ ];
81
+ for (const step of steps) {
82
+ spin += step.kind === 'slow' ? SLOW_SPIN : BURST_SPIN;
83
+ poses.push({ scale: step.scale, spin });
84
+ }
85
+ return poses;
86
+ })();
87
+ function globeKeyframeStyle(initialX, initialY, initialZ, variant, ringIndex) {
88
+ const style = {};
89
+ const write = (index, point) => {
90
+ style[`--g${index}x`] = `${point.x.toFixed(2)}px`;
91
+ style[`--g${index}y`] = `${(-point.y).toFixed(2)}px`;
92
+ style[`--g${index}o`] = globeOpacity(point.z).toFixed(3);
93
+ };
94
+ if (variant === 'G5') {
95
+ BREATH_POSES.forEach((pose, index) => {
96
+ write(index, projectGlobe(initialX * pose.scale, initialY * pose.scale, initialZ * pose.scale, pose.spin));
97
+ });
98
+ return style;
99
+ }
100
+ if (variant === 'G3' || variant === 'G4') {
101
+ const moves = variant === 'G3' ? SEQUENTIAL_MOVES : SCRAMBLED_MOVES;
102
+ ringTurnPoses(initialX, initialY, initialZ, ringIndex, moves)
103
+ .forEach(([x, y, z], index) => write(index, projectGlobe(x, y, z, 0)));
104
+ return style;
105
+ }
106
+ const direction = variant === 'G2' && ringIndex % 2 === 1 ? -1 : 1;
107
+ for (let index = 0; index < SPIN_STEPS; index++) {
108
+ write(index, projectGlobe(initialX, initialY, initialZ, direction * (index / SPIN_STEPS) * Math.PI * 2));
109
+ }
110
+ return style;
111
+ }
112
+ function buildGlobeDots(variant) {
113
+ const dots = [];
114
+ let index = 0;
115
+ RINGS.forEach((ring, ringIndex) => {
116
+ const latitude = (ring.latitude * Math.PI) / 180;
117
+ const initialY = Math.sin(latitude) * RADIUS;
118
+ const ringRadius = Math.cos(latitude) * RADIUS;
119
+ for (let dotIndex = 0; dotIndex < ring.count; dotIndex++) {
120
+ const longitude = (dotIndex / ring.count) * Math.PI * 2;
121
+ dots.push({
122
+ key: index,
123
+ style: globeKeyframeStyle(Math.cos(longitude) * ringRadius, initialY, Math.sin(longitude) * ringRadius, variant, ringIndex),
124
+ });
125
+ index += 1;
126
+ }
127
+ });
128
+ return dots;
129
+ }
130
+ export const globeDots = memoiseByVariant(buildGlobeDots);
@@ -0,0 +1,155 @@
1
+ /* ---- Globe (Helix family) ---- */
2
+ .helix {
3
+ position: absolute;
4
+ inset: 0;
5
+ transform: scale(var(--orb-k, 1));
6
+ }
7
+
8
+ .helixDot {
9
+ position: absolute;
10
+ left: 50%;
11
+ top: 50%;
12
+ width: 2px;
13
+ height: 2px;
14
+ margin: -1px 0 0 -1px;
15
+ border-radius: 50%;
16
+ background: currentColor;
17
+ will-change: transform, opacity;
18
+ }
19
+
20
+ .helix[data-variant="G1"] .helixDot {
21
+ animation: orb-globe-spin 4.5s linear infinite both;
22
+ }
23
+ .helix[data-variant="G2"] .helixDot {
24
+ animation: orb-globe-spin 3.6s linear infinite both;
25
+ }
26
+ .helix[data-variant="G3"] .helixDot {
27
+ animation: orb-globe-ringturn 2.8s linear infinite both;
28
+ }
29
+ .helix[data-variant="G4"] .helixDot {
30
+ animation: orb-globe-ringturn 2.8s linear infinite both;
31
+ }
32
+ .helix[data-variant="G5"] .helixDot {
33
+ animation: orb-globe-breathe 3.6s linear infinite both;
34
+ }
35
+
36
+ @keyframes orb-globe-spin {
37
+ 0%, 100% {
38
+ transform: translate(var(--g0x), var(--g0y));
39
+ opacity: var(--g0o);
40
+ }
41
+ 12.5% {
42
+ transform: translate(var(--g1x), var(--g1y));
43
+ opacity: var(--g1o);
44
+ }
45
+ 25% {
46
+ transform: translate(var(--g2x), var(--g2y));
47
+ opacity: var(--g2o);
48
+ }
49
+ 37.5% {
50
+ transform: translate(var(--g3x), var(--g3y));
51
+ opacity: var(--g3o);
52
+ }
53
+ 50% {
54
+ transform: translate(var(--g4x), var(--g4y));
55
+ opacity: var(--g4o);
56
+ }
57
+ 62.5% {
58
+ transform: translate(var(--g5x), var(--g5y));
59
+ opacity: var(--g5o);
60
+ }
61
+ 75% {
62
+ transform: translate(var(--g6x), var(--g6y));
63
+ opacity: var(--g6o);
64
+ }
65
+ 87.5% {
66
+ transform: translate(var(--g7x), var(--g7y));
67
+ opacity: var(--g7o);
68
+ }
69
+ }
70
+
71
+ @keyframes orb-globe-ringturn {
72
+ 0% { transform: translate(var(--g0x), var(--g0y)); opacity: var(--g0o); }
73
+ 2.5% { transform: translate(var(--g1x), var(--g1y)); opacity: var(--g1o); }
74
+ 5% { transform: translate(var(--g2x), var(--g2y)); opacity: var(--g2o); }
75
+ 7.5%, 10% { transform: translate(var(--g3x), var(--g3y)); opacity: var(--g3o); }
76
+ 12.5% { transform: translate(var(--g4x), var(--g4y)); opacity: var(--g4o); }
77
+ 15% { transform: translate(var(--g5x), var(--g5y)); opacity: var(--g5o); }
78
+ 17.5%, 20% { transform: translate(var(--g6x), var(--g6y)); opacity: var(--g6o); }
79
+ 22.5% { transform: translate(var(--g7x), var(--g7y)); opacity: var(--g7o); }
80
+ 25% { transform: translate(var(--g8x), var(--g8y)); opacity: var(--g8o); }
81
+ 27.5%, 30% { transform: translate(var(--g9x), var(--g9y)); opacity: var(--g9o); }
82
+ 32.5% { transform: translate(var(--g10x), var(--g10y)); opacity: var(--g10o); }
83
+ 35% { transform: translate(var(--g11x), var(--g11y)); opacity: var(--g11o); }
84
+ 37.5%, 40% { transform: translate(var(--g12x), var(--g12y)); opacity: var(--g12o); }
85
+ 42.5% { transform: translate(var(--g13x), var(--g13y)); opacity: var(--g13o); }
86
+ 45% { transform: translate(var(--g14x), var(--g14y)); opacity: var(--g14o); }
87
+ 47.5%, 50% { transform: translate(var(--g15x), var(--g15y)); opacity: var(--g15o); }
88
+ 52.5% { transform: translate(var(--g16x), var(--g16y)); opacity: var(--g16o); }
89
+ 55% { transform: translate(var(--g17x), var(--g17y)); opacity: var(--g17o); }
90
+ 57.5%, 60% { transform: translate(var(--g18x), var(--g18y)); opacity: var(--g18o); }
91
+ 62.5% { transform: translate(var(--g19x), var(--g19y)); opacity: var(--g19o); }
92
+ 65% { transform: translate(var(--g20x), var(--g20y)); opacity: var(--g20o); }
93
+ 67.5%, 70% { transform: translate(var(--g21x), var(--g21y)); opacity: var(--g21o); }
94
+ 72.5% { transform: translate(var(--g22x), var(--g22y)); opacity: var(--g22o); }
95
+ 75% { transform: translate(var(--g23x), var(--g23y)); opacity: var(--g23o); }
96
+ 77.5%, 80% { transform: translate(var(--g24x), var(--g24y)); opacity: var(--g24o); }
97
+ 82.5% { transform: translate(var(--g25x), var(--g25y)); opacity: var(--g25o); }
98
+ 85% { transform: translate(var(--g26x), var(--g26y)); opacity: var(--g26o); }
99
+ 87.5%, 90% { transform: translate(var(--g27x), var(--g27y)); opacity: var(--g27o); }
100
+ 92.5% { transform: translate(var(--g28x), var(--g28y)); opacity: var(--g28o); }
101
+ 95% { transform: translate(var(--g29x), var(--g29y)); opacity: var(--g29o); }
102
+ 97.5%, 100% { transform: translate(var(--g30x), var(--g30y)); opacity: var(--g30o); }
103
+ }
104
+
105
+ @keyframes orb-globe-breathe {
106
+ 0% {
107
+ transform: translate(var(--g0x), var(--g0y));
108
+ opacity: var(--g0o);
109
+ }
110
+ 19% {
111
+ transform: translate(var(--g1x), var(--g1y));
112
+ opacity: var(--g1o);
113
+ }
114
+ 25% {
115
+ transform: translate(var(--g2x), var(--g2y));
116
+ opacity: var(--g2o);
117
+ }
118
+ 44% {
119
+ transform: translate(var(--g3x), var(--g3y));
120
+ opacity: var(--g3o);
121
+ }
122
+ 50% {
123
+ transform: translate(var(--g4x), var(--g4y));
124
+ opacity: var(--g4o);
125
+ }
126
+ 69% {
127
+ transform: translate(var(--g5x), var(--g5y));
128
+ opacity: var(--g5o);
129
+ }
130
+ 75% {
131
+ transform: translate(var(--g6x), var(--g6y));
132
+ opacity: var(--g6o);
133
+ }
134
+ 94% {
135
+ transform: translate(var(--g7x), var(--g7y));
136
+ opacity: var(--g7o);
137
+ }
138
+ 100% {
139
+ transform: translate(var(--g8x), var(--g8y));
140
+ opacity: var(--g8o);
141
+ }
142
+ }
143
+
144
+
145
+ :global([data-orb-paused]) .helix,
146
+ :global([data-orb-paused]) .helixDot {
147
+ animation-play-state: paused;
148
+ }
149
+
150
+ @media (prefers-reduced-motion: reduce) {
151
+ .helix,
152
+ .helixDot {
153
+ animation: none !important;
154
+ }
155
+ }
@@ -0,0 +1,35 @@
1
+ import type { LatticeFrameVariant, LatticeVariant } from './orb-types';
2
+ /**
3
+ * The stacked-PR waits, drawn as frames on the 3×3 grid. Each frame is three
4
+ * rows of three cells:
5
+ * '#' lit · '+' half · '.' resting ink · '-' sitting out (dim)
6
+ * Frames are spaced evenly round the period and every cell eases from one
7
+ * frame to the next linearly, so a lit cell hands over to its
8
+ * neighbour and leaves a short fading trail — that is what makes a shape read
9
+ * as moving rather than blinking from place to place.
10
+ *
11
+ * One vocabulary: a chevron says which way the work is going. Up is the stack
12
+ * being rebuilt; right is out to GitHub; left is in from it. Waiting climbs
13
+ * without a chevron: something is happening, but not by Odyn's hand.
14
+ */
15
+ type Frame = readonly [string, string, string];
16
+ export declare const LATTICE_FRAMES: Record<LatticeFrameVariant, readonly Frame[]>;
17
+ export declare function isLatticeFrameVariant(variant: LatticeVariant): variant is LatticeFrameVariant;
18
+ /** The opacity a frame character stands for, as a CSS value. */
19
+ export declare function frameOpacity(ch: string): string;
20
+ export interface LatticeCell {
21
+ readonly key: string;
22
+ readonly left: number;
23
+ readonly top: number;
24
+ readonly delay: number;
25
+ readonly ax: number;
26
+ readonly ay: number;
27
+ readonly bx: number;
28
+ readonly by: number;
29
+ readonly still: boolean;
30
+ readonly mid: boolean;
31
+ /** Frame-drawn looks only: this cell's opacity and scale at each frame. */
32
+ readonly frames: Readonly<Record<string, string>> | null;
33
+ }
34
+ export declare const latticeCells: (variant: LatticeVariant) => readonly LatticeCell[];
35
+ export {};
@@ -0,0 +1,148 @@
1
+ import { memoiseByVariant } from './orb-geometry-cache';
2
+ const GRID_SIZE = 3;
3
+ const PITCH = 6;
4
+ const MIDPOINT = (GRID_SIZE - 1) / 2;
5
+ const SWIRL = 1.05;
6
+ const SPREAD = 1.6;
7
+ const PERIMETER = (() => {
8
+ const ring = [];
9
+ for (let x = 0; x < GRID_SIZE; x++)
10
+ ring.push([x, 0]);
11
+ for (let y = 1; y < GRID_SIZE; y++)
12
+ ring.push([GRID_SIZE - 1, y]);
13
+ for (let x = GRID_SIZE - 2; x >= 0; x--)
14
+ ring.push([x, GRID_SIZE - 1]);
15
+ for (let y = GRID_SIZE - 2; y >= 1; y--)
16
+ ring.push([0, y]);
17
+ return ring;
18
+ })();
19
+ const PERIMETER_INDEX = new Map(PERIMETER.map(([x, y], index) => [`${x},${y}`, index]));
20
+ function cellDelay(variant, x, y) {
21
+ const deltaX = x - MIDPOINT;
22
+ const deltaY = y - MIDPOINT;
23
+ switch (variant) {
24
+ case 'S1':
25
+ return Math.hypot(deltaX, deltaY) * 700 -
26
+ (deltaX === 0 && deltaY === 0 ? 180 : 0);
27
+ case 'S2':
28
+ return ((x + y) / (2 * (GRID_SIZE - 1))) * 1500;
29
+ case 'S3': {
30
+ const index = PERIMETER_INDEX.get(`${x},${y}`);
31
+ if (index === undefined)
32
+ return 0;
33
+ return -(((PERIMETER.length - index) % PERIMETER.length) / PERIMETER.length) * 1700;
34
+ }
35
+ case 'S4':
36
+ return (x / (GRID_SIZE - 1)) * 1100;
37
+ case 'S5': {
38
+ const index = PERIMETER_INDEX.get(`${x},${y}`);
39
+ if (index === undefined)
40
+ return 0;
41
+ return -(((index * 3) % PERIMETER.length) / PERIMETER.length) * 1700;
42
+ }
43
+ // Frame-drawn: the frames carry the timing, so no cell is offset.
44
+ case 'S6':
45
+ case 'S7':
46
+ case 'S8':
47
+ case 'S9':
48
+ return 0;
49
+ }
50
+ }
51
+ export const LATTICE_FRAMES = {
52
+ // Moving work: restack, moving onto main, carrying work up, switching layers.
53
+ S6: [
54
+ ['...', '...', '.#.'],
55
+ ['...', '.#.', '#+#'],
56
+ ['.#.', '#+#', '+.+'],
57
+ ['#+#', '+.+', '...'],
58
+ ['+.+', '...', '...'],
59
+ ],
60
+ // Sending: submit, pushing, merging on GitHub.
61
+ S7: [
62
+ ['...', '#..', '...'],
63
+ ['#..', '+#.', '#..'],
64
+ ['+#.', '.+#', '+#.'],
65
+ ['.+#', '..+', '.+#'],
66
+ ['..+', '...', '..+'],
67
+ ],
68
+ // Fetching: sync, pulling main, reading the stack, checking gh.
69
+ S8: [
70
+ ['...', '..#', '...'],
71
+ ['..#', '.#+', '..#'],
72
+ ['.#+', '#+.', '.#+'],
73
+ ['#+.', '+..', '#+.'],
74
+ ['+..', '...', '+..'],
75
+ ],
76
+ // Waiting on someone else: CI during a merge, chats finishing first. Climb:
77
+ // the outer columns go up hand over hand while the middle sits out.
78
+ S9: [
79
+ ['.-.', '.-.', '#-.'],
80
+ ['.-.', '#-.', '+-#'],
81
+ ['#-.', '+-#', '.-+'],
82
+ ['+-#', '.-+', '.-.'],
83
+ ['.-+', '.-.', '.-.'],
84
+ ['.-.', '.-.', '.-.'],
85
+ ],
86
+ };
87
+ export function isLatticeFrameVariant(variant) {
88
+ return variant in LATTICE_FRAMES;
89
+ }
90
+ /** The opacity a frame character stands for, as a CSS value. */
91
+ export function frameOpacity(ch) {
92
+ switch (ch) {
93
+ case '#': return '1';
94
+ case '+': return '0.5';
95
+ case '-': return 'var(--orb-dim)';
96
+ default: return 'var(--orb-rest)';
97
+ }
98
+ }
99
+ /**
100
+ * One cell's frames as custom properties (`--orb-f0`, `--orb-s0`, …) that the
101
+ * shared `orb-frames-N` keyframes in orb-lattice.module.css read. Keyframes
102
+ * cannot be generated per cell inside a CSS module, so the cell carries its
103
+ * own values and N keyframes serve every frame-drawn look.
104
+ */
105
+ function frameVars(frames, x, y) {
106
+ const vars = {};
107
+ frames.forEach((frame, i) => {
108
+ const ch = frame[y]?.[x] ?? '.';
109
+ vars[`--orb-f${i}`] = frameOpacity(ch);
110
+ vars[`--orb-s${i}`] = ch === '#' ? '1.15' : '1';
111
+ });
112
+ return vars;
113
+ }
114
+ function swirl(x, y, angle) {
115
+ const deltaX = x - MIDPOINT;
116
+ const deltaY = y - MIDPOINT;
117
+ const cosine = Math.cos(angle);
118
+ const sine = Math.sin(angle);
119
+ return [
120
+ ((deltaX * cosine - deltaY * sine) * SPREAD - deltaX) * PITCH,
121
+ ((deltaX * sine + deltaY * cosine) * SPREAD - deltaY) * PITCH,
122
+ ];
123
+ }
124
+ function buildLatticeCells(variant) {
125
+ const cells = [];
126
+ for (let y = 0; y < GRID_SIZE; y++) {
127
+ for (let x = 0; x < GRID_SIZE; x++) {
128
+ const [ax, ay] = swirl(x, y, -SWIRL);
129
+ const [bx, by] = swirl(x, y, SWIRL);
130
+ const key = `${x},${y}`;
131
+ cells.push({
132
+ key,
133
+ left: x * PITCH,
134
+ top: y * PITCH,
135
+ delay: cellDelay(variant, x, y),
136
+ ax,
137
+ ay,
138
+ bx,
139
+ by,
140
+ still: (variant === 'S3' || variant === 'S5') && !PERIMETER_INDEX.has(key),
141
+ mid: x === MIDPOINT && y === MIDPOINT,
142
+ frames: isLatticeFrameVariant(variant) ? frameVars(LATTICE_FRAMES[variant], x, y) : null,
143
+ });
144
+ }
145
+ }
146
+ return cells;
147
+ }
148
+ export const latticeCells = memoiseByVariant(buildLatticeCells);