@fusion.dev/helix 0.7.1 → 0.8.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/dist/index.d.ts CHANGED
@@ -60,6 +60,7 @@ export * from './navigation/index.js';
60
60
  export * from './notifications/index.js';
61
61
  export * from './number-input/index.js';
62
62
  export * from './option/index.js';
63
+ export * from './orb/index.js';
63
64
  export * from './outline/index.js';
64
65
  export * from './pagination/index.js';
65
66
  export * from './pin-input/index.js';
package/dist/index.js CHANGED
@@ -55,6 +55,7 @@ export * from './navigation/index.js';
55
55
  export * from './notifications/index.js';
56
56
  export * from './number-input/index.js';
57
57
  export * from './option/index.js';
58
+ export * from './orb/index.js';
58
59
  export * from './outline/index.js';
59
60
  export * from './pagination/index.js';
60
61
  export * from './pin-input/index.js';
@@ -0,0 +1,2 @@
1
+ export * from './root/orb-root.js';
2
+ export * from './root/orb-shapes.js';
@@ -0,0 +1,2 @@
1
+ export * from './root/orb-root.js';
2
+ export * from './root/orb-shapes.js';
@@ -0,0 +1 @@
1
+ export declare const styles: import("lit").CSSResult;
@@ -0,0 +1,38 @@
1
+ import { createSheet } from '@fusion.dev/helix-core';
2
+ export const styles = createSheet(`:host {
3
+
4
+ --_orb-size: calc(var(--helix-size-loader-lg) * 1.5);
5
+
6
+ --_orb-color: var(--orb-color, currentColor);
7
+ --_orb-ground: var(--orb-ground, var(--helix-color-background));
8
+
9
+ block-size: var(--orb-size, var(--_orb-size));
10
+
11
+ display: inline-block;
12
+ flex-shrink: 0;
13
+ inline-size: var(--orb-size, var(--_orb-size));
14
+ vertical-align: middle;
15
+ }
16
+
17
+ :host([size="sm"]) {
18
+ --_orb-size: var(--helix-size-loader-md);
19
+ }
20
+
21
+ :host([size="lg"]) {
22
+ --_orb-size: calc(var(--helix-size-loader-lg) * 2);
23
+ }
24
+
25
+ [part="canvas"] {
26
+ block-size: 100%;
27
+
28
+ color: var(--_orb-color);
29
+ display: block;
30
+ inline-size: 100%;
31
+ text-decoration-color: var(--_orb-ground);
32
+ }
33
+
34
+ @media print {
35
+ :host {
36
+ display: none;
37
+ }
38
+ }`);
@@ -0,0 +1,142 @@
1
+ import { HelixElement } from '@fusion.dev/helix-core';
2
+ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
3
+ import { type OrbState } from './orb-shapes.js';
4
+ /**
5
+ * @summary Says what an agent is doing, as a shape rather than a word.
6
+ *
7
+ * @description
8
+ * A cloud of dots on a sphere, drawn to a canvas, with one shape per state:
9
+ * `thinking` is a narrow sash with a wave running through it, `fetching` spreads
10
+ * into a globe with a highlight sweeping pole to pole, `working` flattens and
11
+ * tilts with its bands shearing past each other, and `done` settles into an even
12
+ * ball.
13
+ *
14
+ * **None of them is a separate drawing.** Each is the same banded sphere given
15
+ * different numbers, so changing state interpolates the numbers and every frame
16
+ * in between is a whole shape. A loader that cuts between pictures says the
17
+ * work restarted; one that carries a shape into the next says it moved on.
18
+ *
19
+ * **Nothing spins.** All the movement is inside the shape, which is what makes
20
+ * it read as thought rather than as a thing being rotated.
21
+ *
22
+ * **The palette is read from the tokens**, not given as colors: a dot's ink is
23
+ * mixed toward the ground by how far away it is, and both ends come from
24
+ * `--orb-color` and `--orb-ground`. That is what makes depth survive a theme
25
+ * change without the consumer restating anything.
26
+ *
27
+ * **It stops when nobody is looking**, off screen and in a background tab, and
28
+ * `prefers-reduced-motion` gets one still frame rather than an empty box.
29
+ *
30
+ * @since 0.8.0
31
+ * @status experimental
32
+ * @category feedback
33
+ * @aliases Thinking, Activity, Agent, Status
34
+ * @documentation https://helix.fusion.dev/components/orb
35
+ *
36
+ * @csspart canvas - The surface the cloud is drawn into.
37
+ *
38
+ * @cssproperty --orb-color - Ink of the nearest dots. Defaults to `currentColor`.
39
+ * @cssproperty --orb-ground - What the far dots are mixed toward. Defaults to the page's background.
40
+ * @cssproperty --orb-size - Diameter. Overrides `size`.
41
+ */
42
+ export declare class HelixOrb extends HelixElement {
43
+ static styles: CSSResultGroup;
44
+ /**
45
+ * Accessible name, where the state's own word is not the right one.
46
+ *
47
+ * Empty is the state's word, which is what most callers want and none of them
48
+ * should have to write. A caller with something better to say says it here:
49
+ * the name of the tool being run, or the file being read.
50
+ */
51
+ label: string;
52
+ /**
53
+ * Diameter relative to the surrounding text.
54
+ *
55
+ * Reflected because the stylesheet selects on it. The mesh is chosen from the
56
+ * rendered box rather than from this, so `--orb-size` and a consumer's own
57
+ * `inline-size` get the same treatment as the three named steps.
58
+ */
59
+ size?: 'lg' | 'sm';
60
+ /**
61
+ * What the agent is doing.
62
+ *
63
+ * **A name we do not draw is drawn as `thinking` rather than as nothing.**
64
+ * The set of things an agent can be doing grows faster than this list, and a
65
+ * verb we have not made a shape for should cost a reader a less specific
66
+ * indicator, not an empty box where the indicator was.
67
+ *
68
+ * Not reflected: no rule selects on it, because the state is drawn rather than
69
+ * styled. A consumer writing the attribute can still select on their own
70
+ * markup.
71
+ */
72
+ state: OrbState;
73
+ private canvas;
74
+ private context;
75
+ private motion;
76
+ private visible;
77
+ private sized;
78
+ private frame;
79
+ private running;
80
+ private onScreen;
81
+ /** The box the canvas was last measured at, in CSS pixels. */
82
+ private box;
83
+ /**
84
+ * Where the cloud is now, and where it is heading.
85
+ *
86
+ * **`from` holds numbers rather than the name of a state**, which is what
87
+ * makes an interruption free: changing state mid-transition snapshots the
88
+ * blend that is on the screen at that instant and departs from it. Holding a
89
+ * name forces a choice between the shape being left and the shape being
90
+ * approached, and either one is a jump the reader sees.
91
+ */
92
+ private from;
93
+ private morphAt;
94
+ /**
95
+ * The state's clock, accumulated rather than derived.
96
+ *
97
+ * **`elapsed * rate` looks equivalent and is not.** `rate` is one of the
98
+ * numbers a transition interpolates, and multiplying a running clock by a
99
+ * changing rate throws the phase forward by the elapsed time times the
100
+ * change. A minute in, a rate moving from 2.1 to 2.4 skips the wave thirty
101
+ * seconds ahead, and the orb lurches on every state change. Integrating the
102
+ * rate has no such term.
103
+ */
104
+ private phase;
105
+ private last;
106
+ protected willUpdate(changed: PropertyValues): void;
107
+ protected render(): TemplateResult;
108
+ protected firstUpdated(): void;
109
+ connectedCallback(): void;
110
+ disconnectedCallback(): void;
111
+ private start;
112
+ /**
113
+ * Matches the drawing buffer to the box, and reports whether it changed.
114
+ *
115
+ * Capped at two device pixels: past that the dot count is unchanged and the
116
+ * fill cost doubles again for a mesh already drawn sub-pixel.
117
+ */
118
+ private measure;
119
+ /** Begins a transition from whatever is on the screen right now. */
120
+ private turn;
121
+ /** How far through a transition, `1` when there is not one. */
122
+ private get progress();
123
+ private paramsNow;
124
+ private play;
125
+ private stop;
126
+ private readonly tick;
127
+ private readonly handleVisibility;
128
+ private readonly handleMotion;
129
+ /**
130
+ * One frame.
131
+ *
132
+ * Painter's algorithm: every dot is sorted by depth and drawn back to front,
133
+ * because a near dot has to cover a far one for the cloud to read as a solid
134
+ * rather than as a smear.
135
+ */
136
+ private paint;
137
+ }
138
+ declare global {
139
+ interface HTMLElementTagNameMap {
140
+ 'helix-orb': HelixOrb;
141
+ }
142
+ }
@@ -0,0 +1,262 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
8
+ import { html } from 'lit';
9
+ import { property } from 'lit/decorators.js';
10
+ import { styles } from './orb-root.css.js';
11
+ import { blendParams, evenSphere, fibonacci, meshFor, ORB_STATES, orbPoint, paramsFor, project, smoother, } from './orb-shapes.js';
12
+ const MORPH = 700;
13
+ const STILL_PHASE = 1.4;
14
+ const LABELS = {
15
+ done: 'Done',
16
+ fetching: 'Fetching',
17
+ thinking: 'Thinking',
18
+ working: 'Working',
19
+ };
20
+ let HelixOrb = class HelixOrb extends HelixElement {
21
+ constructor() {
22
+ super(...arguments);
23
+ this.label = '';
24
+ this.state = 'thinking';
25
+ this.context = null;
26
+ this.frame = 0;
27
+ this.running = false;
28
+ this.onScreen = false;
29
+ this.box = 0;
30
+ this.from = paramsFor('thinking');
31
+ this.morphAt = 0;
32
+ this.phase = STILL_PHASE;
33
+ this.last = 0;
34
+ this.tick = () => {
35
+ if (!this.running) {
36
+ return;
37
+ }
38
+ const now = performance.now();
39
+ const dt = Math.min(0.05, (now - this.last) / 1000 || 0.016);
40
+ this.last = now;
41
+ this.paint(dt);
42
+ this.frame = requestAnimationFrame(this.tick);
43
+ };
44
+ this.handleVisibility = () => {
45
+ if (document.visibilityState === 'hidden') {
46
+ this.stop();
47
+ }
48
+ else {
49
+ this.play();
50
+ }
51
+ };
52
+ this.handleMotion = () => {
53
+ if (this.motion?.matches) {
54
+ this.stop();
55
+ this.phase = STILL_PHASE;
56
+ this.morphAt = 0;
57
+ this.paint(0);
58
+ }
59
+ else {
60
+ this.play();
61
+ }
62
+ };
63
+ }
64
+ static { this.styles = [styles]; }
65
+ willUpdate(changed) {
66
+ super.willUpdate(changed);
67
+ const state = ORB_STATES.includes(this.state)
68
+ ? this.state
69
+ : 'thinking';
70
+ hostAria(this.internals, {
71
+ label: this.label || LABELS[state],
72
+ role: state === 'done' ? 'img' : 'progressbar',
73
+ });
74
+ this.internals.ariaBusy = state === 'done' ? 'false' : 'true';
75
+ if (changed.has('state') && changed.get('state') !== undefined) {
76
+ this.turn();
77
+ }
78
+ }
79
+ render() {
80
+ return html `<canvas part="canvas" aria-hidden="true"></canvas>`;
81
+ }
82
+ firstUpdated() {
83
+ this.start();
84
+ }
85
+ connectedCallback() {
86
+ super.connectedCallback();
87
+ if (this.hasUpdated) {
88
+ this.start();
89
+ }
90
+ }
91
+ disconnectedCallback() {
92
+ super.disconnectedCallback();
93
+ this.stop();
94
+ this.visible?.disconnect();
95
+ this.visible = undefined;
96
+ this.sized?.disconnect();
97
+ this.sized = undefined;
98
+ this.motion?.removeEventListener('change', this.handleMotion);
99
+ this.motion = undefined;
100
+ document.removeEventListener('visibilitychange', this.handleVisibility);
101
+ }
102
+ start() {
103
+ const canvas = this.renderRoot.querySelector('canvas');
104
+ if (!canvas) {
105
+ return;
106
+ }
107
+ this.canvas = canvas;
108
+ this.context ??= canvas.getContext('2d');
109
+ if (!this.context) {
110
+ return;
111
+ }
112
+ this.motion ??= window.matchMedia('(prefers-reduced-motion: reduce)');
113
+ this.motion.addEventListener('change', this.handleMotion);
114
+ this.visible ??= new IntersectionObserver(([entry]) => {
115
+ this.onScreen = entry?.isIntersecting ?? false;
116
+ if (this.onScreen && document.visibilityState !== 'hidden') {
117
+ this.play();
118
+ }
119
+ else {
120
+ this.stop();
121
+ }
122
+ });
123
+ this.visible.observe(this);
124
+ document.addEventListener('visibilitychange', this.handleVisibility);
125
+ this.sized ??= new ResizeObserver(() => {
126
+ this.measure();
127
+ this.paint(0);
128
+ });
129
+ this.sized.observe(this);
130
+ this.measure();
131
+ this.paint(0);
132
+ }
133
+ measure() {
134
+ const canvas = this.canvas;
135
+ if (!canvas) {
136
+ return false;
137
+ }
138
+ const size = Math.round(this.getBoundingClientRect().width);
139
+ if (size <= 0 || size === this.box) {
140
+ return false;
141
+ }
142
+ this.box = size;
143
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
144
+ canvas.width = Math.round(size * dpr);
145
+ canvas.height = Math.round(size * dpr);
146
+ this.context?.setTransform(dpr, 0, 0, dpr, 0, 0);
147
+ return true;
148
+ }
149
+ turn() {
150
+ this.from = this.paramsNow();
151
+ this.morphAt = performance.now();
152
+ if (!this.running) {
153
+ this.paint(0);
154
+ this.play();
155
+ }
156
+ }
157
+ get progress() {
158
+ return this.morphAt === 0 ? 1 : Math.min(1, (performance.now() - this.morphAt) / MORPH);
159
+ }
160
+ paramsNow() {
161
+ const to = paramsFor(this.state);
162
+ const k = this.progress;
163
+ return k >= 1 ? to : blendParams(this.from, to, smoother(k));
164
+ }
165
+ play() {
166
+ if (this.running || this.motion?.matches || !this.onScreen) {
167
+ return;
168
+ }
169
+ this.running = true;
170
+ this.last = performance.now();
171
+ this.frame = requestAnimationFrame(this.tick);
172
+ }
173
+ stop() {
174
+ this.running = false;
175
+ cancelAnimationFrame(this.frame);
176
+ }
177
+ paint(dt) {
178
+ const ctx = this.context;
179
+ const size = this.box;
180
+ if (!ctx || size <= 0) {
181
+ return;
182
+ }
183
+ const still = this.motion?.matches ?? false;
184
+ const params = still ? paramsFor(this.state) : this.paramsNow();
185
+ this.phase += dt * params.rate;
186
+ const painted = getComputedStyle(ctx.canvas);
187
+ const ink = readColor(painted.color);
188
+ const ground = readColor(painted.textDecorationColor);
189
+ const mesh = meshFor(size);
190
+ const even = evenSphere(mesh.lanes, mesh.segs);
191
+ const scan = Math.sin(this.phase * 0.85) * 0.93;
192
+ const shell = still ? 1 : 1 + 0.1 * Math.sin(Math.PI * this.progress);
193
+ const dots = [];
194
+ for (let i = 0; i < mesh.ghostCount; i++) {
195
+ const [x, y, z] = fibonacci(i, mesh.ghostCount);
196
+ const [px, py, pz] = project(size, x, y, z);
197
+ dots.push({
198
+ alpha: mesh.ghostAlpha + mesh.ghostAlphaSpan * ((pz + 1) / 2),
199
+ radius: mesh.ghostRadius * mesh.radiusScale,
200
+ shade: mesh.ghostShade,
201
+ x: px,
202
+ y: py,
203
+ z: pz,
204
+ });
205
+ }
206
+ let index = 0;
207
+ for (let band = 0; band < mesh.lanes; band++) {
208
+ const u = mesh.lanes < 2 ? 0.5 : band / (mesh.lanes - 1);
209
+ const edge = params.edge * Math.abs(u - 0.5) * 2;
210
+ for (let s = 0; s < mesh.segs; s++, index++) {
211
+ const [x, y, z] = orbPoint(u, s / mesh.segs, this.phase, params, index, even);
212
+ const [px, py, pz] = project(size, x * shell, y * shell, z * shell);
213
+ const depth = (pz + 1) / 2;
214
+ const boost = params.scan * Math.exp(-((y - scan) ** 2) / 0.012) +
215
+ params.jitter * (0.1 + 0.1 * Math.sin(this.phase * 0.6));
216
+ dots.push({
217
+ alpha: mesh.aMin + (1 - mesh.aMin) * depth,
218
+ radius: (mesh.radiusBase + mesh.radiusDepth * depth) *
219
+ (1 - 0.25 * edge) *
220
+ (1 + 0.55 * boost) *
221
+ mesh.radiusScale,
222
+ shade: mesh.shadeFar - mesh.shadeSpan * depth + 0.18 * edge - 0.34 * boost,
223
+ x: px,
224
+ y: py,
225
+ z: pz,
226
+ });
227
+ }
228
+ }
229
+ dots.sort((a, b) => a.z - b.z);
230
+ ctx.clearRect(0, 0, size, size);
231
+ for (const dot of dots) {
232
+ if (dot.alpha < 0.02) {
233
+ continue;
234
+ }
235
+ const t = Math.min(1, Math.max(0, dot.shade));
236
+ ctx.fillStyle = `rgba(${Math.round(ink[0] + (ground[0] - ink[0]) * t)},${Math.round(ink[1] + (ground[1] - ink[1]) * t)},${Math.round(ink[2] + (ground[2] - ink[2]) * t)},${dot.alpha})`;
237
+ ctx.beginPath();
238
+ ctx.arc(dot.x, dot.y, Math.max(0.3, dot.radius), 0, Math.PI * 2);
239
+ ctx.fill();
240
+ }
241
+ }
242
+ };
243
+ __decorate([
244
+ property()
245
+ ], HelixOrb.prototype, "label", void 0);
246
+ __decorate([
247
+ property({ reflect: true })
248
+ ], HelixOrb.prototype, "size", void 0);
249
+ __decorate([
250
+ property()
251
+ ], HelixOrb.prototype, "state", void 0);
252
+ HelixOrb = __decorate([
253
+ customElement('helix-orb')
254
+ ], HelixOrb);
255
+ export { HelixOrb };
256
+ const readColor = (value) => {
257
+ const parts = value.match(/-?[\d.]+/g);
258
+ if (!parts || parts.length < 3) {
259
+ return [128, 128, 128];
260
+ }
261
+ return [Number(parts[0]) || 0, Number(parts[1]) || 0, Number(parts[2]) || 0];
262
+ };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The states an orb can be in, in the order a task tends to pass through them.
3
+ *
4
+ * Exported so the component can validate an attribute against the same list the
5
+ * generator reads, rather than keeping a second copy that drifts.
6
+ */
7
+ export declare const ORB_STATES: readonly ['thinking', 'fetching', 'working', 'done'];
8
+ /** What the agent is doing. */
9
+ export type OrbState = (typeof ORB_STATES)[number];
10
+ /**
11
+ * The numbers one state is made of.
12
+ *
13
+ * **A state is a set of numbers rather than a function**, and that is the whole
14
+ * design. The first version had a generator per state and interpolated the
15
+ * points they produced, which is a scramble: two shapes with unrelated
16
+ * parameterisations hand each dot a destination chosen by its index rather than
17
+ * by where it is, so the middle of every transition read as a fault. Routing
18
+ * through a sphere hid it at the midpoint and left the quarter marks broken.
19
+ *
20
+ * With one generator and a set of numbers per state, a transition interpolates
21
+ * the numbers and the generator returns a whole, valid shape at every instant.
22
+ * There is no in-between left to get wrong.
23
+ */
24
+ export interface OrbParams {
25
+ /** How far the bands spread from the equator, `0` a line and `1` a full sphere. */
26
+ belt: number;
27
+ /** How far the outer bands fade, which is what makes a sash read as a sash. */
28
+ edge: number;
29
+ /** How far toward an even sphere, `0` the banded grid and `1` fully relaxed. */
30
+ jitter: number;
31
+ /** How fast this state's own clock runs. */
32
+ rate: number;
33
+ /** Which way the axis leans, in radians. */
34
+ rot: number;
35
+ /** Where the sweeping highlight is, `0` absent and `1` full strength. */
36
+ scan: number;
37
+ /** How hard the bands slide past each other. */
38
+ shear: number;
39
+ /** How flattened the ball is along its axis, `1` round. */
40
+ squash: number;
41
+ /** The three-lobed ripple's amplitude. */
42
+ waveA: number;
43
+ /** The five-lobed counter-ripple's amplitude. */
44
+ waveB: number;
45
+ }
46
+ /**
47
+ * Each state, as numbers.
48
+ *
49
+ * They are deliberately close together. Two states that differ in every number
50
+ * take a long, busy route between them; these differ in the two or three that
51
+ * carry the meaning, so a transition reads as one thing becoming another rather
52
+ * than as a shuffle.
53
+ */
54
+ export declare const ORB_PARAMS: Record<OrbState, OrbParams>;
55
+ /** Halfway between two states, key by key. */
56
+ export declare const blendParams: (a: OrbParams, b: OrbParams, k: number) => OrbParams;
57
+ /** The state's numbers, or `thinking`'s where the name is not one we draw. */
58
+ export declare const paramsFor: (state: string | undefined) => OrbParams;
59
+ /** Smootherstep: zero slope and zero acceleration at both ends. */
60
+ export declare const smoother: (t: number) => number;
61
+ /** A point of the Fibonacci sphere, which is the evenest cheap one. */
62
+ export declare const fibonacci: (i: number, n: number) => [number, number, number];
63
+ /**
64
+ * The even sphere `done` settles into, ordered so the band grid can address it.
65
+ *
66
+ * **The azimuth is measured from the same origin and in the same direction as
67
+ * the generator's longitude**, and that is not a detail. With `atan2`'s own
68
+ * range the two orderings sit half a turn apart: every dot is handed a
69
+ * destination across the ball, and the blend carries the whole cloud through its
70
+ * own axis. It showed as a near-empty frame midway through every transition
71
+ * into `done`, and nowhere else, because no single state reads this ordering.
72
+ */
73
+ export declare const evenSphere: (lanes: number, segs: number) => [number, number, number][];
74
+ /**
75
+ * One dot, from its place in the grid and the state's numbers.
76
+ *
77
+ * `u` runs across the bands and `v` around them, both `0` to `1`, and `phase`
78
+ * is the state's own accumulated clock.
79
+ */
80
+ export declare const orbPoint: (u: number, v: number, phase: number, p: OrbParams, index: number, even: [number, number, number][]) => [number, number, number];
81
+ /** The mesh and ink of an orb at one size. */
82
+ export interface OrbMesh {
83
+ aMin: number;
84
+ ghostAlpha: number;
85
+ ghostAlphaSpan: number;
86
+ ghostCount: number;
87
+ ghostRadius: number;
88
+ ghostShade: number;
89
+ lanes: number;
90
+ radiusBase: number;
91
+ radiusDepth: number;
92
+ radiusScale: number;
93
+ segs: number;
94
+ shadeFar: number;
95
+ shadeSpan: number;
96
+ }
97
+ /**
98
+ * The mesh for a given diameter.
99
+ *
100
+ * **The dot budget falls as `size^1.37`** and the two axes of the grid each take
101
+ * its square root, which is the relationship the reference libraries land on
102
+ * across their own three tunings. Dots are drawn sub-pixel and their radius
103
+ * scales as `size^0.6`; small orbs get relatively fatter dots, or the mesh
104
+ * disappears into the pixel grid.
105
+ *
106
+ * **The tonal range is compressed as the orb shrinks.** Depth drawn as a
107
+ * 0.6-wide gray ramp is legible at 64px and invisible at 20, where the far side
108
+ * of the sphere is three pixels across: below 64 the ramp narrows and the alpha
109
+ * floor lifts, trading depth for a mark that is actually on the screen.
110
+ */
111
+ export declare const meshFor: (size: number) => OrbMesh;
112
+ /** Projects a point on the unit sphere to canvas pixels, with its depth. */
113
+ export declare const project: (size: number, x: number, y: number, z: number) => [number, number, number];