@fusion.dev/helix 0.161.0 → 0.162.1
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 +3791 -1141
- package/dist/aura/root/aura-root.d.ts +0 -49
- package/dist/aura/root/aura-root.js +2 -16
- package/dist/element-events.generated.js +1 -0
- package/dist/filter-input/root/filter-input-root.css.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/strands/index.d.ts +2 -0
- package/dist/strands/index.js +2 -0
- package/dist/strands/palette.d.ts +42 -0
- package/dist/strands/palette.js +24 -0
- package/dist/strands/path.d.ts +89 -0
- package/dist/strands/path.js +153 -0
- package/dist/strands/renderer.d.ts +84 -0
- package/dist/strands/renderer.js +255 -0
- package/dist/strands/root/strands-root.css.d.ts +1 -0
- package/dist/strands/root/strands-root.css.js +8 -0
- package/dist/strands/root/strands-root.d.ts +230 -0
- package/dist/strands/root/strands-root.js +466 -0
- package/dist/strands/shader.d.ts +22 -0
- package/dist/strands/shader.js +198 -0
- package/dist/strands/stop/strands-stop.css.d.ts +1 -0
- package/dist/strands/stop/strands-stop.css.js +2 -0
- package/dist/strands/stop/strands-stop.d.ts +44 -0
- package/dist/strands/stop/strands-stop.js +29 -0
- package/lint.json +1 -1
- package/package.json +6 -6
- package/types/jsx.d.ts +496 -0
- package/types/svelte.d.ts +2 -0
- package/types/vue.d.ts +81 -0
|
@@ -325,55 +325,6 @@ export declare class HelixAura extends HelixElement {
|
|
|
325
325
|
protected render(): TemplateResult;
|
|
326
326
|
protected firstUpdated(changed: PropertyValues): void;
|
|
327
327
|
private start;
|
|
328
|
-
/**
|
|
329
|
-
* Whether WebGL here is drawn on the CPU, asked before one is drawn into.
|
|
330
|
-
*
|
|
331
|
-
* **The shader costs what a GPU makes it cheap to ask for.** The note at the
|
|
332
|
-
* top of this file measures it: on Metal the browser shares the compiled
|
|
333
|
-
* program and each aura after the first draws its opening frame in about 2ms,
|
|
334
|
-
* while under SwiftShader nothing is shared and each one compiles the pipeline
|
|
335
|
-
* again for about 450ms. Measured again 15 Sep 2026 on a page of 720 pixel
|
|
336
|
-
* auras, SwiftShader against the same page with this check in place: the worst
|
|
337
|
-
* frame gap went 568ms to 50ms with one aura, 1626ms to 34ms with three and
|
|
338
|
-
* 3200ms to 42ms with six, and a page of three or six ran at 13 frames a
|
|
339
|
-
* second before and at the display's own rate after. What that buys is a field
|
|
340
|
-
* a reader barely sees behind content, against a gradient ramped from the same
|
|
341
|
-
* tokens. So the software path takes the fallback.
|
|
342
|
-
*
|
|
343
|
-
* **The unmasked name is the only one that answers.** `gl.RENDERER` is masked
|
|
344
|
-
* to `WebKit WebGL` on Chromium, the same string on SwiftShader and on Metal,
|
|
345
|
-
* and `WEBGL_debug_renderer_info` is what lifts the mask.
|
|
346
|
-
*
|
|
347
|
-
* **An unreadable renderer draws**, which is the safe direction and not the
|
|
348
|
-
* tidy one. The extension is optional and is withheld deliberately: Firefox's
|
|
349
|
-
* `privacy.resistFingerprinting` and Safari's equivalent both drop it, and a
|
|
350
|
-
* browser may drop it at any time, since a GPU name is a fingerprinting
|
|
351
|
-
* surface. Treating "we cannot tell" as software would take the shader away
|
|
352
|
-
* from every reader on a machine that hides its hardware, which is a real GPU
|
|
353
|
-
* far more often than not. Treating it as hardware costs a machine that hides
|
|
354
|
-
* a software renderer the 450ms, which is the cost the component has today.
|
|
355
|
-
*
|
|
356
|
-
* **A probe context of its own, and given straight back.** Asking the
|
|
357
|
-
* element's own canvas would spend it: `build` below is written around a
|
|
358
|
-
* canvas whose context was force-lost never yielding a working one again, and
|
|
359
|
-
* the probe has to release what it took or a page of auras runs the browser
|
|
360
|
-
* into its cap of about sixteen. One pixel by one, and released through
|
|
361
|
-
* `WEBGL_lose_context` the same way `drop` does.
|
|
362
|
-
*
|
|
363
|
-
* **The same attributes the real context asks for**, because they are part of
|
|
364
|
-
* the question: `powerPreference` can select a different adapter, so a probe
|
|
365
|
-
* asking for a high-performance context would name a GPU the drawing one is
|
|
366
|
-
* never given.
|
|
367
|
-
*
|
|
368
|
-
* **Asked per context rather than once for the page.** The answer is not
|
|
369
|
-
* fixed: a GPU process that crashes takes the page down to SwiftShader for
|
|
370
|
-
* the rest of its life, and an aura rebuilding after that should get the
|
|
371
|
-
* current answer rather than the one from before. It costs a context creation
|
|
372
|
-
* and two calls, measured 15 Sep 2026 at about 2ms under SwiftShader and 2ms
|
|
373
|
-
* on Metal, against the 450ms and 135ms first draws it stands in front of, and
|
|
374
|
-
* it happens once per context rather than once per frame.
|
|
375
|
-
*/
|
|
376
|
-
private static drawsInSoftware;
|
|
377
328
|
/**
|
|
378
329
|
* The context and the program, from whatever canvas can still give one.
|
|
379
330
|
*
|
|
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
7
|
var HelixAura_1;
|
|
8
|
-
import { customElement, HelixAuraFallbackEvent, HelixElement, MAX_FRAME_STEP, PaintGateController, readColor, } from '@fusion.dev/helix-core';
|
|
8
|
+
import { customElement, drawsInSoftware, HelixAuraFallbackEvent, HelixElement, MAX_FRAME_STEP, PaintGateController, readColor, } from '@fusion.dev/helix-core';
|
|
9
9
|
import { html } from 'lit';
|
|
10
10
|
import { property } from 'lit/decorators.js';
|
|
11
11
|
import { tiltFor } from '../axis.js';
|
|
@@ -15,7 +15,6 @@ import { styles } from './aura-root.css.js';
|
|
|
15
15
|
const MAX_CANVAS_EDGE = 4096;
|
|
16
16
|
const MAX_CANVAS_PIXELS = 4_194_304;
|
|
17
17
|
const OPENING_PATIENCE = 500;
|
|
18
|
-
const SOFTWARE_RENDERER = /swiftshader|llvmpipe|softpipe|software rasterizer|basic render/i;
|
|
19
18
|
let HelixAura = class HelixAura extends HelixElement {
|
|
20
19
|
constructor() {
|
|
21
20
|
super(...arguments);
|
|
@@ -163,25 +162,12 @@ let HelixAura = class HelixAura extends HelixElement {
|
|
|
163
162
|
this.open();
|
|
164
163
|
}
|
|
165
164
|
}
|
|
166
|
-
static drawsInSoftware() {
|
|
167
|
-
const probe = document.createElement('canvas');
|
|
168
|
-
probe.width = 1;
|
|
169
|
-
probe.height = 1;
|
|
170
|
-
const gl = probe.getContext('webgl2', HelixAura_1.CONTEXT);
|
|
171
|
-
if (!gl) {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
const debug = gl.getExtension('WEBGL_debug_renderer_info');
|
|
175
|
-
const name = debug ? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL) : null;
|
|
176
|
-
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
|
177
|
-
return typeof name === 'string' && SOFTWARE_RENDERER.test(name);
|
|
178
|
-
}
|
|
179
165
|
build() {
|
|
180
166
|
let canvas = this.renderRoot.querySelector('canvas');
|
|
181
167
|
if (!canvas) {
|
|
182
168
|
return false;
|
|
183
169
|
}
|
|
184
|
-
if (HelixAura_1.
|
|
170
|
+
if (drawsInSoftware(HelixAura_1.CONTEXT)) {
|
|
185
171
|
this.refused = 'software';
|
|
186
172
|
return false;
|
|
187
173
|
}
|
|
@@ -248,6 +248,7 @@ registerElementEvents({
|
|
|
248
248
|
],
|
|
249
249
|
'helix-step': ['helix-select'],
|
|
250
250
|
'helix-steps': ['helix-activate', 'helix-before-activate', 'helix-select'],
|
|
251
|
+
'helix-strands': ['helix-strands-fallback'],
|
|
251
252
|
'helix-switch': ['change', 'helix-before-toggle', 'input'],
|
|
252
253
|
'helix-tab': ['helix-dismiss', 'helix-select'],
|
|
253
254
|
'helix-tab-panel': ['helix-load'],
|
|
@@ -3,6 +3,6 @@ export const styles = createSheet(`:host {--_filter-input-size: var(--filter-inp
|
|
|
3
3
|
var(--filter-input-field-border-color, var(--helix-color-border));border-radius: var(--filter-input-radius, var(--helix-radii-sm));color: var(--helix-color-foreground-subtle);display: flex;flex-wrap: wrap;gap: var(--helix-space-2xs);min-block-size: var(--_filter-input-size);padding-block: var(--helix-space-2xs);padding-inline: var(--filter-input-padding-inline, var(--helix-space-xs));transition:
|
|
4
4
|
border-color var(--helix-duration-fast) var(--helix-easing-standard),
|
|
5
5
|
background-color var(--helix-duration-fast) var(--helix-easing-standard);}::slotted([slot="tags"]) {flex: 0 0 auto;max-inline-size: 100%;}[part="control"] {flex: 1 1 var(--filter-input-control-size, 6ch);}[part="lead"] {align-items: center;align-self: stretch;border-inline-end: var(--helix-border-thin) solid
|
|
6
|
-
var(--filter-input-lead-border-color, var(--helix-color-border));display: flex;margin-block: calc(var(--helix-space-2xs) * -1);margin-inline-start: calc(var(--helix-space-xs) * -1);padding-inline: var(--helix-space-2xs)
|
|
6
|
+
var(--filter-input-lead-border-color, var(--helix-color-border));display: flex;margin-block: calc(var(--helix-space-2xs) * -1);margin-inline-start: calc(var(--helix-space-xs) * -1);padding-inline: var(--helix-space-2xs);}[part="lead"][hidden] {display: none;}[part="lead"],
|
|
7
7
|
slot[name="end"] {--button-size: var(--filter-input-action-size, var(--helix-size-tag));--icon-button-size: var(--filter-input-action-size, var(--helix-size-tag));--button-padding-inline: var(--filter-input-action-padding-inline, var(--helix-space-xs));}slot[name="start"]::slotted(*),
|
|
8
8
|
slot[name="end"]::slotted(*) {color: var(--helix-color-foreground-subtle);flex: 0 0 auto;}[part="clear"] {--icon-size: 0.85em;all: unset;align-items: center;align-self: center;border-radius: var(--helix-radii-sm);box-sizing: border-box;color: var(--filter-input-clear-color, var(--helix-color-foreground-subtle));cursor: pointer;display: flex;flex: 0 0 auto;justify-content: center;min-block-size: var(--helix-size-icon-loose);min-inline-size: var(--helix-size-icon-loose);transition: color var(--helix-duration-fast) var(--helix-easing-standard);}[part="clear"]:hover {background-color: color-mix(in oklab, currentcolor 12%, transparent);color: var(--filter-input-clear-color-hover, var(--helix-color-foreground));}[part="clear"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: var(--helix-size-focus-offset);}:host(:disabled) [part="field"] {cursor: not-allowed;opacity: 0.5;}@media print {:host {print-color-adjust: exact;}}[part="surface"] {--_floating-border-color: var(--filter-input-border-color, var(--helix-color-border));--_floating-border-width: var(--filter-input-border-width, var(--helix-border-floating));--_floating-shadow: var(--filter-input-shadow, var(--helix-shadow-floating));box-sizing: border-box;inline-size: var(--filter-input-panel-size, var(--floating-anchor-inline-size, 100%));max-block-size: var(--floating-available-block-size, 20rem);min-inline-size: max-content;overflow-y: auto;padding-block: var(--helix-space-2xs);padding-inline: var(--helix-space-none);position: absolute;}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -90,6 +90,7 @@ export * from './speed-dial/index.js';
|
|
|
90
90
|
export * from './splitter/index.js';
|
|
91
91
|
export * from './stat/index.js';
|
|
92
92
|
export * from './steps/index.js';
|
|
93
|
+
export * from './strands/index.js';
|
|
93
94
|
export * from './switch/index.js';
|
|
94
95
|
export * from './table/index.js';
|
|
95
96
|
export * from './tabs/index.js';
|
package/dist/index.js
CHANGED
|
@@ -85,6 +85,7 @@ export * from './speed-dial/index.js';
|
|
|
85
85
|
export * from './splitter/index.js';
|
|
86
86
|
export * from './stat/index.js';
|
|
87
87
|
export * from './steps/index.js';
|
|
88
|
+
export * from './strands/index.js';
|
|
88
89
|
export * from './switch/index.js';
|
|
89
90
|
export * from './table/index.js';
|
|
90
91
|
export * from './tabs/index.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ColorBytes, TokenColor } from '@fusion.dev/helix-core';
|
|
2
|
+
/** The colors the ribbon is drawn and composited in, as channels in `0..1`. */
|
|
3
|
+
export interface StrandsPalette {
|
|
4
|
+
/** Most strands. */
|
|
5
|
+
glow: TokenColor;
|
|
6
|
+
/** The page the light is added to. */
|
|
7
|
+
ground: TokenColor;
|
|
8
|
+
/** Whether the ground is light, which turns added light into ink. */
|
|
9
|
+
light: boolean;
|
|
10
|
+
/** The hottest strands, and on a light page the ink everything darkens toward. */
|
|
11
|
+
peak: TokenColor;
|
|
12
|
+
/** A minority of strands, for depth. */
|
|
13
|
+
tint: TokenColor;
|
|
14
|
+
}
|
|
15
|
+
/** The computed colors the canvas carries, one per stop and two for the peak. */
|
|
16
|
+
export type StrandsPaletteStyle = Pick<CSSStyleDeclaration, 'caretColor' | 'color' | 'columnRuleColor' | 'outlineColor' | 'textDecorationColor'>;
|
|
17
|
+
/** The default theme's dark page, ink on black, which is drawn until a read. */
|
|
18
|
+
export declare const RESTING_PALETTE: StrandsPalette;
|
|
19
|
+
/**
|
|
20
|
+
* The palette, from the colors the canvas's stylesheet resolved for it.
|
|
21
|
+
*
|
|
22
|
+
* **Carried on color properties of the canvas, which a canvas never paints**,
|
|
23
|
+
* the way `Aura` carries its ramp: a custom property is handed back as the
|
|
24
|
+
* text that was written, and a color property is resolved by the engine first.
|
|
25
|
+
* Read there, each color takes the `--strands-*` hook a page wrote anywhere
|
|
26
|
+
* above the element, and the token otherwise:
|
|
27
|
+
*
|
|
28
|
+
* - `color`: `--strands-glow`, or `primary`.
|
|
29
|
+
* - `text-decoration-color`: `--strands-tint`, or `info`.
|
|
30
|
+
* - `outline-color`: `--strands-ground`, or the background.
|
|
31
|
+
* - `caret-color` and `column-rule-color`: `--strands-peak`, or the dark and
|
|
32
|
+
* the light page's own peak.
|
|
33
|
+
*
|
|
34
|
+
* **The ground says which way the page runs**, read from its luminance rather
|
|
35
|
+
* than asked of the color mode, which is a private token this tier may not
|
|
36
|
+
* reach for. A dark page adds light toward a bright peak; a light page darkens
|
|
37
|
+
* toward a deep one, because light added to near-white is invisible.
|
|
38
|
+
*
|
|
39
|
+
* @param style The canvas's computed style.
|
|
40
|
+
* @param read Turns a resolved color into bytes, `undefined` where it cannot.
|
|
41
|
+
*/
|
|
42
|
+
export declare const paletteOf: (style: StrandsPaletteStyle, read: (value: string) => ColorBytes | undefined) => StrandsPalette;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const RESTING_PALETTE = {
|
|
2
|
+
glow: [0.88, 0.88, 0.88],
|
|
3
|
+
ground: [0, 0, 0],
|
|
4
|
+
light: false,
|
|
5
|
+
peak: [1, 1, 1],
|
|
6
|
+
tint: [0.6, 0.6, 0.6],
|
|
7
|
+
};
|
|
8
|
+
export const paletteOf = (style, read) => {
|
|
9
|
+
const channels = (value, fallback) => {
|
|
10
|
+
const bytes = read(value);
|
|
11
|
+
return bytes ? [bytes[0] / 255, bytes[1] / 255, bytes[2] / 255] : [...fallback];
|
|
12
|
+
};
|
|
13
|
+
const ground = channels(style.outlineColor, RESTING_PALETTE.ground);
|
|
14
|
+
const light = ground[0] * 0.2126 + ground[1] * 0.7152 + ground[2] * 0.0722 > 0.5;
|
|
15
|
+
return {
|
|
16
|
+
glow: channels(style.color, RESTING_PALETTE.glow),
|
|
17
|
+
ground,
|
|
18
|
+
light,
|
|
19
|
+
peak: light
|
|
20
|
+
? channels(style.columnRuleColor, [0.02, 0.02, 0.02])
|
|
21
|
+
: channels(style.caretColor, RESTING_PALETTE.peak),
|
|
22
|
+
tint: channels(style.textDecorationColor, RESTING_PALETTE.tint),
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of the ribbon, as points the shader joins with a Catmull-Rom
|
|
3
|
+
* spline.
|
|
4
|
+
*
|
|
5
|
+
* Pure, so `path.pure.spec.ts` can hold the geometry without a GPU: what a
|
|
6
|
+
* shape is, where a stop puts it, and which stretch of a page-long path is on
|
|
7
|
+
* screen are all arithmetic.
|
|
8
|
+
*/
|
|
9
|
+
/** The three forms the ribbon takes. */
|
|
10
|
+
export type StrandsVariant = 'fan' | 'ribbon' | 'waves';
|
|
11
|
+
/**
|
|
12
|
+
* One point on the centerline: `x` as a share of the width, `y` in CSS pixels,
|
|
13
|
+
* the ribbon's width as a share of the width, and its brightness from `0` to
|
|
14
|
+
* `1`.
|
|
15
|
+
*
|
|
16
|
+
* **`x` as a share and `y` in pixels**, because the two ends of a page answer
|
|
17
|
+
* differently to a resize: a reader widening the window wants the ribbon to
|
|
18
|
+
* keep its place across the page, and a section's position down the page is
|
|
19
|
+
* pixels the layout already decided.
|
|
20
|
+
*/
|
|
21
|
+
export type PathPoint = readonly [x: number, y: number, width: number, brightness: number];
|
|
22
|
+
/** A stop, resolved: the form it asks for, and the element it is pinned to. */
|
|
23
|
+
export interface PlacedStop {
|
|
24
|
+
/** Where the shape sits across the page, as a share of the width. */
|
|
25
|
+
at: number | undefined;
|
|
26
|
+
/** The target's height, in CSS pixels. */
|
|
27
|
+
height: number;
|
|
28
|
+
/** The target's top, in page pixels. */
|
|
29
|
+
top: number;
|
|
30
|
+
variant: StrandsVariant;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The most points a path may carry, which is the length of the shader's
|
|
34
|
+
* uniform array.
|
|
35
|
+
*
|
|
36
|
+
* WebGL2 guarantees 256 vertex uniform vectors, and this takes 96 of them. A
|
|
37
|
+
* page-long ribbon spends one point per half screen, so 96 covers a page more
|
|
38
|
+
* than forty screens long; a longer one keeps its first 96 points and fades.
|
|
39
|
+
*/
|
|
40
|
+
export declare const MAX_POINTS = 96;
|
|
41
|
+
/**
|
|
42
|
+
* The ribbon drawn inside its own box, which is what an element without
|
|
43
|
+
* `travel` shows.
|
|
44
|
+
*
|
|
45
|
+
* @param variant The form to draw.
|
|
46
|
+
* @param at Where it sits across the box, as a share of the width. A wave
|
|
47
|
+
* crosses the whole width and does not read it.
|
|
48
|
+
* @param height The box's height, in CSS pixels. Its width needs no
|
|
49
|
+
* argument: `x` is a share of it.
|
|
50
|
+
*/
|
|
51
|
+
export declare const boxPath: (variant: StrandsVariant, at: number | undefined, height: number) => PathPoint[];
|
|
52
|
+
/**
|
|
53
|
+
* The ribbon laid down a whole page, which is what an element with `travel`
|
|
54
|
+
* shows.
|
|
55
|
+
*
|
|
56
|
+
* Each stop places its form at its target, in page order. Between two stops
|
|
57
|
+
* the ribbon fills the gap with soft S-curves in `fill`'s place, except
|
|
58
|
+
* between two waves, where it turns off screen so each wave reads alone.
|
|
59
|
+
* Without stops, `fill` runs the length of the page: a fan opens the page and
|
|
60
|
+
* a ribbon follows it, waves cross every screen and a third, and a ribbon
|
|
61
|
+
* meanders the whole way. After a last wave the ribbon does not come back, so
|
|
62
|
+
* the page ends on the wave.
|
|
63
|
+
*
|
|
64
|
+
* @param stops The resolved stops, in any order.
|
|
65
|
+
* @param fill The root's own form, for the stretches no stop claims.
|
|
66
|
+
* @param width The viewport's width, in CSS pixels.
|
|
67
|
+
* @param height The viewport's height, in CSS pixels.
|
|
68
|
+
* @param page The page's height, in CSS pixels.
|
|
69
|
+
*/
|
|
70
|
+
export declare const pagePath: (stops: readonly PlacedStop[], fill: StrandsVariant, width: number, height: number, page: number) => PathPoint[];
|
|
71
|
+
/** One coordinate of a uniform Catmull-Rom segment, the same one the shader runs. */
|
|
72
|
+
export declare const catmull: (p0: number, p1: number, p2: number, p3: number, f: number) => number;
|
|
73
|
+
/**
|
|
74
|
+
* The centerline's `y` at even steps of its parameter, for finding which
|
|
75
|
+
* stretch is on screen.
|
|
76
|
+
*
|
|
77
|
+
* Twenty-four samples a segment, which is finer than any segment is tall on a
|
|
78
|
+
* screen, and computed once a layout rather than once a frame.
|
|
79
|
+
*/
|
|
80
|
+
export declare const lookup: (points: readonly PathPoint[]) => Float32Array;
|
|
81
|
+
/**
|
|
82
|
+
* The stretch of the path that lies between `top` and `bottom`, as a range of
|
|
83
|
+
* its parameter.
|
|
84
|
+
*
|
|
85
|
+
* Drawing only that stretch is what keeps a page-long ribbon as fine as a
|
|
86
|
+
* screen-sized one: the same number of segments, spent where a reader looks.
|
|
87
|
+
* A path that does not reach the window answers an empty range.
|
|
88
|
+
*/
|
|
89
|
+
export declare const visibleRange: (table: Float32Array, top: number, bottom: number) => readonly [number, number];
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
export const MAX_POINTS = 96;
|
|
2
|
+
const NARROW = 760;
|
|
3
|
+
const GLOW = { fan: 1, ribbon: 0.36, waves: 0.7 };
|
|
4
|
+
const WIDTH = { fan: 0.07, ribbon: 0.24, waves: 0.06 };
|
|
5
|
+
const FAN_AT = 0.4;
|
|
6
|
+
const fan = (top, height, at, narrow) => {
|
|
7
|
+
const x = narrow ? Math.max(at, 0.8) : at;
|
|
8
|
+
const y = top + height * 0.58;
|
|
9
|
+
const arm = narrow ? 0.12 : 0.15;
|
|
10
|
+
return [
|
|
11
|
+
[x + 0.38, y - height * 0.73, WIDTH.fan, 0.2],
|
|
12
|
+
[x + arm, y - height * 0.3, WIDTH.fan * 0.5, 0.9],
|
|
13
|
+
[x, y, 0.005, GLOW.fan],
|
|
14
|
+
[x + arm, y + height * 0.3, WIDTH.fan * 0.5, 0.9],
|
|
15
|
+
[x + 0.45, y + height * 0.54, WIDTH.fan * 1.15, 0.7],
|
|
16
|
+
];
|
|
17
|
+
};
|
|
18
|
+
const wave = (y, height, fromRight, narrow) => {
|
|
19
|
+
const w = narrow ? WIDTH.waves * 2 : WIDTH.waves;
|
|
20
|
+
const glow = narrow ? GLOW.waves * 0.65 : GLOW.waves;
|
|
21
|
+
const side = (share) => (fromRight ? 1 - share : share);
|
|
22
|
+
return [
|
|
23
|
+
[side(-0.15), y - height * 0.35, w, 0],
|
|
24
|
+
[side(-0.05), y, w, glow * 0.6],
|
|
25
|
+
[side(0.3), y + 18, w, glow],
|
|
26
|
+
[side(0.7), y - 18, w, glow],
|
|
27
|
+
[side(1.05), y, w, glow * 0.6],
|
|
28
|
+
[side(1.15), y + height * 0.35, w, 0],
|
|
29
|
+
];
|
|
30
|
+
};
|
|
31
|
+
const ribbon = (from, to, height, at, phase, narrow) => {
|
|
32
|
+
const points = [];
|
|
33
|
+
const step = height * 0.55;
|
|
34
|
+
const swing = narrow ? 0.26 : 0.3;
|
|
35
|
+
const wide = narrow ? 0.45 : WIDTH.ribbon;
|
|
36
|
+
const glow = narrow ? GLOW.ribbon * 0.65 : GLOW.ribbon;
|
|
37
|
+
let a = phase;
|
|
38
|
+
for (let y = from; y < to; y += step) {
|
|
39
|
+
a += (step / (1.7 * height)) * Math.PI;
|
|
40
|
+
points.push([at + swing * Math.sin(a), y, wide * (0.8 + 0.3 * Math.sin(a * 0.63 + 0.4)), glow]);
|
|
41
|
+
}
|
|
42
|
+
return { phase: a, points };
|
|
43
|
+
};
|
|
44
|
+
export const boxPath = (variant, at, height) => {
|
|
45
|
+
const narrow = false;
|
|
46
|
+
if (variant === 'fan') {
|
|
47
|
+
return fan(0, height, at ?? FAN_AT, narrow);
|
|
48
|
+
}
|
|
49
|
+
if (variant === 'waves') {
|
|
50
|
+
return wave(height * 0.5, height, false, narrow);
|
|
51
|
+
}
|
|
52
|
+
const start = -height * 0.4;
|
|
53
|
+
const lead = [at ?? 0.5, start - height * 0.4, WIDTH.ribbon, 0];
|
|
54
|
+
const body = ribbon(start, height * 1.4, height, at ?? 0.5, 0, narrow).points.map(([x, y, w]) => [x, y, w, narrow ? GLOW.ribbon * 0.65 * 1.4 : GLOW.ribbon * 1.4]);
|
|
55
|
+
return [lead, ...body];
|
|
56
|
+
};
|
|
57
|
+
export const pagePath = (stops, fill, width, height, page) => {
|
|
58
|
+
const narrow = width < NARROW;
|
|
59
|
+
const placed = [...stops].sort((one, two) => one.top - two.top);
|
|
60
|
+
if (placed.length === 0) {
|
|
61
|
+
if (fill === 'waves') {
|
|
62
|
+
for (let y = height * 0.6; y < page; y += height * 1.3) {
|
|
63
|
+
placed.push({ at: undefined, height, top: y, variant: 'waves' });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (fill === 'fan') {
|
|
67
|
+
placed.push({ at: undefined, height, top: 0, variant: 'fan' });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const points = [[0.6, -height * 0.9, WIDTH.ribbon, 0]];
|
|
71
|
+
let phase = 1.2;
|
|
72
|
+
let cursor = 0;
|
|
73
|
+
let fromRight = true;
|
|
74
|
+
let previous;
|
|
75
|
+
const gap = (to, next) => {
|
|
76
|
+
if (to - cursor < height * 0.7) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (previous === 'waves' && next === 'waves') {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const stretch = ribbon(cursor + height * 0.2, to - height * 0.45, height, 0.5, phase, narrow);
|
|
83
|
+
phase = stretch.phase;
|
|
84
|
+
points.push(...stretch.points);
|
|
85
|
+
const last = stretch.points.at(-1);
|
|
86
|
+
if (last) {
|
|
87
|
+
fromRight = last[0] > 0.5;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
for (const stop of placed) {
|
|
91
|
+
gap(stop.top, stop.variant);
|
|
92
|
+
if (stop.variant === 'fan') {
|
|
93
|
+
const span = Math.min(stop.height, height);
|
|
94
|
+
points.push(...fan(stop.top, span, stop.at ?? FAN_AT, narrow));
|
|
95
|
+
cursor = stop.top + span * 1.12;
|
|
96
|
+
fromRight = true;
|
|
97
|
+
}
|
|
98
|
+
else if (stop.variant === 'waves') {
|
|
99
|
+
points.push(...wave(stop.top, height, fromRight, narrow));
|
|
100
|
+
fromRight = !fromRight;
|
|
101
|
+
cursor = stop.top + height * 0.35;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const y = stop.top + Math.min(stop.height, height) / 2;
|
|
105
|
+
points.push([stop.at ?? 0.5, y, WIDTH.ribbon, GLOW.ribbon]);
|
|
106
|
+
cursor = y;
|
|
107
|
+
}
|
|
108
|
+
previous = stop.variant;
|
|
109
|
+
}
|
|
110
|
+
if (previous !== 'waves') {
|
|
111
|
+
gap(page + height * 0.4, undefined);
|
|
112
|
+
}
|
|
113
|
+
const last = points.at(-1) ?? [0.5, page, WIDTH.ribbon, 0];
|
|
114
|
+
points.push([last[0], Math.max(last[1], page) + height, last[2], 0]);
|
|
115
|
+
return points.slice(0, MAX_POINTS);
|
|
116
|
+
};
|
|
117
|
+
export const catmull = (p0, p1, p2, p3, f) => {
|
|
118
|
+
const f2 = f * f;
|
|
119
|
+
const f3 = f2 * f;
|
|
120
|
+
return (0.5 *
|
|
121
|
+
(2 * p1 +
|
|
122
|
+
(-p0 + p2) * f +
|
|
123
|
+
(2 * p0 - 5 * p1 + 4 * p2 - p3) * f2 +
|
|
124
|
+
(-p0 + 3 * p1 - 3 * p2 + p3) * f3));
|
|
125
|
+
};
|
|
126
|
+
export const lookup = (points) => {
|
|
127
|
+
const n = points.length;
|
|
128
|
+
const samples = Math.max(1, (n - 1) * 24);
|
|
129
|
+
const out = new Float32Array(samples + 1);
|
|
130
|
+
const y = (i) => points[Math.max(0, Math.min(n - 1, i))]?.[1] ?? 0;
|
|
131
|
+
for (let j = 0; j <= samples; j++) {
|
|
132
|
+
const s = (j / samples) * (n - 1);
|
|
133
|
+
const i = Math.min(Math.floor(s), n - 2);
|
|
134
|
+
out[j] = n < 2 ? y(0) : catmull(y(i - 1), y(i), y(i + 1), y(i + 2), s - i);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
};
|
|
138
|
+
export const visibleRange = (table, top, bottom) => {
|
|
139
|
+
const last = table.length - 1;
|
|
140
|
+
let low = Number.POSITIVE_INFINITY;
|
|
141
|
+
let high = Number.NEGATIVE_INFINITY;
|
|
142
|
+
for (let j = 0; j <= last; j++) {
|
|
143
|
+
const y = table[j] ?? 0;
|
|
144
|
+
if (y >= top && y <= bottom) {
|
|
145
|
+
low = Math.min(low, j);
|
|
146
|
+
high = Math.max(high, j);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (high < low || last < 1) {
|
|
150
|
+
return [0, 0];
|
|
151
|
+
}
|
|
152
|
+
return [Math.max(0, (low - 1) / last), Math.min(1, (high + 1) / last)];
|
|
153
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { StrandsPalette } from './palette.js';
|
|
2
|
+
/** The most strands a ribbon draws, at `density` 1. */
|
|
3
|
+
export declare const MAX_STRANDS = 340;
|
|
4
|
+
/** The fewest, at `density` 0. Fewer stops reading as a ribbon at all. */
|
|
5
|
+
export declare const MIN_STRANDS = 60;
|
|
6
|
+
/** What one frame needs, all of it already resolved by the element. */
|
|
7
|
+
export interface StrandsFrame {
|
|
8
|
+
/** How many of the path's points are in use. */
|
|
9
|
+
count: number;
|
|
10
|
+
/** The device pixels per CSS pixel the buffer was sized at. */
|
|
11
|
+
dpr: number;
|
|
12
|
+
/** Overall brightness. */
|
|
13
|
+
gain: number;
|
|
14
|
+
/** The path, four floats a point, padded to the uniform array's length. */
|
|
15
|
+
points: Float32Array;
|
|
16
|
+
/** The stretch of the path on screen, as a range of its parameter. */
|
|
17
|
+
range: readonly [number, number];
|
|
18
|
+
/** How far the page is scrolled, in CSS pixels, or `0` inside a box. */
|
|
19
|
+
scroll: number;
|
|
20
|
+
/** The share of strands drawn soft, and how soft. */
|
|
21
|
+
softness: number;
|
|
22
|
+
/** A multiplier on the ribbon's width. */
|
|
23
|
+
spread: number;
|
|
24
|
+
/** How many strands to draw. */
|
|
25
|
+
strands: number;
|
|
26
|
+
/** Seconds of motion so far. */
|
|
27
|
+
time: number;
|
|
28
|
+
/** The box drawn into, in CSS pixels. */
|
|
29
|
+
view: readonly [number, number];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The pipeline, on one context: strands and dust added into a float buffer,
|
|
33
|
+
* two levels of bloom, and the composite over the page color.
|
|
34
|
+
*
|
|
35
|
+
* **Created whole or not at all.** `create` answers `null` when any program
|
|
36
|
+
* fails to link, so the element has one question to ask and one fallback to
|
|
37
|
+
* take, and never draws half a pipeline.
|
|
38
|
+
*
|
|
39
|
+
* **Float buffers where the GPU offers them, and eight bits where it does not.**
|
|
40
|
+
* Light is added strand by strand, and a hot bend sums far past `1`; an eight
|
|
41
|
+
* bit buffer clips it, so there the energy is scaled down on the way in and
|
|
42
|
+
* back up in the composite, which keeps the bend hot at the cost of some
|
|
43
|
+
* gradation in the dimmest strands.
|
|
44
|
+
*/
|
|
45
|
+
export declare class StrandsRenderer {
|
|
46
|
+
private readonly gl;
|
|
47
|
+
private readonly line;
|
|
48
|
+
private readonly dust;
|
|
49
|
+
private readonly down;
|
|
50
|
+
private readonly blur;
|
|
51
|
+
private readonly composite;
|
|
52
|
+
private readonly ribbon;
|
|
53
|
+
private readonly empty;
|
|
54
|
+
private readonly buffers;
|
|
55
|
+
private readonly half;
|
|
56
|
+
private readonly scale;
|
|
57
|
+
private targets;
|
|
58
|
+
private constructor();
|
|
59
|
+
/**
|
|
60
|
+
* The pipeline on `gl`, or `null` when a program does not link, which the
|
|
61
|
+
* element answers with its gradient.
|
|
62
|
+
*/
|
|
63
|
+
static create(gl: WebGL2RenderingContext): StrandsRenderer | null;
|
|
64
|
+
/**
|
|
65
|
+
* Sizes the buffers to the drawing buffer. The bloom runs at a quarter and a
|
|
66
|
+
* twelfth of it, which is where a glow's width lives and a twentieth of the
|
|
67
|
+
* fill.
|
|
68
|
+
*/
|
|
69
|
+
resize(width: number, height: number): boolean;
|
|
70
|
+
render(frame: StrandsFrame, palette: StrandsPalette): void;
|
|
71
|
+
/** Deletes everything made on the context, ahead of losing it. */
|
|
72
|
+
dispose(): void;
|
|
73
|
+
private release;
|
|
74
|
+
private target;
|
|
75
|
+
/** The uniforms every strand and speck reads. */
|
|
76
|
+
private use;
|
|
77
|
+
private shrink;
|
|
78
|
+
/** A separable blur of `target`, through `swap` and back. */
|
|
79
|
+
private smear;
|
|
80
|
+
private pass;
|
|
81
|
+
private texture;
|
|
82
|
+
private vec3;
|
|
83
|
+
private float;
|
|
84
|
+
}
|