@fusion.dev/helix 0.7.2 → 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/custom-elements.json +922 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/orb/index.d.ts +2 -0
- package/dist/orb/index.js +2 -0
- package/dist/orb/root/orb-root.css.d.ts +1 -0
- package/dist/orb/root/orb-root.css.js +38 -0
- package/dist/orb/root/orb-root.d.ts +142 -0
- package/dist/orb/root/orb-root.js +262 -0
- package/dist/orb/root/orb-shapes.d.ts +113 -0
- package/dist/orb/root/orb-shapes.js +150 -0
- package/package.json +6 -6
- package/types/jsx.d.ts +230 -0
- package/types/svelte.d.ts +1 -0
- package/types/vue.d.ts +41 -0
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 @@
|
|
|
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];
|