@fusion.dev/helix 0.47.0 → 0.47.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4202,7 +4202,7 @@
4202
4202
  "text": "number"
4203
4203
  },
4204
4204
  "default": "1",
4205
- "description": "How much the members of a population differ from one another, `0` a set\nthat is uniform and `1` the resting amount. Clamped to that range: these\nare interpolations, so a larger number extrapolates rather than\nexaggerating, and a streak whose length has passed through zero is drawn\nas a bar across the whole frame.\n\nRead by four of the eleven shapes, the ones made of many similar things,\nwhether those are counted or continuous: `streaks`, whose lanes draw a\nspeed, a length, a width, a spacing and a weight apiece; `particles`, whose\nmotes draw a size, a brightness and a wander; and `rays` and `aurora`, whose\nfan, bundles and folds widen or even out.\n\nInert on the other seven, for three different reasons. `braid`, `waves` and\n`flow` have a single subject and nothing to vary. `threads` and `lattice`\nare even on purpose, a scatter of strands and a regular structure each being\nwhat it is because its members do not differ. `caustics` and `fracture` are\npopulations this does not reach, which is a gap rather than a refusal.",
4205
+ "description": "How much the members of a population differ from one another, `0` a set\nthat is uniform and `1` the resting amount. Clamped to that range: these\nare interpolations, so a larger number extrapolates rather than\nexaggerating, and a streak whose length has passed through zero is drawn\nas a bar across the whole frame.\n\nRead by six of the eleven shapes, the ones made of many similar things,\nwhether those are counted or continuous: `streaks`, whose lanes draw a\nspeed, a length, a width, a spacing and a weight apiece; `particles`, whose\nmotes draw a size, a brightness and a wander; `rays` and `aurora`, whose\nfan, bundles and folds widen or even out; `caustics`, whose three depths of\nfold close on one scale and one weight while the pool over them flattens to\nan even wash; and `fracture`, whose sites come home to their own cell\ncenters, leaving equal territories and no border more contested than\nanother.\n\nInert on the other five, and every one of them a refusal. `braid`, `waves`\nand `flow` have a single subject and nothing to vary. `threads` and\n`lattice` are even on purpose, a scatter of strands and a regular structure\neach being what it is because its members do not differ: `threads` gives\nits seven one amplitude, one thickness, one weight and one frequency on\nthe argument that alike is what makes a weave read as woven, and a lattice\nwhose cells differed would be a scatter with straight lines in it.",
4206
4206
  "attribute": "variance"
4207
4207
  },
4208
4208
  {
@@ -5165,7 +5165,7 @@
5165
5165
  "text": "number"
5166
5166
  },
5167
5167
  "default": "1",
5168
- "description": "How much the members of a population differ from one another, `0` a set\nthat is uniform and `1` the resting amount. Clamped to that range: these\nare interpolations, so a larger number extrapolates rather than\nexaggerating, and a streak whose length has passed through zero is drawn\nas a bar across the whole frame.\n\nRead by four of the eleven shapes, the ones made of many similar things,\nwhether those are counted or continuous: `streaks`, whose lanes draw a\nspeed, a length, a width, a spacing and a weight apiece; `particles`, whose\nmotes draw a size, a brightness and a wander; and `rays` and `aurora`, whose\nfan, bundles and folds widen or even out.\n\nInert on the other seven, for three different reasons. `braid`, `waves` and\n`flow` have a single subject and nothing to vary. `threads` and `lattice`\nare even on purpose, a scatter of strands and a regular structure each being\nwhat it is because its members do not differ. `caustics` and `fracture` are\npopulations this does not reach, which is a gap rather than a refusal.",
5168
+ "description": "How much the members of a population differ from one another, `0` a set\nthat is uniform and `1` the resting amount. Clamped to that range: these\nare interpolations, so a larger number extrapolates rather than\nexaggerating, and a streak whose length has passed through zero is drawn\nas a bar across the whole frame.\n\nRead by six of the eleven shapes, the ones made of many similar things,\nwhether those are counted or continuous: `streaks`, whose lanes draw a\nspeed, a length, a width, a spacing and a weight apiece; `particles`, whose\nmotes draw a size, a brightness and a wander; `rays` and `aurora`, whose\nfan, bundles and folds widen or even out; `caustics`, whose three depths of\nfold close on one scale and one weight while the pool over them flattens to\nan even wash; and `fracture`, whose sites come home to their own cell\ncenters, leaving equal territories and no border more contested than\nanother.\n\nInert on the other five, and every one of them a refusal. `braid`, `waves`\nand `flow` have a single subject and nothing to vary. `threads` and\n`lattice` are even on purpose, a scatter of strands and a regular structure\neach being what it is because its members do not differ: `threads` gives\nits seven one amplitude, one thickness, one weight and one frequency on\nthe argument that alike is what makes a weave read as woven, and a lattice\nwhose cells differed would be a scatter with straight lines in it.",
5169
5169
  "fieldName": "variance"
5170
5170
  },
5171
5171
  {
@@ -5326,7 +5326,7 @@
5326
5326
  {
5327
5327
  "kind": "variable",
5328
5328
  "name": "FRAGMENT",
5329
- "default": "`#version 300 es precision highp float; in vec2 uv; out vec4 color; uniform vec2 uResolution; uniform float uTime; uniform int uShape; uniform vec2 uPointer; uniform float uReach; uniform int uInteraction; uniform float uTilt; uniform float uScale; uniform float uBloom; uniform float uGain; /* How much the members of a population differ from one another, 1 being the resting amount and 0 a set that is uniform. Read by four of the eleven shapes, the ones made of many similar things, whether those are counted or continuous: streaks has lanes, particles has motes, rays has a fan, and aurora has bundles and folds. Braid, waves and flow have a single subject and nothing to vary. Threads and lattice are even on purpose, a scatter of strands and a regular structure each being what it is because its members do not differ. Caustics and fracture are populations this does not reach, which is a gap rather than a refusal. */ uniform float uVariance; /* Where the pointer has been, as x, y and age. Age runs 0 at the moment it was left to 1 when it has faded, so a spent slot costs a compare. **This is what a fluid gets from a framebuffer, bought for eight vec3s.** A real solver advects a dye texture and reads it back the next frame, which needs render targets, a ping-pong pair and about six passes. What a reader actually perceives from that is one thing: the cursor leaves a wake that outlives it. A short history of where it went reproduces that in the single pass this component already makes, at a fixed cost that does not grow with how long the page has been open. What it cannot do is conserve anything, so there is no diffusion and no pressure: the wake decays rather than spreading. Worth knowing before this is asked to be a simulation. */ uniform vec3 uTrail[9]; uniform vec3 uGround; uniform vec3 uHaze; uniform vec3 uGlow; uniform vec3 uPeak; float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); } float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); /* Quintic rather than cubic. Cubic smoothstep leaves a second derivative discontinuity at every cell edge, which four octaves of lighting turn into a faint square lattice: visible on a large soft field and exactly the artifact that reads as cheap. */ vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); return mix( mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y ); } /* Each octave is rotated as well as scaled. Doubling alone stacks every octave on the same axes, so value noise keeps its grid however many are summed. An irrational angle means no two octaves ever line up again. */ const mat2 TURN = mat2(0.8, 0.6, -0.6, 0.8); float fbm(vec2 p) { float sum = 0.0; float amp = 0.5; for (int i = 0; i < 5; i++) { sum += amp * noise(p); p = TURN * p * 2.03; amp *= 0.5; } return sum; } /* Domain warping: the field is sampled at coordinates another field bends. This is the single largest difference between noise that reads as organic and noise that reads as television static, and it costs two more fbm calls rather than a different algorithm. */ float warped(vec2 p, float t) { vec2 q = vec2(fbm(p + vec2(0.0, t)), fbm(p + vec2(5.2, 1.3 - t))); vec2 r = vec2(fbm(p + 4.0 * q + vec2(1.7, 9.2)), fbm(p + 4.0 * q + vec2(8.3, 2.8))); return fbm(p + 4.0 * r); } /* An exponential falloff without the corner. * * Both abs and length have a vertex at zero and their derivative jumps * across it, so a falloff built on either acquires a hard line exactly along * the axis it is brightest on. That is a crease in something meant to be * light, and it is what put a chevron through the braid wherever the * pointer's height crossed a strand. * * Softening the distance rather than the exponent keeps the tails identical * and rounds only the point. The softening scales with the rate, so a tight * falloff is rounded tightly and a broad one broadly: at zero the result is * exp(-0.06) whatever the rate is, six per cent off the peak, which is not * visible as flattening. * * Takes the squared distance, because every caller already has one and a * square root taken here is the one that would have reintroduced the vertex. */ float falloff(float d2, float k) { float soft = 0.06 / k; return exp(-sqrt(d2 + soft * soft) * k); } /* Two strands half a turn apart, inside a wider volume. */ float braid(vec2 q, float t, vec2 qp, float reach) { /* The strands lean toward the pointer where it is alongside them, and are unmoved where it is not. A braid is a thing with a length, so what it can answer with is a bend at a height, not a bulge at a point. */ float alongside = falloff((q.y - qp.y) * (q.y - qp.y), 1.1) * reach; float lean = qp.x * alongside * 0.45; float lit = 0.0; for (int i = 0; i < 2; i++) { float phase = float(i) * 3.14159265; float x = q.x - lean - 0.34 * sin(q.y * 1.6 + t * 1.4 + phase); lit += 0.8 * falloff(x * x, 13.0); } float volume = falloff(q.x * q.x, 2.6); /* Compressed hard across the shaft and stretched along it. The same octaves sampled square read as haze; this way they read as filaments. */ float streak = fbm(vec2(q.x * 9.0, q.y * 0.55 - t * 2.4)); float haze = warped(vec2(q.x * 0.8 + 4.0, q.y * 0.45), t * 0.5); return lit * mix(0.35, 1.65, streak) + volume * 0.3 * mix(0.35, 1.05, haze); } /* A fan from a source above the frame, cut into filaments by angle. */ float rays(vec2 q, float t, vec2 qp, float reach) { /* The pointer occludes, rather than the source moving. Swinging the source re-aims the whole fan, which is a thing a light does and not a thing that happens when something passes in front of one, so the source is fixed and the pointer is a blocker. The shadow is angular, not radial: it is the wedge of directions the object covers as seen from the source, and it only exists further along than the object is. A fragment nearer the source than the blocker is in front of it and stays lit, which is the whole difference between an occluder and a dark patch that follows the cursor. */ vec2 o = vec2(0.0, 1.45); vec2 v = q - o; float dist = length(v); float ang = atan(v.x, -v.y); vec2 vp = qp - o; float distToBlocker = length(vp); float angToBlocker = atan(vp.x, -vp.y); /* Widened with distance, so the object reads as a fixed size rather than as a wedge that grows the further out it is carried. */ float halfAngle = 0.055 / max(distToBlocker, 0.35); float across = (ang - angToBlocker) / halfAngle; /* Cast from the near edge of the object rather than from somewhere past it. Ramping in over a fifth of a unit started the shadow below the thing supposedly casting it, which reads as two objects. */ float behind = smoothstep(-0.03, 0.05, dist - distToBlocker); float shadow = exp(-across * across) * behind * reach; float fan = fbm(vec2(ang * 8.0, dist * 0.5 - t * 1.8)); float dust = warped(vec2(ang * 2.0, dist * 0.7), t * 0.4); float spread = smoothstep(0.85, 0.05, abs(ang)); /* The fan is a population too, even though it is continuous rather than counted: how far apart its light and dark run is how much one ray differs from the next. Both ends travel toward the midpoint, so a variance of 0 is an even wash and a variance of 1 is exactly the pair this always had. */ float rv = clamp(uVariance, 0.0, 1.0); float lit = spread * mix(mix(0.85, 0.2, rv), mix(0.85, 1.5, rv), fan) * mix(0.75, 1.15, dust) * falloff(dot(v, v), 0.8); return lit * (1.0 - shadow * 0.82); } /* Curtains on a drifting horizon. */ float aurora(vec2 q, float t, vec2 qp, float reach) { /* The curtain waves harder where the pointer is, rather than being lifted by it. An aurora is a sheet with an amplitude, and a swell only raised the whole thing locally, which is a bump in a line. Multiplying the amplitude bends it instead: the folds deepen and the ridge travels further, which is what an aurora does when it is disturbed. */ float agitate = falloff((q.x - qp.x) * (q.x - qp.x), 1.5) * reach; /* A curtain stands. It does not hang. A falloff symmetric about the ridge is a band with a wavy center line, and vertical noise on a band is an equalizer. An aurora has a sharp lower border where the field lines reach their lowest and light that thins upward from it, so the top and the bottom cannot be the same function: cut hard below, drawn long above. Two of them, offset and at different depths, because a single sheet reads as a decal and what makes the real thing look like drapery is one curtain seen through another. */ float lit = 0.0; for (int i = 0; i < 2; i++) { float f = float(i); /* Two octaves, not a warp. A warp folds the domain back on itself and puts high frequencies in the ridge, and against a hard top edge every wiggle becomes a spike: the first pass came out a mountain range. What an aurora has is a long undulation with a few folds in it, which is what a low fundamental and one overtone give. */ float ridge = ((noise(vec2(q.x * (0.55 + f * 0.2) + f * 4.0, t * 0.3)) - 0.5) * 1.7 + (noise(vec2(q.x * (1.3 + f * 0.4) + f * 9.0, t * 0.45)) - 0.5) * 0.55) * (1.0 + agitate * 2.0) + f * 0.22; /* **The sheet stands on the ridge rather than hanging from it, and that inversion is the whole difference between an aurora and a landscape.** A hard edge with mass under it is a mountain silhouette: one unbroken ridge line across the frame with a striped solid below it. A curtain's lower border is its sharpest, brightest line, and what rises from it thins and breaks into rays. */ float depth = q.y - ridge; /* Sharp, but not a cliff: the edge is where the light stops, and a step narrower than a pixel is an aliased line rather than an edge. **The border's sharpness varies along it.** A line of even weight reads as drawn rather than lit however it wanders. A real lower border is hard where the sheet faces us and diffuse where it turns away, so the crispness is drawn from the same slow noise the folds are. */ float crisp = mix(0.04, 0.3, noise(vec2(q.x * 0.8 + f * 5.0, t * 0.15))); float edge = smoothstep(-crisp, crisp * 0.5, depth); /* Steep, so the curtain thins instead of filling everything above the border. At 1.5 it still stood at a fifth a whole unit up, which with the bundles over it painted a solid wall of stripes from the border to the top of the frame. A curtain is mostly the bottom of a curtain. */ float rise = exp(-max(depth, 0.0) * (2.9 + f * 1.1)); /* Stretched along the fall, so the striations are the vertical rays a curtain is actually made of rather than texture on a stripe. Not stretched harder than this: at 0.35 a ray held one value for its whole length, which is a picket fence however the bundles are shaped. */ float rays = fbm(vec2(q.x * (9.0 + f * 4.0), q.y * 0.9 - t * (2.2 + f))); /* A rim along the top edge. The brightest part of a curtain is the line where the field lines end, and without it the sheet fades in from nothing and reads as fog with stripes rather than as something with a boundary. Wide enough to see: at k = 26 this was two pixels and might as well not have been there. */ float rim = falloff(depth * depth, 7.0) * 0.45; /* **A curtain is not the width of the sky.** Running edge to edge is what made the ridge read as a horizon: a horizon is continuous and a curtain is not. Broken into lengths by a slow mask, with real gaps between them, it becomes some number of curtains rather than one long one. */ float lengths = smoothstep(0.2, 0.66, noise(vec2(q.x * 1.15 + f * 13.0, t * 0.12))); /* Rays clump. Straight fbm is evenly spaced striping, which is a picket fence whatever else is done to it, and the floor under it lit the sheet everywhere so the rays only ever read as texture on a solid band. Pushing the midtones apart leaves bright bundles with real gaps between them, and the low floor is what lets the gaps be gaps. */ float bundles = smoothstep(0.34, 0.86, rays); /* The curtain is a draped sheet, so it is bright where a fold turns edge on and nearly out where it faces us. A narrow range here is a band of even brightness, which is the other half of what made it read flat. */ float foldNoise = noise(vec2(q.x * 1.5 + f * 7.0, t * 0.25)); /* Bundles and folds are what a curtain is made many of, so both answer variance about their own midpoint: at 0 an even sheet, at 1 the clumping and the deep folds this was tuned to. */ float av = clamp(uVariance, 0.0, 1.0); float bundled = mix(mix(0.85, 0.1, av), mix(0.85, 1.6, av), bundles); float folded = mix(mix(0.775, 0.15, av), mix(0.775, 1.4, av), foldNoise); lit += edge * lengths * (rise * bundled * folded + rim) * (1.0 - f * 0.45); } return lit; } /* Light traveling in lanes, at speed. * * The one shape here that is about motion rather than about a form. Each lane * carries a trail with its own speed, length and phase, so the field reads as * something passing rather than something standing. * * The wrap is deliberately outside the frame. A trail restarting is a pop, so * the travel runs to plus and minus 1.8 while the visible field ends nearer * 1.2, and the reset happens where nobody is looking. * * The pointer is an obstacle: lanes bow around it and slow through it, the way * anything moving past a body does. Pushed outward in proportion to the offset * rather than by its sign, because a sign is a discontinuity and a * discontinuity in a displacement is a crease. */ float streaks(vec2 q, float t, vec2 qp, float reach) { float near = falloff(dot(q - qp, q - qp), 1.6) * reach; vec2 s = q + vec2((q.x - qp.x) * near * 1.1, 0.0); /* Three lanes are visited, not one, and that is the fix for a square edge. Each lane draws its speed, phase and length from a hash of its index, so every one of them is at a different point in its cycle. Sampled from the single lane a fragment falls in, the whole strip changes state at the boundary and the field acquires a hard vertical line at every lane edge: measured at 23 straight edges across the frame, and at a tilt of 90 the same count arrives horizontal, which is what says they belong to the shape's own axes rather than to the screen. Visiting the neighbors and weighting each by its own distance is the same answer particles already uses for the same reason. The weight reaches zero between centers, so nothing is left to step. **Few lanes, and not all of them carry anything.** A streak is an event, and a field of simultaneous events is a texture. A dead lane contributes nothing anywhere rather than being cut off somewhere, so it costs the three lane blend nothing. */ float laneF = s.x * 1.6; float base = floor(laneF); float lit = 0.0; /* Three lanes, which is what the widths support. The tail spreads to at most 0.15 and lanes sit 0.417 apart, so one lane out carries about a fiftieth and the lane that drops out of the visited set, two out, is around a ten millionth. That dropped lane is where a step could appear, and it is the whole reason this loop is not a single lane. */ for (int k = -1; k <= 1; k++) { float id = base + float(k); float within = laneF - (id + 0.5); /* Speed, length, width and spacing all scale from a common resting value, so a variance of 0 is a rank of equal streaks at one speed rather than a special case. Phase is never scaled: lanes sharing a phase march in lockstep, which is not a uniform population but a broken one. Clamped, and the upper half of that is not defensive tidying. These are mix() calls, so a value past 1 extrapolates rather than exaggerating: at 1.6 the length term reaches zero and beyond it goes negative, which makes exp(-behind / len) grow instead of decay and the lane becomes a full-height white bar. Measured at 2, the frame held 93 apparent lanes and six straight edges. There is nothing above 1 worth reaching, the resting spread already being fourfold in speed. */ float v = clamp(uVariance, 0.0, 1.0); float alive = step(0.42, hash(vec2(id, 97.0))); float speed = mix(1.3, 0.55 + hash(vec2(id, 11.0)) * 1.5, v); float phase = hash(vec2(id, 29.0)); /* Long enough that the trail dominates the head, which is what the shape is named for, and no longer: a plume that stays bright for two and a half units is a searchlight, and the frame is two units tall. */ float len = mix(0.85, 0.5 + hash(vec2(id, 47.0)) * 0.8, v); /* **The lane is periodic, not a single streak that is put back when it runs out.** With fract() over a fixed span of 3.6 a head left the top at 1.8 and was moved to -1.8, taking whatever of its tail was still on screen with it. That span is only off screen while the frame is small enough to sit inside it, and the visible half-extent is (|sin tilt| * aspect + |cos tilt|) * scale, which reaches 1.97 at a tilt of 45 and 2.0 at a scale of 2. Past that the removal happens in plain view, which is the popping. Making the field periodic in y removes the event rather than moving it out of sight: heads sit at every integer of u and the whole rank slides through, so there is no frame in which one is created or destroyed and no scale or tilt at which that becomes visible. The gap is the distance between successive heads in a lane, and it is what density is made of. It varies too: lanes alike in spacing read as a grid however much else about them differs, and it has to exceed the tail length, or a streak's tail reaches the next one's nose and the lane reads as a dashed line rather than as separate things traveling. */ float gap = mix(4.2, 3.2 + hash(vec2(id, 59.0)) * 2.2, v); float travel = phase + t * speed * 1.8 / gap; float u = s.y / gap - travel; float w = fract(u); /* **A streak is one solid, not a brightness curve times a fixed width.** Drawn separably it is a dart, and that is what these were: along the lane the light stopped over a fifth of a unit, across it the lane kept its full width the whole way, and the level sets of such a product meet at a point. Straight edges and an apex, which is a paper dart. So each term carries its own width. The tail leaves the head narrow and spreads as it falls behind, and the front is a round cap rather than a full-width wall cut off across the lane. Together that is a teardrop, which is the silhouette the thing actually has. */ float across = within / 1.6; /* Narrow across the lane. Lanes sit 0.417 apart and a sigma of 0.15 overlapped both neighbors, so the frame came out a blue wash with no streaks in it at all. Length against width is what makes a streak read as a streak: at a ratio near four these were stubby rods with rounded ends whichever way they tapered, and the fix for that is proportion rather than another change of profile. */ float wide = mix(0.045, 0.03 + hash(vec2(id, 71.0)) * 0.04, v); /* **A comet is a nucleus with a coma, and a plume behind it.** A round cap the width of the body with the body widening behind it draws a bulb on a shaft; tapering the front instead draws a hairline that reads as a scratch. The fault common to both is a head glow computed from the distance *in front of* a head only, which never puts a round bright thing at the head at all. The glow is centered on the head, measured to whichever head is nearer along the lane, so it is symmetric about the nucleus the way a coma is. */ float behind = (1.0 - w) * gap; float ahead = w * gap; float toHead = min(ahead, behind); float nose = wide * 0.5; float near2 = toHead * toHead + across * across; float nucleus = exp(-near2 / (2.0 * nose * nose)); float coma = exp(-near2 / (2.0 * (nose * 3.0) * (nose * 3.0))) * 0.42; /* The plume opens fast and fades fast, and is never as bright as the head. A tail that holds its brightness for its whole length is a streak of paint; what makes a comet read is that almost all of the light is in the first tenth of it. The onset is what keeps it continuous across a head. The plume exists only behind, so its value would step from nothing to something at the nucleus; ramping it in over a seventh of its own length puts that transition under the brightest part of the coma, where there is nothing left to see. */ float onset = 1.0 - exp(-behind / (0.15 * len)); /* **The straight edge was a min().** Clamping the plume's width with min(nose * (1 + behind * 3), 0.16) saturates at a constant value of behind, and a constant behind is a straight line across the lane: above it the width was fixed and below it grew, and the kink between the two drew a hard diagonal through the frame. A cap on a quantity that varies along one axis is a cap along a line, which is the one shape this thing must not have. An exponential approach reaches the same limit without ever having a corner, so there is no line to see anywhere. */ float spread = nose + (0.11 - nose) * (1.0 - exp(-behind * 1.3)); float tail = exp(-behind / len) * onset * exp(-(across * across) / (2.0 * spread * spread)) * 0.5; /* Slowed where the pointer is, which is the other half of an obstacle. */ float weight = mix(1.0, 0.5 + hash(vec2(id, 83.0)) * 0.95, v); lit += (nucleus + coma + tail) * alive * weight * mix(1.0, 0.45, near); } float grain = fbm(vec2(s.x * 3.0, s.y * 1.4 - t * 1.6)); return lit * mix(0.55, 1.35, grain) * falloff(dot(q, q), 0.42); } /* Broad bands rolling under a warp. * * The softest shape here, and the only one with no filaments in it at all: * where braid and threads are made of lines, this is made of gradients. The * warp is what stops it sliding: bands moved by phase alone travel sideways * like a barcode, and bands moved through a warped domain roll. * * The pointer drags them. Bowing the phase toward it bends the crests without * changing how many there are, which is what a surface does when something * pulls across it. */ float waves(vec2 q, float t, vec2 qp, float reach) { float pull = falloff(dot(q - qp, q - qp), 1.2) * reach; /* **The warp was eating the waves.** At an amplitude of 2.4 against a band frequency of 1.7 it swung the sine's argument by about four radians, which is more than a whole period: the wavefronts were destroyed rather than bent and what came out was turbulence. It read as marbled paper, and at a low scale it filled the frame with near white. A tenth of that bends the fronts and leaves them fronts. */ float warp = warped(vec2(q.x * 0.5, q.y * 0.4), t * 0.3) - 0.5; vec2 w = q + warp * 0.32; /* Two systems crossing, lit where they agree, which is what the shape is named for. **And the frequency has to be high enough to be a wave at all.** At 2.3 the period is 2.7 in shape units against a visible half height of 1, so less than one crest fitted across the frame: there was nothing periodic to see, and every version of this that looked like anything looked like it because the warp was making clouds. Around 8 puts five crests across the frame, which is a wave field rather than a single swell. The two are deliberately not harmonic. Close but unequal frequencies beat against each other, and the slow traveling interference that produces is the thing worth watching. */ float a = sin((w.y * 8.1 + w.x * 2.3) - t * 0.9 - pull * 1.4); float b = sin((w.y * 5.3 - w.x * 3.4) + t * 0.6 + pull * 1.0); /* **Added, these two make a lattice, not a wave field.** Equal amplitudes crossing at an angle give a regular grid of lobes, which came out as bubble wrap and got worse the more of it fitted on screen. Waves are lines. So one system carries the fronts and the other only varies their brightness along their length, which is what a real swell does: the crests stay crests and are taller in some places than others. */ float band = a * 0.5 + 0.5; band *= mix(0.45, 1.0, b * 0.5 + 0.5); float grain = warped(vec2(w.x * 1.2 + 6.0, w.y * 0.85), t * 0.28); return pow(band, 2.2) * mix(0.55, 1.2, grain) * falloff(dot(q, q), 0.4); } /* Luminous threads, woven to a pinch. * * Five curves at different frequencies and speeds, each a thin bright line * rather than a band: what makes this read as woven is that they cross, and * what makes it read as one object rather than five is the pinch, where every * amplitude narrows at once and the whole weave gathers. * * The pointer is the pinch, which is the interaction this shape is for. * Carrying it along drags the gathering point with it and the threads open * behind and close ahead. */ float threads(vec2 q, float t, vec2 qp, float reach) { float pinchAt = mix(0.0, qp.x, reach); /* Narrowed, not collapsed. At 0.88 the amplitude and the spacing both fell to a tenth and all seven threads landed on the same line, which is a bright blob rather than a gathering: what a pinch has to preserve is that there are still threads in it. */ float gather = 1.0 - falloff((q.x - pinchAt) * (q.x - pinchAt), 1.1) * 0.6; /* Seven, and every one of them the same thread. What makes a weave read as a weave is that the strands are alike and only their phase differs, which is also what makes it a wave rather than a wobble. Given its own amplitude, height, thickness and brightness from hashes, each becomes one of seven unrelated squiggles that happen to share a frame. So one amplitude, one thickness, one weight, one frequency, and the phases fanned evenly across a turn. Alike, they cross at regular intervals instead of at coincidences. */ float lit = 0.0; for (int i = 0; i < 7; i++) { float f = float(i); float phase = f * (6.2831853 / 7.0); /* Evenly spaced, and the spacing collapses at the pinch along with the amplitude: threads that only narrow still run parallel through it, where threads that also converge actually gather. It closes less than the amplitude does, so they draw together without stacking. */ float lane = (f - 3.0) * 0.17 * mix(1.0, gather, 0.65); float amp = 0.34 * gather; /* A second harmonic at a third the frequency and a third the amplitude, which bends the long run of each thread without breaking it into wiggles. Low and quiet is the difference between a wave with character and a wave with noise on it. */ float y = lane + sin(q.x * 1.5 + t * 0.55 + phase) * amp + sin(q.x * 0.5 - t * 0.3 + phase) * amp * 0.32; lit += falloff((q.y - y) * (q.y - y), 24.0); } /* Quiet grain. At the old depth it was texture on the threads rather than air around them, and a wave with grain on it is a wobble again. */ float grain = fbm(vec2(q.x * 2.0, q.y * 2.0 - t)); return lit * mix(0.85, 1.12, grain) * falloff(dot(q, q), 0.4); } /* Points adrift, and pushed aside. * * One per cell of a coarse lattice, jittered off center so the lattice never * shows, and drifting on its own small orbit. Nine cells are visited because a * point near a boundary has to be drawn by its neighbors too, which is what * stops them popping in and out at the seams. * * The pointer repels. Pushing along the offset rather than by its sign keeps * the displacement smooth, and it is the one response here where the reader * is plainly moving things rather than lighting them. */ float particles(vec2 q, float t, vec2 qp, float reach) { float lit = 0.0; vec2 cell = floor(q * 2.6); for (int j = -1; j <= 1; j++) { for (int i = -1; i <= 1; i++) { vec2 c = cell + vec2(float(i), float(j)); float a = hash(c); float b = hash(c + 17.3); float v = clamp(uVariance, 0.0, 1.0); vec2 at = (c + 0.5 + (vec2(a, b) - 0.5) * 0.85) / 2.6; at += vec2( sin(t * mix(1.0, 0.5 + a, v) + b * 6.28), cos(t * mix(1.0, 0.42 + b, v) + a * 6.28) ) * 0.07; vec2 away = at - qp; float push = falloff(dot(away, away), 2.2) * reach; at += away * push * 0.55; vec2 off = q - at; /* Size varies with the rest, or a field whose members already differed in brightness and in how fast they wandered is still made of one object repeated. Nine cells are visited here, so unlike a streak lane a mote may be broader as well as tighter without leaving an edge anywhere. */ float grade = mix(52.0, 52.0 * (0.62 + hash(c + 5.7) * 0.95), v); /* **Round, because a particle is round.** Stretching a mote along a direction drawn per mote is not motion, it is orientation, so a field of them reads as grains of rice pointing every which way rather than as particles at all. Motion, if it is ever wanted here, has to come from the direction each mote is actually traveling, which is the wander above rather than a hash. The halo stays. An order of magnitude wider and a tenth the strength, so a mote sits in the field rather than on top of it: a hard little disc on a dark ground is a pixel, and light has a neighborhood. */ float core = falloff(dot(off, off), grade); float halo = falloff(dot(off, off), grade * 0.16) * 0.1; lit += (core + halo) * mix(0.8, 0.35 + a * 0.9, v); } } return lit * falloff(dot(q, q), 0.4); } /* Ink in a viscous medium, drawn through by where the pointer went. * * **The wake is one stroke, not eight marks.** A vortex per trail point, * rotating the sample around its own center, draws eight discrete swirls fading * in place: a row of blobs rather than anything a hand had done. The marks are * a polyline. Distance is measured to the nearest *segment*, age is * interpolated along it, and what a reader sees is a continuous stroke however * far apart the samples fell. * * **And the displacement runs along the stroke, not around it.** Pushing the * sample down the tangent is advection: the pattern is carried the way the * hand went, which is what stirring looks like. Rotating it around a point is * a whirlpool, and eight whirlpools is a washing machine. * * The marks arrive oldest first, so consecutive indices are consecutive in * time. In the ring they are not, and a segment drawn across the wrap joined * the newest mark to the oldest with a bright bar through the middle of the * field. * * Nine, not eight: eight committed marks and the live head, which is held * outside the ring so that committing appends rather than overwrites. A * zero-length segment between the newest mark and the head is skipped below, * which is what makes the commit invisible. */ float flowWake(vec2 at, mat2 turn, vec2 shift, out vec2 push) { push = vec2(0.0); float wake = 0.0; for (int i = 0; i < 8; i++) { vec3 a = uTrail[i]; vec3 b = uTrail[i + 1]; if (a.z >= 1.0 || b.z >= 1.0) { continue; } /* The marks arrive in screen space, and the field is sampled after a rotation, a scale and a parallax shift. Compared without carrying them through the same three, the wake sits somewhere else entirely: at a tilt of 42 and a scale of 1.7 it landed most of a frame from the cursor, which is what read as offset rather than as lagging. */ vec2 pa = turn * (a.xy - shift) * uScale; vec2 pb = turn * (b.xy - shift) * uScale; vec2 ab = pb - pa; float len2 = dot(ab, ab); if (len2 < 1e-6) { continue; } float t = clamp(dot(at - pa, ab) / len2, 0.0, 1.0); vec2 off = at - (pa + ab * t); /* Elongated along the stroke, not round about it. A circular falloff makes the wake a tube of even thickness, which is what a pipe looks like and not what a stirred medium does. Ink pulled through a liquid smears the way the hand went: it reaches further along the direction of travel than across it. Weighting the along component down stretches the ellipse into that direction, and costs one dot product. */ vec2 tangent = ab * inversesqrt(len2); float alongOff = dot(off, tangent); float acrossOff = dot(off, vec2(-tangent.y, tangent.x)); float shaped = acrossOff * acrossOff + alongOff * alongOff * 0.3; /* Squared, so a wake thins as it dies rather than switching off. */ float life = 1.0 - mix(a.z, b.z, t); float w = life * life * falloff(shaped, 3.2); wake += w; push += (ab * inversesqrt(len2)) * w; } /* Limited, because an unbounded displacement folds and a fold is a crease. Where the map from sample to displaced sample stops being one to one, neighboring samples land on the same value and the pattern piles into a hard bright line: a caustic, which is what a sharp diagonal with a bright wedge beside it always is. The sum runs over eight segments of unit tangents, so near the stroke it reached several times the width of the falloff it was displacing across, and folded every time. The falloff is about a third of a unit wide, so the displacement has to stay under that for the map to stay monotone. Saturating rather than clamped: a clamp is a corner, and a corner in a displacement is the same crease by another route. */ float mag = length(push); push = mag > 0.0 ? push * (0.24 / (0.9 + mag)) : vec2(0.0); return wake; } float flow(vec2 q, float t, mat2 turn, vec2 shift) { vec2 push; float wake = flowWake(q, turn, shift, push); /* Already limited inside, so it is applied whole. */ vec2 p = q + push; /* The field is warped by itself twice over, which is what gives a liquid its folded look: each sample is displaced by the field's own value, so structure gathers into sheets rather than staying a cloud. Big shapes first, detail on top of them. At twice these frequencies every octave landed small enough that the frame filled edge to edge with fine crumple at one density: marbled stone, and it looked the same at every scale, gain and bloom because there was no large form for any of them to act on. Isotropic noise cannot make a current, so anything wanting one has to carry a direction, and that is a bigger change than this shape wants. */ float ink = warped(p * 0.55, t * 0.55); float sheet = warped(p * 1.1 + vec2(3.1, 1.7), t * 0.35); /* Voids, not an even swatch. With a floor under each term the lowest the field can reach is a quarter lit, so every fragment lands in the same middle band and there is no structure to look at. And a slow large mass over the top, thresholded rather than merely varied: a gentle modulation over a field that is lit everywhere is still lit everywhere, and what a liquid needs is somewhere it simply is not. The empty parts of the frame are what give the full parts a shape. */ float mass = smoothstep(0.24, 0.72, noise(vec2(p.x * 0.45 - t * 0.1, p.y * 0.4 + t * 0.07))); /* The void floor sits well above zero. At a low scale the mass noise varies less across the frame than the frame is wide, so a reader can sit inside one void and see almost nothing: measured at scale 0.5, which came out near black. Empty has to mean faint, not absent, or the shape can disappear entirely at a setting the property explicitly allows. */ float body = mix(0.05, 1.4, ink) * mix(0.4, 1.2, sheet) * mix(0.22, 1.45, mass); /* The resting state matters more than the stirred one: most readers never move the pointer, and a shape that is dark until touched is a shape most people never see. With the voids in, halving an untouched field takes it to a dim murk that reads as wet rock, so the wake lifts from a resting level that is already worth looking at. */ return body * (0.85 + clamp(wake, 0.0, 1.0) * 0.5) * falloff(dot(q, q), 0.55); } /* Light through moving water, focused into a web. * * **A caustic is where a wavefront folds, not where it is bright.** Sampling * a noise field and lighting the high parts gives clouds; what makes the * pattern on a pool floor is that the surface bends light and neighboring * rays land on the same spot. The cheap stand-in for that is the *zero* of a * displaced wave sum rather than its peak: 1 - |sum| is thin and bright * exactly where the sum crosses, which is the fold. * * abs() puts a derivative discontinuity along each of those lines, and here * that is the drawing rather than a fault: a caustic edge is genuinely sharp, * which is the whole reason it reads as light rather than as fog. */ float caustics(vec2 q, float t, vec2 qp, float reach) { /* The pointer is a stone in the water: the surface it disturbs is a ring of extra phase, so the web tightens around it rather than brightening. */ float dropD = length(q - qp); float drop = falloff(dropD * dropD, 1.4) * reach; float lit = 0.0; for (int i = 0; i < 3; i++) { float f = float(i); vec2 p = q * (1.5 + f * 0.55); /* Warped by the field's own value, so the wavefronts bend the way a surface does rather than sliding as a rigid pattern. */ vec2 w = p + vec2( noise(p * 1.25 + vec2(t * 0.42 + f * 4.0, 0.0)), noise(p * 1.1 - vec2(0.0, t * 0.36 + f * 7.0)) ) * 0.85; float sum = sin(w.x * 3.1 + t * 0.7 + drop * 6.0) + sin(w.y * 2.7 - t * 0.55 - drop * 5.0); float fold = 1.0 - clamp(abs(sum) * 0.5, 0.0, 1.0); lit += pow(fold, 7.0) * (1.0 - f * 0.22); } /* **The pool has a lit part and a dark part.** Three layers of folds at one density fill the frame edge to edge and every version of it looks the same: bright everywhere carries the same amount of information as dark everywhere. A slow depth over the top decides where the light is getting through, so the web has somewhere to be faint and the eye somewhere to start. */ float pool = smoothstep(0.2, 0.78, noise(vec2(q.x * 0.55 - t * 0.09, q.y * 0.5 + t * 0.06))); /* And the stone brightens what it disturbs as well as bending it, or the only evidence of a pointer is a phase shift nobody can see. */ float dim = warped(q * 0.7, t * 0.2); return lit * mix(0.55, 1.25, dim) * mix(0.25, 1.25, pool) * (1.0 + drop * 0.9) * falloff(dot(q, q), 0.4); } /* A lattice in perspective, lit at its nodes. * * **The one shape here that is not weather.** Everything else in this set is * a fluid, a light or a sky, and a design system is none of those: it is a * structure, regular, with the interesting part at the joints. So this is * drawn rather than simulated, and it is the only one with straight lines in * it on purpose. * * Perspective is a divide, not a matrix. Scaling the grid by 1 / (1 + depth) * makes the cells shrink with distance and the whole thing recede, which is * the entire trick and costs one division. */ float lattice(vec2 q, float t, vec2 qp, float reach) { /* The pointer lifts the plane nearest it, so the lattice crowds where a reader is looking. A structure answers by changing its spacing, which is the only thing about a lattice that can move without breaking it. */ vec2 away = q - qp; float near = falloff(dot(away, away), 1.8) * reach; /* **A ground plane, not a mirrored one.** Dividing by abs(p.y) makes two floors meeting along y = 0, which reads as a bowtie with a crease down the middle rather than as anything receding. Dividing by the distance below a horizon is the actual projection: one plane, cells shrinking as they approach it, and nothing above it to fold back. It travels as well, so the structure arrives rather than sitting still. Perspective and motion together are what say the plane is a plane. */ float below = max(1.45 - q.y, 0.14); float depth = 1.0 / below; vec2 g = vec2( q.x * depth * (2.3 + near * 1.4), depth * 1.6 + t * 0.28 ); vec2 cell = fract(g) - 0.5; /* The rails, thinner the further back they are, so the grid does not turn into a solid sheet at the horizon. */ float rail = min(abs(cell.x), abs(cell.y)); float lines = falloff(rail * rail, 24.0); /* The joints, which is where a lattice is actually a lattice, and brighter than the rails between them: a grid of even weight is graph paper. */ float node = falloff(dot(cell, cell), 26.0) * 2.1 * (0.6 + near * 0.9); /* Faded toward the horizon, or the cells pile into a solid sheet exactly where they are smallest. */ float far = smoothstep(0.0, 0.55, below) * smoothstep(3.4, 1.3, below); float dust = warped(q * 0.9, t * 0.25); return (lines * 0.5 + node) * far * mix(0.7, 1.2, dust) * falloff(dot(q, q), 0.42); } /* Cells, lit along the seams between them. * * **Provenance as a picture.** A Voronoi field is a set of territories, each * one everything nearer to its own point than to any other, and the drawing * is not the cells but the boundaries: where two claims meet is exactly * what Trace is about. Nothing here is lit except the disagreements. * * The wall is the second nearest distance minus the nearest, which is zero * on the boundary and grows into the interior. It is a genuine distance, so * it needs no epsilon and produces no crease. */ float fracture(vec2 q, float t, vec2 qp, float reach) { vec2 g = q * 2.1; vec2 base = floor(g); vec2 f = g - base; float first = 9.0; float second = 9.0; for (int j = -1; j <= 1; j++) { for (int i = -1; i <= 1; i++) { vec2 o = vec2(float(i), float(j)); vec2 id = base + o; /* Each site drifts on its own small orbit, so the seams breathe rather than the whole field sliding. */ float a = hash(id) * 6.2831853; float b = hash(id + 41.7) * 6.2831853; vec2 site = o + 0.5 + 0.42 * vec2(sin(t * 0.5 + a), cos(t * 0.42 + b)); /* Sites near the pointer are pushed away from it, which opens the cells there: a territory answers by giving ground. */ vec2 world = (id + 0.5) / 2.1; vec2 shove = world - qp; site += normalize(shove + 0.0001) * falloff(dot(shove, shove), 2.4) * reach * 0.5; float d = length(site - f); if (d < first) { second = first; first = d; } else if (d < second) { second = d; } } } float wall = second - first; float seam = falloff(wall * wall, 17.0); /* **Not every seam is lit the same.** At one brightness the field is a mosaic of equal cells, which is a texture; what makes a set of territories read is that some borders are contested and most are not. A slow field decides which, and the dark stretches give the lit ones a shape. */ float contested = smoothstep(0.24, 0.8, noise(vec2(q.x * 0.6 + t * 0.07, q.y * 0.55 - t * 0.05))); float grain = warped(q * 1.3, t * 0.3); return seam * mix(0.6, 1.25, grain) * mix(0.18, 1.3, contested) * falloff(dot(q, q), 0.42); } /* The sampling frame, the shape branch, and the ramp from field to color. */ void main() { vec2 p = uv * 2.0 - 1.0; float aspect = uResolution.x / max(uResolution.y, 1.0); p.x *= aspect; /* **No global response, parallax included.** Every named interaction is a promise about what the pointer does, and translating the whole field is not any of them: a consumer asking for a light asked for a light, not for a light and a lean. Small is not the test. What answers the pointer is each shape, in the terms it is made of, and the three generics for a consumer who asks for one. */ vec2 q = p; /* **The tilt is an angle on the screen, not in the sampling space.** p.x is stretched by the aspect before anything is rotated, so a rotation applied here is a rotation in an anisotropic space, which is a shear. Working it through: a feature along the shape's own x axis sits where q.y = 0, which is p.y = -tan(a) * p.x, and since p.x is aspect times the screen's x the slope a reader sees is aspect * tan(a). At a widescreen 1.78 a tilt of 14 drew at 23.9 degrees, and the same number drew differently in every box the component was put in. Solving aspect * tan(a) = tan(tilt) gives the angle to rotate by. The two argument atan is what makes 90 work: tan blows up there, while atan(1, 0) is exactly a quarter turn. */ float want = radians(uTilt); float a = atan(sin(want), cos(want) * aspect); mat2 turn = mat2(cos(a), sin(a), -sin(a), cos(a)); q = turn * q * uScale; /* The pointer, carried into the same frame the shapes are drawn in, so each one can answer it in its own terms. */ vec2 qp = turn * uPointer * uScale; float t = uTime * 0.06; /* A shape answers for itself only when nothing else has been asked for. Given a generic response, its own is switched off rather than layered under: two answers to one gesture is how a shared zoom and a shared glow got here in the first place, applied identically to every shape and saying nothing about any of them. */ float own = uInteraction == 0 ? uReach : 0.0; float d = 0.0; if (uShape == 0) { d = braid(q, t, qp, own); } else if (uShape == 1) { d = rays(q, t, qp, own); } else if (uShape == 2) { d = aurora(q, t, qp, own); } else if (uShape == 3) { d = streaks(q, t, qp, own); } else if (uShape == 4) { d = waves(q, t, qp, own); } else if (uShape == 5) { d = threads(q, t, qp, own); } else if (uShape == 6) { d = particles(q, t, qp, own); } else if (uShape == 7) { d = flow(q, t, turn, uPointer * 0.05 * uReach); } else if (uShape == 8) { d = caustics(q, t, qp, own); } else if (uShape == 9) { d = lattice(q, t, qp, own); } else { d = fracture(q, t, qp, own); } /* Bloom as an exponent on the field rather than a term inside each shape. Every shape is a density that falls off, so a power tightens or spreads that falloff and one control means the same thing in all of them. */ d = pow(max(d, 0.0), mix(2.0, 0.5, clamp(uBloom, 0.0, 1.0))) * uGain; /* The generic answers, for a consumer who has asked for one. All three are functions of distance from the pointer and nothing else, which is exactly what makes them generic and exactly what makes them say less. The light option lifts what is lit and puts a floor under itself so there is something at the cursor over a dark field. The shadow option is that gesture inverted, which suits a bright field where a lamp would be invisible. The ripple option is the one with a clock in it, so it keeps moving after the pointer has stopped. */ if (uInteraction != 0 && uInteraction != 4) { vec2 toCursor = p - uPointer; float near = falloff(dot(toCursor, toCursor), 3.0) * uReach; if (uInteraction == 1) { d = d * (1.0 + near * 1.2) + near * 0.08; } else if (uInteraction == 2) { d *= 1.0 - near * 0.85; } else { float rings = sin(length(toCursor) * 13.0 - uTime * 3.2); d *= 1.0 + rings * near * 0.75; } } d *= mix(0.65, 1.35, smoothstep(-1.0, 1.0, q.y)); /* Rolled off rather than clipped. A hard clamp flattens the core into a plateau of one value, which is what makes a bright shader look like a sticker; this compresses toward one and keeps the gradient inside it. */ float lift = 1.0 - exp(-max(d, 0.0) * 1.15); vec3 rgb = mix(uGround, uHaze, smoothstep(0.0, 0.16, lift)); rgb = mix(rgb, uGlow, smoothstep(0.10, 0.46, lift)); rgb = mix(rgb, uPeak, smoothstep(0.40, 0.72, lift) * 0.6); /* Dithered before the buffer quantizes. A falloff this soft spans a few hundred 8-bit steps across a screen, and without a sub-step of noise it paints as visible rings. */ float dither = (hash(gl_FragCoord.xy) - 0.5) / 255.0; color = vec4(rgb + dither, 1.0); } `",
5329
+ "default": "`#version 300 es precision highp float; in vec2 uv; out vec4 color; uniform vec2 uResolution; uniform float uTime; uniform int uShape; uniform vec2 uPointer; uniform float uReach; uniform int uInteraction; uniform float uTilt; uniform float uScale; uniform float uBloom; uniform float uGain; /* How much the members of a population differ from one another, 1 being the resting amount and 0 a set that is uniform. Read by six of the eleven shapes, the ones made of many similar things, whether those are counted or continuous: streaks has lanes, particles has motes, rays has a fan, aurora has bundles and folds, caustics has three depths of fold and a pool over them, and fracture has territories and the borders between them. The five that do not read it refuse it. Braid, waves and flow have a single subject and nothing to vary. Threads and lattice are even on purpose, a scatter of strands and a regular structure each being what it is because its members do not differ. */ uniform float uVariance; /* Where the pointer has been, as x, y and age. Age runs 0 at the moment it was left to 1 when it has faded, so a spent slot costs a compare. **This is what a fluid gets from a framebuffer, bought for eight vec3s.** A real solver advects a dye texture and reads it back the next frame, which needs render targets, a ping-pong pair and about six passes. What a reader actually perceives from that is one thing: the cursor leaves a wake that outlives it. A short history of where it went reproduces that in the single pass this component already makes, at a fixed cost that does not grow with how long the page has been open. What it cannot do is conserve anything, so there is no diffusion and no pressure: the wake decays rather than spreading. Worth knowing before this is asked to be a simulation. */ uniform vec3 uTrail[9]; uniform vec3 uGround; uniform vec3 uHaze; uniform vec3 uGlow; uniform vec3 uPeak; float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); } float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); /* Quintic rather than cubic. Cubic smoothstep leaves a second derivative discontinuity at every cell edge, which four octaves of lighting turn into a faint square lattice: visible on a large soft field and exactly the artifact that reads as cheap. */ vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); return mix( mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y ); } /* Each octave is rotated as well as scaled. Doubling alone stacks every octave on the same axes, so value noise keeps its grid however many are summed. An irrational angle means no two octaves ever line up again. */ const mat2 TURN = mat2(0.8, 0.6, -0.6, 0.8); float fbm(vec2 p) { float sum = 0.0; float amp = 0.5; for (int i = 0; i < 5; i++) { sum += amp * noise(p); p = TURN * p * 2.03; amp *= 0.5; } return sum; } /* Domain warping: the field is sampled at coordinates another field bends. This is the single largest difference between noise that reads as organic and noise that reads as television static, and it costs two more fbm calls rather than a different algorithm. */ float warped(vec2 p, float t) { vec2 q = vec2(fbm(p + vec2(0.0, t)), fbm(p + vec2(5.2, 1.3 - t))); vec2 r = vec2(fbm(p + 4.0 * q + vec2(1.7, 9.2)), fbm(p + 4.0 * q + vec2(8.3, 2.8))); return fbm(p + 4.0 * r); } /* An exponential falloff without the corner. * * Both abs and length have a vertex at zero and their derivative jumps * across it, so a falloff built on either acquires a hard line exactly along * the axis it is brightest on. That is a crease in something meant to be * light, and it is what put a chevron through the braid wherever the * pointer's height crossed a strand. * * Softening the distance rather than the exponent keeps the tails identical * and rounds only the point. The softening scales with the rate, so a tight * falloff is rounded tightly and a broad one broadly: at zero the result is * exp(-0.06) whatever the rate is, six per cent off the peak, which is not * visible as flattening. * * Takes the squared distance, because every caller already has one and a * square root taken here is the one that would have reintroduced the vertex. */ float falloff(float d2, float k) { float soft = 0.06 / k; return exp(-sqrt(d2 + soft * soft) * k); } /* Two strands half a turn apart, inside a wider volume. */ float braid(vec2 q, float t, vec2 qp, float reach) { /* The strands lean toward the pointer where it is alongside them, and are unmoved where it is not. A braid is a thing with a length, so what it can answer with is a bend at a height, not a bulge at a point. */ float alongside = falloff((q.y - qp.y) * (q.y - qp.y), 1.1) * reach; float lean = qp.x * alongside * 0.45; float lit = 0.0; for (int i = 0; i < 2; i++) { float phase = float(i) * 3.14159265; float x = q.x - lean - 0.34 * sin(q.y * 1.6 + t * 1.4 + phase); lit += 0.8 * falloff(x * x, 13.0); } float volume = falloff(q.x * q.x, 2.6); /* Compressed hard across the shaft and stretched along it. The same octaves sampled square read as haze; this way they read as filaments. */ float streak = fbm(vec2(q.x * 9.0, q.y * 0.55 - t * 2.4)); float haze = warped(vec2(q.x * 0.8 + 4.0, q.y * 0.45), t * 0.5); return lit * mix(0.35, 1.65, streak) + volume * 0.3 * mix(0.35, 1.05, haze); } /* A fan from a source above the frame, cut into filaments by angle. */ float rays(vec2 q, float t, vec2 qp, float reach) { /* The pointer occludes, rather than the source moving. Swinging the source re-aims the whole fan, which is a thing a light does and not a thing that happens when something passes in front of one, so the source is fixed and the pointer is a blocker. The shadow is angular, not radial: it is the wedge of directions the object covers as seen from the source, and it only exists further along than the object is. A fragment nearer the source than the blocker is in front of it and stays lit, which is the whole difference between an occluder and a dark patch that follows the cursor. */ vec2 o = vec2(0.0, 1.45); vec2 v = q - o; float dist = length(v); float ang = atan(v.x, -v.y); vec2 vp = qp - o; float distToBlocker = length(vp); float angToBlocker = atan(vp.x, -vp.y); /* Widened with distance, so the object reads as a fixed size rather than as a wedge that grows the further out it is carried. */ float halfAngle = 0.055 / max(distToBlocker, 0.35); float across = (ang - angToBlocker) / halfAngle; /* Cast from the near edge of the object rather than from somewhere past it. Ramping in over a fifth of a unit started the shadow below the thing supposedly casting it, which reads as two objects. */ float behind = smoothstep(-0.03, 0.05, dist - distToBlocker); float shadow = exp(-across * across) * behind * reach; float fan = fbm(vec2(ang * 8.0, dist * 0.5 - t * 1.8)); float dust = warped(vec2(ang * 2.0, dist * 0.7), t * 0.4); float spread = smoothstep(0.85, 0.05, abs(ang)); /* The fan is a population too, even though it is continuous rather than counted: how far apart its light and dark run is how much one ray differs from the next. Both ends travel toward the midpoint, so a variance of 0 is an even wash and a variance of 1 is exactly the pair this always had. */ float rv = clamp(uVariance, 0.0, 1.0); float lit = spread * mix(mix(0.85, 0.2, rv), mix(0.85, 1.5, rv), fan) * mix(0.75, 1.15, dust) * falloff(dot(v, v), 0.8); return lit * (1.0 - shadow * 0.82); } /* Curtains on a drifting horizon. */ float aurora(vec2 q, float t, vec2 qp, float reach) { /* The curtain waves harder where the pointer is, rather than being lifted by it. An aurora is a sheet with an amplitude, and a swell only raised the whole thing locally, which is a bump in a line. Multiplying the amplitude bends it instead: the folds deepen and the ridge travels further, which is what an aurora does when it is disturbed. */ float agitate = falloff((q.x - qp.x) * (q.x - qp.x), 1.5) * reach; /* A curtain stands. It does not hang. A falloff symmetric about the ridge is a band with a wavy center line, and vertical noise on a band is an equalizer. An aurora has a sharp lower border where the field lines reach their lowest and light that thins upward from it, so the top and the bottom cannot be the same function: cut hard below, drawn long above. Two of them, offset and at different depths, because a single sheet reads as a decal and what makes the real thing look like drapery is one curtain seen through another. */ float lit = 0.0; for (int i = 0; i < 2; i++) { float f = float(i); /* Two octaves, not a warp. A warp folds the domain back on itself and puts high frequencies in the ridge, and against a hard top edge every wiggle becomes a spike: the first pass came out a mountain range. What an aurora has is a long undulation with a few folds in it, which is what a low fundamental and one overtone give. */ float ridge = ((noise(vec2(q.x * (0.55 + f * 0.2) + f * 4.0, t * 0.3)) - 0.5) * 1.7 + (noise(vec2(q.x * (1.3 + f * 0.4) + f * 9.0, t * 0.45)) - 0.5) * 0.55) * (1.0 + agitate * 2.0) + f * 0.22; /* **The sheet stands on the ridge rather than hanging from it, and that inversion is the whole difference between an aurora and a landscape.** A hard edge with mass under it is a mountain silhouette: one unbroken ridge line across the frame with a striped solid below it. A curtain's lower border is its sharpest, brightest line, and what rises from it thins and breaks into rays. */ float depth = q.y - ridge; /* Sharp, but not a cliff: the edge is where the light stops, and a step narrower than a pixel is an aliased line rather than an edge. **The border's sharpness varies along it.** A line of even weight reads as drawn rather than lit however it wanders. A real lower border is hard where the sheet faces us and diffuse where it turns away, so the crispness is drawn from the same slow noise the folds are. */ float crisp = mix(0.04, 0.3, noise(vec2(q.x * 0.8 + f * 5.0, t * 0.15))); float edge = smoothstep(-crisp, crisp * 0.5, depth); /* Steep, so the curtain thins instead of filling everything above the border. At 1.5 it still stood at a fifth a whole unit up, which with the bundles over it painted a solid wall of stripes from the border to the top of the frame. A curtain is mostly the bottom of a curtain. */ float rise = exp(-max(depth, 0.0) * (2.9 + f * 1.1)); /* Stretched along the fall, so the striations are the vertical rays a curtain is actually made of rather than texture on a stripe. Not stretched harder than this: at 0.35 a ray held one value for its whole length, which is a picket fence however the bundles are shaped. */ float rays = fbm(vec2(q.x * (9.0 + f * 4.0), q.y * 0.9 - t * (2.2 + f))); /* A rim along the top edge. The brightest part of a curtain is the line where the field lines end, and without it the sheet fades in from nothing and reads as fog with stripes rather than as something with a boundary. Wide enough to see: at k = 26 this was two pixels and might as well not have been there. */ float rim = falloff(depth * depth, 7.0) * 0.45; /* **A curtain is not the width of the sky.** Running edge to edge is what made the ridge read as a horizon: a horizon is continuous and a curtain is not. Broken into lengths by a slow mask, with real gaps between them, it becomes some number of curtains rather than one long one. */ float lengths = smoothstep(0.2, 0.66, noise(vec2(q.x * 1.15 + f * 13.0, t * 0.12))); /* Rays clump. Straight fbm is evenly spaced striping, which is a picket fence whatever else is done to it, and the floor under it lit the sheet everywhere so the rays only ever read as texture on a solid band. Pushing the midtones apart leaves bright bundles with real gaps between them, and the low floor is what lets the gaps be gaps. */ float bundles = smoothstep(0.34, 0.86, rays); /* The curtain is a draped sheet, so it is bright where a fold turns edge on and nearly out where it faces us. A narrow range here is a band of even brightness, which is the other half of what made it read flat. */ float foldNoise = noise(vec2(q.x * 1.5 + f * 7.0, t * 0.25)); /* Bundles and folds are what a curtain is made many of, so both answer variance about their own midpoint: at 0 an even sheet, at 1 the clumping and the deep folds this was tuned to. */ float av = clamp(uVariance, 0.0, 1.0); float bundled = mix(mix(0.85, 0.1, av), mix(0.85, 1.6, av), bundles); float folded = mix(mix(0.775, 0.15, av), mix(0.775, 1.4, av), foldNoise); lit += edge * lengths * (rise * bundled * folded + rim) * (1.0 - f * 0.45); } return lit; } /* Light traveling in lanes, at speed. * * The one shape here that is about motion rather than about a form. Each lane * carries a trail with its own speed, length and phase, so the field reads as * something passing rather than something standing. * * The wrap is deliberately outside the frame. A trail restarting is a pop, so * the travel runs to plus and minus 1.8 while the visible field ends nearer * 1.2, and the reset happens where nobody is looking. * * The pointer is an obstacle: lanes bow around it and slow through it, the way * anything moving past a body does. Pushed outward in proportion to the offset * rather than by its sign, because a sign is a discontinuity and a * discontinuity in a displacement is a crease. */ float streaks(vec2 q, float t, vec2 qp, float reach) { float near = falloff(dot(q - qp, q - qp), 1.6) * reach; vec2 s = q + vec2((q.x - qp.x) * near * 1.1, 0.0); /* Three lanes are visited, not one, and that is the fix for a square edge. Each lane draws its speed, phase and length from a hash of its index, so every one of them is at a different point in its cycle. Sampled from the single lane a fragment falls in, the whole strip changes state at the boundary and the field acquires a hard vertical line at every lane edge: measured at 23 straight edges across the frame, and at a tilt of 90 the same count arrives horizontal, which is what says they belong to the shape's own axes rather than to the screen. Visiting the neighbors and weighting each by its own distance is the same answer particles already uses for the same reason. The weight reaches zero between centers, so nothing is left to step. **Few lanes, and not all of them carry anything.** A streak is an event, and a field of simultaneous events is a texture. A dead lane contributes nothing anywhere rather than being cut off somewhere, so it costs the three lane blend nothing. */ float laneF = s.x * 1.6; float base = floor(laneF); float lit = 0.0; /* Three lanes, which is what the widths support. The tail spreads to at most 0.15 and lanes sit 0.417 apart, so one lane out carries about a fiftieth and the lane that drops out of the visited set, two out, is around a ten millionth. That dropped lane is where a step could appear, and it is the whole reason this loop is not a single lane. */ for (int k = -1; k <= 1; k++) { float id = base + float(k); float within = laneF - (id + 0.5); /* Speed, length, width and spacing all scale from a common resting value, so a variance of 0 is a rank of equal streaks at one speed rather than a special case. Phase is never scaled: lanes sharing a phase march in lockstep, which is not a uniform population but a broken one. Clamped, and the upper half of that is not defensive tidying. These are mix() calls, so a value past 1 extrapolates rather than exaggerating: at 1.6 the length term reaches zero and beyond it goes negative, which makes exp(-behind / len) grow instead of decay and the lane becomes a full-height white bar. Measured at 2, the frame held 93 apparent lanes and six straight edges. There is nothing above 1 worth reaching, the resting spread already being fourfold in speed. */ float v = clamp(uVariance, 0.0, 1.0); float alive = step(0.42, hash(vec2(id, 97.0))); float speed = mix(1.3, 0.55 + hash(vec2(id, 11.0)) * 1.5, v); float phase = hash(vec2(id, 29.0)); /* Long enough that the trail dominates the head, which is what the shape is named for, and no longer: a plume that stays bright for two and a half units is a searchlight, and the frame is two units tall. */ float len = mix(0.85, 0.5 + hash(vec2(id, 47.0)) * 0.8, v); /* **The lane is periodic, not a single streak that is put back when it runs out.** With fract() over a fixed span of 3.6 a head left the top at 1.8 and was moved to -1.8, taking whatever of its tail was still on screen with it. That span is only off screen while the frame is small enough to sit inside it, and the visible half-extent is (|sin tilt| * aspect + |cos tilt|) * scale, which reaches 1.97 at a tilt of 45 and 2.0 at a scale of 2. Past that the removal happens in plain view, which is the popping. Making the field periodic in y removes the event rather than moving it out of sight: heads sit at every integer of u and the whole rank slides through, so there is no frame in which one is created or destroyed and no scale or tilt at which that becomes visible. The gap is the distance between successive heads in a lane, and it is what density is made of. It varies too: lanes alike in spacing read as a grid however much else about them differs, and it has to exceed the tail length, or a streak's tail reaches the next one's nose and the lane reads as a dashed line rather than as separate things traveling. */ float gap = mix(4.2, 3.2 + hash(vec2(id, 59.0)) * 2.2, v); float travel = phase + t * speed * 1.8 / gap; float u = s.y / gap - travel; float w = fract(u); /* **A streak is one solid, not a brightness curve times a fixed width.** Drawn separably it is a dart, and that is what these were: along the lane the light stopped over a fifth of a unit, across it the lane kept its full width the whole way, and the level sets of such a product meet at a point. Straight edges and an apex, which is a paper dart. So each term carries its own width. The tail leaves the head narrow and spreads as it falls behind, and the front is a round cap rather than a full-width wall cut off across the lane. Together that is a teardrop, which is the silhouette the thing actually has. */ float across = within / 1.6; /* Narrow across the lane. Lanes sit 0.417 apart and a sigma of 0.15 overlapped both neighbors, so the frame came out a blue wash with no streaks in it at all. Length against width is what makes a streak read as a streak: at a ratio near four these were stubby rods with rounded ends whichever way they tapered, and the fix for that is proportion rather than another change of profile. */ float wide = mix(0.045, 0.03 + hash(vec2(id, 71.0)) * 0.04, v); /* **A comet is a nucleus with a coma, and a plume behind it.** A round cap the width of the body with the body widening behind it draws a bulb on a shaft; tapering the front instead draws a hairline that reads as a scratch. The fault common to both is a head glow computed from the distance *in front of* a head only, which never puts a round bright thing at the head at all. The glow is centered on the head, measured to whichever head is nearer along the lane, so it is symmetric about the nucleus the way a coma is. */ float behind = (1.0 - w) * gap; float ahead = w * gap; float toHead = min(ahead, behind); float nose = wide * 0.5; float near2 = toHead * toHead + across * across; float nucleus = exp(-near2 / (2.0 * nose * nose)); float coma = exp(-near2 / (2.0 * (nose * 3.0) * (nose * 3.0))) * 0.42; /* The plume opens fast and fades fast, and is never as bright as the head. A tail that holds its brightness for its whole length is a streak of paint; what makes a comet read is that almost all of the light is in the first tenth of it. The onset is what keeps it continuous across a head. The plume exists only behind, so its value would step from nothing to something at the nucleus; ramping it in over a seventh of its own length puts that transition under the brightest part of the coma, where there is nothing left to see. */ float onset = 1.0 - exp(-behind / (0.15 * len)); /* **The straight edge was a min().** Clamping the plume's width with min(nose * (1 + behind * 3), 0.16) saturates at a constant value of behind, and a constant behind is a straight line across the lane: above it the width was fixed and below it grew, and the kink between the two drew a hard diagonal through the frame. A cap on a quantity that varies along one axis is a cap along a line, which is the one shape this thing must not have. An exponential approach reaches the same limit without ever having a corner, so there is no line to see anywhere. */ float spread = nose + (0.11 - nose) * (1.0 - exp(-behind * 1.3)); float tail = exp(-behind / len) * onset * exp(-(across * across) / (2.0 * spread * spread)) * 0.5; /* Slowed where the pointer is, which is the other half of an obstacle. */ float weight = mix(1.0, 0.5 + hash(vec2(id, 83.0)) * 0.95, v); lit += (nucleus + coma + tail) * alive * weight * mix(1.0, 0.45, near); } float grain = fbm(vec2(s.x * 3.0, s.y * 1.4 - t * 1.6)); return lit * mix(0.55, 1.35, grain) * falloff(dot(q, q), 0.42); } /* Broad bands rolling under a warp. * * The softest shape here, and the only one with no filaments in it at all: * where braid and threads are made of lines, this is made of gradients. The * warp is what stops it sliding: bands moved by phase alone travel sideways * like a barcode, and bands moved through a warped domain roll. * * The pointer drags them. Bowing the phase toward it bends the crests without * changing how many there are, which is what a surface does when something * pulls across it. */ float waves(vec2 q, float t, vec2 qp, float reach) { float pull = falloff(dot(q - qp, q - qp), 1.2) * reach; /* **The warp was eating the waves.** At an amplitude of 2.4 against a band frequency of 1.7 it swung the sine's argument by about four radians, which is more than a whole period: the wavefronts were destroyed rather than bent and what came out was turbulence. It read as marbled paper, and at a low scale it filled the frame with near white. A tenth of that bends the fronts and leaves them fronts. */ float warp = warped(vec2(q.x * 0.5, q.y * 0.4), t * 0.3) - 0.5; vec2 w = q + warp * 0.32; /* Two systems crossing, lit where they agree, which is what the shape is named for. **And the frequency has to be high enough to be a wave at all.** At 2.3 the period is 2.7 in shape units against a visible half height of 1, so less than one crest fitted across the frame: there was nothing periodic to see, and every version of this that looked like anything looked like it because the warp was making clouds. Around 8 puts five crests across the frame, which is a wave field rather than a single swell. The two are deliberately not harmonic. Close but unequal frequencies beat against each other, and the slow traveling interference that produces is the thing worth watching. */ float a = sin((w.y * 8.1 + w.x * 2.3) - t * 0.9 - pull * 1.4); float b = sin((w.y * 5.3 - w.x * 3.4) + t * 0.6 + pull * 1.0); /* **Added, these two make a lattice, not a wave field.** Equal amplitudes crossing at an angle give a regular grid of lobes, which came out as bubble wrap and got worse the more of it fitted on screen. Waves are lines. So one system carries the fronts and the other only varies their brightness along their length, which is what a real swell does: the crests stay crests and are taller in some places than others. */ float band = a * 0.5 + 0.5; band *= mix(0.45, 1.0, b * 0.5 + 0.5); float grain = warped(vec2(w.x * 1.2 + 6.0, w.y * 0.85), t * 0.28); return pow(band, 2.2) * mix(0.55, 1.2, grain) * falloff(dot(q, q), 0.4); } /* Luminous threads, woven to a pinch. * * Five curves at different frequencies and speeds, each a thin bright line * rather than a band: what makes this read as woven is that they cross, and * what makes it read as one object rather than five is the pinch, where every * amplitude narrows at once and the whole weave gathers. * * The pointer is the pinch, which is the interaction this shape is for. * Carrying it along drags the gathering point with it and the threads open * behind and close ahead. */ float threads(vec2 q, float t, vec2 qp, float reach) { float pinchAt = mix(0.0, qp.x, reach); /* Narrowed, not collapsed. At 0.88 the amplitude and the spacing both fell to a tenth and all seven threads landed on the same line, which is a bright blob rather than a gathering: what a pinch has to preserve is that there are still threads in it. */ float gather = 1.0 - falloff((q.x - pinchAt) * (q.x - pinchAt), 1.1) * 0.6; /* Seven, and every one of them the same thread. What makes a weave read as a weave is that the strands are alike and only their phase differs, which is also what makes it a wave rather than a wobble. Given its own amplitude, height, thickness and brightness from hashes, each becomes one of seven unrelated squiggles that happen to share a frame. So one amplitude, one thickness, one weight, one frequency, and the phases fanned evenly across a turn. Alike, they cross at regular intervals instead of at coincidences. */ float lit = 0.0; for (int i = 0; i < 7; i++) { float f = float(i); float phase = f * (6.2831853 / 7.0); /* Evenly spaced, and the spacing collapses at the pinch along with the amplitude: threads that only narrow still run parallel through it, where threads that also converge actually gather. It closes less than the amplitude does, so they draw together without stacking. */ float lane = (f - 3.0) * 0.17 * mix(1.0, gather, 0.65); float amp = 0.34 * gather; /* A second harmonic at a third the frequency and a third the amplitude, which bends the long run of each thread without breaking it into wiggles. Low and quiet is the difference between a wave with character and a wave with noise on it. */ float y = lane + sin(q.x * 1.5 + t * 0.55 + phase) * amp + sin(q.x * 0.5 - t * 0.3 + phase) * amp * 0.32; lit += falloff((q.y - y) * (q.y - y), 24.0); } /* Quiet grain. At the old depth it was texture on the threads rather than air around them, and a wave with grain on it is a wobble again. */ float grain = fbm(vec2(q.x * 2.0, q.y * 2.0 - t)); return lit * mix(0.85, 1.12, grain) * falloff(dot(q, q), 0.4); } /* Points adrift, and pushed aside. * * One per cell of a coarse lattice, jittered off center so the lattice never * shows, and drifting on its own small orbit. Nine cells are visited because a * point near a boundary has to be drawn by its neighbors too, which is what * stops them popping in and out at the seams. * * The pointer repels. Pushing along the offset rather than by its sign keeps * the displacement smooth, and it is the one response here where the reader * is plainly moving things rather than lighting them. */ float particles(vec2 q, float t, vec2 qp, float reach) { float lit = 0.0; vec2 cell = floor(q * 2.6); for (int j = -1; j <= 1; j++) { for (int i = -1; i <= 1; i++) { vec2 c = cell + vec2(float(i), float(j)); float a = hash(c); float b = hash(c + 17.3); float v = clamp(uVariance, 0.0, 1.0); vec2 at = (c + 0.5 + (vec2(a, b) - 0.5) * 0.85) / 2.6; at += vec2( sin(t * mix(1.0, 0.5 + a, v) + b * 6.28), cos(t * mix(1.0, 0.42 + b, v) + a * 6.28) ) * 0.07; vec2 away = at - qp; float push = falloff(dot(away, away), 2.2) * reach; at += away * push * 0.55; vec2 off = q - at; /* Size varies with the rest, or a field whose members already differed in brightness and in how fast they wandered is still made of one object repeated. Nine cells are visited here, so unlike a streak lane a mote may be broader as well as tighter without leaving an edge anywhere. */ float grade = mix(52.0, 52.0 * (0.62 + hash(c + 5.7) * 0.95), v); /* **Round, because a particle is round.** Stretching a mote along a direction drawn per mote is not motion, it is orientation, so a field of them reads as grains of rice pointing every which way rather than as particles at all. Motion, if it is ever wanted here, has to come from the direction each mote is actually traveling, which is the wander above rather than a hash. The halo stays. An order of magnitude wider and a tenth the strength, so a mote sits in the field rather than on top of it: a hard little disc on a dark ground is a pixel, and light has a neighborhood. */ float core = falloff(dot(off, off), grade); float halo = falloff(dot(off, off), grade * 0.16) * 0.1; lit += (core + halo) * mix(0.8, 0.35 + a * 0.9, v); } } return lit * falloff(dot(q, q), 0.4); } /* Ink in a viscous medium, drawn through by where the pointer went. * * **The wake is one stroke, not eight marks.** A vortex per trail point, * rotating the sample around its own center, draws eight discrete swirls fading * in place: a row of blobs rather than anything a hand had done. The marks are * a polyline. Distance is measured to the nearest *segment*, age is * interpolated along it, and what a reader sees is a continuous stroke however * far apart the samples fell. * * **And the displacement runs along the stroke, not around it.** Pushing the * sample down the tangent is advection: the pattern is carried the way the * hand went, which is what stirring looks like. Rotating it around a point is * a whirlpool, and eight whirlpools is a washing machine. * * The marks arrive oldest first, so consecutive indices are consecutive in * time. In the ring they are not, and a segment drawn across the wrap joined * the newest mark to the oldest with a bright bar through the middle of the * field. * * Nine, not eight: eight committed marks and the live head, which is held * outside the ring so that committing appends rather than overwrites. A * zero-length segment between the newest mark and the head is skipped below, * which is what makes the commit invisible. */ float flowWake(vec2 at, mat2 turn, vec2 shift, out vec2 push) { push = vec2(0.0); float wake = 0.0; for (int i = 0; i < 8; i++) { vec3 a = uTrail[i]; vec3 b = uTrail[i + 1]; if (a.z >= 1.0 || b.z >= 1.0) { continue; } /* The marks arrive in screen space, and the field is sampled after a rotation, a scale and a parallax shift. Compared without carrying them through the same three, the wake sits somewhere else entirely: at a tilt of 42 and a scale of 1.7 it landed most of a frame from the cursor, which is what read as offset rather than as lagging. */ vec2 pa = turn * (a.xy - shift) * uScale; vec2 pb = turn * (b.xy - shift) * uScale; vec2 ab = pb - pa; float len2 = dot(ab, ab); if (len2 < 1e-6) { continue; } float t = clamp(dot(at - pa, ab) / len2, 0.0, 1.0); vec2 off = at - (pa + ab * t); /* Elongated along the stroke, not round about it. A circular falloff makes the wake a tube of even thickness, which is what a pipe looks like and not what a stirred medium does. Ink pulled through a liquid smears the way the hand went: it reaches further along the direction of travel than across it. Weighting the along component down stretches the ellipse into that direction, and costs one dot product. */ vec2 tangent = ab * inversesqrt(len2); float alongOff = dot(off, tangent); float acrossOff = dot(off, vec2(-tangent.y, tangent.x)); float shaped = acrossOff * acrossOff + alongOff * alongOff * 0.3; /* Squared, so a wake thins as it dies rather than switching off. */ float life = 1.0 - mix(a.z, b.z, t); float w = life * life * falloff(shaped, 3.2); wake += w; push += (ab * inversesqrt(len2)) * w; } /* Limited, because an unbounded displacement folds and a fold is a crease. Where the map from sample to displaced sample stops being one to one, neighboring samples land on the same value and the pattern piles into a hard bright line: a caustic, which is what a sharp diagonal with a bright wedge beside it always is. The sum runs over eight segments of unit tangents, so near the stroke it reached several times the width of the falloff it was displacing across, and folded every time. The falloff is about a third of a unit wide, so the displacement has to stay under that for the map to stay monotone. Saturating rather than clamped: a clamp is a corner, and a corner in a displacement is the same crease by another route. */ float mag = length(push); push = mag > 0.0 ? push * (0.24 / (0.9 + mag)) : vec2(0.0); return wake; } float flow(vec2 q, float t, mat2 turn, vec2 shift) { vec2 push; float wake = flowWake(q, turn, shift, push); /* Already limited inside, so it is applied whole. */ vec2 p = q + push; /* The field is warped by itself twice over, which is what gives a liquid its folded look: each sample is displaced by the field's own value, so structure gathers into sheets rather than staying a cloud. Big shapes first, detail on top of them. At twice these frequencies every octave landed small enough that the frame filled edge to edge with fine crumple at one density: marbled stone, and it looked the same at every scale, gain and bloom because there was no large form for any of them to act on. Isotropic noise cannot make a current, so anything wanting one has to carry a direction, and that is a bigger change than this shape wants. */ float ink = warped(p * 0.55, t * 0.55); float sheet = warped(p * 1.1 + vec2(3.1, 1.7), t * 0.35); /* Voids, not an even swatch. With a floor under each term the lowest the field can reach is a quarter lit, so every fragment lands in the same middle band and there is no structure to look at. And a slow large mass over the top, thresholded rather than merely varied: a gentle modulation over a field that is lit everywhere is still lit everywhere, and what a liquid needs is somewhere it simply is not. The empty parts of the frame are what give the full parts a shape. */ float mass = smoothstep(0.24, 0.72, noise(vec2(p.x * 0.45 - t * 0.1, p.y * 0.4 + t * 0.07))); /* The void floor sits well above zero. At a low scale the mass noise varies less across the frame than the frame is wide, so a reader can sit inside one void and see almost nothing: measured at scale 0.5, which came out near black. Empty has to mean faint, not absent, or the shape can disappear entirely at a setting the property explicitly allows. */ float body = mix(0.05, 1.4, ink) * mix(0.4, 1.2, sheet) * mix(0.22, 1.45, mass); /* The resting state matters more than the stirred one: most readers never move the pointer, and a shape that is dark until touched is a shape most people never see. With the voids in, halving an untouched field takes it to a dim murk that reads as wet rock, so the wake lifts from a resting level that is already worth looking at. */ return body * (0.85 + clamp(wake, 0.0, 1.0) * 0.5) * falloff(dot(q, q), 0.55); } /* Light through moving water, focused into a web. * * **A caustic is where a wavefront folds, not where it is bright.** Sampling * a noise field and lighting the high parts gives clouds; what makes the * pattern on a pool floor is that the surface bends light and neighboring * rays land on the same spot. The cheap stand-in for that is the *zero* of a * displaced wave sum rather than its peak: 1 - |sum| is thin and bright * exactly where the sum crosses, which is the fold. * * abs() puts a derivative discontinuity along each of those lines, and here * that is the drawing rather than a fault: a caustic edge is genuinely sharp, * which is the whole reason it reads as light rather than as fog. */ float caustics(vec2 q, float t, vec2 qp, float reach) { /* The pointer is a stone in the water: the surface it disturbs is a ring of extra phase, so the web tightens around it rather than brightening. */ float dropD = length(q - qp); float drop = falloff(dropD * dropD, 1.4) * reach; /* The layers are the population, and the pool says which of their cells are bright. Three depths of fold, each at its own frequency and carrying its own weight in the sum, are three sizes of the same thing, and the slow depth over the top is how far the lit part of the pool is from the dark part. Each travels toward the middle of its own spread, so a variance of 0 is one scale of web lit evenly from edge to edge and a variance of 1 is exactly the water this was tuned to. The total weight is the same at either end, three layers of 0.78 being the 1.0, 0.78 and 0.56 they spread into, so evening them out does not also dim the frame. The grain is left out of it. It is the texture on the light rather than a member of anything, which is the same reason threads keeps its. Clamped, and the upper half of that is not tidying: these are mix() calls, so a value past 1 extrapolates rather than exaggerating. The near layer's frequency runs 2.05 - 0.55v and the far layer's weight 0.78 - 0.22v, so at 2 the near web is drawn at 0.95, under half the scale it was tuned at, and past 3.5 the far layer's weight crosses zero and that layer lights everything the folds are not. */ float cv = clamp(uVariance, 0.0, 1.0); float lit = 0.0; for (int i = 0; i < 3; i++) { float f = float(i); vec2 p = q * mix(2.05, 1.5 + f * 0.55, cv); /* Warped by the field's own value, so the wavefronts bend the way a surface does rather than sliding as a rigid pattern. */ vec2 w = p + vec2( noise(p * 1.25 + vec2(t * 0.42 + f * 4.0, 0.0)), noise(p * 1.1 - vec2(0.0, t * 0.36 + f * 7.0)) ) * 0.85; float sum = sin(w.x * 3.1 + t * 0.7 + drop * 6.0) + sin(w.y * 2.7 - t * 0.55 - drop * 5.0); float fold = 1.0 - clamp(abs(sum) * 0.5, 0.0, 1.0); lit += pow(fold, 7.0) * mix(0.78, 1.0 - f * 0.22, cv); } /* **The pool has a lit part and a dark part.** Three layers of folds at one density fill the frame edge to edge and every version of it looks the same: bright everywhere carries the same amount of information as dark everywhere. A slow depth over the top decides where the light is getting through, so the web has somewhere to be faint and the eye somewhere to start. */ float pool = smoothstep(0.2, 0.78, noise(vec2(q.x * 0.55 - t * 0.09, q.y * 0.5 + t * 0.06))); /* And the stone brightens what it disturbs as well as bending it, or the only evidence of a pointer is a phase shift nobody can see. */ float dim = warped(q * 0.7, t * 0.2); return lit * mix(0.55, 1.25, dim) * mix(mix(0.75, 0.25, cv), mix(0.75, 1.25, cv), pool) * (1.0 + drop * 0.9) * falloff(dot(q, q), 0.4); } /* A lattice in perspective, lit at its nodes. * * **The one shape here that is not weather.** Everything else in this set is * a fluid, a light or a sky, and a design system is none of those: it is a * structure, regular, with the interesting part at the joints. So this is * drawn rather than simulated, and it is the only one with straight lines in * it on purpose. * * Perspective is a divide, not a matrix. Scaling the grid by 1 / (1 + depth) * makes the cells shrink with distance and the whole thing recede, which is * the entire trick and costs one division. */ float lattice(vec2 q, float t, vec2 qp, float reach) { /* The pointer lifts the plane nearest it, so the lattice crowds where a reader is looking. A structure answers by changing its spacing, which is the only thing about a lattice that can move without breaking it. */ vec2 away = q - qp; float near = falloff(dot(away, away), 1.8) * reach; /* **A ground plane, not a mirrored one.** Dividing by abs(p.y) makes two floors meeting along y = 0, which reads as a bowtie with a crease down the middle rather than as anything receding. Dividing by the distance below a horizon is the actual projection: one plane, cells shrinking as they approach it, and nothing above it to fold back. It travels as well, so the structure arrives rather than sitting still. Perspective and motion together are what say the plane is a plane. */ float below = max(1.45 - q.y, 0.14); float depth = 1.0 / below; vec2 g = vec2( q.x * depth * (2.3 + near * 1.4), depth * 1.6 + t * 0.28 ); vec2 cell = fract(g) - 0.5; /* The rails, thinner the further back they are, so the grid does not turn into a solid sheet at the horizon. */ float rail = min(abs(cell.x), abs(cell.y)); float lines = falloff(rail * rail, 24.0); /* The joints, which is where a lattice is actually a lattice, and brighter than the rails between them: a grid of even weight is graph paper. */ float node = falloff(dot(cell, cell), 26.0) * 2.1 * (0.6 + near * 0.9); /* Faded toward the horizon, or the cells pile into a solid sheet exactly where they are smallest. */ float far = smoothstep(0.0, 0.55, below) * smoothstep(3.4, 1.3, below); float dust = warped(q * 0.9, t * 0.25); return (lines * 0.5 + node) * far * mix(0.7, 1.2, dust) * falloff(dot(q, q), 0.42); } /* Cells, lit along the seams between them. * * **Provenance as a picture.** A Voronoi field is a set of territories, each * one everything nearer to its own point than to any other, and the drawing * is not the cells but the boundaries: where two claims meet is exactly * what Trace is about. Nothing here is lit except the disagreements. * * The wall is the second nearest distance minus the nearest, which is zero * on the boundary and grows into the interior. It is a genuine distance, so * it needs no epsilon and produces no crease. */ float fracture(vec2 q, float t, vec2 qp, float reach) { /* The territories are the population, and how far each site wanders off its own cell center is the whole of what makes one differ from the next: the rates are already shared, so the spread is the amplitude and nothing else. At 0 every site sits at its center and every claim is the same size, which draws the lattice this is a Voronoi of: a grid. That is the answer rather than a collision with the shape above, which is a structure in perspective lit at its joints where this is a set of claims that happen to be equal. **The alternative is a shared phase, and it is the wrong uniform.** Keeping the swing and collapsing a and b would leave every site on the same orbit, which is not a set of equal territories but one territory repeated and the whole field sliding rigidly under it. The lanes in streaks say the same thing about a phase from the other side. At 0 the seams hold still, which is what a set of equal claims looks like and not a shape that has stopped: the grain still travels over them and the pointer still shoves the sites near it apart, so the one thing that moves a territory is something arriving. Clamped for the usual reason, that these are interpolations, and here there is a hard edge as well as an ugly one. Nine cells are visited, so the two nearest sites have to be inside them: the swing reaches 0.42 * v * sqrt(2) of a cell at its corner, which passes a whole cell at v = 1.68, and past that a site can be nearer than anything the loop looked at and the seam snaps along the lattice it was drawn to hide. */ float fv = clamp(uVariance, 0.0, 1.0); vec2 g = q * 2.1; vec2 base = floor(g); vec2 f = g - base; float first = 9.0; float second = 9.0; for (int j = -1; j <= 1; j++) { for (int i = -1; i <= 1; i++) { vec2 o = vec2(float(i), float(j)); vec2 id = base + o; /* Each site drifts on its own small orbit, so the seams breathe rather than the whole field sliding. */ float a = hash(id) * 6.2831853; float b = hash(id + 41.7) * 6.2831853; vec2 site = o + 0.5 + 0.42 * fv * vec2(sin(t * 0.5 + a), cos(t * 0.42 + b)); /* Sites near the pointer are pushed away from it, which opens the cells there: a territory answers by giving ground. */ vec2 world = (id + 0.5) / 2.1; vec2 shove = world - qp; site += normalize(shove + 0.0001) * falloff(dot(shove, shove), 2.4) * reach * 0.5; float d = length(site - f); if (d < first) { second = first; first = d; } else if (d < second) { second = d; } } } float wall = second - first; float seam = falloff(wall * wall, 17.0); /* **Not every seam is lit the same.** At one brightness the field is a mosaic of equal cells, which is a texture; what makes a set of territories read is that some borders are contested and most are not. A slow field decides which, and the dark stretches give the lit ones a shape. Which is the second thing variance answers for here, from the other direction: how far apart the contested borders and the quiet ones are lit. Both ends travel toward the midpoint, so a variance of 0 is that mosaic, every border at one brightness, and the paragraph above is a description of what asking for no variance gets rather than a fault. */ float contested = smoothstep(0.24, 0.8, noise(vec2(q.x * 0.6 + t * 0.07, q.y * 0.55 - t * 0.05))); float grain = warped(q * 1.3, t * 0.3); return seam * mix(0.6, 1.25, grain) * mix(mix(0.74, 0.18, fv), mix(0.74, 1.3, fv), contested) * falloff(dot(q, q), 0.42); } /* The sampling frame, the shape branch, and the ramp from field to color. */ void main() { vec2 p = uv * 2.0 - 1.0; float aspect = uResolution.x / max(uResolution.y, 1.0); p.x *= aspect; /* **No global response, parallax included.** Every named interaction is a promise about what the pointer does, and translating the whole field is not any of them: a consumer asking for a light asked for a light, not for a light and a lean. Small is not the test. What answers the pointer is each shape, in the terms it is made of, and the three generics for a consumer who asks for one. */ vec2 q = p; /* **The tilt is an angle on the screen, not in the sampling space.** p.x is stretched by the aspect before anything is rotated, so a rotation applied here is a rotation in an anisotropic space, which is a shear. Working it through: a feature along the shape's own x axis sits where q.y = 0, which is p.y = -tan(a) * p.x, and since p.x is aspect times the screen's x the slope a reader sees is aspect * tan(a). At a widescreen 1.78 a tilt of 14 drew at 23.9 degrees, and the same number drew differently in every box the component was put in. Solving aspect * tan(a) = tan(tilt) gives the angle to rotate by. The two argument atan is what makes 90 work: tan blows up there, while atan(1, 0) is exactly a quarter turn. */ float want = radians(uTilt); float a = atan(sin(want), cos(want) * aspect); mat2 turn = mat2(cos(a), sin(a), -sin(a), cos(a)); q = turn * q * uScale; /* The pointer, carried into the same frame the shapes are drawn in, so each one can answer it in its own terms. */ vec2 qp = turn * uPointer * uScale; float t = uTime * 0.06; /* A shape answers for itself only when nothing else has been asked for. Given a generic response, its own is switched off rather than layered under: two answers to one gesture is how a shared zoom and a shared glow got here in the first place, applied identically to every shape and saying nothing about any of them. */ float own = uInteraction == 0 ? uReach : 0.0; float d = 0.0; if (uShape == 0) { d = braid(q, t, qp, own); } else if (uShape == 1) { d = rays(q, t, qp, own); } else if (uShape == 2) { d = aurora(q, t, qp, own); } else if (uShape == 3) { d = streaks(q, t, qp, own); } else if (uShape == 4) { d = waves(q, t, qp, own); } else if (uShape == 5) { d = threads(q, t, qp, own); } else if (uShape == 6) { d = particles(q, t, qp, own); } else if (uShape == 7) { d = flow(q, t, turn, uPointer * 0.05 * uReach); } else if (uShape == 8) { d = caustics(q, t, qp, own); } else if (uShape == 9) { d = lattice(q, t, qp, own); } else { d = fracture(q, t, qp, own); } /* Bloom as an exponent on the field rather than a term inside each shape. Every shape is a density that falls off, so a power tightens or spreads that falloff and one control means the same thing in all of them. */ d = pow(max(d, 0.0), mix(2.0, 0.5, clamp(uBloom, 0.0, 1.0))) * uGain; /* The generic answers, for a consumer who has asked for one. All three are functions of distance from the pointer and nothing else, which is exactly what makes them generic and exactly what makes them say less. The light option lifts what is lit and puts a floor under itself so there is something at the cursor over a dark field. The shadow option is that gesture inverted, which suits a bright field where a lamp would be invisible. The ripple option is the one with a clock in it, so it keeps moving after the pointer has stopped. */ if (uInteraction != 0 && uInteraction != 4) { vec2 toCursor = p - uPointer; float near = falloff(dot(toCursor, toCursor), 3.0) * uReach; if (uInteraction == 1) { d = d * (1.0 + near * 1.2) + near * 0.08; } else if (uInteraction == 2) { d *= 1.0 - near * 0.85; } else { float rings = sin(length(toCursor) * 13.0 - uTime * 3.2); d *= 1.0 + rings * near * 0.75; } } d *= mix(0.65, 1.35, smoothstep(-1.0, 1.0, q.y)); /* Rolled off rather than clipped. A hard clamp flattens the core into a plateau of one value, which is what makes a bright shader look like a sticker; this compresses toward one and keeps the gradient inside it. */ float lift = 1.0 - exp(-max(d, 0.0) * 1.15); vec3 rgb = mix(uGround, uHaze, smoothstep(0.0, 0.16, lift)); rgb = mix(rgb, uGlow, smoothstep(0.10, 0.46, lift)); rgb = mix(rgb, uPeak, smoothstep(0.40, 0.72, lift) * 0.6); /* Dithered before the buffer quantizes. A falloff this soft spans a few hundred 8-bit steps across a screen, and without a sub-step of noise it paints as visible rings. */ float dither = (hash(gl_FragCoord.xy) - 0.5) / 255.0; color = vec4(rgb + dither, 1.0); } `",
5330
5330
  "description": "One program, branching on a uniform, for every shape.\n\nA branch on a uniform is uniform across the whole draw, so every lane takes\nthe same path and it costs a jump rather than divergence. A program per shape\nwould mean compiling and linking on a property change, which is the one\nmoment a reader is watching.\n\n**No backticks anywhere inside this string, including in comments.** One ends\nthe template literal early, and what follows is parsed as JavaScript: it has\nsilently stripped every brace out of a shader and left the driver reporting a\nsyntax error two hundred lines from the cause. The build fails on it."
5331
5331
  }
5332
5332
  ],
@@ -13253,7 +13253,7 @@
13253
13253
  "declarations": [
13254
13254
  {
13255
13255
  "kind": "class",
13256
- "description": "The track scroll-snaps, so a drag and a trackpad swipe move it with the\nbrowser's own momentum and the controls only have to scroll.\n\nSlides take the full track by default. `per-view` fits more than one across,\nup to five, and is declared on the tier below because it decides how many\npositions the track has rather than only how wide a slide is,\nwhich is what a run of cards wants rather than a run of full-width panels.",
13256
+ "description": "The track scroll-snaps, so a drag and a trackpad swipe move it with the\nbrowser's own momentum and the controls only have to scroll.\n\nSlides take the full track by default. `per-view` fits more than one across,\nat any count, and is declared on the tier below because it decides how many\npositions the track has rather than only how wide a slide is,\nwhich is what a run of cards wants rather than a run of full-width panels.\nThis sheet divides the track by the number that tier publishes rather than\nenumerating the values it will answer for, so there is no count it silently\ndraws one across.",
13257
13257
  "name": "HelixCarousel",
13258
13258
  "cssProperties": [
13259
13259
  {
@@ -13280,6 +13280,14 @@
13280
13280
  "module": "src/carousel/root/carousel-root.ts"
13281
13281
  }
13282
13282
  },
13283
+ {
13284
+ "description": "How many slides go across, written onto the track by the component so a sheet divides the track by the same number the positions are counted from. Read it rather than selecting on `per-view`, which cannot be enumerated without a ceiling. See `perView`.",
13285
+ "name": "--carousel-per-view",
13286
+ "inheritedFrom": {
13287
+ "name": "HelixHeadlessCarousel",
13288
+ "module": "src/carousel/root/carousel-root.ts"
13289
+ }
13290
+ },
13283
13291
  {
13284
13292
  "description": "How far short of a whole pixel the run ends, on the last slide alone, for a sheet to take up as `margin-inline-end`. See `runEnd`.",
13285
13293
  "name": "--carousel-run-end",
@@ -13515,7 +13523,7 @@
13515
13523
  "type": {
13516
13524
  "text": "number | undefined"
13517
13525
  },
13518
- "description": "How many slides are shown across the track at once. Absent shows one.\n\n**Here rather than on the styled tier, because it decides how many places\nthe track can stop.** From the stylesheet it looks like a width, one rule\nper value setting a custom property that the slides divide the track by.\nBut a carousel showing four of six slides has three positions, not six, and\neverything that counts positions is in this file. Declared up there, `goTo`\nclamps to the last *slide*, so three of the six indicators point at an\noffset the track has already reached: pressing next walks the dots to the\nend while the slides stop moving a third of the way through.\n\nNo default, because it reflects. A reflected property with a value writes\nits attribute on upgrade, and the host then carries something a consumer's\nserver markup never had, which React reports as a mismatch. The one-across\ncase is the absent attribute, and the styles read it that way.",
13526
+ "description": "How many slides are shown across the track at once. Absent shows one.\n\n**Here rather than on the styled tier, because it decides how many places\nthe track can stop.** From the stylesheet it looks like a width, one rule\nper value setting a custom property that the slides divide the track by.\nBut a carousel showing four of six slides has three positions, not six, and\neverything that counts positions is in this file. Declared up there, `goTo`\nclamps to the last *slide*, so three of the six indicators point at an\noffset the track has already reached: pressing next walks the dots to the\nend while the slides stop moving a third of the way through.\n\n**The number is published to the styles rather than enumerated by them**,\nas `--carousel-per-view` on the track, which is what keeps a width and a\ncount from disagreeing. A sheet can only name a value it wrote a rule for,\nso the styled tier enumerated `per-view=\"2\"` through `5` and called five a\njudgment; above it the pane fell back to one across while this file went on\ncounting positions. Measured in Chromium over eight slides, six across gave\nthree dots, a pane taking the whole track and five slides reachable only by\ndragging. Written from here there is no ceiling to exceed, and a consumer's\nown sheet reads the same declaration the styled one does. `runEnd` publishes\nits correction the same way and for the same reason.\n\n**On the track and not on the host.** A custom property on the host is a\nreflected attribute by another name: it would arrive on upgrade, and a\nconsumer's server markup never had it. Inside the component's own shadow\nroot it serializes with the rest of the template on the server, and the\ntrack's own declaration beats anything a consumer inherits in, so the\nattribute is the one answer to how wide a pane is.\n\nA value the track cannot honor is taken to the nearest one it can, by\n`across`, and both tiers read that rather than this.\n\nNo default, because it reflects. A reflected property with a value writes\nits attribute on upgrade, and the host then carries something a consumer's\nserver markup never had, which React reports as a mismatch. The one-across\ncase is the absent attribute, and the styles read it that way.",
13519
13527
  "attribute": "per-view",
13520
13528
  "reflects": true,
13521
13529
  "inheritedFrom": {
@@ -13523,6 +13531,20 @@
13523
13531
  "module": "src/carousel/root/carousel-root.ts"
13524
13532
  }
13525
13533
  },
13534
+ {
13535
+ "kind": "field",
13536
+ "name": "across",
13537
+ "type": {
13538
+ "text": "number"
13539
+ },
13540
+ "privacy": "private",
13541
+ "description": "`perView` as both tiers use it: a whole number of slides, one at least.\n\n**The clamp is here because the arithmetic is here**, and the two readers\nof this number have to be given the same one. A fraction is floored, since\na position is a whole slide along: one and a half across leaves a last\nposition the track runs out before reaching, which is the fault `goTo`\nalready carries a comment about. Zero and a negative divide the track into\nnothing and into a pane facing the wrong way, and a non-numeric attribute\nparses to `NaN`, which `Math.max` propagates: it counted `NaN` positions,\ndrew no indicators at all and left `index` at `NaN`. All of them read as\none across, which is what a carousel with nothing sayable asked for.\n\nNot clamped to the number of slides. Six across four slides is four panes\nat a sixth of the track each and the one place to stand, which is what was\nasked for as nearly as it can be had, and the slide count is a fact the\nserver does not have: clamping to it would draw one width there and another\non upgrade.",
13542
+ "readonly": true,
13543
+ "inheritedFrom": {
13544
+ "name": "HelixHeadlessCarousel",
13545
+ "module": "src/carousel/root/carousel-root.ts"
13546
+ }
13547
+ },
13526
13548
  {
13527
13549
  "kind": "field",
13528
13550
  "name": "positions",
@@ -14336,7 +14358,7 @@
14336
14358
  "type": {
14337
14359
  "text": "number | undefined"
14338
14360
  },
14339
- "description": "How many slides are shown across the track at once. Absent shows one.\n\n**Here rather than on the styled tier, because it decides how many places\nthe track can stop.** From the stylesheet it looks like a width, one rule\nper value setting a custom property that the slides divide the track by.\nBut a carousel showing four of six slides has three positions, not six, and\neverything that counts positions is in this file. Declared up there, `goTo`\nclamps to the last *slide*, so three of the six indicators point at an\noffset the track has already reached: pressing next walks the dots to the\nend while the slides stop moving a third of the way through.\n\nNo default, because it reflects. A reflected property with a value writes\nits attribute on upgrade, and the host then carries something a consumer's\nserver markup never had, which React reports as a mismatch. The one-across\ncase is the absent attribute, and the styles read it that way.",
14361
+ "description": "How many slides are shown across the track at once. Absent shows one.\n\n**Here rather than on the styled tier, because it decides how many places\nthe track can stop.** From the stylesheet it looks like a width, one rule\nper value setting a custom property that the slides divide the track by.\nBut a carousel showing four of six slides has three positions, not six, and\neverything that counts positions is in this file. Declared up there, `goTo`\nclamps to the last *slide*, so three of the six indicators point at an\noffset the track has already reached: pressing next walks the dots to the\nend while the slides stop moving a third of the way through.\n\n**The number is published to the styles rather than enumerated by them**,\nas `--carousel-per-view` on the track, which is what keeps a width and a\ncount from disagreeing. A sheet can only name a value it wrote a rule for,\nso the styled tier enumerated `per-view=\"2\"` through `5` and called five a\njudgment; above it the pane fell back to one across while this file went on\ncounting positions. Measured in Chromium over eight slides, six across gave\nthree dots, a pane taking the whole track and five slides reachable only by\ndragging. Written from here there is no ceiling to exceed, and a consumer's\nown sheet reads the same declaration the styled one does. `runEnd` publishes\nits correction the same way and for the same reason.\n\n**On the track and not on the host.** A custom property on the host is a\nreflected attribute by another name: it would arrive on upgrade, and a\nconsumer's server markup never had it. Inside the component's own shadow\nroot it serializes with the rest of the template on the server, and the\ntrack's own declaration beats anything a consumer inherits in, so the\nattribute is the one answer to how wide a pane is.\n\nA value the track cannot honor is taken to the nearest one it can, by\n`across`, and both tiers read that rather than this.\n\nNo default, because it reflects. A reflected property with a value writes\nits attribute on upgrade, and the host then carries something a consumer's\nserver markup never had, which React reports as a mismatch. The one-across\ncase is the absent attribute, and the styles read it that way.",
14340
14362
  "fieldName": "perView",
14341
14363
  "inheritedFrom": {
14342
14364
  "name": "HelixHeadlessCarousel",
@@ -139,17 +139,23 @@ export declare class HelixAura extends HelixElement {
139
139
  * exaggerating, and a streak whose length has passed through zero is drawn
140
140
  * as a bar across the whole frame.
141
141
  *
142
- * Read by four of the eleven shapes, the ones made of many similar things,
142
+ * Read by six of the eleven shapes, the ones made of many similar things,
143
143
  * whether those are counted or continuous: `streaks`, whose lanes draw a
144
144
  * speed, a length, a width, a spacing and a weight apiece; `particles`, whose
145
- * motes draw a size, a brightness and a wander; and `rays` and `aurora`, whose
146
- * fan, bundles and folds widen or even out.
147
- *
148
- * Inert on the other seven, for three different reasons. `braid`, `waves` and
149
- * `flow` have a single subject and nothing to vary. `threads` and `lattice`
150
- * are even on purpose, a scatter of strands and a regular structure each being
151
- * what it is because its members do not differ. `caustics` and `fracture` are
152
- * populations this does not reach, which is a gap rather than a refusal.
145
+ * motes draw a size, a brightness and a wander; `rays` and `aurora`, whose
146
+ * fan, bundles and folds widen or even out; `caustics`, whose three depths of
147
+ * fold close on one scale and one weight while the pool over them flattens to
148
+ * an even wash; and `fracture`, whose sites come home to their own cell
149
+ * centers, leaving equal territories and no border more contested than
150
+ * another.
151
+ *
152
+ * Inert on the other five, and every one of them a refusal. `braid`, `waves`
153
+ * and `flow` have a single subject and nothing to vary. `threads` and
154
+ * `lattice` are even on purpose, a scatter of strands and a regular structure
155
+ * each being what it is because its members do not differ: `threads` gives
156
+ * its seven one amplitude, one thickness, one weight and one frequency on
157
+ * the argument that alike is what makes a weave read as woven, and a lattice
158
+ * whose cells differed would be a scatter with straight lines in it.
153
159
  */
154
160
  variance: number;
155
161
  /**