@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.
- package/custom-elements.json +213 -13
- package/dist/bar-chart/root/bar-chart-root.css.js +2 -2
- package/dist/strands/path.d.ts +51 -2
- package/dist/strands/path.js +255 -42
- package/dist/strands/renderer.d.ts +10 -0
- package/dist/strands/renderer.js +5 -2
- package/dist/strands/root/strands-root.d.ts +29 -6
- package/dist/strands/root/strands-root.js +38 -4
- package/dist/strands/shader.d.ts +5 -3
- package/dist/strands/shader.js +122 -18
- package/dist/strands/stop/strands-stop.d.ts +5 -3
- package/lint.json +1 -1
- package/package.json +6 -6
- package/types/jsx.d.ts +90 -24
- package/types/vue.d.ts +25 -5
package/dist/strands/shader.d.ts
CHANGED
|
@@ -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 =
|
|
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(-
|
|
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 =
|
|
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";
|
package/dist/strands/shader.js
CHANGED
|
@@ -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 =
|
|
49
|
-
float pace = 1.0 + uDrift *
|
|
50
|
-
float th = along * 0.002
|
|
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.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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) *
|
|
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
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
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
|
-
|
|
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`,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
*
|