@odyn-app/api 0.0.0-stage → 0.1.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.
- package/README.md +24 -2
- package/dist/index.d.ts +492 -0
- package/dist/index.js +487 -0
- package/dist/ui/button.d.ts +10 -0
- package/dist/ui/button.js +31 -0
- package/dist/ui/cn.d.ts +3 -0
- package/dist/ui/cn.js +6 -0
- package/dist/ui/index.d.ts +10 -0
- package/dist/ui/index.js +10 -0
- package/dist/ui/orb/orb-base.module.css +21 -0
- package/dist/ui/orb/orb-geometry-cache.d.ts +1 -0
- package/dist/ui/orb/orb-geometry-cache.js +11 -0
- package/dist/ui/orb/orb-helix-geometry.d.ts +7 -0
- package/dist/ui/orb/orb-helix-geometry.js +130 -0
- package/dist/ui/orb/orb-helix.module.css +155 -0
- package/dist/ui/orb/orb-lattice-geometry.d.ts +35 -0
- package/dist/ui/orb/orb-lattice-geometry.js +148 -0
- package/dist/ui/orb/orb-lattice.module.css +165 -0
- package/dist/ui/orb/orb-lens-primary.module.css +195 -0
- package/dist/ui/orb/orb-lens-secondary.module.css +143 -0
- package/dist/ui/orb/orb-lens.module.css +44 -0
- package/dist/ui/orb/orb-morph-geometry.d.ts +7 -0
- package/dist/ui/orb/orb-morph-geometry.js +88 -0
- package/dist/ui/orb/orb-morph.module.css +62 -0
- package/dist/ui/orb/orb-ring-geometry.d.ts +8 -0
- package/dist/ui/orb/orb-ring-geometry.js +42 -0
- package/dist/ui/orb/orb-ring.module.css +113 -0
- package/dist/ui/orb/orb-types.d.ts +23 -0
- package/dist/ui/orb/orb-types.js +48 -0
- package/dist/ui/orb/orb.d.ts +17 -0
- package/dist/ui/orb/orb.js +40 -0
- package/dist/ui/setting-row.d.ts +9 -0
- package/dist/ui/setting-row.js +9 -0
- package/dist/ui/skeleton.d.ts +3 -0
- package/dist/ui/skeleton.js +6 -0
- package/dist/ui/theme.css +232 -0
- package/package.json +56 -4
|
@@ -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);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/* --- Lattice: discrete dots on a fixed 3×3 grid ------------------- */
|
|
2
|
+
|
|
3
|
+
.lattice {
|
|
4
|
+
position: absolute;
|
|
5
|
+
left: 0;
|
|
6
|
+
top: 0;
|
|
7
|
+
width: 28px;
|
|
8
|
+
height: 28px;
|
|
9
|
+
transform-origin: 0 0;
|
|
10
|
+
/* Three 3px dots on a 6px pitch measure 15px, so the grid is offset to sit
|
|
11
|
+
centred on the 28px stage. It deliberately does not fill the stage: that
|
|
12
|
+
is what keeps its visual weight level with the Lens circles. */
|
|
13
|
+
transform: scale(var(--orb-k, 1)) translate(6.5px, 6.5px);
|
|
14
|
+
/* Resting ink of an unlit cell — the grid stays legible between beats.
|
|
15
|
+
--orb-dim is for cells sitting a choreography out entirely. */
|
|
16
|
+
--orb-rest: 0.14;
|
|
17
|
+
--orb-dim: 0.07;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Light ink on a dark surface reads dimmer at the same alpha. */
|
|
21
|
+
:global(.dark) .lattice {
|
|
22
|
+
--orb-rest: 0.2;
|
|
23
|
+
--orb-dim: 0.1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
.cell {
|
|
28
|
+
position: absolute;
|
|
29
|
+
width: 3px;
|
|
30
|
+
height: 3px;
|
|
31
|
+
border-radius: 50%;
|
|
32
|
+
background: currentColor;
|
|
33
|
+
opacity: var(--orb-rest);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* One wave shape drives all three sweeps. What separates them is the pair of
|
|
37
|
+
duration and per-cell stagger: the stagger sets how fast the wavefront
|
|
38
|
+
travels, the duration how many cells it holds lit at once — which is to
|
|
39
|
+
say, how wide the band reads. */
|
|
40
|
+
.lattice[data-variant="S1"] .cell {
|
|
41
|
+
animation: orb-wave 1.7s var(--orb-ease-in-out) infinite both;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.lattice[data-variant="S2"] .cell {
|
|
45
|
+
animation: orb-wave 1.7s var(--orb-ease-in-out) infinite both;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.lattice[data-variant="S4"] .cell {
|
|
49
|
+
animation: orb-wave 1.6s var(--orb-ease-in-out) infinite both;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.lattice[data-variant="S3"] .cell {
|
|
53
|
+
animation: orb-comet 1.7s var(--orb-ease-smooth) infinite both;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Interior cells sit out `orbit` and drop back, so the ring reads as a
|
|
57
|
+
ring and the travelling head has something to stand out against. */
|
|
58
|
+
.lattice[data-variant="S3"] .cell[data-still] {
|
|
59
|
+
animation: none;
|
|
60
|
+
opacity: var(--orb-dim);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.lattice[data-variant="S5"] .cell {
|
|
64
|
+
animation: orb-comet 1.7s var(--orb-ease-smooth) infinite both;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.lattice[data-variant="S5"] .cell[data-still] {
|
|
68
|
+
animation: none;
|
|
69
|
+
opacity: var(--orb-dim);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Swells and subsides on the same symmetric curve, so there is no flash and
|
|
73
|
+
no hard edge — the cell rises out of its resting ink and sinks back into
|
|
74
|
+
it. The long tail after 56% is the gap between beats. */
|
|
75
|
+
@keyframes orb-wave {
|
|
76
|
+
0% {
|
|
77
|
+
opacity: var(--orb-rest);
|
|
78
|
+
transform: scale(1);
|
|
79
|
+
animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);
|
|
80
|
+
}
|
|
81
|
+
28% {
|
|
82
|
+
opacity: 1;
|
|
83
|
+
transform: scale(1.18);
|
|
84
|
+
animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);
|
|
85
|
+
}
|
|
86
|
+
56% {
|
|
87
|
+
opacity: var(--orb-rest);
|
|
88
|
+
transform: scale(1);
|
|
89
|
+
}
|
|
90
|
+
100% {
|
|
91
|
+
opacity: var(--orb-rest);
|
|
92
|
+
transform: scale(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Starts lit and decays, so staggered ring cells form a head and tail.
|
|
97
|
+
The decay spans ~3.5 of the 8 ring positions — enough to read as a comet. */
|
|
98
|
+
@keyframes orb-comet {
|
|
99
|
+
0% {
|
|
100
|
+
opacity: 1;
|
|
101
|
+
transform: scale(1.2);
|
|
102
|
+
/* Linear, so the cells behind the head form an even gradient instead
|
|
103
|
+
of collapsing to rest within the first two. */
|
|
104
|
+
animation-timing-function: linear;
|
|
105
|
+
}
|
|
106
|
+
45% {
|
|
107
|
+
opacity: var(--orb-rest);
|
|
108
|
+
transform: scale(1);
|
|
109
|
+
}
|
|
110
|
+
100% {
|
|
111
|
+
opacity: var(--orb-rest);
|
|
112
|
+
transform: scale(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
/* --- The stacked-PR waits: frame-drawn on the same grid ------------
|
|
118
|
+
Each cell carries its own opacity and scale per frame as custom properties
|
|
119
|
+
(`--orb-f0`, `--orb-s0`, … from LATTICE_FRAMES in orb-lattice-geometry.ts),
|
|
120
|
+
so one keyframe per FRAME COUNT serves every look. The frames sit evenly
|
|
121
|
+
round the period; the timing function applies between each pair. */
|
|
122
|
+
.lattice[data-variant="S6"] .cell,
|
|
123
|
+
.lattice[data-variant="S7"] .cell,
|
|
124
|
+
.lattice[data-variant="S8"] .cell {
|
|
125
|
+
animation: orb-frames-5 1.3s linear infinite both;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Waiting climbs: the outer columns go up hand over hand, the middle sits
|
|
129
|
+
out, and the last frame rests before the next climb. */
|
|
130
|
+
.lattice[data-variant="S9"] .cell {
|
|
131
|
+
animation: orb-frames-6 1.6s linear infinite both;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@keyframes orb-frames-5 {
|
|
135
|
+
0%, 100% { opacity: var(--orb-f0); transform: scale(var(--orb-s0)); }
|
|
136
|
+
20% { opacity: var(--orb-f1); transform: scale(var(--orb-s1)); }
|
|
137
|
+
40% { opacity: var(--orb-f2); transform: scale(var(--orb-s2)); }
|
|
138
|
+
60% { opacity: var(--orb-f3); transform: scale(var(--orb-s3)); }
|
|
139
|
+
80% { opacity: var(--orb-f4); transform: scale(var(--orb-s4)); }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@keyframes orb-frames-6 {
|
|
143
|
+
0%, 100% { opacity: var(--orb-f0); transform: scale(var(--orb-s0)); }
|
|
144
|
+
16.667% { opacity: var(--orb-f1); transform: scale(var(--orb-s1)); }
|
|
145
|
+
33.333% { opacity: var(--orb-f2); transform: scale(var(--orb-s2)); }
|
|
146
|
+
50% { opacity: var(--orb-f3); transform: scale(var(--orb-s3)); }
|
|
147
|
+
66.667% { opacity: var(--orb-f4); transform: scale(var(--orb-s4)); }
|
|
148
|
+
83.333% { opacity: var(--orb-f5); transform: scale(var(--orb-s5)); }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
:global([data-orb-paused]) .lattice,
|
|
152
|
+
:global([data-orb-paused]) .cell {
|
|
153
|
+
animation-play-state: paused;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@media (prefers-reduced-motion: reduce) {
|
|
157
|
+
.lattice,
|
|
158
|
+
.cell {
|
|
159
|
+
animation: none !important;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.cell[data-mid] {
|
|
163
|
+
opacity: 1 !important;
|
|
164
|
+
}
|
|
165
|
+
}
|