@fusion.dev/helix 0.9.0 → 0.10.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.
@@ -54613,6 +54613,40 @@
54613
54613
  }
54614
54614
  ]
54615
54615
  },
54616
+ {
54617
+ "kind": "javascript-module",
54618
+ "path": "src/orb/root/orb-color.ts",
54619
+ "declarations": [
54620
+ {
54621
+ "kind": "function",
54622
+ "name": "readColor",
54623
+ "return": {
54624
+ "type": {
54625
+ "text": "[number, number, number]"
54626
+ }
54627
+ },
54628
+ "parameters": [
54629
+ {
54630
+ "name": "value",
54631
+ "type": {
54632
+ "text": "string"
54633
+ }
54634
+ }
54635
+ ],
54636
+ "description": "A computed color as three sRGB bytes, whatever space it was written in.\n\n**Painting it is the only way to read it.** A color-typed property is\nresolved before it is reported, but resolved is not the same as converted:\nChrome hands back `lab(100 0 0)` for a token built through `oklab`, and this\nused to scrape the first three numbers out of that and call them red, green\nand blue. White arrived as `rgb(100, 0, 0)`. In light mode the near dots were\n`lab(0 0 0)`, which scrapes to black and looks right, so only dark mode showed\nit: dark red mixed toward black, on a near-black ground.\n\nA 1x1 fill and `getImageData` is a readback, so the answer is cached against\nthe string it came from. There are two per orb and they change when the theme\ndoes."
54637
+ }
54638
+ ],
54639
+ "exports": [
54640
+ {
54641
+ "kind": "js",
54642
+ "name": "readColor",
54643
+ "declaration": {
54644
+ "name": "readColor",
54645
+ "module": "src/orb/root/orb-color.ts"
54646
+ }
54647
+ }
54648
+ ]
54649
+ },
54616
54650
  {
54617
54651
  "kind": "javascript-module",
54618
54652
  "path": "src/orb/root/orb-root.css.ts",
@@ -54639,7 +54673,7 @@
54639
54673
  "declarations": [
54640
54674
  {
54641
54675
  "kind": "class",
54642
- "description": "A cloud of dots on a sphere, drawn to a canvas, with one shape per state:\n`thinking` is a narrow sash with a wave running through it, `fetching` spreads\ninto a globe with a highlight sweeping pole to pole, `working` flattens and\ntilts with its bands shearing past each other, and `done` settles into an even\nball.\n\n**None of them is a separate drawing.** Each is the same banded sphere given\ndifferent numbers, so changing state interpolates the numbers and every frame\nin between is a whole shape. A loader that cuts between pictures says the\nwork restarted; one that carries a shape into the next says it moved on.\n\n**Nothing spins.** All the movement is inside the shape, which is what makes\nit read as thought rather than as a thing being rotated.\n\n**The palette is read from the tokens**, not given as colors: a dot's ink is\nmixed toward the ground by how far away it is, and both ends come from\n`--orb-color` and `--orb-ground`. That is what makes depth survive a theme\nchange without the consumer restating anything.\n\n**It stops when nobody is looking**, off screen and in a background tab, and\n`prefers-reduced-motion` gets one still frame rather than an empty box.",
54676
+ "description": "A cloud of dots, drawn to a canvas, with one shape per state. `connecting`\nand `thinking` are the same leaning sash and differ in how agitated it is;\n`waiting` and `done` are the same settled ball at different tempos, which is\nthe difference between not having started and having finished. `fetching` is\na globe with a light going round it, `listening` a closed sphere with a\nripple crossing it, and `working` a ring.\n\n**None of them is a separate drawing.** Each is the same banded grid given\ndifferent numbers, so changing state interpolates the numbers and every frame\nin between is a whole shape. A loader that cuts between pictures says the\nwork restarted; one that carries a shape into the next says it moved on.\n\n**A change is measured before it is played.** Two states differing in one\nsmall number are given less time than a sash becoming a ring, and the\nsilhouette leads what the shape is doing, so a light is never seen orbiting\nsomething that has not finished becoming round.\n\n**Nothing spins.** The whole cloud holds still and everything that moves is\nmoving inside it, which is what makes it read as thought rather than as a\nthing being rotated. The one exception is the light, which travels because a\nring of evenly spaced dots is unchanged by rotation: without something fixed\nin space passing over them, a state that turns has nothing a reader can see\nturning.\n\n**The palette is read from the tokens**, not given as colors: a dot's ink is\nmixed toward the ground by how far away it is, and both ends come from\n`--orb-color` and `--orb-ground`. That is what makes depth survive a theme\nchange without the consumer restating anything.\n\n**It stops when nobody is looking**, off screen and in a background tab, and\n`prefers-reduced-motion` gets one still frame rather than an empty box.",
54643
54677
  "name": "HelixOrb",
54644
54678
  "cssProperties": [
54645
54679
  {
@@ -54788,6 +54822,16 @@
54788
54822
  "privacy": "private",
54789
54823
  "default": "0"
54790
54824
  },
54825
+ {
54826
+ "kind": "field",
54827
+ "name": "reach",
54828
+ "type": {
54829
+ "text": "number"
54830
+ },
54831
+ "privacy": "private",
54832
+ "default": "0",
54833
+ "description": "How far the current change travels, `0` to `1`."
54834
+ },
54791
54835
  {
54792
54836
  "kind": "field",
54793
54837
  "name": "phase",
@@ -54839,6 +54883,16 @@
54839
54883
  },
54840
54884
  "description": "Begins a transition from whatever is on the screen right now."
54841
54885
  },
54886
+ {
54887
+ "kind": "field",
54888
+ "name": "span",
54889
+ "type": {
54890
+ "text": "number"
54891
+ },
54892
+ "privacy": "private",
54893
+ "description": "How long this particular change is given.\n\n**Near states cross quickly and far ones are not hurried.** One length for\nevery pair read as rushed going from `waiting` to `done`, which differ in\ntwo small numbers, and slack going from `thinking` to `working`, which is a\nsash becoming a ring.",
54894
+ "readonly": true
54895
+ },
54842
54896
  {
54843
54897
  "kind": "field",
54844
54898
  "name": "progress",
@@ -55151,7 +55205,7 @@
55151
55205
  "slug": "sizes"
55152
55206
  },
55153
55207
  {
55154
- "description": "The four shapes, side by side.",
55208
+ "description": "The seven shapes, side by side.",
55155
55209
  "slug": "states"
55156
55210
  }
55157
55211
  ],
@@ -55190,9 +55244,9 @@
55190
55244
  "kind": "variable",
55191
55245
  "name": "ORB_STATES",
55192
55246
  "type": {
55193
- "text": "['thinking', 'fetching', 'working', 'done']"
55247
+ "text": "[\n 'waiting',\n 'connecting',\n 'thinking',\n 'fetching',\n 'listening',\n 'working',\n 'done',\n]"
55194
55248
  },
55195
- "default": "['thinking', 'fetching', 'working', 'done']",
55249
+ "default": "[ 'waiting', 'connecting', 'thinking', 'fetching', 'listening', 'working', 'done', ]",
55196
55250
  "description": "The states an orb can be in, in the order a task tends to pass through them.\n\nExported so the component can validate an attribute against the same list the\ngenerator reads, rather than keeping a second copy that drifts."
55197
55251
  },
55198
55252
  {
@@ -55201,7 +55255,7 @@
55201
55255
  "type": {
55202
55256
  "text": "Record<OrbState, OrbParams>"
55203
55257
  },
55204
- "default": "{ /* Settled: an even ball, breathing, with nothing traveling through it. */ done: { belt: 1, edge: 0, jitter: 1, rate: 1.1, rot: 0, scan: 0, shear: 0, squash: 1, waveA: 0, waveB: 0, }, /* The sash opened out into a globe, with a highlight sweeping pole to pole: the shape of looking through something. */ fetching: { belt: 1, edge: 0, jitter: 0, rate: 2.4, rot: 0, scan: 1, shear: 0, squash: 1, waveA: 0, waveB: 0, }, /* A narrow sash with a wave running through it, tipped so the band crosses the face rather than sitting level. */ thinking: { belt: 0.47, edge: 1, jitter: 0, rate: 2.1, rot: 1.52, scan: 0, shear: 0, squash: 1, waveA: 0.16, waveB: 0.07, }, /* Flattened and tilted, bands shearing past each other. **The squash is what makes this state legible at 20px**: `fetching` and `working` are both full spheres, and at that size a difference in motion alone is not a difference a reader can see. A silhouette is. */ working: { belt: 1, edge: 0, jitter: 0, rate: 2.4, rot: 0.5, scan: 0, shear: 1.3, squash: 0.72, waveA: 0, waveB: 0, }, }",
55258
+ "default": "{ /* The same sash as `thinking` on the same lean, thinner and calmer: the route between them is a narrowing rather than a change of shape. */ connecting: { belt: 0.32, breathe: 0.07, edge: 1, jitter: 0, orbit: 0, pulse: 0, rate: 1.05, ring: 0, rot: 1.52, shear: 0, squash: 1, waveA: 0.035, waveB: 0, }, /* Settled: an even ball, breathing, with nothing traveling through it. */ done: { belt: 1, breathe: 0.035, edge: 0, jitter: 1, orbit: 0, pulse: 0, rate: 1.1, ring: 0, rot: 0, shear: 0, squash: 1, waveA: 0, waveB: 0, }, /* A globe with a light going round it, which is the shape of looking through something. */ fetching: { belt: 1, breathe: 0, edge: 0, jitter: 0.62, orbit: 1, pulse: 0, rate: 2, ring: 0, rot: 0, shear: 0, squash: 1, waveA: 0, waveB: 0, }, /* Receptive: a closed sphere with a ripple crossing it, slower than `connecting` and symmetric where that one is a band. */ listening: { belt: 1, breathe: 0.03, edge: 0, jitter: 1, orbit: 0, pulse: 0.055, rate: 1.15, ring: 0, rot: 0, shear: 0, squash: 1, waveA: 0, waveB: 0, }, /* A narrow sash with a wave running through it, tipped so the band crosses the face rather than sitting level. */ thinking: { belt: 0.47, breathe: 0, edge: 1, jitter: 0, orbit: 0, pulse: 0, rate: 2.1, ring: 0, rot: 1.52, shear: 0, squash: 1, waveA: 0.16, waveB: 0.07, }, /* The same settled ball as `done` at a slower tempo, which is the difference between not having started and having finished. */ waiting: { belt: 1, breathe: 0.075, edge: 0, jitter: 1, orbit: 0, pulse: 0, rate: 0.55, ring: 0, rot: 0, shear: 0, squash: 1, waveA: 0, waveB: 0, }, /* **A ring, which is a different object rather than an adjusted ball.** This was a squashed sphere with its bands shearing, and a sphere adjusted is still a sphere: at 20px it read as the same mark as every other state. A torus has a hole. The bands run around the tube, so the shear twists it like rope instead of sliding flat layers past each other, and the light is what makes that turning visible at all. */ working: { belt: 1, breathe: 0.035, edge: 0, jitter: 0, orbit: 0.7, pulse: 0, rate: 1.9, ring: 1, rot: 0.3, shear: 0.62, squash: 1, waveA: 0, waveB: 0, }, }",
55205
55259
  "description": "Each state, as numbers.\n\nThey are deliberately close together. Two states that differ in every number\ntake a long, busy route between them; these differ in the two or three that\ncarry the meaning, so a transition reads as one thing becoming another rather\nthan as a shuffle."
55206
55260
  },
55207
55261
  {
@@ -55234,6 +55288,75 @@
55234
55288
  ],
55235
55289
  "description": "Halfway between two states, key by key."
55236
55290
  },
55291
+ {
55292
+ "kind": "function",
55293
+ "name": "stateDistance",
55294
+ "return": {
55295
+ "type": {
55296
+ "text": "number"
55297
+ }
55298
+ },
55299
+ "parameters": [
55300
+ {
55301
+ "name": "a",
55302
+ "type": {
55303
+ "text": "OrbParams"
55304
+ }
55305
+ },
55306
+ {
55307
+ "name": "b",
55308
+ "type": {
55309
+ "text": "OrbParams"
55310
+ }
55311
+ }
55312
+ ],
55313
+ "description": "How far apart two states are, `0` to `1`, each key against its own range.\n\nTwo states differing in one small number should not be given the time\n`thinking` to `done` needs. One length for every pair reads as rushed at one\nend of the set and slack at the other."
55314
+ },
55315
+ {
55316
+ "kind": "function",
55317
+ "name": "blendStaged",
55318
+ "return": {
55319
+ "type": {
55320
+ "text": "OrbParams"
55321
+ }
55322
+ },
55323
+ "parameters": [
55324
+ {
55325
+ "name": "a",
55326
+ "type": {
55327
+ "text": "OrbParams"
55328
+ }
55329
+ },
55330
+ {
55331
+ "name": "b",
55332
+ "type": {
55333
+ "text": "OrbParams"
55334
+ }
55335
+ },
55336
+ {
55337
+ "name": "shape",
55338
+ "type": {
55339
+ "text": "number"
55340
+ }
55341
+ },
55342
+ {
55343
+ "name": "character",
55344
+ "type": {
55345
+ "text": "number"
55346
+ }
55347
+ }
55348
+ ],
55349
+ "description": "Two states blended on two clocks: the silhouette, and then what it is doing.\n\n**The other order shows a light orbiting a sash.** Every number moving on one\ncurve makes a change of state read as a cross-fade between two pictures; the\nshape arriving first makes it read as one thing becoming another."
55350
+ },
55351
+ {
55352
+ "kind": "variable",
55353
+ "name": "TRAIL",
55354
+ "type": {
55355
+ "text": "number"
55356
+ },
55357
+ "default": "0.2",
55358
+ "description": "How far the character trails the silhouette, as a share of the run."
55359
+ },
55237
55360
  {
55238
55361
  "kind": "function",
55239
55362
  "name": "paramsFor",
@@ -55446,6 +55569,30 @@
55446
55569
  "module": "src/orb/root/orb-shapes.ts"
55447
55570
  }
55448
55571
  },
55572
+ {
55573
+ "kind": "js",
55574
+ "name": "stateDistance",
55575
+ "declaration": {
55576
+ "name": "stateDistance",
55577
+ "module": "src/orb/root/orb-shapes.ts"
55578
+ }
55579
+ },
55580
+ {
55581
+ "kind": "js",
55582
+ "name": "blendStaged",
55583
+ "declaration": {
55584
+ "name": "blendStaged",
55585
+ "module": "src/orb/root/orb-shapes.ts"
55586
+ }
55587
+ },
55588
+ {
55589
+ "kind": "js",
55590
+ "name": "TRAIL",
55591
+ "declaration": {
55592
+ "name": "TRAIL",
55593
+ "module": "src/orb/root/orb-shapes.ts"
55594
+ }
55595
+ },
55449
55596
  {
55450
55597
  "kind": "js",
55451
55598
  "name": "paramsFor",
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Reading a computed color the engine has resolved but not converted.
3
+ */
4
+ /**
5
+ * A computed color as three sRGB bytes, whatever space it was written in.
6
+ *
7
+ * **Painting it is the only way to read it.** A color-typed property is
8
+ * resolved before it is reported, but resolved is not the same as converted:
9
+ * Chrome hands back `lab(100 0 0)` for a token built through `oklab`, and this
10
+ * used to scrape the first three numbers out of that and call them red, green
11
+ * and blue. White arrived as `rgb(100, 0, 0)`. In light mode the near dots were
12
+ * `lab(0 0 0)`, which scrapes to black and looks right, so only dark mode showed
13
+ * it: dark red mixed toward black, on a near-black ground.
14
+ *
15
+ * A 1x1 fill and `getImageData` is a readback, so the answer is cached against
16
+ * the string it came from. There are two per orb and they change when the theme
17
+ * does.
18
+ */
19
+ export declare const readColor: (value: string) => [number, number, number];
@@ -0,0 +1,25 @@
1
+ let swatch;
2
+ const known = new Map();
3
+ export const readColor = (value) => {
4
+ const seen = known.get(value);
5
+ if (seen) {
6
+ return seen;
7
+ }
8
+ if (swatch === undefined) {
9
+ swatch = document.createElement('canvas').getContext('2d', { willReadFrequently: true });
10
+ }
11
+ if (!swatch) {
12
+ return [128, 128, 128];
13
+ }
14
+ swatch.clearRect(0, 0, 1, 1);
15
+ swatch.fillStyle = '#808080';
16
+ swatch.fillStyle = value;
17
+ swatch.fillRect(0, 0, 1, 1);
18
+ const [red, green, blue] = swatch.getImageData(0, 0, 1, 1).data;
19
+ const rgb = [red ?? 128, green ?? 128, blue ?? 128];
20
+ if (known.size > 64) {
21
+ known.clear();
22
+ }
23
+ known.set(value, rgb);
24
+ return rgb;
25
+ };
@@ -5,19 +5,29 @@ import { type OrbState } from './orb-shapes.js';
5
5
  * @summary Says what an agent is doing, as a shape rather than a word.
6
6
  *
7
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.
8
+ * A cloud of dots, drawn to a canvas, with one shape per state. `connecting`
9
+ * and `thinking` are the same leaning sash and differ in how agitated it is;
10
+ * `waiting` and `done` are the same settled ball at different tempos, which is
11
+ * the difference between not having started and having finished. `fetching` is
12
+ * a globe with a light going round it, `listening` a closed sphere with a
13
+ * ripple crossing it, and `working` a ring.
13
14
  *
14
- * **None of them is a separate drawing.** Each is the same banded sphere given
15
+ * **None of them is a separate drawing.** Each is the same banded grid given
15
16
  * different numbers, so changing state interpolates the numbers and every frame
16
17
  * in between is a whole shape. A loader that cuts between pictures says the
17
18
  * work restarted; one that carries a shape into the next says it moved on.
18
19
  *
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.
20
+ * **A change is measured before it is played.** Two states differing in one
21
+ * small number are given less time than a sash becoming a ring, and the
22
+ * silhouette leads what the shape is doing, so a light is never seen orbiting
23
+ * something that has not finished becoming round.
24
+ *
25
+ * **Nothing spins.** The whole cloud holds still and everything that moves is
26
+ * moving inside it, which is what makes it read as thought rather than as a
27
+ * thing being rotated. The one exception is the light, which travels because a
28
+ * ring of evenly spaced dots is unchanged by rotation: without something fixed
29
+ * in space passing over them, a state that turns has nothing a reader can see
30
+ * turning.
21
31
  *
22
32
  * **The palette is read from the tokens**, not given as colors: a dot's ink is
23
33
  * mixed toward the ground by how far away it is, and both ends come from
@@ -91,6 +101,8 @@ export declare class HelixOrb extends HelixElement {
91
101
  */
92
102
  private from;
93
103
  private morphAt;
104
+ /** How far the current change travels, `0` to `1`. */
105
+ private reach;
94
106
  /**
95
107
  * The state's clock, accumulated rather than derived.
96
108
  *
@@ -118,6 +130,15 @@ export declare class HelixOrb extends HelixElement {
118
130
  private measure;
119
131
  /** Begins a transition from whatever is on the screen right now. */
120
132
  private turn;
133
+ /**
134
+ * How long this particular change is given.
135
+ *
136
+ * **Near states cross quickly and far ones are not hurried.** One length for
137
+ * every pair read as rushed going from `waiting` to `done`, which differ in
138
+ * two small numbers, and slack going from `thinking` to `working`, which is a
139
+ * sash becoming a ring.
140
+ */
141
+ private get span();
121
142
  /** How far through a transition, `1` when there is not one. */
122
143
  private get progress();
123
144
  private paramsNow;
@@ -135,6 +156,14 @@ export declare class HelixOrb extends HelixElement {
135
156
  */
136
157
  private paint;
137
158
  }
159
+ /**
160
+ * The numbers out of a resolved color.
161
+ *
162
+ * The engine reports color-typed properties as `rgb()` or `color()`, so the
163
+ * digits are already there to be read. Deliberately forgiving all the same: a
164
+ * value this cannot parse costs the orb its depth rather than its existence,
165
+ * and a test environment with no layout engine is exactly such a value.
166
+ */
138
167
  declare global {
139
168
  interface HTMLElementTagNameMap {
140
169
  'helix-orb': HelixOrb;
@@ -7,14 +7,20 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ import { readColor } from './orb-color.js';
10
11
  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;
12
+ import { blendStaged, evenSphere, fibonacci, meshFor, ORB_STATES, orbPoint, paramsFor, project, smoother, stateDistance, TRAIL, } from './orb-shapes.js';
13
+ const MORPH_NEAR = 380;
14
+ const MORPH_REACH = 580;
15
+ const LIGHT_Y = 0.36;
13
16
  const STILL_PHASE = 1.4;
14
17
  const LABELS = {
18
+ connecting: 'Connecting',
15
19
  done: 'Done',
16
20
  fetching: 'Fetching',
21
+ listening: 'Listening',
17
22
  thinking: 'Thinking',
23
+ waiting: 'Waiting',
18
24
  working: 'Working',
19
25
  };
20
26
  let HelixOrb = class HelixOrb extends HelixElement {
@@ -29,6 +35,7 @@ let HelixOrb = class HelixOrb extends HelixElement {
29
35
  this.box = 0;
30
36
  this.from = paramsFor('thinking');
31
37
  this.morphAt = 0;
38
+ this.reach = 0;
32
39
  this.phase = STILL_PHASE;
33
40
  this.last = 0;
34
41
  this.tick = () => {
@@ -148,19 +155,26 @@ let HelixOrb = class HelixOrb extends HelixElement {
148
155
  }
149
156
  turn() {
150
157
  this.from = this.paramsNow();
158
+ this.reach = stateDistance(this.from, paramsFor(this.state));
151
159
  this.morphAt = performance.now();
152
160
  if (!this.running) {
153
161
  this.paint(0);
154
162
  this.play();
155
163
  }
156
164
  }
165
+ get span() {
166
+ return MORPH_NEAR + MORPH_REACH * this.reach;
167
+ }
157
168
  get progress() {
158
- return this.morphAt === 0 ? 1 : Math.min(1, (performance.now() - this.morphAt) / MORPH);
169
+ return this.morphAt === 0 ? 1 : Math.min(1, (performance.now() - this.morphAt) / this.span);
159
170
  }
160
171
  paramsNow() {
161
172
  const to = paramsFor(this.state);
162
173
  const k = this.progress;
163
- return k >= 1 ? to : blendParams(this.from, to, smoother(k));
174
+ if (k >= 1) {
175
+ return to;
176
+ }
177
+ return blendStaged(this.from, to, smoother(k), smoother(Math.max(0, k - TRAIL) / (1 - TRAIL)));
164
178
  }
165
179
  play() {
166
180
  if (this.running || this.motion?.matches || !this.onScreen) {
@@ -188,8 +202,14 @@ let HelixOrb = class HelixOrb extends HelixElement {
188
202
  const ground = readColor(painted.textDecorationColor);
189
203
  const mesh = meshFor(size);
190
204
  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);
205
+ const spin = this.phase * 0.9;
206
+ const lx = Math.cos(spin) * 0.9;
207
+ const lz = Math.sin(spin) * 0.9;
208
+ const llen = Math.hypot(lx, LIGHT_Y, lz) || 1;
209
+ const swell = still || this.progress >= 1
210
+ ? 1
211
+ : 1 + (0.035 + 0.1 * this.reach) * Math.sin(Math.PI * this.progress ** 0.82);
212
+ const shell = swell * (1 + params.breathe * Math.sin(this.phase * 0.62));
193
213
  const dots = [];
194
214
  for (let i = 0; i < mesh.ghostCount; i++) {
195
215
  const [x, y, z] = fibonacci(i, mesh.ghostCount);
@@ -211,7 +231,8 @@ let HelixOrb = class HelixOrb extends HelixElement {
211
231
  const [x, y, z] = orbPoint(u, s / mesh.segs, this.phase, params, index, even);
212
232
  const [px, py, pz] = project(size, x * shell, y * shell, z * shell);
213
233
  const depth = (pz + 1) / 2;
214
- const boost = params.scan * Math.exp(-((y - scan) ** 2) / 0.012) +
234
+ const facing = (x * lx + y * LIGHT_Y + z * lz) / llen;
235
+ const boost = params.orbit * (facing > 0 ? facing ** 11 : 0) +
215
236
  params.jitter * (0.1 + 0.1 * Math.sin(this.phase * 0.6));
216
237
  dots.push({
217
238
  alpha: mesh.aMin + (1 - mesh.aMin) * depth,
@@ -253,10 +274,3 @@ HelixOrb = __decorate([
253
274
  customElement('helix-orb')
254
275
  ], HelixOrb);
255
276
  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
- };
@@ -4,7 +4,7 @@
4
4
  * Exported so the component can validate an attribute against the same list the
5
5
  * generator reads, rather than keeping a second copy that drifts.
6
6
  */
7
- export declare const ORB_STATES: readonly ['thinking', 'fetching', 'working', 'done'];
7
+ export declare const ORB_STATES: readonly ['waiting', 'connecting', 'thinking', 'fetching', 'listening', 'working', 'done'];
8
8
  /** What the agent is doing. */
9
9
  export type OrbState = (typeof ORB_STATES)[number];
10
10
  /**
@@ -28,12 +28,18 @@ export interface OrbParams {
28
28
  edge: number;
29
29
  /** How far toward an even sphere, `0` the banded grid and `1` fully relaxed. */
30
30
  jitter: number;
31
+ /** How far the whole cloud swells and settles, `0` still. */
32
+ breathe: number;
33
+ /** Strength of a highlight traveling around the ball, `0` absent. */
34
+ orbit: number;
35
+ /** A ripple over the surface, applied after the sphere is closed. */
36
+ pulse: number;
31
37
  /** How fast this state's own clock runs. */
32
38
  rate: number;
39
+ /** How far the ball has opened into a ring, `0` a sphere and `1` a torus. */
40
+ ring: number;
33
41
  /** Which way the axis leans, in radians. */
34
42
  rot: number;
35
- /** Where the sweeping highlight is, `0` absent and `1` full strength. */
36
- scan: number;
37
43
  /** How hard the bands slide past each other. */
38
44
  shear: number;
39
45
  /** How flattened the ball is along its axis, `1` round. */
@@ -54,6 +60,24 @@ export interface OrbParams {
54
60
  export declare const ORB_PARAMS: Record<OrbState, OrbParams>;
55
61
  /** Halfway between two states, key by key. */
56
62
  export declare const blendParams: (a: OrbParams, b: OrbParams, k: number) => OrbParams;
63
+ /**
64
+ * How far apart two states are, `0` to `1`, each key against its own range.
65
+ *
66
+ * Two states differing in one small number should not be given the time
67
+ * `thinking` to `done` needs. One length for every pair reads as rushed at one
68
+ * end of the set and slack at the other.
69
+ */
70
+ export declare const stateDistance: (a: OrbParams, b: OrbParams) => number;
71
+ /**
72
+ * Two states blended on two clocks: the silhouette, and then what it is doing.
73
+ *
74
+ * **The other order shows a light orbiting a sash.** Every number moving on one
75
+ * curve makes a change of state read as a cross-fade between two pictures; the
76
+ * shape arriving first makes it read as one thing becoming another.
77
+ */
78
+ export declare const blendStaged: (a: OrbParams, b: OrbParams, shape: number, character: number) => OrbParams;
79
+ /** How far the character trails the silhouette, as a share of the run. */
80
+ export declare const TRAIL = 0.2;
57
81
  /** The state's numbers, or `thinking`'s where the name is not one we draw. */
58
82
  export declare const paramsFor: (state: string | undefined) => OrbParams;
59
83
  /** Smootherstep: zero slope and zero acceleration at both ends. */
@@ -1,12 +1,38 @@
1
- export const ORB_STATES = ['thinking', 'fetching', 'working', 'done'];
1
+ export const ORB_STATES = [
2
+ 'waiting',
3
+ 'connecting',
4
+ 'thinking',
5
+ 'fetching',
6
+ 'listening',
7
+ 'working',
8
+ 'done',
9
+ ];
2
10
  export const ORB_PARAMS = {
11
+ connecting: {
12
+ belt: 0.32,
13
+ breathe: 0.07,
14
+ edge: 1,
15
+ jitter: 0,
16
+ orbit: 0,
17
+ pulse: 0,
18
+ rate: 1.05,
19
+ ring: 0,
20
+ rot: 1.52,
21
+ shear: 0,
22
+ squash: 1,
23
+ waveA: 0.035,
24
+ waveB: 0,
25
+ },
3
26
  done: {
4
27
  belt: 1,
28
+ breathe: 0.035,
5
29
  edge: 0,
6
30
  jitter: 1,
31
+ orbit: 0,
32
+ pulse: 0,
7
33
  rate: 1.1,
34
+ ring: 0,
8
35
  rot: 0,
9
- scan: 0,
10
36
  shear: 0,
11
37
  squash: 1,
12
38
  waveA: 0,
@@ -14,11 +40,29 @@ export const ORB_PARAMS = {
14
40
  },
15
41
  fetching: {
16
42
  belt: 1,
43
+ breathe: 0,
17
44
  edge: 0,
18
- jitter: 0,
19
- rate: 2.4,
45
+ jitter: 0.62,
46
+ orbit: 1,
47
+ pulse: 0,
48
+ rate: 2,
49
+ ring: 0,
50
+ rot: 0,
51
+ shear: 0,
52
+ squash: 1,
53
+ waveA: 0,
54
+ waveB: 0,
55
+ },
56
+ listening: {
57
+ belt: 1,
58
+ breathe: 0.03,
59
+ edge: 0,
60
+ jitter: 1,
61
+ orbit: 0,
62
+ pulse: 0.055,
63
+ rate: 1.15,
64
+ ring: 0,
20
65
  rot: 0,
21
- scan: 1,
22
66
  shear: 0,
23
67
  squash: 1,
24
68
  waveA: 0,
@@ -26,25 +70,46 @@ export const ORB_PARAMS = {
26
70
  },
27
71
  thinking: {
28
72
  belt: 0.47,
73
+ breathe: 0,
29
74
  edge: 1,
30
75
  jitter: 0,
76
+ orbit: 0,
77
+ pulse: 0,
31
78
  rate: 2.1,
79
+ ring: 0,
32
80
  rot: 1.52,
33
- scan: 0,
34
81
  shear: 0,
35
82
  squash: 1,
36
83
  waveA: 0.16,
37
84
  waveB: 0.07,
38
85
  },
86
+ waiting: {
87
+ belt: 1,
88
+ breathe: 0.075,
89
+ edge: 0,
90
+ jitter: 1,
91
+ orbit: 0,
92
+ pulse: 0,
93
+ rate: 0.55,
94
+ ring: 0,
95
+ rot: 0,
96
+ shear: 0,
97
+ squash: 1,
98
+ waveA: 0,
99
+ waveB: 0,
100
+ },
39
101
  working: {
40
102
  belt: 1,
103
+ breathe: 0.035,
41
104
  edge: 0,
42
105
  jitter: 0,
43
- rate: 2.4,
44
- rot: 0.5,
45
- scan: 0,
46
- shear: 1.3,
47
- squash: 0.72,
106
+ orbit: 0.7,
107
+ pulse: 0,
108
+ rate: 1.9,
109
+ ring: 1,
110
+ rot: 0.3,
111
+ shear: 0.62,
112
+ squash: 1,
48
113
  waveA: 0,
49
114
  waveB: 0,
50
115
  },
@@ -57,6 +122,51 @@ export const blendParams = (a, b, k) => {
57
122
  }
58
123
  return out;
59
124
  };
125
+ const SPANS = {
126
+ belt: 1,
127
+ breathe: 0.08,
128
+ edge: 1,
129
+ jitter: 1,
130
+ orbit: 1,
131
+ pulse: 0.09,
132
+ rate: 2.5,
133
+ ring: 1,
134
+ rot: 1.6,
135
+ shear: 1.3,
136
+ squash: 1,
137
+ waveA: 0.2,
138
+ waveB: 0.1,
139
+ };
140
+ const STRUCTURE = [
141
+ 'belt',
142
+ 'edge',
143
+ 'jitter',
144
+ 'ring',
145
+ 'rot',
146
+ 'shear',
147
+ 'squash',
148
+ 'waveA',
149
+ 'waveB',
150
+ ];
151
+ const CHARACTER = ['breathe', 'orbit', 'pulse', 'rate'];
152
+ export const stateDistance = (a, b) => {
153
+ let sum = 0;
154
+ for (const key of KEYS) {
155
+ sum += Math.abs(b[key] - a[key]) / SPANS[key];
156
+ }
157
+ return Math.min(1, sum / 3.2);
158
+ };
159
+ export const blendStaged = (a, b, shape, character) => {
160
+ const out = {};
161
+ for (const key of STRUCTURE) {
162
+ out[key] = a[key] + (b[key] - a[key]) * shape;
163
+ }
164
+ for (const key of CHARACTER) {
165
+ out[key] = a[key] + (b[key] - a[key]) * character;
166
+ }
167
+ return out;
168
+ };
169
+ export const TRAIL = 0.2;
60
170
  export const paramsFor = (state) => ORB_PARAMS[state] ?? ORB_PARAMS.thinking;
61
171
  const TAU = Math.PI * 2;
62
172
  export const smoother = (t) => t * t * t * (t * (t * 6 - 15) + 10);
@@ -92,12 +202,28 @@ export const evenSphere = (lanes, segs) => {
92
202
  return out;
93
203
  };
94
204
  const SPAN = 1.86;
205
+ const RING_HOLE = 0.64;
206
+ const RING_TUBE = 0.36;
95
207
  export const orbPoint = (u, v, phase, p, index, even) => {
96
208
  const th = v * TAU + p.shear * phase * 0.55 * (u - 0.5) * 2;
97
209
  const wave = p.waveA * Math.sin(th * 3 - phase * 1.7 + u * 3.1) + p.waveB * Math.sin(th * 5 + phase * 1.1);
98
210
  const y = Math.max(-0.995, Math.min(0.995, (u - 0.5) * SPAN * p.belt + wave));
99
211
  const r = Math.sqrt(Math.max(0, 1 - y * y));
100
212
  let point = [r * Math.cos(th), y * p.squash, r * Math.sin(th)];
213
+ if (p.ring > 0.001) {
214
+ const ph = u * TAU;
215
+ const rr = RING_HOLE + RING_TUBE * Math.cos(ph);
216
+ const torus = [
217
+ rr * Math.cos(th),
218
+ RING_TUBE * Math.sin(ph),
219
+ rr * Math.sin(th),
220
+ ];
221
+ point = [
222
+ point[0] + (torus[0] - point[0]) * p.ring,
223
+ point[1] + (torus[1] - point[1]) * p.ring,
224
+ point[2] + (torus[2] - point[2]) * p.ring,
225
+ ];
226
+ }
101
227
  if (p.rot) {
102
228
  const c = Math.cos(p.rot);
103
229
  const s = Math.sin(p.rot);
@@ -111,7 +237,12 @@ export const orbPoint = (u, v, phase, p, index, even) => {
111
237
  point[2] + (e[2] - point[2]) * p.jitter,
112
238
  ];
113
239
  const len = Math.hypot(point[0], point[1], point[2]) || 1;
114
- point = [point[0] / len, point[1] / len, point[2] / len];
240
+ const k = 1 + (1 / len - 1) * (1 - p.ring);
241
+ point = [point[0] * k, point[1] * k, point[2] * k];
242
+ }
243
+ if (p.pulse > 0.001) {
244
+ const ripple = 1 + p.pulse * Math.sin(phase * 1.5 - point[1] * 3.2);
245
+ point = [point[0] * ripple, point[1] * ripple, point[2] * ripple];
115
246
  }
116
247
  return point;
117
248
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix",
3
3
  "description": "Styled component tier. Extends the headless tier with token-driven presentation.",
4
- "version": "0.9.0",
4
+ "version": "0.10.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -46,14 +46,14 @@
46
46
  },
47
47
  "bugs": "https://github.com/project-fusion/fusion/issues",
48
48
  "dependencies": {
49
- "@fusion.dev/helix-core": "0.9.0",
50
- "@fusion.dev/helix-headless": "0.9.0",
51
- "@fusion.dev/helix-styles": "0.9.0"
49
+ "@fusion.dev/helix-core": "0.10.0",
50
+ "@fusion.dev/helix-headless": "0.10.0",
51
+ "@fusion.dev/helix-styles": "0.10.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@custom-elements-manifest/analyzer": "^0.11.0",
55
- "@fusion.dev/helix-cem": "0.9.0",
56
- "@fusion.dev/helix-testing": "0.9.0",
55
+ "@fusion.dev/helix-cem": "0.10.0",
56
+ "@fusion.dev/helix-testing": "0.10.0",
57
57
  "@fusion.dev/scripts": "0.0.0-dev",
58
58
  "@happy-dom/global-registrator": "^20.0.0",
59
59
  "@lit-labs/ssr": "^3.3.0",
package/types/jsx.d.ts CHANGED
@@ -26765,19 +26765,29 @@ export type CustomElements = {
26765
26765
  >;
26766
26766
 
26767
26767
  /**
26768
- * A cloud of dots on a sphere, drawn to a canvas, with one shape per state:
26769
- * `thinking` is a narrow sash with a wave running through it, `fetching` spreads
26770
- * into a globe with a highlight sweeping pole to pole, `working` flattens and
26771
- * tilts with its bands shearing past each other, and `done` settles into an even
26772
- * ball.
26773
- *
26774
- * **None of them is a separate drawing.** Each is the same banded sphere given
26768
+ * A cloud of dots, drawn to a canvas, with one shape per state. `connecting`
26769
+ * and `thinking` are the same leaning sash and differ in how agitated it is;
26770
+ * `waiting` and `done` are the same settled ball at different tempos, which is
26771
+ * the difference between not having started and having finished. `fetching` is
26772
+ * a globe with a light going round it, `listening` a closed sphere with a
26773
+ * ripple crossing it, and `working` a ring.
26774
+ *
26775
+ * **None of them is a separate drawing.** Each is the same banded grid given
26775
26776
  * different numbers, so changing state interpolates the numbers and every frame
26776
26777
  * in between is a whole shape. A loader that cuts between pictures says the
26777
26778
  * work restarted; one that carries a shape into the next says it moved on.
26778
26779
  *
26779
- * **Nothing spins.** All the movement is inside the shape, which is what makes
26780
- * it read as thought rather than as a thing being rotated.
26780
+ * **A change is measured before it is played.** Two states differing in one
26781
+ * small number are given less time than a sash becoming a ring, and the
26782
+ * silhouette leads what the shape is doing, so a light is never seen orbiting
26783
+ * something that has not finished becoming round.
26784
+ *
26785
+ * **Nothing spins.** The whole cloud holds still and everything that moves is
26786
+ * moving inside it, which is what makes it read as thought rather than as a
26787
+ * thing being rotated. The one exception is the light, which travels because a
26788
+ * ring of evenly spaced dots is unchanged by rotation: without something fixed
26789
+ * in space passing over them, a state that turns has nothing a reader can see
26790
+ * turning.
26781
26791
  *
26782
26792
  * **The palette is read from the tokens**, not given as colors: a dot's ink is
26783
26793
  * mixed toward the ground by how far away it is, and both ends come from
@@ -42654,19 +42664,29 @@ export type CustomElementsSolidJs = {
42654
42664
  >;
42655
42665
 
42656
42666
  /**
42657
- * A cloud of dots on a sphere, drawn to a canvas, with one shape per state:
42658
- * `thinking` is a narrow sash with a wave running through it, `fetching` spreads
42659
- * into a globe with a highlight sweeping pole to pole, `working` flattens and
42660
- * tilts with its bands shearing past each other, and `done` settles into an even
42661
- * ball.
42662
- *
42663
- * **None of them is a separate drawing.** Each is the same banded sphere given
42667
+ * A cloud of dots, drawn to a canvas, with one shape per state. `connecting`
42668
+ * and `thinking` are the same leaning sash and differ in how agitated it is;
42669
+ * `waiting` and `done` are the same settled ball at different tempos, which is
42670
+ * the difference between not having started and having finished. `fetching` is
42671
+ * a globe with a light going round it, `listening` a closed sphere with a
42672
+ * ripple crossing it, and `working` a ring.
42673
+ *
42674
+ * **None of them is a separate drawing.** Each is the same banded grid given
42664
42675
  * different numbers, so changing state interpolates the numbers and every frame
42665
42676
  * in between is a whole shape. A loader that cuts between pictures says the
42666
42677
  * work restarted; one that carries a shape into the next says it moved on.
42667
42678
  *
42668
- * **Nothing spins.** All the movement is inside the shape, which is what makes
42669
- * it read as thought rather than as a thing being rotated.
42679
+ * **A change is measured before it is played.** Two states differing in one
42680
+ * small number are given less time than a sash becoming a ring, and the
42681
+ * silhouette leads what the shape is doing, so a light is never seen orbiting
42682
+ * something that has not finished becoming round.
42683
+ *
42684
+ * **Nothing spins.** The whole cloud holds still and everything that moves is
42685
+ * moving inside it, which is what makes it read as thought rather than as a
42686
+ * thing being rotated. The one exception is the light, which travels because a
42687
+ * ring of evenly spaced dots is unchanged by rotation: without something fixed
42688
+ * in space passing over them, a state that turns has nothing a reader can see
42689
+ * turning.
42670
42690
  *
42671
42691
  * **The palette is read from the tokens**, not given as colors: a dot's ink is
42672
42692
  * mixed toward the ground by how far away it is, and both ends come from
package/types/vue.d.ts CHANGED
@@ -10542,7 +10542,7 @@ export interface WcToolkitVueGlobalComponents {
10542
10542
  HelixOptionVueProps,
10543
10543
  HelixOptionVueEvents
10544
10544
  >;
10545
- /** A cloud of dots on a sphere, drawn to a canvas, with one shape per state: 'thinking' is a narrow sash with a wave running through it, 'fetching' spreads into a globe with a highlight sweeping pole to pole, 'working' flattens and tilts with its bands shearing past each other, and 'done' settles into an even ball. **None of them is a separate drawing.** Each is the same banded sphere given different numbers, so changing state interpolates the numbers and every frame in between is a whole shape. A loader that cuts between pictures says the work restarted; one that carries a shape into the next says it moved on. **Nothing spins.** All the movement is inside the shape, which is what makes it read as thought rather than as a thing being rotated. **The palette is read from the tokens**, not given as colors: a dot's ink is mixed toward the ground by how far away it is, and both ends come from '--orb-color' and '--orb-ground'. That is what makes depth survive a theme change without the consumer restating anything. **It stops when nobody is looking**, off screen and in a background tab, and 'prefers-reduced-motion' gets one still frame rather than an empty box. */
10545
+ /** A cloud of dots, drawn to a canvas, with one shape per state. 'connecting' and 'thinking' are the same leaning sash and differ in how agitated it is; 'waiting' and 'done' are the same settled ball at different tempos, which is the difference between not having started and having finished. 'fetching' is a globe with a light going round it, 'listening' a closed sphere with a ripple crossing it, and 'working' a ring. **None of them is a separate drawing.** Each is the same banded grid given different numbers, so changing state interpolates the numbers and every frame in between is a whole shape. A loader that cuts between pictures says the work restarted; one that carries a shape into the next says it moved on. **A change is measured before it is played.** Two states differing in one small number are given less time than a sash becoming a ring, and the silhouette leads what the shape is doing, so a light is never seen orbiting something that has not finished becoming round. **Nothing spins.** The whole cloud holds still and everything that moves is moving inside it, which is what makes it read as thought rather than as a thing being rotated. The one exception is the light, which travels because a ring of evenly spaced dots is unchanged by rotation: without something fixed in space passing over them, a state that turns has nothing a reader can see turning. **The palette is read from the tokens**, not given as colors: a dot's ink is mixed toward the ground by how far away it is, and both ends come from '--orb-color' and '--orb-ground'. That is what makes depth survive a theme change without the consumer restating anything. **It stops when nobody is looking**, off screen and in a background tab, and 'prefers-reduced-motion' gets one still frame rather than an empty box. */
10546
10546
  "helix-orb": DefineCustomElement<
10547
10547
  HelixOrb,
10548
10548
  HelixOrbVueProps,