@fusion.dev/helix 0.173.0 → 0.174.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.
@@ -1,37 +1,91 @@
1
1
  export const MAX_POINTS = 96;
2
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) => {
3
+ const GLOW = { bend: 1, ribbon: 0.36, waves: 0.7 };
4
+ const WIDTH = { bend: 0.12, ribbon: 0.24, waves: 0.06 };
5
+ const BEND_AT = 0.5;
6
+ const BEND_EXIT = 0.3;
7
+ export const TURN = 0.5;
8
+ const bend = (top, height, at, narrow, width, reach) => {
7
9
  const x = narrow ? Math.max(at, 0.8) : at;
8
10
  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
- ];
11
+ const out = (down) => x + (down * down * height) / (2 * TURN) / width;
12
+ const points = [];
13
+ for (let step = -7; step * 0.15 <= reach + 1e-9; step++) {
14
+ const down = step * 0.15;
15
+ const upper = down < 0;
16
+ const wide = upper
17
+ ? 0.02 + (WIDTH.bend - 0.02) * Math.min(1, -down / 0.6)
18
+ : 0.02 + (WIDTH.bend * 1.4 - 0.02) * (down / reach);
19
+ const glow = upper ? Math.max(0, 1 + down * 1.07) : 1 - down * 0.73;
20
+ points.push([out(down), y + down * height, wide, GLOW.bend * glow]);
21
+ }
22
+ return points;
17
23
  };
18
- const wave = (y, height, fromRight, narrow) => {
24
+ const wave = (y, height, fromRight, narrow, width, from) => {
19
25
  const w = narrow ? WIDTH.waves * 2 : WIDTH.waves;
20
26
  const glow = narrow ? GLOW.waves * 0.65 : GLOW.waves;
21
27
  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
- ];
28
+ if (from !== undefined) {
29
+ const across = (share) => from + ((fromRight ? 0 : 1) - from) * share;
30
+ return [
31
+ [across(0.03), y - height * 0.62, WIDTH.ribbon * 0.7, GLOW.ribbon],
32
+ [across(0.12), y - height * 0.36, WIDTH.ribbon * 0.5, glow * 0.55],
33
+ [across(0.38), y - height * 0.1, w, glow],
34
+ [across(0.66), y + height * 0.04, w, glow],
35
+ [across(1), y + height * 0.14, w, glow * 0.6],
36
+ [across(1.5), y + height * 0.3, w, 0],
37
+ ];
38
+ }
39
+ const beyond = height / width;
40
+ const rise = Math.min(18, width / 80);
41
+ const span = 1 + 2 * beyond;
42
+ const steps = Math.ceil(span / ((height * 0.45) / width));
43
+ const points = [];
44
+ for (let k = 0; k <= steps; k++) {
45
+ const share = -beyond + (k / steps) * span;
46
+ const out = share < 0 ? -share / beyond : share > 1 ? (share - 1) / beyond : 0;
47
+ const lift = share < 0 ? -out * out : out * out;
48
+ const ripple = share >= 0 && share <= 1 ? rise * Math.sin(2 * Math.PI * share) : 0;
49
+ points.push([
50
+ side(share),
51
+ y + lift * height * 0.35 + ripple,
52
+ w,
53
+ glow * Math.max(0, 1 - out * 1.6),
54
+ ]);
55
+ }
56
+ return points;
30
57
  };
31
- const ribbon = (from, to, height, at, phase, narrow) => {
58
+ const swingOf = (height, width, narrow) => Math.min(narrow ? 0.26 : 0.3, (height * TURN) / width);
59
+ const RIBBON_STEP = 0.35;
60
+ const bridge = (from, to, first, last) => {
61
+ const length = to[1] - from[1];
62
+ const gaps = [];
63
+ let gap = first;
64
+ let sum = 0;
65
+ while (sum + Math.min(gap, last) * 1.5 < length) {
66
+ gaps.push(Math.min(gap, last));
67
+ sum += Math.min(gap, last);
68
+ gap *= 1.33;
69
+ }
70
+ const scale = length / (sum + Math.min(gap, last));
71
+ const out = [];
72
+ let at = 0;
73
+ for (const one of gaps) {
74
+ at += one * scale;
75
+ const f = at / length;
76
+ out.push([
77
+ from[0] + (to[0] - from[0]) * f,
78
+ from[1] + (to[1] - from[1]) * f,
79
+ from[2] + (to[2] - from[2]) * f,
80
+ from[3] + (to[3] - from[3]) * f,
81
+ ]);
82
+ }
83
+ return out;
84
+ };
85
+ const ribbon = (from, to, height, at, phase, narrow, width) => {
32
86
  const points = [];
33
- const step = height * 0.55;
34
- const swing = narrow ? 0.26 : 0.3;
87
+ const step = height * RIBBON_STEP;
88
+ const swing = swingOf(height, width, narrow);
35
89
  const wide = narrow ? 0.45 : WIDTH.ribbon;
36
90
  const glow = narrow ? GLOW.ribbon * 0.65 : GLOW.ribbon;
37
91
  let a = phase;
@@ -41,17 +95,17 @@ const ribbon = (from, to, height, at, phase, narrow) => {
41
95
  }
42
96
  return { phase: a, points };
43
97
  };
44
- export const boxPath = (variant, at, height) => {
98
+ export const boxPath = (variant, at, height, width) => {
45
99
  const narrow = false;
46
- if (variant === 'fan') {
47
- return fan(0, height, at ?? FAN_AT, narrow);
100
+ if (variant === 'bend') {
101
+ return bend(0, height, at ?? BEND_AT, narrow, width, 0.6);
48
102
  }
49
103
  if (variant === 'waves') {
50
- return wave(height * 0.5, height, false, narrow);
104
+ return wave(height * 0.5, height, false, narrow, width);
51
105
  }
52
106
  const start = -height * 0.4;
53
107
  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]);
108
+ const body = ribbon(start, height * 1.4, height, at ?? 0.5, 0, narrow, width).points.map(([x, y, w]) => [x, y, w, narrow ? GLOW.ribbon * 0.65 * 1.4 : GLOW.ribbon * 1.4]);
55
109
  return [lead, ...body];
56
110
  };
57
111
  export const pagePath = (stops, fill, width, height, page) => {
@@ -63,42 +117,92 @@ export const pagePath = (stops, fill, width, height, page) => {
63
117
  placed.push({ at: undefined, height, top: y, variant: 'waves' });
64
118
  }
65
119
  }
66
- else if (fill === 'fan') {
67
- placed.push({ at: undefined, height, top: 0, variant: 'fan' });
120
+ else if (fill === 'bend') {
121
+ placed.push({ at: undefined, height, top: 0, variant: 'bend' });
68
122
  }
69
123
  }
70
124
  const points = [[0.6, -height * 0.9, WIDTH.ribbon, 0]];
125
+ const turn = (RIBBON_STEP / 1.7) * Math.PI;
71
126
  let phase = 1.2;
72
127
  let cursor = 0;
73
128
  let fromRight = true;
74
129
  let previous;
130
+ let joined;
75
131
  const gap = (to, next) => {
76
132
  if (to - cursor < height * 0.7) {
77
133
  return;
78
134
  }
79
- if (previous === 'waves' && next === 'waves') {
135
+ if (previous === 'waves' && next === 'waves' && to - cursor < height * 1.5) {
80
136
  return;
81
137
  }
82
- const stretch = ribbon(cursor + height * 0.2, to - height * 0.45, height, 0.5, phase, narrow);
138
+ const end = next === 'waves' ? to - height * 0.95 : to - height * 0.45;
139
+ const stretch = ribbon(cursor + height * 0.2, end, height, 0.5, phase, narrow, width);
83
140
  phase = stretch.phase;
84
- points.push(...stretch.points);
85
- const last = stretch.points.at(-1);
141
+ let run = stretch.points;
142
+ let heading;
143
+ if (next === 'waves') {
144
+ for (let k = run.length - 2; k > 0; k--) {
145
+ const [one, two, three] = run.slice(k - 1, k + 2);
146
+ if ((two[0] - one[0]) * (three[0] - two[0]) <= 0) {
147
+ run = run.slice(0, k + 1);
148
+ break;
149
+ }
150
+ if (k === 1) {
151
+ heading = three[0] < two[0];
152
+ }
153
+ }
154
+ }
155
+ if (previous === 'waves' && run.length > 1) {
156
+ run = run.map((point, k) => k < 3 ? [point[0], point[1], point[2], point[3] * Math.max(0, k - 1) * 0.5] : point);
157
+ }
158
+ points.push(...run);
159
+ const last = run.at(-1);
86
160
  if (last) {
87
- fromRight = last[0] > 0.5;
161
+ fromRight = heading ?? last[0] > 0.5;
162
+ joined = last[0];
88
163
  }
89
164
  };
90
165
  for (const stop of placed) {
166
+ joined = undefined;
91
167
  gap(stop.top, stop.variant);
92
- if (stop.variant === 'fan') {
168
+ if (stop.variant === 'bend') {
93
169
  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;
170
+ const at = stop.at ?? BEND_AT;
171
+ const slope = BEND_EXIT / TURN;
172
+ const swing = swingOf(height, width, narrow);
173
+ const steepest = (swing * width * Math.PI) / (1.7 * height);
174
+ const a = Math.acos(Math.min(1, slope / steepest));
175
+ const x = 0.5 + swing * Math.sin(a);
176
+ const bent = bend(stop.top, span, at, narrow, width, narrow ? 0.6 : BEND_EXIT);
177
+ const exit = bent.at(-1);
178
+ points.push(...bent);
179
+ if (!narrow && x > exit[0]) {
180
+ const drop = Math.max(height * 0.3, ((x - exit[0]) * width) / slope);
181
+ const start = [x, exit[1] + drop, WIDTH.ribbon, GLOW.ribbon];
182
+ points.push(...bridge(exit, start, span * 0.15, height * RIBBON_STEP));
183
+ cursor = start[1] - height * 0.2;
184
+ phase = a - turn;
185
+ }
186
+ else {
187
+ cursor = exit[1] + height * 0.1;
188
+ phase = Math.PI / 2 - turn;
189
+ }
96
190
  fromRight = true;
97
191
  }
98
192
  else if (stop.variant === 'waves') {
99
- points.push(...wave(stop.top, height, fromRight, narrow));
100
- fromRight = !fromRight;
101
- cursor = stop.top + height * 0.35;
193
+ const crossing = wave(stop.top, height, fromRight, narrow, width, joined);
194
+ const end = points.at(-1);
195
+ const lead = crossing[0];
196
+ if (joined !== undefined && end && lead) {
197
+ points.push(...bridge(end, lead, height * RIBBON_STEP, height * 0.26));
198
+ }
199
+ points.push(...crossing);
200
+ phase =
201
+ joined === undefined
202
+ ? (fromRight ? Math.PI * 1.5 : Math.PI / 2) - turn
203
+ : (fromRight ? Math.PI / 2 : Math.PI * 1.5) - turn;
204
+ fromRight = joined === undefined ? !fromRight : fromRight;
205
+ cursor = stop.top + height * (joined === undefined ? 0.35 : 0.5);
102
206
  }
103
207
  else {
104
208
  const y = stop.top + Math.min(stop.height, height) / 2;
@@ -111,7 +215,9 @@ export const pagePath = (stops, fill, width, height, page) => {
111
215
  gap(page + height * 0.4, undefined);
112
216
  }
113
217
  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]);
218
+ const before = points.at(-2) ?? last;
219
+ const on = [2 * last[0] - before[0], 2 * last[1] - before[1], last[2], 0];
220
+ points.push(on, [on[0], Math.max(on[1], page) + height, last[2], 0]);
115
221
  return points.slice(0, MAX_POINTS);
116
222
  };
117
223
  export const catmull = (p0, p1, p2, p3, f) => {
@@ -3,10 +3,24 @@ import type { StrandsPalette } from './palette.js';
3
3
  export declare const MAX_STRANDS = 340;
4
4
  /** The fewest, at `density` 0. Fewer stops reading as a ribbon at all. */
5
5
  export declare const MIN_STRANDS = 60;
6
+ /** Specks of dust around the strands at a `dust` of `1`. */
7
+ export declare const DUST = 600;
8
+ /** The most specks a frame draws, at a `dust` of `4`. */
9
+ export declare const MAX_DUST: number;
6
10
  /** What one frame needs, all of it already resolved by the element. */
7
11
  export interface StrandsFrame {
12
+ /** How much the glow around the strands adds, as a multiplier. */
13
+ bloom: number;
14
+ /** How far apart the brightest and dimmest strands are, from `0` to `1`. */
15
+ contrast: number;
8
16
  /** How many of the path's points are in use. */
9
17
  count: number;
18
+ /** How much nearness shows, from `0`, flat, to `1`. */
19
+ depth: number;
20
+ /** How many specks of dust drift around the strands. */
21
+ dust: number;
22
+ /** How differently each strand moves, from `0`, as one, to `1`. */
23
+ drift: number;
10
24
  /** The device pixels per CSS pixel the buffer was sized at. */
11
25
  dpr: number;
12
26
  /** Overall brightness. */
@@ -2,7 +2,11 @@ import { BLUR_FRAGMENT, COMPOSITE_FRAGMENT, DOWN_FRAGMENT, DUST_FRAGMENT, DUST_V
2
2
  export const MAX_STRANDS = 340;
3
3
  export const MIN_STRANDS = 60;
4
4
  const SEGMENTS = 320;
5
- const DUST = 600;
5
+ export const DUST = 600;
6
+ export const MAX_DUST = DUST * 4;
7
+ const SPREAD = 1.35;
8
+ const BLOOM = 0.75;
9
+ const GAIN = 1.615;
6
10
  export class StrandsRenderer {
7
11
  constructor(gl, programs) {
8
12
  this.targets = null;
@@ -82,6 +86,7 @@ export class StrandsRenderer {
82
86
  this.vec3(this.line, 'uTint', palette.tint);
83
87
  this.vec3(this.line, 'uPeak', palette.peak);
84
88
  this.float(this.line, 'uSoftness', frame.softness);
89
+ this.float(this.line, 'uContrast', frame.contrast);
85
90
  gl.bindVertexArray(this.ribbon);
86
91
  gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, (SEGMENTS + 1) * 2, frame.strands);
87
92
  this.use(this.dust, frame);
@@ -89,7 +94,9 @@ export class StrandsRenderer {
89
94
  this.vec3(this.dust, 'uPeak', palette.peak);
90
95
  this.float(this.dust, 'uStrands', frame.strands);
91
96
  gl.bindVertexArray(this.empty);
92
- gl.drawArrays(gl.POINTS, 0, DUST);
97
+ if (frame.dust > 0) {
98
+ gl.drawArrays(gl.POINTS, 0, frame.dust);
99
+ }
93
100
  gl.disable(gl.BLEND);
94
101
  }
95
102
  const { far, farSwap, near, nearSwap } = targets;
@@ -108,7 +115,8 @@ export class StrandsRenderer {
108
115
  this.vec3(this.composite, 'uGround', palette.ground);
109
116
  this.vec3(this.composite, 'uInk', palette.peak);
110
117
  this.float(this.composite, 'uLight', palette.light ? 1 : 0);
111
- this.float(this.composite, 'uGain', (1.7 * frame.gain) / this.scale);
118
+ this.float(this.composite, 'uGain', (GAIN * frame.gain) / this.scale);
119
+ this.float(this.composite, 'uBloom', BLOOM * frame.bloom);
112
120
  gl.bindVertexArray(this.empty);
113
121
  gl.drawArrays(gl.TRIANGLES, 0, 3);
114
122
  }
@@ -169,7 +177,9 @@ export class StrandsRenderer {
169
177
  gl.uniform1f(program.uniforms.get('uScroll') ?? null, frame.scroll);
170
178
  gl.uniform2f(program.uniforms.get('uT') ?? null, frame.range[0], frame.range[1]);
171
179
  gl.uniform1f(program.uniforms.get('uTime') ?? null, frame.time);
172
- gl.uniform1f(program.uniforms.get('uSpread') ?? null, frame.spread);
180
+ gl.uniform1f(program.uniforms.get('uSpread') ?? null, SPREAD * frame.spread);
181
+ gl.uniform1f(program.uniforms.get('uDepth') ?? null, frame.depth);
182
+ gl.uniform1f(program.uniforms.get('uDrift') ?? null, frame.drift);
173
183
  gl.uniform1f(program.uniforms.get('uDpr') ?? null, frame.dpr);
174
184
  gl.uniform1f(program.uniforms.get('uScale') ?? null, this.scale);
175
185
  }
@@ -1,5 +1,5 @@
1
1
  import { createSheet } from '@fusion.dev/helix-core';
2
- export const styles = createSheet(`:host {--_strands-glow: var(--strands-glow, var(--helix-color-primary));--_strands-ground: var(--strands-ground, var(--helix-color-background));--_strands-tint: var(--strands-tint, var(--helix-color-info));position: relative;display: block;inline-size: 100%;block-size: 100%;background-image: linear-gradient(
2
+ export const styles = createSheet(`:host {--_strands-glow: var(--strands-glow, var(--helix-color-primary));--_strands-ground: var(--strands-ground, var(--helix-color-background));--_strands-tint: var(--strands-tint, var(--helix-color-accent));position: relative;display: block;inline-size: 100%;block-size: 100%;background-image: linear-gradient(
3
3
  115deg,
4
4
  var(--_strands-ground) 20%,
5
5
  color-mix(in oklab, var(--_strands-glow) 22%, var(--_strands-ground)) 48%,
@@ -11,16 +11,16 @@ import { type StrandsVariant } from '../path.js';
11
11
  * roll over one another, and light runs along them.
12
12
  *
13
13
  * Its palette comes from the tokens. The strands are `--helix-color-primary`,
14
- * a few are `--helix-color-info` for depth, and the page they light is
14
+ * a few are `--helix-color-accent` for depth, and the page they light is
15
15
  * `--helix-color-background`. On a light page the light becomes ink, because
16
16
  * light added to near-white cannot be seen.
17
17
  *
18
- * **Three forms, set with `variant`.** `fan` is a tight, bright bend with its
18
+ * **Three forms, set with `variant`.** `bend` turns bright strands through one tight point, with its
19
19
  * arms opening to the right. `ribbon`, the default, is a broad, soft ribbon in
20
20
  * long curves. `waves` is sharp strands crossing the full width.
21
21
  *
22
22
  * **In its own box, by default.** The element fills the box it is given, the
23
- * way an image does, and `at` places the fan's bend or the ribbon's axis across
23
+ * way an image does, and `at` places the bend's apex or the ribbon's axis across
24
24
  * it. Content placed inside sits over the ribbon, in the `content` part.
25
25
  *
26
26
  * **Down the whole page, with `travel`.** The element fixes itself behind the
@@ -28,8 +28,9 @@ import { type StrandsVariant } from '../path.js';
28
28
  * bottom, so scrolling travels along it. Put `helix-strands-stop` elements
29
29
  * inside to pin a form to a section: each names an element with `for` and says
30
30
  * which form the ribbon takes there. Between stops the ribbon fills the gap in
31
- * its own `variant`, and between two waves it turns off screen so each wave
32
- * reads alone. Set `z-index` on the element to move it.
31
+ * its own `variant`, and between two waves less than a screen and a half
32
+ * apart it turns off screen so each wave reads alone. Set `z-index` on the
33
+ * element to move it.
33
34
  *
34
35
  * The ribbon stops moving in these cases:
35
36
  * - `prefers-reduced-motion` is set. The element shows one still frame, and in
@@ -61,16 +62,16 @@ import { type StrandsVariant } from '../path.js';
61
62
  * @cssproperty --strands-glow - The color of most strands. Defaults to `--helix-color-primary`.
62
63
  * @cssproperty --strands-ground - The page the light is added to. Defaults to `--helix-color-background`.
63
64
  * @cssproperty --strands-peak - The hottest strands, and on a light page the ink the light darkens toward. Defaults to `--helix-color-primary-hover` on a dark page and `--helix-color-primary-active` on a light one.
64
- * @cssproperty --strands-tint - The color of a few strands, for depth. Defaults to `--helix-color-info`.
65
+ * @cssproperty --strands-tint - The color of a few strands, for depth. Defaults to `--helix-color-accent`, which is the primary itself unless the theme relates an accent to the brand, as `accent: { from: 'brand', rotate: 40 }` does.
65
66
  *
66
67
  * @cssstate fallback - The element shows its gradient: the renderer draws on the CPU, there is no WebGL, or the browser took the context back. Cleared once the ribbon draws again.
67
68
  */
68
69
  export declare class HelixStrands extends HelixElement {
69
70
  static styles: CSSResultGroup;
70
71
  /**
71
- * Where the form sits across the box, as a share of the width: the fan's
72
- * bend or the ribbon's axis. `0` is the left edge and `1` the right. A wave
73
- * crosses the whole width and does not read it. Unset, a fan bends at `0.4`
72
+ * Where the form sits across the box, as a share of the width: the bend's
73
+ * apex or the ribbon's axis. `0` is the left edge and `1` the right. A wave
74
+ * crosses the whole width and does not read it. Unset, a bend's apex sits at `0.5`
74
75
  * and a ribbon runs down the middle.
75
76
  *
76
77
  * **A share rather than a length**, so the ribbon keeps its place when the
@@ -78,17 +79,47 @@ export declare class HelixStrands extends HelixElement {
78
79
  * own.
79
80
  */
80
81
  at?: number;
82
+ /**
83
+ * How much the glow around the strands adds, as a multiplier. `0` draws the
84
+ * strands alone, crisp and dimmer, and `1` is the default glow.
85
+ */
86
+ bloom: number;
87
+ /**
88
+ * How far apart the brightest and dimmest strands are, from `0`, most of
89
+ * them alike, to `1`, a few burning bright over many faint ones. Clamped to
90
+ * that range. The ribbon as a whole keeps its strength.
91
+ */
92
+ contrast: number;
81
93
  /**
82
94
  * How many strands the ribbon has, from `0`, sixty, to `1`, three hundred and
83
95
  * forty. Clamped to that range.
84
96
  */
85
97
  density: number;
98
+ /**
99
+ * How much the ribbon's depth shows, from `0`, flat, to `1`. Clamped to that
100
+ * range. The strands at the front of the ribbon's roll are drawn thicker,
101
+ * brighter, and further from its axis, and those at the back thinner, dimmer,
102
+ * and out of focus.
103
+ */
104
+ depth: number;
105
+ /**
106
+ * How differently each strand moves, from `0`, the whole ribbon rolling and
107
+ * waving as one, to `1`, each strand at its own pace and angle, crossing
108
+ * over the others. Clamped to that range.
109
+ */
110
+ drift: number;
111
+ /**
112
+ * How many specks of dust drift around the strands, as a multiplier: `0`
113
+ * draws none, `1` six hundred, and `4`, the most, two thousand four
114
+ * hundred. They gather where the ribbon is brightest.
115
+ */
116
+ dust: number;
86
117
  /** Overall brightness, as a multiplier on the light. */
87
118
  gain: number;
88
119
  /**
89
120
  * The share of strands drawn out of focus where the ribbon is wide, from `0`,
90
121
  * every strand sharp, to `1`, every one soft. Clamped to that range. A tight
91
- * form, such as a fan's bend or a wave, stays sharp whatever this says.
122
+ * form, such as a bend's apex or a wave, stays sharp whatever this says.
92
123
  */
93
124
  softness: number;
94
125
  /** How fast the ribbon moves, as a multiplier on time. `0` holds it still. */
@@ -10,18 +10,24 @@ import { html } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { paletteOf, RESTING_PALETTE } from '../palette.js';
12
12
  import { boxPath, lookup, MAX_POINTS, pagePath, visibleRange, } from '../path.js';
13
- import { MAX_STRANDS, MIN_STRANDS, StrandsRenderer } from '../renderer.js';
13
+ import { DUST, MAX_DUST, MAX_STRANDS, MIN_STRANDS, StrandsRenderer } from '../renderer.js';
14
14
  import { styles } from './strands-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
18
  const TRAVEL_EASE = 0.09;
19
+ const PACE = 0.25;
19
20
  let HelixStrands = class HelixStrands extends HelixElement {
20
21
  constructor() {
21
22
  super(...arguments);
23
+ this.bloom = 1;
24
+ this.contrast = 1;
22
25
  this.density = 0.8;
26
+ this.depth = 1;
27
+ this.drift = 1;
28
+ this.dust = 1;
23
29
  this.gain = 1;
24
- this.softness = 0.55;
30
+ this.softness = 0;
25
31
  this.speed = 1;
26
32
  this.spread = 1;
27
33
  this.travel = false;
@@ -303,7 +309,7 @@ let HelixStrands = class HelixStrands extends HelixElement {
303
309
  this.table = lookup(path);
304
310
  }
305
311
  else {
306
- path = boxPath(variant, this.at, this.box.height);
312
+ path = boxPath(variant, this.at, this.box.height, this.box.width);
307
313
  }
308
314
  this.points.fill(0);
309
315
  path.forEach((point, i) => {
@@ -327,7 +333,7 @@ let HelixStrands = class HelixStrands extends HelixElement {
327
333
  at: Number.isFinite(at) ? at : undefined,
328
334
  height: rect.height,
329
335
  top: rect.top + scroll,
330
- variant: variant === 'fan' || variant === 'waves' ? variant : 'ribbon',
336
+ variant: variant === 'bend' || variant === 'waves' ? variant : 'ribbon',
331
337
  });
332
338
  }
333
339
  return placed;
@@ -364,9 +370,15 @@ let HelixStrands = class HelixStrands extends HelixElement {
364
370
  ? visibleRange(this.table, this.offset - view[1] * 0.4, this.offset + view[1] * 1.4)
365
371
  : [0, 1];
366
372
  const density = Math.min(1, Math.max(0, this.density));
373
+ const share = (value) => Math.min(1, Math.max(0, value));
367
374
  renderer.render({
375
+ bloom: Math.max(0, this.bloom),
376
+ contrast: share(this.contrast),
368
377
  count: this.count,
378
+ depth: share(this.depth),
369
379
  dpr: this.dpr,
380
+ drift: share(this.drift),
381
+ dust: Math.round(Math.min(MAX_DUST, Math.max(0, this.dust * DUST))),
370
382
  gain: Math.max(0, this.gain),
371
383
  points: this.points,
372
384
  range,
@@ -425,7 +437,7 @@ let HelixStrands = class HelixStrands extends HelixElement {
425
437
  }
426
438
  const step = Math.min(this.held, MAX_FRAME_STEP);
427
439
  this.held = 0;
428
- this.elapsed += step * Math.max(0, this.speed);
440
+ this.elapsed += step * PACE * Math.max(0, this.speed);
429
441
  if (this.travel) {
430
442
  const target = window.scrollY;
431
443
  this.offset += (target - this.offset) * (1 - Math.exp(-step / TRAVEL_EASE));
@@ -439,9 +451,24 @@ let HelixStrands = class HelixStrands extends HelixElement {
439
451
  __decorate([
440
452
  property({ type: Number })
441
453
  ], HelixStrands.prototype, "at", void 0);
454
+ __decorate([
455
+ property({ type: Number })
456
+ ], HelixStrands.prototype, "bloom", void 0);
457
+ __decorate([
458
+ property({ type: Number })
459
+ ], HelixStrands.prototype, "contrast", void 0);
442
460
  __decorate([
443
461
  property({ type: Number })
444
462
  ], HelixStrands.prototype, "density", void 0);
463
+ __decorate([
464
+ property({ type: Number })
465
+ ], HelixStrands.prototype, "depth", void 0);
466
+ __decorate([
467
+ property({ type: Number })
468
+ ], HelixStrands.prototype, "drift", void 0);
469
+ __decorate([
470
+ property({ type: Number })
471
+ ], HelixStrands.prototype, "dust", void 0);
445
472
  __decorate([
446
473
  property({ type: Number })
447
474
  ], HelixStrands.prototype, "gain", void 0);
@@ -1,9 +1,9 @@
1
1
  /** Each strand as a ribbon of quads, wide enough to anti-alias and to blur. */
2
- export declare const LINE_VERTEX = "#version 300 es\nlayout(location = 0) in vec2 aVert;\nlayout(location = 1) in float aId;\nuniform float uDpr;\nuniform float uSoftness;\nuniform vec3 uGlow;\nuniform vec3 uTint;\nuniform vec3 uPeak;\nout float vSide;\nout vec3 vColor;\n\nuniform vec4 uPts[96];\nuniform int uCount;\nuniform vec2 uView;\nuniform float uScroll;\nuniform vec2 uT;\nuniform float uTime;\nuniform float uSpread;\n\nfloat h1(float n) { return fract(sin(n * 127.1 + 311.7) * 43758.5453); }\n\nvec4 pt(int i) { return uPts[clamp(i, 0, uCount - 1)]; }\n\nvec4 spline(float t) {\n float s = clamp(t, 0.0, 1.0) * float(uCount - 1);\n int i = min(int(floor(s)), uCount - 2);\n float f = s - float(i);\n vec4 p0 = pt(i - 1);\n vec4 p1 = pt(i);\n vec4 p2 = pt(i + 1);\n vec4 p3 = pt(i + 2);\n float f2 = f * f;\n float f3 = f2 * f;\n return 0.5 * ((2.0 * p1) + (-p0 + p2) * f + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * f2\n + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * f3);\n}\n\nvec2 place(vec4 c) { return vec2(c.x * uView.x, c.y - uScroll); }\n\nvec3 strandAt(float id, float t, out vec3 info) {\n vec4 c = spline(t);\n vec2 p = place(c);\n vec2 q = place(spline(t + 0.0004));\n vec2 tg = q - p;\n tg = length(tg) > 1e-4 ? normalize(tg) : vec2(0.0, 1.0);\n vec2 nr = vec2(-tg.y, tg.x);\n float r1 = h1(id + 0.13);\n float r2 = h1(id + 1.71);\n float r3 = h1(id + 3.37);\n float r4 = h1(id + 5.93);\n float n = (r1 + r2 + r3) / 1.5 - 1.0;\n float z = (r4 - 0.5) * 2.0;\n float along = c.y + c.x * uView.x;\n float th = along * 0.0033 + uTime * 0.25 + r2 * 0.5;\n float width = c.z * uSpread;\n float fan = width * uView.x * (1.0 + 0.25 * sin(uTime * 0.4 + along * 0.0045));\n float sway = sin(along * 0.006 + uTime * 0.23) * mix(6.0, 30.0, smoothstep(0.02, 0.2, width));\n float lat = sway + fan * (n * cos(th) + z * 0.55 * sin(th));\n lat += sin(along * 0.01 + uTime * (0.6 + 0.5 * r1) + r1 * 6.283) * 12.0 * (0.3 + r3);\n info = vec3(max(c.w, 0.0), width, along);\n return vec3(p + nr * lat, z * sin(th));\n}\n\nvoid main() {\n float id = aId;\n float r5 = h1(id + 7.77);\n float r6 = h1(id + 9.19);\n float r7 = h1(id + 11.3);\n float t = mix(uT.x, uT.y, aVert.x);\n vec3 info;\n vec3 unused;\n vec3 a = strandAt(id, t, info);\n vec3 b = strandAt(id, t + (uT.y - uT.x) / 700.0, unused);\n vec2 tg = b.xy - a.xy;\n tg = length(tg) > 1e-4 ? normalize(tg) : vec2(0.0, 1.0);\n vec2 nrm = vec2(-tg.y, tg.x);\n float soft = smoothstep(0.06, 0.28, info.y) * step(1.0 - uSoftness, r6);\n float base = mix(0.35, 1.3, r7 * r7) * uDpr * (1.0 + 0.3 * a.z);\n float w = max(base + soft * mix(8.0, 36.0, r6) * uDpr, 1.0);\n float drawW = w + 2.5 * uDpr;\n vec2 res = uView * uDpr;\n vec2 px = a.xy * uDpr + nrm * aVert.y * drawW * 0.5;\n gl_Position = vec4(px.x / res.x * 2.0 - 1.0, 1.0 - px.y / res.y * 2.0, 0.0, 1.0);\n vSide = aVert.y * drawW / w;\n float shimmer = 0.5 + 0.5 * sin(info.z * 0.044 - uTime * (2.0 + 2.0 * r5) + r6 * 6.283);\n shimmer *= shimmer;\n float bright = mix(0.25, 1.0, pow(r5, 1.4));\n float energy = min(base, w) / w;\n float depth = 0.7 + 0.3 * a.z;\n vec3 col = mix(uGlow, uTint, step(0.83, r6) * 0.9);\n col = mix(col, uPeak, step(0.93, r7) * 0.6);\n vColor = col * energy * info.x * (0.35 + 0.65 * shimmer) * bright * depth;\n}\n";
2
+ export declare const LINE_VERTEX = "#version 300 es\nlayout(location = 0) in vec2 aVert;\nlayout(location = 1) in float aId;\nuniform float uDpr;\nuniform float uSoftness;\nuniform float uContrast;\nuniform vec3 uGlow;\nuniform vec3 uTint;\nuniform vec3 uPeak;\nout float vSide;\nout vec3 vColor;\n\nuniform vec4 uPts[96];\nuniform int uCount;\nuniform vec2 uView;\nuniform float uScroll;\nuniform vec2 uT;\nuniform float uTime;\nuniform float uSpread;\nuniform float uDepth;\nuniform float uDrift;\n\nfloat h1(float n) { return fract(sin(n * 127.1 + 311.7) * 43758.5453); }\n\nvec4 pt(int i) { return uPts[clamp(i, 0, uCount - 1)]; }\n\nvec4 spline(float t) {\n float s = clamp(t, 0.0, 1.0) * float(uCount - 1);\n int i = min(int(floor(s)), uCount - 2);\n float f = s - float(i);\n vec4 p0 = pt(i - 1);\n vec4 p1 = pt(i);\n vec4 p2 = pt(i + 1);\n vec4 p3 = pt(i + 2);\n float f2 = f * f;\n float f3 = f2 * f;\n return 0.5 * ((2.0 * p1) + (-p0 + p2) * f + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * f2\n + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * f3);\n}\n\nvec2 place(vec4 c) { return vec2(c.x * uView.x, c.y - uScroll); }\n\nvec3 strandAt(float id, float t, out vec3 info) {\n vec4 c = spline(t);\n vec2 p = place(c);\n vec2 q = place(spline(t + 0.0004));\n vec2 tg = q - p;\n tg = length(tg) > 1e-4 ? normalize(tg) : vec2(0.0, 1.0);\n vec2 nr = vec2(-tg.y, tg.x);\n float r1 = h1(id + 0.13);\n float r2 = h1(id + 1.71);\n float r3 = h1(id + 3.37);\n float r4 = h1(id + 5.93);\n float r8 = h1(id + 13.7);\n float r9 = h1(id + 17.3);\n float n = (r1 + r2 + r3) / 1.5 - 1.0;\n float z = (r4 - 0.5) * 2.0;\n float along = c.y + c.x * uView.x;\n float pace = 1.0 + uDrift * (r8 - 0.5) * 1.2;\n float th = along * 0.002 * (1.0 + uDrift * (r9 - 0.5) * 0.3) + uTime * 0.25 * pace\n + r2 * (0.5 + uDrift * 2.6);\n float width = c.z * uSpread;\n float breadth = width * uView.x * (1.0 + 0.25 * sin(uTime * 0.4 + along * 0.0045));\n float sway = sin(along * 0.006 + uTime * 0.23) * mix(6.0, 30.0, smoothstep(0.02, 0.2, width));\n float ct = cos(th);\n float st = sin(th);\n float depth = clamp((z * 0.55 * ct - n * st) / 1.15, -1.0, 1.0);\n float lat = sway + breadth * (n * ct + z * 0.55 * st) * (1.0 + uDepth * 0.35 * depth);\n lat += sin(along * 0.006 * (1.0 + uDrift * (r9 - 0.5) * 0.3) + uTime * (0.6 + 0.5 * r1) * pace\n + r1 * 6.283) * 12.0 * (0.3 + r3) * (1.0 + uDrift * 0.5);\n info = vec3(max(c.w, 0.0), width, along);\n return vec3(p + nr * lat, depth);\n}\n\nvoid main() {\n float id = aId;\n float r5 = h1(id + 7.77);\n float r6 = h1(id + 9.19);\n float r7 = h1(id + 11.3);\n float t = mix(uT.x, uT.y, aVert.x);\n vec3 info;\n vec3 unused;\n vec3 a = strandAt(id, t, info);\n vec3 b = strandAt(id, t + (uT.y - uT.x) / 700.0, unused);\n vec2 tg = b.xy - a.xy;\n tg = length(tg) > 1e-4 ? normalize(tg) : vec2(0.0, 1.0);\n vec2 nrm = vec2(-tg.y, tg.x);\n float soft = smoothstep(0.06, 0.28, info.y) * step(1.0 - uSoftness, r6);\n /* Nearer is thicker, and the back of the ribbon falls out of focus: wider\n at the same energy, so it reads as haze behind the sharp strands. */\n float base = mix(0.35, 1.3, r7 * r7) * uDpr * (1.0 + (0.3 + 0.5 * uDepth) * a.z);\n float focus = uDepth * smoothstep(-0.1, -0.9, a.z) * 10.0 * uDpr;\n float w = max(base + soft * mix(8.0, 36.0, r6) * uDpr + focus, 1.0);\n float drawW = w + 2.5 * uDpr;\n vec2 res = uView * uDpr;\n vec2 px = a.xy * uDpr + nrm * aVert.y * drawW * 0.5;\n gl_Position = vec4(px.x / res.x * 2.0 - 1.0, 1.0 - px.y / res.y * 2.0, 0.0, 1.0);\n vSide = aVert.y * drawW / w;\n float shimmer = 0.5 + 0.5 * sin(info.z * 0.044 - uTime * (2.0 + 2.0 * r5) + r6 * 6.283);\n shimmer *= shimmer;\n /* Contrast spreads the strands' brightness: a few burn and most fall back.\n The mean is held, so the ribbon as a whole keeps its strength. */\n float spread = mix(1.4, 4.5, uContrast);\n float least = mix(0.25, 0.04, uContrast);\n float bright = mix(least, 1.0, pow(r5, spread)) * 0.5625 / (least + (1.0 - least) / (spread + 1.0));\n float energy = min(base, w) / w;\n float fall = 0.3 + 0.5 * uDepth;\n float depth = (1.0 - fall * (1.0 - a.z) * 0.5) / (1.0 - fall * 0.5);\n float rest = mix(0.35, 0.15, uContrast);\n vec3 col = mix(uGlow, uTint, step(0.83, r6) * 0.9);\n col = mix(col, uPeak, step(0.93, r7) * 0.6);\n vColor = col * energy * info.x * (rest + (1.0 - rest) * shimmer) * bright * depth;\n}\n";
3
3
  /** A soft profile across the ribbon, added into the buffer. */
4
4
  export declare const LINE_FRAGMENT = "#version 300 es\nprecision highp float;\nin float vSide;\nin vec3 vColor;\nuniform float uScale;\nout vec4 outColor;\nvoid main() {\n outColor = vec4(vColor * exp(-vSide * vSide * 2.4) * uScale, 1.0);\n}\n";
5
5
  /** Specks drifting along the strands, thickest where the ribbon is bright. */
6
- export declare const DUST_VERTEX = "#version 300 es\nuniform float uDpr;\nuniform float uStrands;\nuniform vec3 uGlow;\nuniform vec3 uPeak;\nout vec3 vColor;\n\nuniform vec4 uPts[96];\nuniform int uCount;\nuniform vec2 uView;\nuniform float uScroll;\nuniform vec2 uT;\nuniform float uTime;\nuniform float uSpread;\n\nfloat h1(float n) { return fract(sin(n * 127.1 + 311.7) * 43758.5453); }\n\nvec4 pt(int i) { return uPts[clamp(i, 0, uCount - 1)]; }\n\nvec4 spline(float t) {\n float s = clamp(t, 0.0, 1.0) * float(uCount - 1);\n int i = min(int(floor(s)), uCount - 2);\n float f = s - float(i);\n vec4 p0 = pt(i - 1);\n vec4 p1 = pt(i);\n vec4 p2 = pt(i + 1);\n vec4 p3 = pt(i + 2);\n float f2 = f * f;\n float f3 = f2 * f;\n return 0.5 * ((2.0 * p1) + (-p0 + p2) * f + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * f2\n + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * f3);\n}\n\nvec2 place(vec4 c) { return vec2(c.x * uView.x, c.y - uScroll); }\n\nvec3 strandAt(float id, float t, out vec3 info) {\n vec4 c = spline(t);\n vec2 p = place(c);\n vec2 q = place(spline(t + 0.0004));\n vec2 tg = q - p;\n tg = length(tg) > 1e-4 ? normalize(tg) : vec2(0.0, 1.0);\n vec2 nr = vec2(-tg.y, tg.x);\n float r1 = h1(id + 0.13);\n float r2 = h1(id + 1.71);\n float r3 = h1(id + 3.37);\n float r4 = h1(id + 5.93);\n float n = (r1 + r2 + r3) / 1.5 - 1.0;\n float z = (r4 - 0.5) * 2.0;\n float along = c.y + c.x * uView.x;\n float th = along * 0.0033 + uTime * 0.25 + r2 * 0.5;\n float width = c.z * uSpread;\n float fan = width * uView.x * (1.0 + 0.25 * sin(uTime * 0.4 + along * 0.0045));\n float sway = sin(along * 0.006 + uTime * 0.23) * mix(6.0, 30.0, smoothstep(0.02, 0.2, width));\n float lat = sway + fan * (n * cos(th) + z * 0.55 * sin(th));\n lat += sin(along * 0.01 + uTime * (0.6 + 0.5 * r1) + r1 * 6.283) * 12.0 * (0.3 + r3);\n info = vec3(max(c.w, 0.0), width, along);\n return vec3(p + nr * lat, z * sin(th));\n}\n\nvoid main() {\n float id = float(gl_VertexID);\n float sid = floor(h1(id + 0.5) * uStrands);\n float r = h1(id + 2.3);\n float r2 = h1(id + 4.1);\n float t = mix(uT.x, uT.y, fract(r + uTime * 0.003 * (0.3 + r2)));\n vec3 info;\n vec3 a = strandAt(sid, t, info);\n vec2 off = (vec2(h1(id + 6.1), h1(id + 8.3)) - 0.5) * (24.0 + (150.0 + info.y * uView.x) * h1(id + 9.7));\n vec2 res = uView * uDpr;\n vec2 px = (a.xy + off) * uDpr;\n gl_Position = vec4(px.x / res.x * 2.0 - 1.0, 1.0 - px.y / res.y * 2.0, 0.0, 1.0);\n gl_PointSize = mix(1.2, 3.4, r2 * r2) * uDpr;\n float tw = 0.35 + 0.65 * (0.5 + 0.5 * sin(uTime * (0.5 + 1.8 * r) + r2 * 40.0));\n vColor = mix(uGlow, uPeak, 0.55) * tw * mix(0.2, 0.9, r) * info.x * 0.8;\n}\n";
6
+ export declare const DUST_VERTEX = "#version 300 es\nuniform float uDpr;\nuniform float uStrands;\nuniform vec3 uGlow;\nuniform vec3 uPeak;\nout vec3 vColor;\n\nuniform vec4 uPts[96];\nuniform int uCount;\nuniform vec2 uView;\nuniform float uScroll;\nuniform vec2 uT;\nuniform float uTime;\nuniform float uSpread;\nuniform float uDepth;\nuniform float uDrift;\n\nfloat h1(float n) { return fract(sin(n * 127.1 + 311.7) * 43758.5453); }\n\nvec4 pt(int i) { return uPts[clamp(i, 0, uCount - 1)]; }\n\nvec4 spline(float t) {\n float s = clamp(t, 0.0, 1.0) * float(uCount - 1);\n int i = min(int(floor(s)), uCount - 2);\n float f = s - float(i);\n vec4 p0 = pt(i - 1);\n vec4 p1 = pt(i);\n vec4 p2 = pt(i + 1);\n vec4 p3 = pt(i + 2);\n float f2 = f * f;\n float f3 = f2 * f;\n return 0.5 * ((2.0 * p1) + (-p0 + p2) * f + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * f2\n + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * f3);\n}\n\nvec2 place(vec4 c) { return vec2(c.x * uView.x, c.y - uScroll); }\n\nvec3 strandAt(float id, float t, out vec3 info) {\n vec4 c = spline(t);\n vec2 p = place(c);\n vec2 q = place(spline(t + 0.0004));\n vec2 tg = q - p;\n tg = length(tg) > 1e-4 ? normalize(tg) : vec2(0.0, 1.0);\n vec2 nr = vec2(-tg.y, tg.x);\n float r1 = h1(id + 0.13);\n float r2 = h1(id + 1.71);\n float r3 = h1(id + 3.37);\n float r4 = h1(id + 5.93);\n float r8 = h1(id + 13.7);\n float r9 = h1(id + 17.3);\n float n = (r1 + r2 + r3) / 1.5 - 1.0;\n float z = (r4 - 0.5) * 2.0;\n float along = c.y + c.x * uView.x;\n float pace = 1.0 + uDrift * (r8 - 0.5) * 1.2;\n float th = along * 0.002 * (1.0 + uDrift * (r9 - 0.5) * 0.3) + uTime * 0.25 * pace\n + r2 * (0.5 + uDrift * 2.6);\n float width = c.z * uSpread;\n float breadth = width * uView.x * (1.0 + 0.25 * sin(uTime * 0.4 + along * 0.0045));\n float sway = sin(along * 0.006 + uTime * 0.23) * mix(6.0, 30.0, smoothstep(0.02, 0.2, width));\n float ct = cos(th);\n float st = sin(th);\n float depth = clamp((z * 0.55 * ct - n * st) / 1.15, -1.0, 1.0);\n float lat = sway + breadth * (n * ct + z * 0.55 * st) * (1.0 + uDepth * 0.35 * depth);\n lat += sin(along * 0.006 * (1.0 + uDrift * (r9 - 0.5) * 0.3) + uTime * (0.6 + 0.5 * r1) * pace\n + r1 * 6.283) * 12.0 * (0.3 + r3) * (1.0 + uDrift * 0.5);\n info = vec3(max(c.w, 0.0), width, along);\n return vec3(p + nr * lat, depth);\n}\n\nvoid main() {\n float id = float(gl_VertexID);\n float sid = floor(h1(id + 0.5) * uStrands);\n float r = h1(id + 2.3);\n float r2 = h1(id + 4.1);\n float t = mix(uT.x, uT.y, fract(r + uTime * 0.003 * (0.3 + r2)));\n vec3 info;\n vec3 a = strandAt(sid, t, info);\n vec2 off = (vec2(h1(id + 6.1), h1(id + 8.3)) - 0.5) * (24.0 + (150.0 + info.y * uView.x) * h1(id + 9.7));\n vec2 res = uView * uDpr;\n vec2 px = (a.xy + off) * uDpr;\n gl_Position = vec4(px.x / res.x * 2.0 - 1.0, 1.0 - px.y / res.y * 2.0, 0.0, 1.0);\n gl_PointSize = mix(1.2, 3.4, r2 * r2) * uDpr;\n float tw = 0.35 + 0.65 * (0.5 + 0.5 * sin(uTime * (0.5 + 1.8 * r) + r2 * 40.0));\n /* By the root of the ribbon's brightness, so specks show along a dim ribbon\n as well as round a bright bend. */\n vColor = mix(uGlow, uPeak, 0.55) * tw * mix(0.2, 0.9, r) * sqrt(info.x) * 0.8;\n}\n";
7
7
  export declare const DUST_FRAGMENT = "#version 300 es\nprecision highp float;\nin vec3 vColor;\nuniform float uScale;\nout vec4 outColor;\nvoid main() {\n vec2 c = gl_PointCoord - 0.5;\n outColor = vec4(vColor * exp(-dot(c, c) * 16.0) * uScale, 1.0);\n}\n";
8
8
  /** One triangle over the whole target, from three vertex ids and no buffer. */
9
9
  export declare const QUAD_VERTEX = "#version 300 es\nout vec2 vUv;\nvoid main() {\n vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);\n vUv = p;\n gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}\n";
@@ -19,4 +19,4 @@ export declare const BLUR_FRAGMENT = "#version 300 es\nprecision highp float;\ni
19
19
  * the same energy darkens the ground toward the peak as ink. A step of noise
20
20
  * under a code value hides the banding a slow gradient shows on eight bits.
21
21
  */
22
- export declare const COMPOSITE_FRAGMENT = "#version 300 es\nprecision highp float;\nin vec2 vUv;\nuniform sampler2D uScene;\nuniform sampler2D uNear;\nuniform sampler2D uFar;\nuniform vec3 uGround;\nuniform vec3 uInk;\nuniform float uLight;\nuniform float uGain;\nout vec4 outColor;\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }\nvoid main() {\n vec3 e = texture(uScene, vUv).rgb + texture(uNear, vUv).rgb * 1.1 + texture(uFar, vUv).rgb * 0.9;\n e *= uGain;\n vec3 dark = 1.0 - (1.0 - uGround) * exp(-e);\n float lum = dot(e, vec3(0.3, 0.55, 0.15));\n vec3 light = mix(uGround, uInk, 1.0 - exp(-lum * 1.4));\n vec3 c = mix(dark, light, uLight);\n c += (hash(gl_FragCoord.xy) - 0.5) / 255.0;\n outColor = vec4(c, 1.0);\n}\n";
22
+ export declare const COMPOSITE_FRAGMENT = "#version 300 es\nprecision highp float;\nin vec2 vUv;\nuniform sampler2D uScene;\nuniform sampler2D uNear;\nuniform sampler2D uFar;\nuniform vec3 uGround;\nuniform vec3 uInk;\nuniform float uLight;\nuniform float uGain;\nuniform float uBloom;\nout vec4 outColor;\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }\nvoid main() {\n vec3 e = texture(uScene, vUv).rgb + (texture(uNear, vUv).rgb * 1.1 + texture(uFar, vUv).rgb * 0.9) * uBloom;\n e *= uGain;\n vec3 dark = 1.0 - (1.0 - uGround) * exp(-e);\n float lum = dot(e, vec3(0.3, 0.55, 0.15));\n vec3 light = mix(uGround, uInk, 1.0 - exp(-lum * 1.4));\n vec3 c = mix(dark, light, uLight);\n c += (hash(gl_FragCoord.xy) - 0.5) / 255.0;\n outColor = vec4(c, 1.0);\n}\n";