@fusion.dev/helix 0.181.1 → 0.182.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,9 +1,11 @@
1
+ /** The most elements a ribbon dims behind, through the root's \`quiet\`. */
2
+ export declare const MAX_QUIET = 4;
1
3
  /** 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 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";
4
+ 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 float vSigma;\nout vec3 vColor;\n\nuniform vec4 uPts[96];\nuniform vec4 uLen[24];\nuniform int uCount;\nuniform vec2 uView;\nuniform float uScroll;\nuniform vec2 uT;\nuniform float uTime;\nuniform float uSpread;\nuniform float uDepth;\nuniform float uDrift;\nuniform vec4 uQuiet[4];\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\n/* **Quiet behind words.** Each rect the root names, left, top, right and\n bottom in the path's own pixels, dims the light inside it to a faint glow,\n easing back to full over a feather either side of its edge. An unused rect\n is empty, and a point is never inside it. */\nfloat quietAt(vec2 p) {\n vec2 at = vec2(p.x, p.y + uScroll);\n float light = 1.0;\n for (int i = 0; i < 4; i++) {\n vec4 r = uQuiet[i];\n if (r.z <= r.x) {\n continue;\n }\n vec2 size = (r.zw - r.xy) * 0.5;\n vec2 off = abs(at - (r.xy + size)) - size;\n float outside = length(max(off, 0.0)) + min(max(off.x, off.y), 0.0);\n light = min(light, mix(0.18, 1.0, smoothstep(-72.0, 72.0, outside)));\n }\n return light;\n}\n\nfloat len(int i) {\n int k = clamp(i, 0, uCount - 1);\n return uLen[k >> 2][k & 3];\n}\n\n/* Arc length at `t`, on the same spline as the centerline, so it only grows. */\nfloat lengthAt(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 float f2 = f * f;\n float f3 = f2 * f;\n float p0 = len(i - 1);\n float p1 = len(i);\n float p2 = len(i + 1);\n float p3 = len(i + 2);\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\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 /* **Laminar: every strand keeps its lane.** A strand's place across the\n ribbon is its lane `n`, and a thin second coordinate `z` gives the\n sheet a little thickness. The whole sheet rolls by one angle, so where it\n turns edge on, every strand converges at once and fans out again, as a\n twisted band of silk does. Each strand rolling at its own random pace and\n waving its own random wave crossed every neighbor and read as tangled\n hair.\n\n **Drift is shear, smooth across the lanes.** The roll's phase and pace,\n and the ribbon's wave, vary with the lane rather than with a random\n number, so neighbors move together and the edges of the ribbon flow at\n their own pace past the middle, like layers of a fluid. */\n float n = (r1 + r2 + r3) / 1.5 - 1.0;\n float z = (r4 - 0.5) * 2.0 * 0.3;\n float along = lengthAt(t);\n float pace = 1.0 + uDrift * 0.25 * n;\n float th = along * 0.002 + uTime * 0.25 * pace + uDrift * 0.6 * n;\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.0, -1.0, 1.0);\n /* **No strand sits further out than the path allows for.** The path keeps\n every turn wider than 1.3 breadths, the reach `path.ts` lays it by, and a\n few strands, at the edge of the cross-section and near in depth, reached\n past that and hooked in a turn. A soft limit leaves the strands near the\n axis where they are and eases the far ones in toward 1.3 breadths. */\n float limit = breadth * 1.3 + 1.0;\n float across = breadth * (n * ct + z * 0.55 * st) * (1.0 + uDepth * 0.35 * depth);\n float lat = sway + limit * tanh(across / limit);\n lat += sin(along * 0.0045 + uTime * 0.5 * pace + uDrift * 1.2 * n) * 10.0 * (1.0 + uDrift * 0.5);\n /* **No strand crosses the center of the turn it is on.** A strand is the\n centerline moved sideways by `lat`, and on the inside of a turn whose\n radius is less than that, the moved line runs back over itself: a loop,\n or a fold where its quads flip. `curve` is the turn's curvature toward\n `nr`, from the spline either side of this point, and an offset on the\n inside is eased toward the center so that `curve * lat` stays under 1.\n Offsets on the outside, and on a straight run, are left as they are. */\n /* **Smooth, or it kinks every strand.** The spline's own curvature jumps at\n each point, so the turn is read across a whole segment, which averages the\n jump away. The squeeze eases in as a soft maximum, so there is no corner\n where it starts.\n\n **A safety net, not the shape.** The path keeps every turn wider than\n its strands reach, so a strand on the inside stays under 0.9 of the way\n to the center and needs nothing. The squeeze waits until 0.6 of the way\n and acts gently past it: squeezing from the first sign of a turn\n compressed the inside strands wherever the curvature changed, and they\n turned in an L where the ribbon swept round. */\n float reach = 0.5 / float(max(uCount - 1, 1));\n vec2 back = place(spline(t - reach));\n vec2 ahead = place(spline(t + reach));\n vec2 velocity = (ahead - back) * 0.5;\n float curve = dot(ahead - 2.0 * p + back, nr) / max(dot(velocity, velocity), 1e-2);\n float inward = curve * lat;\n float excess = inward - 0.6;\n float squeeze = 0.5 * (excess + sqrt(excess * excess + 0.01));\n lat /= 1.0 + squeeze;\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) * 4.0 * uDpr;\n float w = max(base + soft * mix(8.0, 36.0, r6) * uDpr + focus, 1.0);\n /* **A profile no narrower than a pixel can carry.** The strand's light falls\n off across it as a Gaussian a quarter of its width wide, which for a strand\n a pixel wide is a quarter of a pixel: narrower than the pixels sampling it,\n so a thin strand broke into a row of dots wherever it ran between pixel\n centers. Its spread is held to at least 0.7 of a device pixel, and its peak\n comes down by the same factor, so it carries the same light and reads as\n one continuous thread. The quad covers three spreads either side. */\n float designed = w * 0.228;\n float sigma = max(designed, 0.7);\n float drawW = max(w, sigma * 6.0) + 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 * 0.5;\n vSigma = sigma;\n /* **Light travels along the strand slowly and long.** A pulse every 140\n pixels broke each strand into dashes; one every 1,800 or so is light\n moving down a fiber. */\n float shimmer = 0.5 + 0.5 * sin(info.z * 0.0035 - uTime * (0.6 + 0.4 * r5) + r6 * 6.283);\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.35, 0.14, 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 * (designed / sigma) * quietAt(a.xy);\n}\n";
3
5
  /** A soft profile across the ribbon, added into the buffer. */
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";
6
+ export declare const LINE_FRAGMENT = "#version 300 es\nprecision highp float;\nin float vSide;\nin float vSigma;\nin vec3 vColor;\nuniform float uScale;\nout vec4 outColor;\nvoid main() {\n float d = vSide / vSigma;\n outColor = vec4(vColor * exp(-0.5 * d * d) * uScale, 1.0);\n}\n";
5
7
  /** 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;\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";
8
+ 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 vec4 uLen[24];\nuniform int uCount;\nuniform vec2 uView;\nuniform float uScroll;\nuniform vec2 uT;\nuniform float uTime;\nuniform float uSpread;\nuniform float uDepth;\nuniform float uDrift;\nuniform vec4 uQuiet[4];\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\n/* **Quiet behind words.** Each rect the root names, left, top, right and\n bottom in the path's own pixels, dims the light inside it to a faint glow,\n easing back to full over a feather either side of its edge. An unused rect\n is empty, and a point is never inside it. */\nfloat quietAt(vec2 p) {\n vec2 at = vec2(p.x, p.y + uScroll);\n float light = 1.0;\n for (int i = 0; i < 4; i++) {\n vec4 r = uQuiet[i];\n if (r.z <= r.x) {\n continue;\n }\n vec2 size = (r.zw - r.xy) * 0.5;\n vec2 off = abs(at - (r.xy + size)) - size;\n float outside = length(max(off, 0.0)) + min(max(off.x, off.y), 0.0);\n light = min(light, mix(0.18, 1.0, smoothstep(-72.0, 72.0, outside)));\n }\n return light;\n}\n\nfloat len(int i) {\n int k = clamp(i, 0, uCount - 1);\n return uLen[k >> 2][k & 3];\n}\n\n/* Arc length at `t`, on the same spline as the centerline, so it only grows. */\nfloat lengthAt(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 float f2 = f * f;\n float f3 = f2 * f;\n float p0 = len(i - 1);\n float p1 = len(i);\n float p2 = len(i + 1);\n float p3 = len(i + 2);\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\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 /* **Laminar: every strand keeps its lane.** A strand's place across the\n ribbon is its lane `n`, and a thin second coordinate `z` gives the\n sheet a little thickness. The whole sheet rolls by one angle, so where it\n turns edge on, every strand converges at once and fans out again, as a\n twisted band of silk does. Each strand rolling at its own random pace and\n waving its own random wave crossed every neighbor and read as tangled\n hair.\n\n **Drift is shear, smooth across the lanes.** The roll's phase and pace,\n and the ribbon's wave, vary with the lane rather than with a random\n number, so neighbors move together and the edges of the ribbon flow at\n their own pace past the middle, like layers of a fluid. */\n float n = (r1 + r2 + r3) / 1.5 - 1.0;\n float z = (r4 - 0.5) * 2.0 * 0.3;\n float along = lengthAt(t);\n float pace = 1.0 + uDrift * 0.25 * n;\n float th = along * 0.002 + uTime * 0.25 * pace + uDrift * 0.6 * n;\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.0, -1.0, 1.0);\n /* **No strand sits further out than the path allows for.** The path keeps\n every turn wider than 1.3 breadths, the reach `path.ts` lays it by, and a\n few strands, at the edge of the cross-section and near in depth, reached\n past that and hooked in a turn. A soft limit leaves the strands near the\n axis where they are and eases the far ones in toward 1.3 breadths. */\n float limit = breadth * 1.3 + 1.0;\n float across = breadth * (n * ct + z * 0.55 * st) * (1.0 + uDepth * 0.35 * depth);\n float lat = sway + limit * tanh(across / limit);\n lat += sin(along * 0.0045 + uTime * 0.5 * pace + uDrift * 1.2 * n) * 10.0 * (1.0 + uDrift * 0.5);\n /* **No strand crosses the center of the turn it is on.** A strand is the\n centerline moved sideways by `lat`, and on the inside of a turn whose\n radius is less than that, the moved line runs back over itself: a loop,\n or a fold where its quads flip. `curve` is the turn's curvature toward\n `nr`, from the spline either side of this point, and an offset on the\n inside is eased toward the center so that `curve * lat` stays under 1.\n Offsets on the outside, and on a straight run, are left as they are. */\n /* **Smooth, or it kinks every strand.** The spline's own curvature jumps at\n each point, so the turn is read across a whole segment, which averages the\n jump away. The squeeze eases in as a soft maximum, so there is no corner\n where it starts.\n\n **A safety net, not the shape.** The path keeps every turn wider than\n its strands reach, so a strand on the inside stays under 0.9 of the way\n to the center and needs nothing. The squeeze waits until 0.6 of the way\n and acts gently past it: squeezing from the first sign of a turn\n compressed the inside strands wherever the curvature changed, and they\n turned in an L where the ribbon swept round. */\n float reach = 0.5 / float(max(uCount - 1, 1));\n vec2 back = place(spline(t - reach));\n vec2 ahead = place(spline(t + reach));\n vec2 velocity = (ahead - back) * 0.5;\n float curve = dot(ahead - 2.0 * p + back, nr) / max(dot(velocity, velocity), 1e-2);\n float inward = curve * lat;\n float excess = inward - 0.6;\n float squeeze = 0.5 * (excess + sqrt(excess * excess + 0.01));\n lat /= 1.0 + squeeze;\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 * quietAt(a.xy + off);\n}\n";
7
9
  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
10
  /** One triangle over the whole target, from three vertex ids and no buffer. */
9
11
  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";
@@ -1,6 +1,10 @@
1
1
  import { MAX_POINTS } from './path.js';
2
+ export const MAX_QUIET = 4;
3
+ const QUIET_FLOOR = '0.18';
4
+ const QUIET_FEATHER = '72.0';
2
5
  const PATH = `
3
6
  uniform vec4 uPts[${MAX_POINTS}];
7
+ uniform vec4 uLen[${MAX_POINTS / 4}];
4
8
  uniform int uCount;
5
9
  uniform vec2 uView;
6
10
  uniform float uScroll;
@@ -9,6 +13,7 @@ uniform float uTime;
9
13
  uniform float uSpread;
10
14
  uniform float uDepth;
11
15
  uniform float uDrift;
16
+ uniform vec4 uQuiet[${MAX_QUIET}];
12
17
 
13
18
  float h1(float n) { return fract(sin(n * 127.1 + 311.7) * 43758.5453); }
14
19
 
@@ -30,6 +35,46 @@ vec4 spline(float t) {
30
35
 
31
36
  vec2 place(vec4 c) { return vec2(c.x * uView.x, c.y - uScroll); }
32
37
 
38
+ /* **Quiet behind words.** Each rect the root names, left, top, right and
39
+ bottom in the path's own pixels, dims the light inside it to a faint glow,
40
+ easing back to full over a feather either side of its edge. An unused rect
41
+ is empty, and a point is never inside it. */
42
+ float quietAt(vec2 p) {
43
+ vec2 at = vec2(p.x, p.y + uScroll);
44
+ float light = 1.0;
45
+ for (int i = 0; i < ${MAX_QUIET}; i++) {
46
+ vec4 r = uQuiet[i];
47
+ if (r.z <= r.x) {
48
+ continue;
49
+ }
50
+ vec2 size = (r.zw - r.xy) * 0.5;
51
+ vec2 off = abs(at - (r.xy + size)) - size;
52
+ float outside = length(max(off, 0.0)) + min(max(off.x, off.y), 0.0);
53
+ light = min(light, mix(${QUIET_FLOOR}, 1.0, smoothstep(-${QUIET_FEATHER}, ${QUIET_FEATHER}, outside)));
54
+ }
55
+ return light;
56
+ }
57
+
58
+ float len(int i) {
59
+ int k = clamp(i, 0, uCount - 1);
60
+ return uLen[k >> 2][k & 3];
61
+ }
62
+
63
+ /* Arc length at \`t\`, on the same spline as the centerline, so it only grows. */
64
+ float lengthAt(float t) {
65
+ float s = clamp(t, 0.0, 1.0) * float(uCount - 1);
66
+ int i = min(int(floor(s)), uCount - 2);
67
+ float f = s - float(i);
68
+ float f2 = f * f;
69
+ float f3 = f2 * f;
70
+ float p0 = len(i - 1);
71
+ float p1 = len(i);
72
+ float p2 = len(i + 1);
73
+ float p3 = len(i + 2);
74
+ return 0.5 * ((2.0 * p1) + (-p0 + p2) * f + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * f2
75
+ + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * f3);
76
+ }
77
+
33
78
  vec3 strandAt(float id, float t, out vec3 info) {
34
79
  vec4 c = spline(t);
35
80
  vec2 p = place(c);
@@ -43,21 +88,65 @@ vec3 strandAt(float id, float t, out vec3 info) {
43
88
  float r4 = h1(id + 5.93);
44
89
  float r8 = h1(id + 13.7);
45
90
  float r9 = h1(id + 17.3);
91
+ /* **Laminar: every strand keeps its lane.** A strand's place across the
92
+ ribbon is its lane \`n\`, and a thin second coordinate \`z\` gives the
93
+ sheet a little thickness. The whole sheet rolls by one angle, so where it
94
+ turns edge on, every strand converges at once and fans out again, as a
95
+ twisted band of silk does. Each strand rolling at its own random pace and
96
+ waving its own random wave crossed every neighbor and read as tangled
97
+ hair.
98
+
99
+ **Drift is shear, smooth across the lanes.** The roll's phase and pace,
100
+ and the ribbon's wave, vary with the lane rather than with a random
101
+ number, so neighbors move together and the edges of the ribbon flow at
102
+ their own pace past the middle, like layers of a fluid. */
46
103
  float n = (r1 + r2 + r3) / 1.5 - 1.0;
47
- float z = (r4 - 0.5) * 2.0;
48
- float along = c.y + c.x * uView.x;
49
- float pace = 1.0 + uDrift * (r8 - 0.5) * 1.2;
50
- float th = along * 0.002 * (1.0 + uDrift * (r9 - 0.5) * 0.3) + uTime * 0.25 * pace
51
- + r2 * (0.5 + uDrift * 2.6);
104
+ float z = (r4 - 0.5) * 2.0 * 0.3;
105
+ float along = lengthAt(t);
106
+ float pace = 1.0 + uDrift * 0.25 * n;
107
+ float th = along * 0.002 + uTime * 0.25 * pace + uDrift * 0.6 * n;
52
108
  float width = c.z * uSpread;
53
109
  float breadth = width * uView.x * (1.0 + 0.25 * sin(uTime * 0.4 + along * 0.0045));
54
110
  float sway = sin(along * 0.006 + uTime * 0.23) * mix(6.0, 30.0, smoothstep(0.02, 0.2, width));
55
111
  float ct = cos(th);
56
112
  float st = sin(th);
57
- float depth = clamp((z * 0.55 * ct - n * st) / 1.15, -1.0, 1.0);
58
- float lat = sway + breadth * (n * ct + z * 0.55 * st) * (1.0 + uDepth * 0.35 * depth);
59
- lat += sin(along * 0.006 * (1.0 + uDrift * (r9 - 0.5) * 0.3) + uTime * (0.6 + 0.5 * r1) * pace
60
- + r1 * 6.283) * 12.0 * (0.3 + r3) * (1.0 + uDrift * 0.5);
113
+ float depth = clamp((z * 0.55 * ct - n * st) / 1.0, -1.0, 1.0);
114
+ /* **No strand sits further out than the path allows for.** The path keeps
115
+ every turn wider than 1.3 breadths, the reach \`path.ts\` lays it by, and a
116
+ few strands, at the edge of the cross-section and near in depth, reached
117
+ past that and hooked in a turn. A soft limit leaves the strands near the
118
+ axis where they are and eases the far ones in toward 1.3 breadths. */
119
+ float limit = breadth * 1.3 + 1.0;
120
+ float across = breadth * (n * ct + z * 0.55 * st) * (1.0 + uDepth * 0.35 * depth);
121
+ float lat = sway + limit * tanh(across / limit);
122
+ lat += sin(along * 0.0045 + uTime * 0.5 * pace + uDrift * 1.2 * n) * 10.0 * (1.0 + uDrift * 0.5);
123
+ /* **No strand crosses the center of the turn it is on.** A strand is the
124
+ centerline moved sideways by \`lat\`, and on the inside of a turn whose
125
+ radius is less than that, the moved line runs back over itself: a loop,
126
+ or a fold where its quads flip. \`curve\` is the turn's curvature toward
127
+ \`nr\`, from the spline either side of this point, and an offset on the
128
+ inside is eased toward the center so that \`curve * lat\` stays under 1.
129
+ Offsets on the outside, and on a straight run, are left as they are. */
130
+ /* **Smooth, or it kinks every strand.** The spline's own curvature jumps at
131
+ each point, so the turn is read across a whole segment, which averages the
132
+ jump away. The squeeze eases in as a soft maximum, so there is no corner
133
+ where it starts.
134
+
135
+ **A safety net, not the shape.** The path keeps every turn wider than
136
+ its strands reach, so a strand on the inside stays under 0.9 of the way
137
+ to the center and needs nothing. The squeeze waits until 0.6 of the way
138
+ and acts gently past it: squeezing from the first sign of a turn
139
+ compressed the inside strands wherever the curvature changed, and they
140
+ turned in an L where the ribbon swept round. */
141
+ float reach = 0.5 / float(max(uCount - 1, 1));
142
+ vec2 back = place(spline(t - reach));
143
+ vec2 ahead = place(spline(t + reach));
144
+ vec2 velocity = (ahead - back) * 0.5;
145
+ float curve = dot(ahead - 2.0 * p + back, nr) / max(dot(velocity, velocity), 1e-2);
146
+ float inward = curve * lat;
147
+ float excess = inward - 0.6;
148
+ float squeeze = 0.5 * (excess + sqrt(excess * excess + 0.01));
149
+ lat /= 1.0 + squeeze;
61
150
  info = vec3(max(c.w, 0.0), width, along);
62
151
  return vec3(p + nr * lat, depth);
63
152
  }
@@ -72,6 +161,7 @@ uniform vec3 uGlow;
72
161
  uniform vec3 uTint;
73
162
  uniform vec3 uPeak;
74
163
  out float vSide;
164
+ out float vSigma;
75
165
  out vec3 vColor;
76
166
  ${PATH}
77
167
  void main() {
@@ -91,19 +181,31 @@ void main() {
91
181
  /* Nearer is thicker, and the back of the ribbon falls out of focus: wider
92
182
  at the same energy, so it reads as haze behind the sharp strands. */
93
183
  float base = mix(0.35, 1.3, r7 * r7) * uDpr * (1.0 + (0.3 + 0.5 * uDepth) * a.z);
94
- float focus = uDepth * smoothstep(-0.1, -0.9, a.z) * 10.0 * uDpr;
184
+ float focus = uDepth * smoothstep(-0.1, -0.9, a.z) * 4.0 * uDpr;
95
185
  float w = max(base + soft * mix(8.0, 36.0, r6) * uDpr + focus, 1.0);
96
- float drawW = w + 2.5 * uDpr;
186
+ /* **A profile no narrower than a pixel can carry.** The strand's light falls
187
+ off across it as a Gaussian a quarter of its width wide, which for a strand
188
+ a pixel wide is a quarter of a pixel: narrower than the pixels sampling it,
189
+ so a thin strand broke into a row of dots wherever it ran between pixel
190
+ centers. Its spread is held to at least 0.7 of a device pixel, and its peak
191
+ comes down by the same factor, so it carries the same light and reads as
192
+ one continuous thread. The quad covers three spreads either side. */
193
+ float designed = w * 0.228;
194
+ float sigma = max(designed, 0.7);
195
+ float drawW = max(w, sigma * 6.0) + 2.5 * uDpr;
97
196
  vec2 res = uView * uDpr;
98
197
  vec2 px = a.xy * uDpr + nrm * aVert.y * drawW * 0.5;
99
198
  gl_Position = vec4(px.x / res.x * 2.0 - 1.0, 1.0 - px.y / res.y * 2.0, 0.0, 1.0);
100
- vSide = aVert.y * drawW / w;
101
- float shimmer = 0.5 + 0.5 * sin(info.z * 0.044 - uTime * (2.0 + 2.0 * r5) + r6 * 6.283);
102
- shimmer *= shimmer;
199
+ vSide = aVert.y * drawW * 0.5;
200
+ vSigma = sigma;
201
+ /* **Light travels along the strand slowly and long.** A pulse every 140
202
+ pixels broke each strand into dashes; one every 1,800 or so is light
203
+ moving down a fiber. */
204
+ float shimmer = 0.5 + 0.5 * sin(info.z * 0.0035 - uTime * (0.6 + 0.4 * r5) + r6 * 6.283);
103
205
  /* Contrast spreads the strands' brightness: a few burn and most fall back.
104
206
  The mean is held, so the ribbon as a whole keeps its strength. */
105
207
  float spread = mix(1.4, 4.5, uContrast);
106
- float least = mix(0.25, 0.04, uContrast);
208
+ float least = mix(0.35, 0.14, uContrast);
107
209
  float bright = mix(least, 1.0, pow(r5, spread)) * 0.5625 / (least + (1.0 - least) / (spread + 1.0));
108
210
  float energy = min(base, w) / w;
109
211
  float fall = 0.3 + 0.5 * uDepth;
@@ -111,17 +213,19 @@ void main() {
111
213
  float rest = mix(0.35, 0.15, uContrast);
112
214
  vec3 col = mix(uGlow, uTint, step(0.83, r6) * 0.9);
113
215
  col = mix(col, uPeak, step(0.93, r7) * 0.6);
114
- vColor = col * energy * info.x * (rest + (1.0 - rest) * shimmer) * bright * depth;
216
+ vColor = col * energy * info.x * (rest + (1.0 - rest) * shimmer) * bright * depth * (designed / sigma) * quietAt(a.xy);
115
217
  }
116
218
  `;
117
219
  export const LINE_FRAGMENT = `#version 300 es
118
220
  precision highp float;
119
221
  in float vSide;
222
+ in float vSigma;
120
223
  in vec3 vColor;
121
224
  uniform float uScale;
122
225
  out vec4 outColor;
123
226
  void main() {
124
- outColor = vec4(vColor * exp(-vSide * vSide * 2.4) * uScale, 1.0);
227
+ float d = vSide / vSigma;
228
+ outColor = vec4(vColor * exp(-0.5 * d * d) * uScale, 1.0);
125
229
  }
126
230
  `;
127
231
  export const DUST_VERTEX = `#version 300 es
@@ -147,7 +251,7 @@ void main() {
147
251
  float tw = 0.35 + 0.65 * (0.5 + 0.5 * sin(uTime * (0.5 + 1.8 * r) + r2 * 40.0));
148
252
  /* By the root of the ribbon's brightness, so specks show along a dim ribbon
149
253
  as well as round a bright bend. */
150
- vColor = mix(uGlow, uPeak, 0.55) * tw * mix(0.2, 0.9, r) * sqrt(info.x) * 0.8;
254
+ vColor = mix(uGlow, uPeak, 0.55) * tw * mix(0.2, 0.9, r) * sqrt(info.x) * 0.8 * quietAt(a.xy + off);
151
255
  }
152
256
  `;
153
257
  export const DUST_FRAGMENT = `#version 300 es
@@ -8,9 +8,11 @@ import type { StrandsVariant } from '../path.js';
8
8
  * @description
9
9
  * Placed inside a `helix-strands` with `travel`, a stop names an element with
10
10
  * `for` and says which form the ribbon takes when the page reaches it: a `bend`
11
- * turning through it, a `ribbon` passing through `at`, or `waves` crossing at
12
- * its top edge. The stops are read in page order, whatever order they are
13
- * written in, and a stop naming no element is skipped.
11
+ * turning through it, a `ribbon` passing through `at`, a `sweep` crossing in one
12
+ * diagonal from the page's top corner on the side `at` is nearer to the
13
+ * element's far top corner, or `waves` crossing at its top edge. The stops are
14
+ * read in page order, whatever order they are written in, and a stop naming no
15
+ * element is skipped.
14
16
  *
15
17
  * A stop draws nothing itself and is never shown.
16
18
  *