@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,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
+ }
@@ -0,0 +1,195 @@
1
+ /* focus — attention travels the cast: each circle pulls into focus in turn.
2
+ Four circles on the corners of a square, one size for all of them, so the
3
+ only thing separating them is which one is sharp.
4
+
5
+ A second longer than the three-circle version it grew out of: the square
6
+ has four stations to visit and each one keeps the same unhurried second.
7
+
8
+ The delays count down rather than up because a more negative delay seeds a
9
+ circle further into its cycle: -3s of a 4s cycle runs three quarters ahead,
10
+ which is what sends focus round the square clockwise. */
11
+ .variants[data-variant="B1"] :global([data-orb-shape]) {
12
+ --orb-d: 6px;
13
+ animation: orb-focus 4s var(--orb-ease-smooth) infinite both;
14
+ }
15
+
16
+ .variants[data-variant="B1"] :global([data-orb-shape="a"]) {
17
+ --orb-ox: -4.5px;
18
+ --orb-oy: -4.5px;
19
+ animation-delay: 0s;
20
+ }
21
+
22
+ .variants[data-variant="B1"] :global([data-orb-shape="b"]) {
23
+ --orb-ox: 4.5px;
24
+ --orb-oy: -4.5px;
25
+ animation-delay: -3s;
26
+ }
27
+
28
+ .variants[data-variant="B1"] :global([data-orb-shape="c"]) {
29
+ --orb-ox: 4.5px;
30
+ --orb-oy: 4.5px;
31
+ animation-delay: -2s;
32
+ }
33
+
34
+ .variants[data-variant="B1"] :global([data-orb-shape="d"]) {
35
+ --orb-ox: -4.5px;
36
+ --orb-oy: 4.5px;
37
+ animation-delay: -1s;
38
+ }
39
+
40
+ /* Opacity gradient around the square: active = 1.0, next neighbour = 0.30,
41
+ diagonal = 0.10, far = 0.05. Two circles are always clearly visible, the
42
+ rest are ghost hints. */
43
+ @keyframes orb-focus {
44
+ 0%,
45
+ 100% {
46
+ opacity: 0.05;
47
+ filter: blur(2px);
48
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.12);
49
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
50
+ }
51
+ 12% {
52
+ opacity: 1;
53
+ filter: blur(0);
54
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1);
55
+ animation-timing-function: linear;
56
+ }
57
+ 22% {
58
+ opacity: 1;
59
+ filter: blur(0);
60
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1);
61
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
62
+ }
63
+ /* Next neighbour — one quarter away: clearly visible */
64
+ 38% {
65
+ opacity: 0.3;
66
+ filter: blur(1.2px);
67
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.06);
68
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
69
+ }
70
+ /* Diagonal — half a cycle away: ghost */
71
+ 58% {
72
+ opacity: 0.1;
73
+ filter: blur(1.8px);
74
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.1);
75
+ animation-timing-function: linear;
76
+ }
77
+ /* Far neighbour — three quarters away: barely there */
78
+ 82% {
79
+ opacity: 0.05;
80
+ filter: blur(2px);
81
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.12);
82
+ }
83
+ }
84
+
85
+ /* drift — the cast circles the stage on one track, sharp at the front and
86
+ blurred away at the back, so the orb reads as looking around. Uniform
87
+ size: the depth cue is doing the work, a size ladder would fight it. */
88
+ .variants[data-variant="B2"] :global([data-orb-shape]) {
89
+ animation: orb-revolve 3.3s linear infinite both;
90
+ }
91
+
92
+ /* Evenly spaced around the track, so one is always at the front. */
93
+ .variants[data-variant="B2"] :global([data-orb-shape="a"]) {
94
+ animation-delay: 0s;
95
+ }
96
+
97
+ .variants[data-variant="B2"] :global([data-orb-shape="b"]) {
98
+ animation-delay: -1.1s;
99
+ }
100
+
101
+ .variants[data-variant="B2"] :global([data-orb-shape="c"]) {
102
+ animation-delay: -2.2s;
103
+ }
104
+
105
+ /* rotate() then translateY() walks a circle. Linear all the way: an eased
106
+ rotation on a circular path reads as a wobble, not as travel. */
107
+ @keyframes orb-revolve {
108
+ 0% {
109
+ opacity: 1;
110
+ filter: blur(0);
111
+ transform: rotate(0deg) translateY(6.5px) scale(1);
112
+ }
113
+ 25% {
114
+ opacity: 0.55;
115
+ filter: blur(1.3px);
116
+ transform: rotate(90deg) translateY(6.5px) scale(0.82);
117
+ }
118
+ 50% {
119
+ opacity: 0.28;
120
+ filter: blur(2.4px);
121
+ transform: rotate(180deg) translateY(6.5px) scale(0.66);
122
+ }
123
+ 75% {
124
+ opacity: 0.55;
125
+ filter: blur(1.3px);
126
+ transform: rotate(270deg) translateY(6.5px) scale(0.82);
127
+ }
128
+ 100% {
129
+ opacity: 1;
130
+ filter: blur(0);
131
+ transform: rotate(360deg) translateY(6.5px) scale(1);
132
+ }
133
+ }
134
+
135
+ /* bloom — shapes emanate from the centre, blurring out as they grow.
136
+ Linear keeps the total ink even; on a front-loaded curve the shapes
137
+ jump to their large, blurred end state and the orb alternates between
138
+ a heavy blot and an empty haze. */
139
+ .variants[data-variant="B3"] :global([data-orb-shape]) {
140
+ animation: orb-bloom 4.2s linear infinite both;
141
+ }
142
+
143
+ .variants[data-variant="B3"] :global([data-orb-shape="a"]) {
144
+ animation-delay: 0s;
145
+ }
146
+
147
+ .variants[data-variant="B3"] :global([data-orb-shape="b"]) {
148
+ animation-delay: -1.4s;
149
+ }
150
+
151
+ .variants[data-variant="B3"] :global([data-orb-shape="c"]) {
152
+ animation-delay: -2.8s;
153
+ }
154
+
155
+ /* Each ripple dies at 62% and waits out the rest, so the three overlapping
156
+ blooms leave gaps. Without the gap the aggregate is a constant haze and
157
+ the outward motion stops reading at all. Sharp circle appears, holds
158
+ briefly, then dissolves outward — blur only kicks in once opacity starts
159
+ dropping, so the circle stays crisp while it's visible and the blur reads
160
+ as the ripple dissipating. */
161
+ @keyframes orb-bloom {
162
+ 0% {
163
+ opacity: 0;
164
+ filter: blur(0);
165
+ transform: scale(0.35);
166
+ animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
167
+ }
168
+ 8% {
169
+ opacity: 1;
170
+ filter: blur(0);
171
+ transform: scale(0.55);
172
+ animation-timing-function: linear;
173
+ }
174
+ 24% {
175
+ opacity: 1;
176
+ filter: blur(0);
177
+ transform: scale(0.72);
178
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
179
+ }
180
+ 42% {
181
+ opacity: 0.1;
182
+ filter: blur(1.8px);
183
+ transform: scale(1.5);
184
+ }
185
+ 62% {
186
+ opacity: 0;
187
+ filter: blur(2.8px);
188
+ transform: scale(2.4);
189
+ }
190
+ 100% {
191
+ opacity: 0;
192
+ filter: blur(2.8px);
193
+ transform: scale(2.4);
194
+ }
195
+ }
@@ -0,0 +1,143 @@
1
+ /* converge — a single circle traces an equilateral triangle (top → bottom-right
2
+ → bottom-left → top) with handoff-style easing: full size and sharp at each
3
+ vertex, smaller and slightly blurred in transit. orbB breathes at the
4
+ centroid as a subtle depth cue; orbC is hidden. */
5
+ .variants[data-variant="B4"] :global([data-orb-shape="a"]) {
6
+ animation: orb-converge 3.6s linear infinite both;
7
+ }
8
+ .variants[data-variant="B4"] :global([data-orb-shape="b"]) {
9
+ animation: orb-breathe 3.6s ease-in-out infinite both;
10
+ }
11
+ .variants[data-variant="B4"] :global([data-orb-shape="c"]) {
12
+ display: none;
13
+ }
14
+
15
+ @keyframes orb-converge {
16
+ 0% {
17
+ transform: translate(0px, -5px) scale(1);
18
+ filter: blur(0);
19
+ animation-timing-function: linear;
20
+ }
21
+ 10% {
22
+ transform: translate(0px, -5px) scale(1);
23
+ filter: blur(0);
24
+ animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
25
+ }
26
+ 22% {
27
+ transform: translate(2.15px, -1.25px) scale(0.72);
28
+ filter: blur(0.8px);
29
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
30
+ }
31
+ 33% {
32
+ transform: translate(4.3px, 2.5px) scale(1);
33
+ filter: blur(0);
34
+ animation-timing-function: linear;
35
+ }
36
+ 43% {
37
+ transform: translate(4.3px, 2.5px) scale(1);
38
+ filter: blur(0);
39
+ animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
40
+ }
41
+ 55% {
42
+ transform: translate(0px, 2.5px) scale(0.72);
43
+ filter: blur(0.8px);
44
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
45
+ }
46
+ 66% {
47
+ transform: translate(-4.3px, 2.5px) scale(1);
48
+ filter: blur(0);
49
+ animation-timing-function: linear;
50
+ }
51
+ 77% {
52
+ transform: translate(-4.3px, 2.5px) scale(1);
53
+ filter: blur(0);
54
+ animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
55
+ }
56
+ 88% {
57
+ transform: translate(-2.15px, -1.25px) scale(0.72);
58
+ filter: blur(0.8px);
59
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
60
+ }
61
+ 100% {
62
+ transform: translate(0px, -5px) scale(1);
63
+ filter: blur(0);
64
+ }
65
+ }
66
+
67
+ /* handoff — the cast crosses the focal plane one after another, always left
68
+ to right, like work being passed on. The shorthand curve is only a
69
+ fallback; every segment below sets its own. */
70
+ .variants[data-variant="B5"] :global([data-orb-shape]) {
71
+ animation: orb-handoff 2.8s linear infinite both;
72
+ }
73
+
74
+ /* Half a cycle apart, so one is always at the focal plane while the other is
75
+ invisible at an end and the loop point cannot be seen. */
76
+ .variants[data-variant="B5"] :global([data-orb-shape="a"]) {
77
+ animation-delay: 0s;
78
+ }
79
+
80
+ .variants[data-variant="B5"] :global([data-orb-shape="c"]) {
81
+ animation-delay: -1.4s;
82
+ }
83
+
84
+ /* The third holds the centre and breathes — a soft depth cue behind the
85
+ traffic rather than another traveller. */
86
+ .variants[data-variant="B5"] :global([data-orb-shape="b"]) {
87
+ animation-name: orb-breathe;
88
+ animation-duration: 3.6s;
89
+ }
90
+
91
+ /* Enters small from the left, reaches standard size at the focal plane, then
92
+ shrinks and fades out to the right. At the dwell (centre) the circle is
93
+ exactly 1× — no pulsing, no bounce, just a clean handoff. */
94
+ @keyframes orb-handoff {
95
+ 0% {
96
+ opacity: 0;
97
+ filter: blur(2.4px);
98
+ transform: translateX(-11px) scale(0.55);
99
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
100
+ }
101
+ 22% {
102
+ opacity: 1;
103
+ filter: blur(0);
104
+ transform: translateX(-1px) scale(1);
105
+ animation-timing-function: linear;
106
+ }
107
+ 37% {
108
+ opacity: 1;
109
+ filter: blur(0);
110
+ transform: translateX(0) scale(1);
111
+ animation-timing-function: linear;
112
+ }
113
+ 52% {
114
+ opacity: 1;
115
+ filter: blur(0);
116
+ transform: translateX(1px) scale(1);
117
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
118
+ }
119
+ 70% {
120
+ opacity: 0;
121
+ filter: blur(2.4px);
122
+ transform: translateX(11px) scale(0.55);
123
+ }
124
+ 100% {
125
+ opacity: 0;
126
+ filter: blur(2.4px);
127
+ transform: translateX(11px) scale(0.55);
128
+ }
129
+ }
130
+
131
+ @keyframes orb-breathe {
132
+ 0%,
133
+ 100% {
134
+ opacity: 0.16;
135
+ filter: blur(2.4px);
136
+ transform: scale(1.2);
137
+ }
138
+ 50% {
139
+ opacity: 0.32;
140
+ filter: blur(1.6px);
141
+ transform: scale(0.98);
142
+ }
143
+ }
@@ -0,0 +1,44 @@
1
+ /* Shared stage for the Lens family. Variant choreography is split by group. */
2
+ .lens {
3
+ position: absolute;
4
+ left: 0;
5
+ top: 0;
6
+ width: 28px;
7
+ height: 28px;
8
+ transform-origin: 0 0;
9
+ transform: scale(var(--orb-k, 1));
10
+ }
11
+
12
+ .shape {
13
+ position: absolute;
14
+ left: 50%;
15
+ top: 50%;
16
+ width: var(--orb-d, 7px);
17
+ height: var(--orb-d, 7px);
18
+ margin: calc(var(--orb-d, 7px) / -2) 0 0 calc(var(--orb-d, 7px) / -2);
19
+ border-radius: 50%;
20
+ background: currentColor;
21
+ }
22
+
23
+ :global([data-orb-paused]) .lens,
24
+ :global([data-orb-paused]) .shape {
25
+ animation-play-state: paused;
26
+ }
27
+
28
+ @media (prefers-reduced-motion: reduce) {
29
+ .lens,
30
+ .shape {
31
+ animation: none !important;
32
+ }
33
+
34
+ .shape {
35
+ opacity: 0.3 !important;
36
+ filter: blur(1.4px) !important;
37
+ transform: none !important;
38
+ }
39
+
40
+ .shape:first-child {
41
+ opacity: 1 !important;
42
+ filter: blur(0) !important;
43
+ }
44
+ }
@@ -0,0 +1,7 @@
1
+ import type { CSSProperties } from 'react';
2
+ import type { MorphVariant } from './orb-types';
3
+ export interface MorphDot {
4
+ readonly key: number;
5
+ readonly style: CSSProperties;
6
+ }
7
+ export declare const morphDots: (variant: MorphVariant) => readonly MorphDot[];
@@ -0,0 +1,88 @@
1
+ import { memoiseByVariant } from './orb-geometry-cache';
2
+ const DOT_COUNT = 8;
3
+ const RADIUS = 7;
4
+ const circle = (index) => {
5
+ const angle = (index / DOT_COUNT) * Math.PI * 2 - Math.PI / 2;
6
+ return [Math.cos(angle) * RADIUS, Math.sin(angle) * RADIUS];
7
+ };
8
+ const square = (index) => {
9
+ const halfSize = RADIUS * 0.85;
10
+ const corners = [
11
+ [-halfSize, -halfSize],
12
+ [halfSize, -halfSize],
13
+ [halfSize, halfSize],
14
+ [-halfSize, halfSize],
15
+ ];
16
+ const progress = ((index / DOT_COUNT) * 4 + 0.5) % 4;
17
+ const side = Math.floor(progress) % 4;
18
+ const fraction = progress - Math.floor(progress);
19
+ const from = corners[side];
20
+ const to = corners[(side + 1) % 4];
21
+ return [
22
+ from[0] + (to[0] - from[0]) * fraction,
23
+ from[1] + (to[1] - from[1]) * fraction,
24
+ ];
25
+ };
26
+ const circleAt = (turn) => (index) => {
27
+ const angle = (index / DOT_COUNT) * Math.PI * 2 - Math.PI / 2 + turn;
28
+ return [Math.cos(angle) * RADIUS, Math.sin(angle) * RADIUS];
29
+ };
30
+ const scatter = (index) => {
31
+ const angle = (index / DOT_COUNT) * Math.PI * 2 - Math.PI / 2;
32
+ return [-Math.cos(angle) * RADIUS, Math.sin(angle) * RADIUS];
33
+ };
34
+ const diamond = (index) => {
35
+ const corners = [
36
+ [0, -RADIUS],
37
+ [RADIUS, 0],
38
+ [0, RADIUS],
39
+ [-RADIUS, 0],
40
+ ];
41
+ const progress = (index / DOT_COUNT) * 4;
42
+ const side = Math.floor(progress) % 4;
43
+ const fraction = progress - Math.floor(progress);
44
+ const from = corners[side];
45
+ const to = corners[(side + 1) % 4];
46
+ return [
47
+ from[0] + (to[0] - from[0]) * fraction,
48
+ from[1] + (to[1] - from[1]) * fraction,
49
+ ];
50
+ };
51
+ const centre = (index) => {
52
+ const angle = (index / DOT_COUNT) * Math.PI * 2 - Math.PI / 2;
53
+ return [Math.cos(angle) * 1.5, Math.sin(angle) * 1.5];
54
+ };
55
+ function morphShapes(variant) {
56
+ switch (variant) {
57
+ case 'M1':
58
+ return [circle, square, diamond, square];
59
+ case 'M2':
60
+ return [centre, circle, centre, circle];
61
+ case 'M3':
62
+ return [circleAt(0), circleAt(Math.PI / 2), circleAt(Math.PI), circleAt(Math.PI * 1.5)];
63
+ case 'M4':
64
+ return [circle, diamond, circle, diamond];
65
+ case 'M5':
66
+ return [circle, scatter, circle, scatter];
67
+ }
68
+ }
69
+ function buildMorphDots(variant) {
70
+ const shapes = morphShapes(variant);
71
+ const pair = (point) => `${point[0].toFixed(1)}px, ${point[1].toFixed(1)}px`;
72
+ return Array.from({ length: DOT_COUNT }, (_, index) => ({
73
+ key: index,
74
+ style: {
75
+ '--m-1': pair(shapes[0](index)),
76
+ '--m-2': pair(shapes[1](index)),
77
+ '--m-3': pair(shapes[2](index)),
78
+ '--m-4': pair(shapes[3](index)),
79
+ ...(variant === 'M5'
80
+ ? {
81
+ '--m-depth': Math.abs(Math.cos((index / DOT_COUNT) * Math.PI * 2 - Math.PI / 2)).toFixed(2),
82
+ animationDelay: `${-index * 10}ms`,
83
+ }
84
+ : {}),
85
+ },
86
+ }));
87
+ }
88
+ export const morphDots = memoiseByVariant(buildMorphDots);
@@ -0,0 +1,62 @@
1
+ /* ---- Morph ---- */
2
+ .morph {
3
+ position: absolute;
4
+ inset: 0;
5
+ transform: scale(var(--orb-k, 1));
6
+ }
7
+
8
+ .morphDot {
9
+ position: absolute;
10
+ left: 50%;
11
+ top: 50%;
12
+ width: 3px;
13
+ height: 3px;
14
+ margin: -1.5px 0 0 -1.5px;
15
+ border-radius: 50%;
16
+ background: currentColor;
17
+ animation: orb-morph 4.8s cubic-bezier(0.4, 0, 0.2, 1) infinite both;
18
+ }
19
+
20
+ @keyframes orb-morph {
21
+ 0%, 5% { transform: translate(var(--m-1)); }
22
+ 25%, 30% { transform: translate(var(--m-2)); }
23
+ 50%, 55% { transform: translate(var(--m-3)); }
24
+ 75%, 80% { transform: translate(var(--m-4)); }
25
+ 100% { transform: translate(var(--m-1)); }
26
+ }
27
+
28
+ .morph[data-variant="M2"] {
29
+ animation: orb-morph-twist 9.6s linear infinite;
30
+ }
31
+
32
+ .morph[data-variant="M4"] {
33
+ animation: orb-morph-twist 9.6s linear infinite;
34
+ }
35
+
36
+ .morph[data-variant="M5"] .morphDot {
37
+ animation: orb-morph-scatter 2.8s cubic-bezier(0.4, 0, 0.2, 1) infinite both;
38
+ }
39
+
40
+ @keyframes orb-morph-scatter {
41
+ 0%, 12% { transform: translate(var(--m-1)); opacity: 1; }
42
+ 38%, 62% { transform: translate(var(--m-2)); opacity: calc(1 - 0.6 * var(--m-depth, 0)); }
43
+ 88%, 100% { transform: translate(var(--m-1)); opacity: 1; }
44
+ }
45
+
46
+ @keyframes orb-morph-twist {
47
+ from { transform: scale(var(--orb-k, 1)) rotate(0deg); }
48
+ to { transform: scale(var(--orb-k, 1)) rotate(360deg); }
49
+ }
50
+
51
+
52
+ :global([data-orb-paused]) .morph,
53
+ :global([data-orb-paused]) .morphDot {
54
+ animation-play-state: paused;
55
+ }
56
+
57
+ @media (prefers-reduced-motion: reduce) {
58
+ .morph,
59
+ .morphDot {
60
+ animation: none !important;
61
+ }
62
+ }