@mkbabb/glass-ui 3.10.0 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ToggleGroupItem-Cev4pOVK.js → ToggleGroupItem-COu_4IW4.js} +17 -13
- package/dist/api/index.d.ts +1 -1
- package/dist/api.js +2 -2
- package/dist/aurora.js +77 -77
- package/dist/components/custom/aurora/constants/presets.d.ts +62 -5
- package/dist/components/custom/aurora/index.d.ts +1 -1
- package/dist/components/custom/dock/GlassDock.vue.d.ts +5 -1
- package/dist/components/custom/expandable-container/ExpandableContainer.vue.d.ts +10 -7
- package/dist/components/custom/handmark/brush.d.ts +23 -0
- package/dist/components/custom/handmark/geometry.d.ts +20 -2
- package/dist/components/custom/handmark/index.d.ts +2 -2
- package/dist/components/custom/handmark/useHandMark.d.ts +26 -5
- package/dist/components/ui/toggle/Toggle.vue.d.ts +1 -1
- package/dist/components/ui/toggle/index.d.ts +1 -1
- package/dist/components/ui/toggle-group/ToggleGroup.vue.d.ts +7 -1
- package/dist/components/ui/toggle-group/ToggleGroupItem.vue.d.ts +3 -1
- package/dist/components/ui/toggle-group/toggleGroupContext.d.ts +13 -0
- package/dist/composables/dark/index.d.ts +1 -1
- package/dist/composables/dark/useGlobalDark.d.ts +21 -1
- package/dist/controls.js +1 -1
- package/dist/dark.d.ts +1 -1
- package/dist/dark.js +1 -1
- package/dist/dock.js +191 -174
- package/dist/expandable-container.js +67 -49
- package/dist/fourier-field.js +1 -1
- package/dist/glass-ui.css +1 -1
- package/dist/glass-ui.js +2 -2
- package/dist/handmark.js +176 -123
- package/dist/labeled-field.js +1 -1
- package/dist/{presets-Dkdv55Gt.js → presets-BP_ALM0m.js} +14 -1
- package/dist/{slider-CyzcxBoh.js → slider-ClgpP7EB.js} +1 -1
- package/dist/slider.js +1 -1
- package/dist/styles/animations.css +40 -0
- package/dist/styles/components.css +1 -1
- package/dist/styles/tokens.css +5 -0
- package/dist/styles/utilities.css +44 -13
- package/dist/toggle-group.js +1 -1
- package/dist/{useGlobalDark-d6waty-X.js → useGlobalDark-D1wgEqfW.js} +21 -2
- package/package.json +10 -4
- package/src/styles/animations.css +40 -0
- package/src/styles/tokens.css +5 -0
- package/src/styles/utilities.css +44 -13
package/dist/aurora.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { t as e } from "./_plugin-vue_export-helper-ok8pgSl5.js";
|
|
2
2
|
import { a as t, c as n, i as r, l as i, n as a, o, r as s, s as c, t as l, u } from "./compile-BOXjLUxu.js";
|
|
3
|
-
import {
|
|
4
|
-
import { a as
|
|
3
|
+
import { i as d, n as f, r as p, t as m } from "./presets-BP_ALM0m.js";
|
|
4
|
+
import { a as h, c as g, i as _, l as v, o as y, t as b } from "./color-C5ls_QF7.js";
|
|
5
5
|
import { t as ee } from "./useIntersectionPause-Cck5Uxk8.js";
|
|
6
|
-
import { t as
|
|
7
|
-
import { computed as x, createElementBlock as
|
|
8
|
-
import { interpolateHue as
|
|
6
|
+
import { t as te } from "./useScrollProgress-CffFGLa5.js";
|
|
7
|
+
import { computed as x, createElementBlock as ne, createElementVNode as S, defineComponent as re, normalizeClass as ie, normalizeStyle as C, onBeforeUnmount as w, onMounted as T, openBlock as E, readonly as ae, ref as D, unref as O, watch as k } from "vue";
|
|
8
|
+
import { interpolateHue as oe, rawOklabToOklch as se, srgbToOKLab as ce } from "@mkbabb/value.js";
|
|
9
9
|
//#region src/components/custom/aurora/constants/shaders/aurora.vert.ts
|
|
10
|
-
var ce = "#version 300 es\nin vec2 aPos;\nout vec2 vUv;\nvoid main() {\n vUv = aPos * 0.5 + 0.5;\n gl_Position = vec4(aPos, 0.0, 1.0);\n}", le = "// ── Palette LUT (W5 — OKLCh/OKLab interpolation) ──────────────────────────────\n// The endpoints stay CPU-baked to LINEAR sRGB (the Aras precompute pattern); only\n// the BETWEEN-endpoint interpolation runs in perceptual space. AX.W11 — the ramp\n// itself (the smoothstep ease + the OKLab-rectangular-vs-OKLCh-hue-arc dispatch on\n// uHuePath) is the shared samplePaletteRamp from procedural-color.glsl, the SAME\n// source the WebGPU twin splices; samplePalette here only selects the bracketing\n// stop pair + the raw inter-stop t and hands them to the shared ramp.\nvec3 samplePalette(float id) {\n id = clamp(id, 0.0, 1.0);\n float scaled = id * float(uStopCount - 1);\n int i0 = int(floor(scaled));\n int i1 = min(i0 + 1, uStopCount - 1);\n float t = fract(scaled);\n return samplePaletteRamp(uPalette[i0], uPalette[i1], t, uHuePath);\n}\n\n// ── Nuclei field ──────────────────────────────────────────────────────────\nvoid nucleiField(vec2 p, float t, out float paletteId, out float valueMod) {\n float accumBias = 0.0;\n float accumValue = 0.0;\n float accumW = 0.0;\n for (int i = 0; i < MAX_NUCLEI; i++) {\n if (i >= uNucleiCount) break;\n vec2 posI = uNucleiPos[i]\n + uNucleiDriftRadius[i] * vec2(\n cos(t * uNucleiDrift * K_NUCLEI + uNucleiDriftPhase[i]),\n sin(t * uNucleiDrift * K_NUCLEI + uNucleiDriftPhase[i] * 1.13)\n );\n vec2 diff = p - posI;\n // Anisotropic Gaussian: rotate diff into the nucleus's local frame\n // (major axis along uNucleiAngle), then scale the major-axis component by\n // 1/elongation so the squared distance describes an ellipse. Defaults\n // 1.0/0.0 reduce to the isotropic dot(diff, diff).\n float ca = cos(uNucleiAngle[i]);\n float sa = sin(uNucleiAngle[i]);\n vec2 local = vec2( ca * diff.x + sa * diff.y,\n -sa * diff.x + ca * diff.y);\n float along = local.x / max(uNucleiElong[i], 0.01);\n float across = local.y;\n float d2 = along * along + across * across;\n float r = max(uNucleiRadius[i], 0.01);\n float w = exp(-uSoftmaxBeta * d2 / (r * r));\n accumBias += w * uNucleiPaletteBias[i];\n accumValue += w * uNucleiValueBias[i];\n accumW += w;\n }\n paletteId = accumBias / max(accumW, 1e-4);\n valueMod = accumValue / max(accumW, 1e-4);\n\n // Palette drift — slow global paletteId breathe between adjacent stops. The\n // rate rides K_PAL (perceptible ~30–60s hue cycle at the default coefficient);\n // the amplitude lifts the authored coefficient into a 0.03..0.06 paletteId band\n // so the palette visibly travels rather than dithering within one stop.\n float palAmp = clamp(uPaletteDrift * 6.0, 0.0, 0.06);\n paletteId += palAmp * sin(t * uPaletteDrift * K_PAL);\n paletteId = clamp(paletteId, 0.0, 1.0);\n}\n", ue = "// ── Flow field ────────────────────────────────────────────────────────────\n// Structure-tensor prototype (body defined post-sampleBase in the mediums block).\n// Returns vec3(dir.x, dir.y, coherence) — the minor-eigenvector tangent + A.\nvec3 structureTensorField(vec2 p, float t, vec2 fallbackDir);\n\nvec2 flowField(vec2 p, float t) {\n vec2 dir = vec2(1.0, 0.0);\n if (uFlowPattern == 1) {\n // radial\n dir = normalize(p - uFlowFocal + 1e-4);\n } else if (uFlowPattern == 2) {\n // swirl — tangent to radial\n vec2 rad = normalize(p - uFlowFocal + 1e-4);\n dir = vec2(-rad.y, rad.x);\n } else if (uFlowPattern == 3) {\n // diagonal\n float a = radians(uFlowAngle);\n dir = vec2(cos(a), sin(a));\n } else if (uFlowPattern == 4) {\n // multi — curl-noise driven\n float n = fbm(p * 2.0 + t * 0.02);\n float a = n * 6.2831;\n dir = vec2(cos(a), sin(a));\n } else if (uFlowPattern == 5) {\n // tensor / ETF — the color field's OWN edge-tangent flow (AW.W4.1). The\n // diagonal angle seeds the smooth fallback the coherence-weighted blend\n // relaxes toward in low-structure zones. Returns directly (no curl/cursor\n // post-mix — the tensor field already carries the structure orientation;\n // the cursor warp still drives the underlying color field in domainWarp).\n float a = radians(uFlowAngle);\n vec2 fallback = vec2(cos(a), sin(a));\n return structureTensorField(p, t, fallback).xy;\n }\n // curl — perturb by noise. Radial gets much less curl so rays stay clean.\n if (uFlowCurl > 0.0) {\n float n = fbm(p * 3.0) - 0.5;\n float curlAmt = uFlowCurl;\n if (uFlowPattern == 1) curlAmt *= 0.25; // radial: preserve ray clarity\n if (uFlowPattern == 2) curlAmt *= 0.55; // swirl: allow moderate curl\n float a = atan(dir.y, dir.x) + n * 3.14159 * curlAmt;\n dir = vec2(cos(a), sin(a));\n }\n // cursor influence — swirl around the cursor position\n if (uCursorStrength > 0.001) {\n vec2 toCur = uCursor - p;\n float d = length(toCur);\n float r = max(uCursorRadius, 0.01);\n float w = exp(-(d * d) / (r * r * 0.5));\n // swirl tangent\n vec2 tangent = vec2(-toCur.y, toCur.x) / max(d, 1e-4);\n float a0 = atan(dir.y, dir.x);\n float a1 = atan(tangent.y, tangent.x);\n // blend angle\n float da = a1 - a0;\n da = atan(sin(da), cos(da)); // wrap to [-pi, pi]\n float a = a0 + da * w * uCursorStrength;\n dir = vec2(cos(a), sin(a));\n }\n // AW.W8.1 — velocity-reactive swirl-BURST: a fast flick (uCursorBurst, decaying\n // over ~1s) injects a transient flow bias along the pointer velocity, distinct from\n // the steady attraction above (the stateless swirl is SUPERSEDED by this form). The\n // burst is gated by the master tempo scalar on the CPU side (it decays to 0 under\n // PRM/pause), so this term cannot leak motion under reduced-motion.\n if (uCursorBurst > 0.001) {\n vec2 toCur = uCursor - p;\n float d = length(toCur);\n float r = max(uCursorRadius, 0.01);\n float w = exp(-(d * d) / (r * r * 0.7));\n // Bias the flow toward the velocity direction, scaled by the decaying burst.\n vec2 velDir = uCursorVelocity / max(length(uCursorVelocity), 1e-4);\n float a0 = atan(dir.y, dir.x);\n float a1 = atan(velDir.y, velDir.x);\n float da = atan(sin(a1 - a0), cos(a1 - a0));\n float a = a0 + da * w * uCursorBurst * 0.8;\n dir = vec2(cos(a), sin(a));\n }\n return dir;\n}\n", de = "// Khronos PBR-Neutral tonemap (hue + saturation preserving)\nvec3 aces(vec3 color) {\n const float startCompression = 0.8 - 0.04;\n const float desaturation = 0.15;\n float x = min(color.r, min(color.g, color.b));\n float offset = x < 0.08 ? x - 6.25 * x * x : 0.04;\n color -= offset;\n float peak = max(color.r, max(color.g, color.b));\n if (peak < startCompression) return clamp(color, 0.0, 1.0);\n float d = 1.0 - startCompression;\n float newPeak = 1.0 - d * d / (peak + d - startCompression);\n color *= newPeak / peak;\n float g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0);\n return clamp(mix(color, newPeak * vec3(1.0), g), 0.0, 1.0);\n}\n", fe = "// ── Curved swept brushstroke primitive ────────────────────────────────────\n// A stroke is a curved spine from A to B with quadratic bulge K (sideways offset\n// at midpoint). Width varies along length via a shape profile. Edge is ragged\n// (bristle-modulated), ends are rounded blobs, inside has streak modulation.\n//\n// For pixel p, we invert an approximate projection onto the curved spine:\n// 1) project p onto straight AB axis -> along0, cross0\n// 2) at parameter along0, spine offset = 4·K·along0·(1-along0) perpendicular\n// 3) cross = cross0 - spineOffset, refine along one iteration\n// Coverage blends paint atop col based on per-stroke color sampled from the\n// underlying base at spine midpoint — so overlapping strokes from different\n// palette regions meet at hard, ragged demarcations.\n\nstruct StrokeHit {\n float coverage; // 0..1\n vec3 color;\n float alongT; // 0..1 along spine, for internal modulation\n float crossN; // cross distance / current half-width, signed\n float edgeN; // distance to edge in half-widths (0 = at edge, 1 = at spine)\n};\n\nStrokeHit noHit() { return StrokeHit(0.0, vec3(0.0), 0.0, 0.0, 0.0); }\n\nvec2 rotateDir(vec2 dir, float angle) {\n float ca = cos(angle), sa = sin(angle);\n return vec2(dir.x * ca - dir.y * sa, dir.x * sa + dir.y * ca);\n}\n\nvec2 safeDir(vec2 dir) {\n float len = length(dir);\n return len > 1e-4 ? dir / len : vec2(1.0, 0.0);\n}\n\n// Shape profile along the stroke. type:\n// 0 tapered — thin-fat-thin (classic brush)\n// 1 load-drag — fat start, tapers to a point\n// 2 dab — ellipse-like, round center\n// 3 even — near-constant, slight end taper\n// 4 comma — van-Gogh atomic dab: ASYMMETRIC taper, fat near one end and\n// drawn to a fine point at the other (the divisionist crescent\n// mark). The width peak sits off-centre (~0.32) so the two ends\n// taper at DIFFERENT rates — a loaded round head + a dragged tail.\nfloat strokeShape(float t, int type) {\n t = clamp(t, 0.0, 1.0);\n if (type == 1) {\n // loaded at t=0, tapering\n return pow(1.0 - t, 0.55) * smoothstep(0.0, 0.08, t);\n } else if (type == 2) {\n // dab / blob\n float d = t - 0.5;\n return exp(-d * d * 12.0);\n } else if (type == 3) {\n // mostly even, slight end softening\n return smoothstep(0.0, 0.08, t) * smoothstep(1.0, 0.92, t) * 0.95 + 0.05;\n } else if (type == 4) {\n // comma / crescent — width peaks at ~0.32 then drags to a fine tail. The two\n // sides of the peak fall at different exponents (a fat round head, a long thin\n // tail), so the mark reads as a directional van-Gogh comma, not a symmetric oval.\n float peak = 0.32;\n float head = smoothstep(0.0, peak, t); // rounded loaded head\n float tail = pow(clamp((1.0 - t) / (1.0 - peak), 0.0, 1.0), 1.6); // dragged thin tail\n return clamp(min(head, tail) * 1.04, 0.0, 1.0);\n }\n // 0 tapered\n return smoothstep(0.0, 0.22, t) * smoothstep(1.0, 0.78, t);\n}\n\n// A single curved stroke.\n// a, b — endpoints\n// halfW — base half-width (world units)\n// bulge — signed perpendicular midpoint offset, in world units\n// shapeType — 0..3\n// bristleFreq — spatial frequency of edge raggedness\n// bristleAmp — 0..0.5 fraction of halfW chewed away at edge extrema\n// streakSeed — uniqueness seed for internal streaks\n// colAtMid — pre-sampled color at midpoint\nStrokeHit curvedStroke(vec2 p, vec2 a, vec2 b, float halfW,\n float bulge, int shapeType,\n float bristleFreq, float bristleAmp,\n float streakSeed, vec3 colAtMid) {\n vec2 ab = b - a;\n float L = length(ab);\n if (L < 1e-5) return noHit();\n vec2 tang = ab / L;\n vec2 norm = vec2(-tang.y, tang.x);\n\n vec2 rel = p - a;\n float along0 = dot(rel, tang) / L; // straight projection 0..1 along AB\n float cross0 = dot(rel, norm); // signed cross\n\n // Spine sideways offset due to bulge (parabolic)\n float bend = 4.0 * bulge * along0 * (1.0 - along0);\n float cross1 = cross0 - bend;\n\n // One refinement step: as the spine bends, the closest-point along shifts.\n // Spine tangent differs from straight tangent; first-order correction:\n float dBend = 4.0 * bulge * (1.0 - 2.0 * along0); // d(bend)/d(along)\n float along1 = along0 + (cross1 * dBend) / (L * (1.0 + dBend * dBend / (L * L)));\n\n // Bristle-ragged edge: half-width gets chewed by 1D noise along the spine\n float edgeNoise = fbm(vec2(along1 * bristleFreq, streakSeed * 7.3)) - 0.5;\n float edgeNoise2 = fbm(vec2(along1 * bristleFreq * 2.3, streakSeed * 3.1 + 17.0)) - 0.5;\n float edgeMod = 1.0 - bristleAmp * (0.6 + 0.4 * edgeNoise) * (0.5 + edgeNoise2);\n\n // Width profile along the stroke\n float shape = strokeShape(along1, shapeType);\n float halfWNow = halfW * shape * edgeMod;\n\n // Inside-segment coverage\n float cov = 0.0;\n if (along1 >= 0.0 && along1 <= 1.0 && halfWNow > 1e-6) {\n float cn = abs(cross1) / halfWNow;\n // fwidth-derived AA: half-width from the field's screen-space gradient so the\n // edge holds ~1px regardless of zoom/DPR (mirrors metaball.frag.ts:252-255).\n float aaC = max(fwidth(cn), 1e-6);\n cov = 1.0 - smoothstep(1.0 - aaC, 1.0 + aaC, cn);\n }\n\n // End-cap blobs (rounded tips, not perpendicular cuts)\n float capA = 0.0, capB = 0.0;\n {\n vec2 capCenterA = a + norm * (bulge * 0.0); // at a\n vec2 capCenterB = a + tang * L + norm * bend * 0.0 + norm * 0.0; // at b; bend is 0 at endpoints\n capCenterB = b;\n float endShapeA = strokeShape(0.02, shapeType);\n float endShapeB = strokeShape(0.98, shapeType);\n float rA = halfW * endShapeA * (0.9 + 0.2 * fbm(vec2(streakSeed * 13.0)));\n float rB = halfW * endShapeB * (0.9 + 0.2 * fbm(vec2(streakSeed * 19.0 + 5.0)));\n float dA = length(p - capCenterA);\n float dB = length(p - capCenterB);\n // fwidth-derived AA on the clean Euclidean cap distances (mirrors metaball.frag.ts:252-255).\n float aaA = max(fwidth(dA), 1e-6);\n float aaB = max(fwidth(dB), 1e-6);\n capA = 1.0 - smoothstep(rA - aaA, rA + aaA, dA);\n capB = 1.0 - smoothstep(rB - aaB, rB + aaB, dB);\n // Only apply caps if we're BEYOND the segment; otherwise the segment wins.\n capA *= (along1 < 0.05) ? 1.0 : 0.0;\n capB *= (along1 > 0.95) ? 1.0 : 0.0;\n }\n\n float coverage = max(cov, max(capA, capB));\n if (coverage < 0.002) return noHit();\n\n // Edge distance (for impasto later): 0 at edge, 1 at spine\n float edgeDist = halfWNow > 1e-6 ? clamp(1.0 - abs(cross1) / halfWNow, 0.0, 1.0) : 0.0;\n\n return StrokeHit(\n coverage,\n colAtMid,\n clamp(along1, 0.0, 1.0),\n halfWNow > 1e-6 ? cross1 / halfWNow : 0.0,\n edgeDist\n );\n}\n\n// The painterly STROKE mediums (those that deposit via paintStrokeLayers/paintOver):\n// oil (3), van-Gogh (5), oil-pastel (6). For these the stroke OVER-composite runs in\n// OKLab so overlapping complementaries mix through a chromatic path, not the linear-\n// RGB grey mud; the smooth/atmospheric pole never reaches paintOver so it pays nothing.\nbool isPainterlyStroke() {\n return uMedium == 3 || uMedium == 5 || uMedium == 6;\n}\n\n// OKLab OVER-composite: lerp L,a,b of the over-color toward the under-color (the\n// Ottosson matrices spliced from procedural-color.glsl.ts — no new color math). Two\n// complementary-hued strokes overlapping transition through OKLab, where the midpoint\n// holds chroma, instead of the linear mix() that desaturates toward grey (slice 8 F2).\nvec3 paintOverOklab(vec3 under, vec3 over, float alpha) {\n vec3 labU = linOklab(under);\n vec3 labO = linOklab(over);\n return max(oklabToLinearSrgb(mix(labU, labO, alpha)), vec3(0.0));\n}\n\n// Composite stroke over col with internal streaking + ACCUMULATE paint HEIGHT.\n// streakAmp — 0..0.2 how much internal streaks darken/lighten\n// impastoAmp — 0..1 paint-thickness contribution (the height the relight reads)\n// hardness — 0..1 how crisp the compositing transition is (1 = razor, 0 = creamy)\n//\n// AW.W4.2 — the fixed-RGB edge rim (the phantom upper-left light) is RETIRED. Each\n// stroke now deposits PAINT HEIGHT into the height accumulator (coverage x per-stroke\n// thickness, perturbed by the bristle/streak fbm for ridges/grooves); mediumOil\n// derives a normal from the accumulated height gradient (dFdx/dFdy) and relights it\n// once with the movable uLightDir source (relightImpasto), in linear before aces().\n//\n// AX.W13 — the internal streak now perturbs HUE + CHROMA in OKLCh (not value-only),\n// so a single stroke carries a small hue gradient (broken color at the ATOM level —\n// adjacent strokes shimmer like real impasto, not flat stamped swatches; slice 8 F5);\n// and the OVER-composite runs in OKLab on the painterly stroke mediums so overlapping\n// complementaries mix as pigment, not the linear-mix grey (slice 8 F2).\n// impastoFloor — the per-stroke height-crown floor: oil's 0.4+0.6*edgeN reads\n// floor=0.4 (the crown tapers to 40% at the stroke edge); the van-Gogh\n// profile passes floor=1.0 so each atomic dab catches a FULL-height\n// glint (scope item 1d). The crown magnitude is floor + (1-floor)*edgeN.\nvoid paintOver(inout vec3 col, inout float height, StrokeHit s,\n float streakFreq, float streakAmp,\n float impastoAmp, float hardness, float streakSeed,\n float impastoFloor) {\n if (s.coverage < 0.002) return;\n float strokeOpacity = clamp(uStrokeAmount, 0.0, 1.0);\n if (strokeOpacity <= 0.001) return;\n vec3 c = s.color;\n\n // Internal streaks — fbm along spine, modulated by cross position. Gives\n // the loaded-brush look: some bristles carry more pigment than others.\n float streakA = fbm(vec2(s.alongT * streakFreq, s.crossN * 2.7 + streakSeed));\n float streakB = fbm(vec2(s.alongT * streakFreq * 0.6 + streakSeed * 3.7, s.crossN * 4.1));\n float streak = 0.6 * (streakA - 0.5) + 0.4 * (streakB - 0.5);\n\n // ── WITHIN-stroke OKLCh broken color (AX.W13, slice 8 F5). The streak fBm perturbs\n // the stroke's HUE + CHROMA along alongT/crossN, seeded per-stroke, so a single\n // stroke reads a small hue gradient (the atom-level broken-color shimmer). Value is\n // carried by the same streak so the loaded-brush light/dark survives. Gated to the\n // painterly stroke mediums + uBrokenColor (the smooth pole keeps the cheap value-only\n // streak). A second decorrelated fBm drives the hue axis so hue ≠ a scaled value.\n float hueStreak = fbm(vec2(s.alongT * streakFreq * 0.45 + streakSeed * 5.3,\n s.crossN * 1.9 + streakSeed * 1.7)) - 0.5;\n if (isPainterlyStroke() && uBrokenColor > 0.001) {\n vec3 lch = oklabToOklch(linOklab(c));\n // ±~10° within-stroke hue swing + ±~9% chroma, scaled by uBrokenColor. Smaller\n // than the per-stroke cell jitter (brokenColorJitter) — this is the intra-stroke\n // gradient layered on top of the per-cell pigment patch.\n lch.z += hueStreak * (20.0 * PI / 180.0) * uBrokenColor;\n lch.y = max(lch.y * (1.0 + (streak) * 0.18 * uBrokenColor), 0.0);\n c = max(oklabToLinearSrgb(oklchToOklab(lch)), vec3(0.0));\n }\n // Value streak (the loaded-brush light/dark across the bristles) — kept for every\n // medium that reaches paintOver (the smooth pole never does). The along-spine value\n // modulation is the loaded-brush light/dark that gives each stroke its OWN directional\n // luminance band — the structure-tensor signal a low-contrast color field (a uniform-hue\n // oil field) has no other source for (the §4.2 anisotropy lift on the flat-palette case).\n c *= 1.0 + streak * streakAmp * 3.2;\n\n // Cross-stroke ridge/valley shading — a catch-light along the spine centre, darkening to\n // the stroke edges. This is the strongest DIRECTIONAL luminance cue: it carves a bright\n // ridge down the stroke's length and dark grooves between adjacent strokes, so a field of\n // parallel strokes reads as parallel light/dark bands (the high-A van-Gogh signature) even\n // where the underlying colour is flat. s.edgeN is 0 at the edge, 1 at the spine.\n float ridgeShade = smoothstep(0.0, 0.55, s.edgeN);\n c *= 1.0 + (ridgeShade - 0.5) * 0.30;\n\n float softLimit = mix(0.35, 0.98, hardness);\n float alpha = smoothstep(0.0, 1.0 - softLimit, s.coverage) * strokeOpacity;\n // OKLab OVER-composite on the painterly stroke mediums (overlap mixes as pigment,\n // not grey); the smooth pole never reaches paintOver, so it keeps the cheap path.\n col = isPainterlyStroke() ? paintOverOklab(col, c, alpha) : mix(col, c, alpha);\n\n // ── Accumulate paint HEIGHT (AW.W4.2). Thickness peaks at the stroke spine and\n // falls toward the impastoFloor at the edge (edgeN: 0=edge, 1=spine), perturbed by\n // the bristle streak so ridges/grooves form. The relight reads the gradient of this\n // field. The van-Gogh profile's floor=1.0 gives every atomic dab a full-height crown.\n float ridge = 0.7 + 0.3 * (streak + 0.5);\n float crown = impastoFloor + (1.0 - impastoFloor) * s.edgeN;\n height += s.coverage * impastoAmp * crown * ridge * alpha;\n}\n\n// AW.W4.2 — relight the accumulated paint height. Derive a normal from the height\n// gradient (dFdx/dFdy — derivatives are in-pattern; fwidth is already used for AA),\n// then apply diffuse + Blinn specular from the movable uLightDir/uLightColor source.\n// LINEAR-space lighting BEFORE aces() (the tonemap/OETF stay locked); thin strokes\n// inherit a low shininess, thick impasto raises it (high-height ridges catch a sharp\n// glint). canvasBase is the tooth height so the canvas weave also catches the light.\nvec3 relightImpasto(vec3 col, float height, float canvasBase) {\n float h = height + canvasBase;\n // Height-gradient normal. heightScale trades relief depth for surface flatness.\n float heightScale = 6.0;\n vec3 N = normalize(vec3(-dFdx(h) * 40.0, -dFdy(h) * 40.0, 1.0 / heightScale));\n vec3 L = normalize(uLightDir);\n vec3 V = vec3(0.0, 0.0, 1.0); // view straight on\n vec3 H = normalize(L + V);\n float diff = max(dot(N, L), 0.0);\n // Thick impasto → high shininess (sharp glint); thin paint → broad soft sheen.\n float shininess = mix(8.0, 64.0, clamp(height * 2.0, 0.0, 1.0));\n float spec = pow(max(dot(N, H), 0.0), shininess);\n // Modulate the lit terms by how much paint sits here (no relief on bare ground).\n float relief = clamp(h * 1.5, 0.0, 1.0);\n vec3 lit = col * (1.0 + uImpasto * relief * (diff - 0.5) * 0.5 * uLightColor);\n // The specular glint is gated to ACCUMULATED-thick paint (height²) — a per-fine-stroke\n // sharp glint on every thin fill dab read as bright isotropic SPECKLE that buried the\n // directional stroke bands (the §4.2 anisotropy / §4.3 slope sink on the low-contrast\n // oil field). Only where real impasto has built up does the raked glint fire, so the\n // highlight tracks the macro ridges, not the fine speckle.\n float specGate = clamp(height, 0.0, 1.0);\n lit += uImpasto * relief * spec * specGate * 0.5 * uLightColor;\n return lit;\n}\n\n// Best-of-9-neighbor placement: sample 3x3 surrounding cells and take the\n// stroke that covers this pixel most. Breaks the grid by per-cell jitter and\n// sparse density via noise thresholding.\n// energyGrade — the SBR energy-grade magnitude (0 = none, the oil/oil-pastel\n// baseline; 1 = the full van-Gogh Starry-Night cascade — long\n// confident strokes in the lights/coherent zones, short dabs in\n// the darks/flat zones). The van-Gogh profile carries 1.0 (the\n// energy grade is a PROFILE field, not a buried uMedium==5 branch).\nStrokeHit bestOil(vec2 p, float cellSize, float lenMul, float halfWMul,\n float jitterAmt, float density, int shapeType,\n float bristleAmp, vec2 flow, float t, float seed,\n float energyGrade) {\n vec2 cell = floor(p / cellSize);\n\n StrokeHit best = noHit();\n // 3x3 neighborhood\n for (int dy = -1; dy <= 1; dy++) {\n for (int dx = -1; dx <= 1; dx++) {\n vec2 cc = cell + vec2(float(dx), float(dy));\n vec2 hh = hash22(cc + seed) - 0.5;\n // Density gate — noise threshold; sparse placement.\n float gate = hash21(cc * 1.7 + seed * 0.3);\n if (gate > density) continue;\n\n vec2 center = (cc + 0.5 + hh * jitterAmt) * cellSize;\n\n // Per-stroke direction source — the AW.W4.1 strokeOrient switch:\n // uStrokeOrient==0 (flow) — the layer-provided hand-authored flowField.\n // uStrokeOrient==1 (tensor) — the structure-tensor minor eigenvector at the\n // cell center (the color field's edge-tangent),\n // so strokes hug the color zones (van Gogh).\n // coh carries the per-cell coherence A for the W4.3 energy grading (long\n // confident strokes where structure is coherent, stubby dabs in flat zones).\n vec2 f;\n float coh = 1.0;\n if (uStrokeOrient == 1) {\n vec3 tf = structureTensorField(center, t, safeDir(flow));\n f = safeDir(tf.xy);\n coh = tf.z;\n } else {\n f = safeDir(flow);\n }\n // Per-stroke deterministic local perturbation so alternate stroke layers stay\n // live. Tensor strokes get FAR less angular jitter (the field already orients them\n // — over-jittering the tensor strokes scatters their directions and the rendered\n // field reads isotropic, the §4.2 anisotropy floor). The flow-oriented (non-tensor)\n // strokes keep the full jitter (they have no per-cell orientation source).\n float jScale = (uStrokeOrient == 1) ? 0.15 : 1.0;\n float angJ = (hash21(cc + seed + 11.0) - 0.5) * 0.9 * jScale; // +/- 0.45/0.07 rad\n float curlScale = (uStrokeOrient == 1) ? 0.30 : 1.0;\n float localCurl = (fbm(center * (2.6 + seed * 0.11) + seed * 1.9) - 0.5) * 0.55 * uFlowCurl * curlScale;\n vec2 dir = rotateDir(f, angJ + localCurl);\n\n // AX.W13 — the SBR energy grade is a PROFILE field (energyGrade), not a buried\n // uMedium==5 branch. The van-Gogh profile passes energyGrade=1.0 (the full\n // Starry-Night cascade); oil/oil-pastel pass 0.0 (uniform length). The grade\n // modulates stroke length by local luminance (bright → long confident strokes,\n // dark → short dabs, the Kolmogorov/Batchelor congruence) AND by coherence\n // (coherent zones → long, flat → stubby), lerped in by energyGrade so a profile\n // can dial the cascade from off (0) to full (1).\n float energy = 1.0;\n if (energyGrade > 0.001) {\n float luma = clamp(dot(sampleBase(center, t), W_LUMA), 0.0, 1.0);\n float graded = mix(0.5, 1.5, luma) * mix(0.6, 1.0, coh);\n energy = mix(1.0, graded, energyGrade);\n }\n\n float lenV = cellSize * lenMul * (0.65 + 0.55 * hash21(cc + seed + 23.0)) * energy;\n float halfW = cellSize * halfWMul * (0.70 + 0.55 * hash21(cc + seed + 41.0));\n float bulge = (hash21(cc + seed + 53.0) - 0.5) * lenV * 0.35;\n\n vec2 a = center - dir * (lenV * 0.5);\n vec2 b = center + dir * (lenV * 0.5);\n\n vec3 colMid = brokenColorJitter(\n sampleBase(center, t),\n hash21(cc + seed + 89.0),\n hash21(cc * 2.3 + seed + 97.0),\n 1.0\n );\n\n StrokeHit h = curvedStroke(p, a, b, halfW, bulge, shapeType,\n 7.0, bristleAmp,\n hash21(cc + seed + 67.0), colMid);\n if (h.coverage > best.coverage) best = h;\n }\n }\n return best;\n}\n", pe = "// ── Medium overlays ───────────────────────────────────────────────────────\n\n// A quick re-computation of base color for edge-mask sampling\nvec3 sampleBase(vec2 p, float t) {\n vec2 pw = domainWarp(p, t);\n float id; float vm;\n nucleiField(pw, t, id, vm);\n vec3 c = samplePalette(id);\n c *= 1.0 + uValueVariance * vm;\n return c;\n}\n\n// ── Structure-tensor / edge-tangent-flow (AW.W4.1) ─────────────────────────\n// The keystone of the painterly engine: derive stroke orientation from the COLOR\n// FIELD's OWN structure, not a hand-authored flow pattern. A 3x3 Sobel over\n// luma(sampleBase) yields the gradient (Gx,Gy); the 2x2 structure tensor\n// J = [[Gx², GxGy],[GxGy, Gy²]] eigen-decomposes closed-form into the MINOR\n// eigenvector (the edge-TANGENT — least color change, the stroke direction; the\n// MAJOR eigenvector is the gradient/edge-NORMAL and would make strokes cross the\n// bands) and the coherence A = (λ1-λ2)/(λ1+λ2). Low-coherence (flat) zones relax\n// toward the smooth fallbackDir by (1-A) so tensor noise never reads as jitter.\n// Single-pass small-tap WebGL2 form; the Gaussian-smoothed multi-tap + LIC smear\n// is the AW.W7 WebGPU multi-pass fold. Returns vec3(tangent.x, tangent.y, A).\nvec3 structureTensorField(vec2 p, float t, vec2 fallbackDir) {\n float e = 0.0035; // small-tap neighborhood (edge-mask scale)\n // 3x3 luma samples.\n float l00 = dot(sampleBase(p + vec2(-e, -e), t), W_LUMA);\n float l10 = dot(sampleBase(p + vec2( 0.0, -e), t), W_LUMA);\n float l20 = dot(sampleBase(p + vec2( e, -e), t), W_LUMA);\n float l01 = dot(sampleBase(p + vec2(-e, 0.0), t), W_LUMA);\n float l21 = dot(sampleBase(p + vec2( e, 0.0), t), W_LUMA);\n float l02 = dot(sampleBase(p + vec2(-e, e), t), W_LUMA);\n float l12 = dot(sampleBase(p + vec2( 0.0, e), t), W_LUMA);\n float l22 = dot(sampleBase(p + vec2( e, e), t), W_LUMA);\n // Sobel derivatives.\n float Gx = (l20 + 2.0 * l21 + l22) - (l00 + 2.0 * l01 + l02);\n float Gy = (l02 + 2.0 * l12 + l22) - (l00 + 2.0 * l10 + l20);\n // Structure tensor components.\n float Jxx = Gx * Gx;\n float Jyy = Gy * Gy;\n float Jxy = Gx * Gy;\n // Closed-form 2x2 eigenvalues: λ = 0.5(tr ± sqrt((Jxx-Jyy)² + 4Jxy²)).\n float tr = Jxx + Jyy;\n float disc = sqrt(max((Jxx - Jyy) * (Jxx - Jyy) + 4.0 * Jxy * Jxy, 0.0));\n float lambdaMaj = 0.5 * (tr + disc);\n float lambdaMin = 0.5 * (tr - disc);\n // The MAJOR eigenvector points along the gradient (the edge NORMAL); the MINOR\n // is its perpendicular — the edge TANGENT (the stroke direction). The principal\n // angle is θ = 0.5·atan2(2·Jxy, Jxx-Jyy); this form is robust at Jxy≈0 (where the\n // (λ-Jyy, Jxy) eigenvector formula collapses to a zero vector). The tangent is at\n // θ + 90°. Guard the fully-isotropic (no-gradient) degenerate case → fallback dir.\n vec2 tangent;\n if (disc < 1e-7) {\n tangent = fallbackDir;\n } else {\n float theta = 0.5 * atan(2.0 * Jxy, Jxx - Jyy); // major (gradient) angle\n tangent = vec2(-sin(theta), cos(theta)); // perpendicular = tangent\n }\n // Coherence A in [0,1] — high where one eigenvalue dominates (a strong edge),\n // ~0 in flat zones. Relax toward the smooth fallback by (1 - A).\n float A = (lambdaMaj - lambdaMin) / (lambdaMaj + lambdaMin + 1e-6);\n A = clamp(A, 0.0, 1.0);\n // Coherence-weighted blend: full tensor in coherent zones, smooth in flat ones.\n // Resolve the 180° sign ambiguity toward the fallback so the blend is continuous.\n if (dot(tangent, fallbackDir) < 0.0) tangent = -tangent;\n // The blend weight is a low-power curve (A^0.35) so even MODERATE-coherence zones queue\n // strongly along the tangent (a flowing van-Gogh band reads coherent the way the\n // structure-tensor metric measures it; a linear A under-orients the mid-band and the\n // rendered field reads too isotropic — the §4.2 anisotropy lift). The flat-zone (A≈0)\n // relax to the fallback survives (0^0.35=0), so tensor noise never reads as jitter.\n float blendW = pow(A, 0.28);\n vec2 dir = normalize(mix(fallbackDir, tangent, blendW) + 1e-6);\n return vec3(dir, A);\n}\n\nvec3 mediumWatercolor(vec3 col, vec2 p, float t) {\n // Wet-edge cauliflowers via luma-gradient magnitude\n float eps = 0.004;\n vec3 cx1 = sampleBase(p + vec2(eps, 0.0), t);\n vec3 cx2 = sampleBase(p - vec2(eps, 0.0), t);\n vec3 cy1 = sampleBase(p + vec2(0.0, eps), t);\n vec3 cy2 = sampleBase(p - vec2(0.0, eps), t);\n float gx = dot(cx1 - cx2, W_LUMA);\n float gy = dot(cy1 - cy2, W_LUMA);\n float edge = sqrt(gx * gx + gy * gy) / (2.0 * eps);\n float mask = smoothstep(0.0, 2.5, edge);\n col *= mix(1.0, 0.78, mask * uWetEdge);\n\n // Granulation — pigment settles in paper tooth\n float paper = 0.5 * vnoise(p * 160.0) + 0.5 * vnoise(p * 360.0);\n float pigLoad = 1.0 - dot(col, W_LUMA);\n col *= 1.0 - uGranulation * pigLoad * (paper - 0.5);\n\n // Wash banding — faint horizontal wet gradient\n float band = fbm(vec2(p.x * 1.5, p.y * 0.4));\n col *= 1.0 + 0.04 * (band - 0.5);\n return col;\n}\n\nvec3 mediumPastel(vec3 col, vec2 p, float t) {\n vec2 flow = flowField(p, t);\n vec2 perp = vec2(-flow.y, flow.x);\n float along = dot(p, flow) * uStrokeScale;\n float across = dot(p, perp) * uStrokeScale;\n across += 0.03 * (vnoise(p * 260.0) - 0.5);\n\n float aniso = mix(1.0, 0.18, uStrokeAnisotropy);\n float stroke = fbm(vec2(along * aniso, across));\n col *= mix(1.0, 0.82 + 0.32 * stroke, uStrokeAmount);\n\n // Pastel tooth — tiny high-frequency grain\n float tooth = vnoise(p * 800.0);\n col *= 1.0 - 0.08 * uStrokeAmount * (tooth - 0.5);\n return col;\n}\n", me = "// ── Medium id constants (mirror uniformBridge.ts MEDIUM_ID — the uMedium ladder) ──\n// The StrokeProfile selector (AX.W12 + AX.W13) dispatches on the medium id: oil,\n// van-Gogh, and oil-pastel each AUTHOR a first-class profile in profileFor.\n#define MEDIUM_OIL 3\n#define MEDIUM_CRAYON 4\n#define MEDIUM_VANGOGH 5\n#define MEDIUM_OILPASTEL 6\n\n// ── Crayon — DRY wax pigment on paper tooth (first-class medium, uMedium==4) ──\n// Crayon is not strokes. It's dry pigment crumbs dragged across paper tooth — the\n// DRY tooth-multiply model: anisotropic tooth noise multiplied into the base, a hard\n// SCUMBLE that lets the lower color show through the broken upper layer, and stable\n// OKLCh broken-color pigment patches. NO sheen, NO burnish film — that waxy gloss is\n// the OIL-PASTEL deposition's signature (mediumOilPastel), distinct from dry crayon\n// (AX.W13 slice 8 F1 — the split). Crayon shares the SUBSTRATE (the structure-tensor\n// orientation, the tooth noise, OKLCh brokenColorJitter), not the dispatch body.\nvec3 mediumCrayon(vec3 col, vec2 p, float t) {\n // Orient the deposition along the structure-tensor edge-tangent (the bridge forces\n // tensor for crayon, so read the tensor field directly).\n vec2 flow = structureTensorField(p, t, flowField(p, t)).xy;\n float ang = atan(flow.y, flow.x);\n float ca = cos(-ang), sa = sin(-ang);\n vec2 pr = vec2(p.x * ca - p.y * sa, p.x * sa + p.y * ca);\n\n float aniso = mix(0.45, 0.95, uStrokeAnisotropy);\n float scale = max(uStrokeScale * 1.6, 180.0);\n\n // ── Paper TOOTH height field — the relief the dry pigment rides. Anisotropic\n // (squished along flow, coarse cross-flow). Peaks = ridges, valleys = paper pits.\n float t1 = vnoise(vec2(pr.x * scale * aniso, pr.y * scale));\n float t2 = vnoise(vec2(pr.x * scale * aniso * 0.4, pr.y * scale * 0.4) + 11.0);\n float t3 = vnoise(vec2(pr.x * scale * aniso * 2.1, pr.y * scale * 2.1) + 23.0);\n float tooth = 0.55 * t1 + 0.30 * t2 + 0.15 * t3;\n\n // ── DRY TOOTH-MULTIPLY — the crayon multiplies the tooth relief into the base\n // color (dark in the pits, bright on the ridges). The pressure (uStrokeAmount) sets\n // how hard the multiply bites. This is the DRY model — no pigment build-up, no sheen.\n float pressure = clamp(uStrokeAmount, 0.0, 1.0);\n float bite = mix(0.18, 0.46, pressure); // multiply strength\n vec3 result = col * (1.0 - bite * (0.5 - tooth));\n\n // ── HARD SCUMBLE — a broken upper layer that lets the lower color show through the\n // gaps. A coarse mask gates a HARD (high-contrast) dragged pass so the dry crayon\n // reads as broken pigment crumbs, not a smooth wash. Drag follows the tensor flow.\n float scumbleMask = smoothstep(0.42, 0.78, vnoise(vec2(pr.x * scale * 0.55, pr.y * scale * 0.22) + 7.0));\n float scumbleCoverage = scumbleMask * pressure * 0.7; // hard broken upper layer\n vec3 paper = col * mix(0.80, 0.94, pressure); // the paper showing through\n result = mix(paper, result, scumbleCoverage);\n\n // ── Broken-color pigment: stable wax/pigment patches in OKLCh (W5), not flicker.\n vec2 pigmentCell = floor(pr * max(scale * 0.18, 32.0));\n float pigmentMask = smoothstep(0.28, 0.82, vnoise(pr * scale * 0.21 + 19.0));\n result = brokenColorJitter(\n result,\n hash21(pigmentCell + 17.0),\n hash21(pigmentCell * 2.1 + 31.0),\n 0.45 + 0.55 * pigmentMask\n );\n\n // Crayon is saturation-amplified (the wax chroma punch — but DRY, no sheen).\n result = saturate3(result, 1.08);\n\n return result;\n}\n\n// ── StrokeProfile — the oil-stroke parameter vector (AX.W12, slice 8 F6) ──────\n// The SBR \"stroke = parameter vector\" canon (facet 11): the per-mode if-ladder knobs\n// become struct FIELDS — logic-as-DATA, not an imperative branch. profileFor(medium,\n// mode) populates the profile for a (medium, mode) pair; paintStrokeLayers(profile) runs\n// the four-layer bestOil/paintOver cascade off it. A new medium (the W13 van-Gogh /\n// oil-pastel profiles) AUTHORS a profile entry — it never edits a monolith.\nstruct StrokeProfile {\n int shapeType; // 0 tapered, 1 load-drag, 2 dab, 3 even, 4 comma/crescent\n float bristleAmp; // edge raggedness 0..0.5\n float streakFreq; // internal-streak spatial frequency\n float streakAmp; // internal-streak amplitude\n float impastoAmp; // paint-thickness contribution (the relight reads the height)\n float hardness; // edge-compositing crispness 0..1\n float toothScale; // canvas-tooth (linen weave) spatial frequency\n float toothAmp; // canvas-tooth amplitude\n float pigmentSat; // pigment saturation boost (OKLCh chroma scale)\n float densityBig; // layer-1 (big gestural) placement density gate\n float densityMed; // layer-2 (medium body) placement density gate\n float densitySml; // layer-3 (small dabs) placement density gate\n float energyGrade; // AX.W13 — SBR energy-grade magnitude (0 off; 1 the van-Gogh\n // Starry-Night length cascade). The energy grade is a PROFILE\n // field, NOT a buried uMedium==5 bestOil branch (slice 8 F0).\n float impastoFloor;// AX.W13 — the per-stroke height-crown floor (0.4 oil falloff;\n // 1.0 van-Gogh FULL-height crown so each dab catches its glint).\n float densityFill; // AX.W13 — layer-4 fill-dab density. Oil/oil-pastel fill the\n // bald spots (0.95, full coverage); van-Gogh sets it LOW so the\n // sparse atomic dabs keep their visible inter-stroke ground gaps\n // (the atomicity read — a 0.95 fill would close every gap to a smear).\n float groundFloor; // AX.W13 — the bare-ground darken floor. The gaps between strokes\n // (low accumulated height) multiply toward this floor, so van-Gogh's\n // sparse atomic dabs read over a visibly darker underpainting (the\n // Starry-Night visible ground). Oil/oil-pastel keep 1.0 (no darken).\n};\n\n// The (medium, mode) -> StrokeProfile selector. The if-ladder's knobs are the profile's\n// fields; a new medium adds a CASE here (never an edit to a dispatch body). mode is the\n// uStrokeMode oil-stroke sub-mode (0 oil, 1 palette-knife, 3 brushwork). crayon\n// (uMedium==4) is a PEER tooth-multiply medium that never reaches the stroke cascade.\n// The painterly STROKE mediums are oil (MEDIUM_OIL), van-Gogh (MEDIUM_VANGOGH) and\n// oil-pastel (MEDIUM_OILPASTEL) — each AUTHORS its own profile entry below.\nStrokeProfile profileFor(int medium, int mode) {\n // The oil baseline (mode 0) — balanced modern-abstract/palette-knife hybrid.\n StrokeProfile prof = StrokeProfile(\n 0, // shapeType — tapered\n 0.25, // bristleAmp\n 9.0, // streakFreq\n 0.09, // streakAmp\n 0.9, // impastoAmp\n 0.80, // hardness\n 240.0, // toothScale\n 0.09, // toothAmp\n 1.03, // pigmentSat\n 0.65, // densityBig\n 0.78, // densityMed\n 0.90, // densitySml\n 0.0, // energyGrade — oil is uniform-length (no Starry-Night cascade)\n 0.4, // impastoFloor — oil's 0.4+0.6·edgeN crown falloff\n 0.92, // densityFill — oil fills DENSELY with the directional low-relief fill so the\n // flat-colour regions between the macro strokes are TEXTURED with\n // flow-aligned fine strokes (the §4.2 anisotropy needs every pixel\n // to carry directional gradient; a flat untextured region reads as\n // an isotropic/degenerate tensor and drags mean A down). The fill\n // is now directional + low-streak, so dense coverage textures\n // WITHOUT the prior round-dab speckle.\n 1.0 // groundFloor — oil keeps the full base (no ground darken; fills everywhere)\n );\n\n // ── Van-Gogh — first-class atomic comma/crescent dabs (slice 8 F0). NOT a length\n // modulation of oil: sparse high-contrast SEPARABLE marks with their own asymmetric\n // shape, the full Starry-Night energy cascade (energyGrade=1), and full-height\n // impasto crowns so each dab catches its own glint. The mode sub-modes don't apply\n // (van-Gogh has its own grammar), so this returns early.\n if (medium == MEDIUM_VANGOGH) {\n prof.shapeType = 4; // comma / crescent — the divisionist atomic dab\n prof.bristleAmp = 0.22; // a touch of raggedness on the loaded head\n prof.streakFreq = 16.0; // within-stroke streaks (the loaded comma) — kept in the\n // FFT inertial band (a too-fine streak falls past the §4.3\n // resolved-band edge and reads as steeper, not shallower)\n prof.streakAmp = 0.22;\n prof.impastoAmp = 1.1; // paint per atomic dab — eased off 1.5 so the impasto crown\n // does not push the dab past the PBR-Neutral 0.8 highlight\n // knee (where it compresses + loses the directional contrast\n // the §4.2 anisotropy reads)\n prof.hardness = 0.92; // crisp separable edges (visible inter-stroke gaps)\n prof.toothScale = 220.0;\n prof.toothAmp = 0.06;\n prof.pigmentSat = 0.60; // the van-Gogh chroma — pulled DOWN off the hand-set 1.12\n // so the rendered colorfulness lands inside [55.67, 95.67]\n // (the hand-set punch + the saturated indigo/cobalt/yellow\n // ramp pushed C past the garish ceiling — §4.1 bidirectional)\n // SPARSE-BUT-FLOWING placement — visible inter-stroke canvas gaps (the atomicity read)\n // yet enough directional coverage that the swirl-rows read as coherent flowing bands\n // (the §4.2 anisotropy needs the strokes to dominate the gradient field; too-sparse\n // gates left large flat-ground regions that read isotropic and dragged mean A down).\n prof.densityBig = 0.58;\n prof.densityMed = 0.68;\n prof.densitySml = 0.74;\n prof.energyGrade = 1.0; // the full Starry-Night length cascade (the profile field)\n prof.impastoFloor = 1.0; // FULL-height crowns — every atomic dab catches its glint\n prof.densityFill = 0.30; // a LIGHT directional fill — textures the dark ground with\n // flow-aligned fine strokes so the gaps carry directional\n // gradient (the §4.2 A lift: an untextured dark ground reads\n // isotropic) WHILE staying open enough that the bright atomic\n // dabs still read separable over it (the gap-fraction floor\n // stays satisfied — the fill is fine + dark, not a smear).\n prof.groundFloor = 0.31; // darken the bare-ground gaps to the visible Starry-Night\n // underpainting so the bright impasto dabs read separable —\n // the dab↔ground contrast injects MID-scale energy (the §4.3\n // β-flatten: the PBR-Neutral highlight compression concentrated\n // energy at low-k and steepened β). 0.31 is the balance point:\n // deep enough to hold β in-band, light enough that the textured\n // ground keeps the §4.2 coherence the too-dark 0.26 floor cost.\n return prof;\n }\n\n // ── Oil-pastel — broad smeared directional strokes deposited via the brush engine\n // (slice 8 F1). Creamy soft edges (low hardness), heavy build-up, and a chroma punch.\n // Distinct from the dry-crayon tooth-multiply (mediumCrayon): oil-pastel LAYS strokes.\n if (medium == MEDIUM_OILPASTEL) {\n prof.shapeType = 0; // tapered directional smear — a round dab reads locally\n // isotropic (steep β / low A); the creamy oil-pastel mark\n // is a broad DIRECTIONAL smear, which carries the §4.2\n // coherence + adds the directional mid-band the §4.3 slope\n // needs (the round-dab profile rolled β off to −2.8)\n prof.bristleAmp = 0.10; // smooth creamy edges (waxy pastel, not bristle)\n prof.streakFreq = 18.0; // finer internal smear — adds the mid/high-band the smooth\n // round-dab profile lacked (the β-too-steep fix); the\n // directional along-spine streak flattens the §4.3 slope\n // without crossing the strokes\n prof.streakAmp = 0.20;\n prof.impastoAmp = 0.7; // build-up, but flatter than oil impasto\n prof.hardness = 0.42; // CREAMY — soft compositing, strokes blend on overlap\n prof.toothScale = 280.0;\n prof.toothAmp = 0.12; // the pastel tooth reads stronger\n prof.pigmentSat = 0.80; // the waxy chroma — eased off 1.16 so the rendered C clears\n // the §4.1 band ceiling with margin while the K-M subtractive\n // overlap path (paintOverOklab) keeps the chroma OFF the grey floor\n prof.hardness = 0.58; // creamy but the strokes still REGISTER — a too-soft blend\n // (0.42) smeared the marks into the smooth colour zones and\n // the field rolled off too steep (β≈−2.9, a near-gradient);\n // a crisper deposit keeps the mid/high-band the −5/3 slope needs\n prof.densityBig = 0.62; // broad directional coverage — but NOT over-dense: too many\n // overlapping creamy strokes point in slightly-varied\n // directions and the local §4.2 coherence DROPS (over-dense\n // measured A=0.57 < the 0.62-density A=0.63). The mid-density\n // flow-aligned deposit is the coherence sweet spot.\n prof.densityMed = 0.78;\n prof.densitySml = 0.90;\n prof.energyGrade = 0.80; // a strong Starry-Night length cascade — varies stroke length\n // by luma/coherence so the dab-size energy spreads across\n // scales (the −5/3 cascade the uniform-length 0.0 grade lacked,\n // the §4.3 β-too-steep fix). Not the full van-Gogh 1.0 (oil-\n // pastel keeps its creamy broad character).\n prof.impastoFloor = 0.55; // a softer crown than van-Gogh's full-height glint\n return prof;\n }\n\n // ── Oil sub-modes (mode dispatch on the oil medium).\n if (mode == 1) { // palette knife — razor edges, heavy impasto\n prof.shapeType = 3; // flat, even\n prof.bristleAmp = 0.12;\n prof.streakFreq = 4.0; prof.streakAmp = 0.05;\n prof.impastoAmp = 1.6;\n prof.hardness = 0.95;\n prof.toothAmp = 0.04;\n prof.densityBig = 0.80; prof.densityMed = 0.88; prof.densitySml = 0.70;\n } else if (mode == 3) { // thick brushwork — heavy bristle brush\n prof.shapeType = 0; // tapered\n prof.bristleAmp = 0.32;\n prof.streakFreq = 14.0; prof.streakAmp = 0.14;\n prof.impastoAmp = 1.2;\n prof.hardness = 0.85;\n prof.toothAmp = 0.07;\n }\n return prof;\n}\n\n// The single parameterized four-layer stroke cascade (AX.W12). The four hand-unrolled\n// bestOil/paintOver invocations collapse into ONE body driven by the profile + the\n// uniform-derived per-layer scale/anisotropy multipliers (which are mode-INVARIANT — the\n// per-layer offsets, seeds, and len/wid muls are fixed across modes, so they stay here as\n// the cascade's structure; only the profile's knobs differentiate the medium). The\n// mode passes through for the two mode-special-cased shapes (knife layer-3 dabs +\n// layer-4 fill shape), which are placement details of the cascade, not profile knobs.\nvoid paintStrokeLayers(inout vec3 col, inout float height, StrokeProfile prof,\n int mode, vec2 p, float t) {\n // Scales & multipliers from uniforms (mode-invariant cascade structure).\n float baseScale = max(uStrokeScale * 0.006, 0.008);\n // Three primary layers: big gestural, medium body, small dabs.\n // W-AUR-STUDIO D5 — the −5/3 radii respacing was MEASURED + REVERTED (recorded, not\n // silently struck). The φ-adjacent geometric candidate (2.4 / 1.45 / 0.87, a fixed\n // ≈1.66× step) moved oil-pastel β −2.534 → −2.413 — TOWARD the −5/3 band but NOT into\n // it (floor −1.85), so the keep-iff-into-band gate failed. The single-pass WebGL2 path\n // is the true limiter (HC-aurora §5): the oil-pastel β + the oil/oil-pastel anisotropy\n // residual is the named AY.W-AUR-T5 multi-pass anisotropic-Kuwahara candidate. The\n // hand-set 2.4 / 1.1 / 0.45 stays — it is van-Gogh's landed-band spacing.\n float sBig = baseScale * 2.4;\n float sMed = baseScale * 1.1;\n float sSml = baseScale * 0.45;\n\n // Strokes are elongated HARD (long·thin) so the rendered field reads strongly\n // directional — the §4.2 structure-tensor anisotropy lift. A short fat dab is locally\n // isotropic and drags the measured A down; a long thin stroke that hugs the tensor\n // tangent is what gives Starry Night its A≈0.83. The width muls are pulled tighter to\n // keep the long·thin aspect.\n float lenMulBig = mix(3.0, 5.2, uStrokeAnisotropy);\n float widMulBig = mix(0.42, 0.22, uStrokeAnisotropy);\n float lenMulMed = mix(2.8, 4.8, uStrokeAnisotropy);\n float widMulMed = mix(0.40, 0.21, uStrokeAnisotropy);\n float lenMulSml = mix(2.2, 3.6, uStrokeAnisotropy);\n float widMulSml = mix(0.36, 0.23, uStrokeAnisotropy);\n\n float jitterAmt = 0.75; // large jitter — no grid\n vec2 flow = flowField(p, t);\n\n // Layer 1 — big gestural strokes (sparse, shaping)\n StrokeHit hBig = bestOil(p, sBig, lenMulBig, widMulBig, jitterAmt * 0.55,\n prof.densityBig, prof.shapeType, prof.bristleAmp, flow, t, 1.3,\n prof.energyGrade);\n paintOver(col, height, hBig, prof.streakFreq * 0.7, prof.streakAmp,\n uImpasto * prof.impastoAmp * uStrokeAmount, prof.hardness, 1.3, prof.impastoFloor);\n\n // Layer 2 — medium body strokes\n StrokeHit hMed = bestOil(p + vec2(11.3, 3.7), sMed, lenMulMed, widMulMed,\n jitterAmt, prof.densityMed, prof.shapeType, prof.bristleAmp, flow, t, 2.7,\n prof.energyGrade);\n paintOver(col, height, hMed, prof.streakFreq, prof.streakAmp,\n uImpasto * prof.impastoAmp * uStrokeAmount, prof.hardness, 2.7, prof.impastoFloor);\n\n // Layer 3 — small strokes (more frequent, smaller). Even the knife layer-3 stays the\n // medium's DIRECTIONAL shape, not a round dab — the round-dab sparkle was a fine-scale\n // isotropic speckle that dragged the §4.2 anisotropy down + rolled the §4.3 slope shallow.\n int smlShape = prof.shapeType;\n StrokeHit hSml = bestOil(p + vec2(-5.1, 8.4), sSml, lenMulSml, widMulSml,\n jitterAmt * 0.9, prof.densitySml, smlShape,\n prof.bristleAmp * 0.85, flow, t, 4.1, prof.energyGrade);\n paintOver(col, height, hSml, prof.streakFreq * 1.4, prof.streakAmp * 0.6,\n uImpasto * prof.impastoAmp * 0.45 * uStrokeAmount, prof.hardness, 4.1, prof.impastoFloor);\n\n // Layer 4 — fill strokes (dense, fine) — cover bald spots. The density is\n // PROFILE-driven (prof.densityFill): oil/oil-pastel fill to full coverage; van-Gogh\n // keeps it sparse so the bare-ground gaps between its atomic dabs survive. The fill is\n // ELONGATED + flow-oriented (NOT a round dab) — a dense round-dab fill is locally\n // isotropic and was the dominant §4.2 anisotropy sink; an oriented fine stroke keeps the\n // fill directional so the whole field reads coherent.\n float sFill = baseScale * 0.22;\n float lenMulFill = mix(2.0, 3.2, uStrokeAnisotropy);\n float widMulFill = mix(0.34, 0.24, uStrokeAnisotropy);\n int fillShape = (mode == 1) ? 3 : prof.shapeType; // knife=even; others inherit the medium's directional shape\n StrokeHit hFill = bestOil(p + vec2(3.9, -6.2), sFill, lenMulFill, widMulFill,\n jitterAmt * 0.8, prof.densityFill, fillShape,\n prof.bristleAmp * 0.6, flow, t, 8.9, prof.energyGrade);\n // The fill is a low-relief, low-streak base coat — it covers bald spots WITHOUT adding\n // fine bright speckle (a high-impasto/high-streak fill caught a per-dab glint that read\n // as isotropic noise over the directional macro strokes — the §4.2/§4.3 sink). Its\n // positional jitter is tamed too so the fill strokes stay flow-aligned, not scattered.\n paintOver(col, height, hFill, prof.streakFreq * 1.2, prof.streakAmp * 0.35,\n uImpasto * prof.impastoAmp * 0.18 * uStrokeAmount, prof.hardness * 0.9, 8.9, prof.impastoFloor);\n\n // Optional crosshatch layer\n if (uStrokeLayers == 2) {\n vec2 flow2 = vec2(-flow.y, flow.x);\n StrokeHit hX = bestOil(p + vec2(7.3, -2.1), sMed, lenMulMed * 0.9, widMulMed,\n jitterAmt, prof.densityMed * 0.7, prof.shapeType, prof.bristleAmp, flow2, t, 6.5,\n prof.energyGrade);\n paintOver(col, height, hX, prof.streakFreq, prof.streakAmp * 0.85,\n uImpasto * prof.impastoAmp * 0.55 * uStrokeAmount, prof.hardness, 6.5, prof.impastoFloor);\n }\n}\n\n// The shared stroke-medium SUBSTRATE finish (AX.W12 + AX.W13): paint the four-layer\n// cascade off the profile, then canvas-tooth + relight + saturation. The three painterly\n// STROKE mediums (oil / van-Gogh / oil-pastel) each AUTHOR their own profile and call\n// this — the substrate is shared, the PROFILE (not a dispatch-body fork) differentiates\n// the medium (slice 8 F6). mode passes through for the cascade's mode-special-cased shapes.\nvec3 paintStrokeMedium(vec3 col, vec2 p, float t, StrokeProfile prof, int mode) {\n vec3 result = col;\n // AW.W4.2 — accumulated paint HEIGHT across the stroke layers. The relight reads\n // its gradient for the normal; the canvas tooth seeds the base relief.\n float height = 0.0;\n paintStrokeLayers(result, height, prof, mode, p, t);\n\n // Canvas tooth — linen weave\n float tooth1 = vnoise(p * prof.toothScale);\n float tooth2 = vnoise(p * prof.toothScale * vec2(0.6, 2.4) + 37.0);\n float tooth = (0.6 * tooth1 + 0.4 * tooth2) - 0.5;\n result *= 1.0 + tooth * prof.toothAmp * uCanvasGrain;\n\n // AW.W4.2 — relight the accumulated paint height with the movable uLightDir\n // source (diffuse + Blinn specular, in LINEAR before aces()). The canvas tooth\n // is the base relief term so the weave also catches the raking light.\n float canvasBase = tooth * prof.toothAmp * 0.5;\n result = relightImpasto(result, height, canvasBase);\n\n // AX.W13 — the visible ground. The bare-ground gaps (low accumulated stroke height)\n // multiply toward prof.groundFloor, so van-Gogh's sparse atomic dabs read as separable\n // marks over a darker underpainting (oil/oil-pastel keep groundFloor=1.0 → no-op). The\n // smoothstep keeps the impasto dabs (high height) at full value; only the open ground\n // between them darkens.\n result *= mix(prof.groundFloor, 1.0, smoothstep(0.0, 0.38, height));\n\n // Pigment saturation boost\n result = saturate3(result, prof.pigmentSat);\n\n return result;\n}\n\nvec3 mediumOil(vec3 col, vec2 p, float t) {\n // The oil medium is a thin body: fetch the oil profile (mode-dispatched), paint via\n // the shared stroke substrate. uStrokeMode: 0 oil (gestural), 1 knife, 3 brushwork.\n int mode = uStrokeMode;\n StrokeProfile prof = profileFor(MEDIUM_OIL, mode);\n return paintStrokeMedium(col, p, t, prof, mode);\n}\n\n// AX.W13 — the van-Gogh atomic-stroke medium is now a FIRST-CLASS body, NOT a\n// mediumOil passthrough (slice 8 F0). It composes its OWN vangogh StrokeProfile (the\n// comma/crescent shapeType, sparse high-contrast atomic placement with visible\n// inter-stroke gaps, the full Starry-Night energy-grade cascade as a PROFILE field,\n// and full-height impasto crowns) through the shared stroke substrate. The bridge forces\n// the ETF (tensor) orientation so the swirl-rows queue along the color field's tangent;\n// the within-stroke OKLCh broken color (paintOver) + the OKLab stroke OVER-composite\n// give pigment-true atom-level shimmer. No subject matter — the \"source image\" is the\n// generated nuclei field, so dabs trace its iso-bands (the Starry-Night swirl-rows).\nvec3 mediumVangogh(vec3 col, vec2 p, float t) {\n StrokeProfile prof = profileFor(MEDIUM_VANGOGH, 0);\n return paintStrokeMedium(col, p, t, prof, 0);\n}\n\n// AX.W13 — the oil-pastel medium (uMedium==6) is a DISTINCT stroke-deposition body,\n// split out of the dry-crayon tooth-multiply (mediumCrayon) (slice 8 F1). It DEPOSITS\n// broad smeared directional strokes via the same brush engine with a creamy soft\n// hardness, heavy pigment build-up where strokes overlap, and a chroma punch — the\n// stroke-deposition model that gives oil-pastel its depth and less-uniform read.\n// Shares the SUBSTRATE (the stroke cascade + tooth + relight) with oil/van-Gogh, NOT\n// the dispatch body of dry crayon. The bridge forces tensor orientation.\nvec3 mediumOilPastel(vec3 col, vec2 p, float t) {\n StrokeProfile prof = profileFor(MEDIUM_OILPASTEL, 0);\n return paintStrokeMedium(col, p, t, prof, 0);\n}", A = "\n", he = `#version 300 es
|
|
10
|
+
var le = "#version 300 es\nin vec2 aPos;\nout vec2 vUv;\nvoid main() {\n vUv = aPos * 0.5 + 0.5;\n gl_Position = vec4(aPos, 0.0, 1.0);\n}", ue = "// ── Palette LUT (W5 — OKLCh/OKLab interpolation) ──────────────────────────────\n// The endpoints stay CPU-baked to LINEAR sRGB (the Aras precompute pattern); only\n// the BETWEEN-endpoint interpolation runs in perceptual space. AX.W11 — the ramp\n// itself (the smoothstep ease + the OKLab-rectangular-vs-OKLCh-hue-arc dispatch on\n// uHuePath) is the shared samplePaletteRamp from procedural-color.glsl, the SAME\n// source the WebGPU twin splices; samplePalette here only selects the bracketing\n// stop pair + the raw inter-stop t and hands them to the shared ramp.\nvec3 samplePalette(float id) {\n id = clamp(id, 0.0, 1.0);\n float scaled = id * float(uStopCount - 1);\n int i0 = int(floor(scaled));\n int i1 = min(i0 + 1, uStopCount - 1);\n float t = fract(scaled);\n return samplePaletteRamp(uPalette[i0], uPalette[i1], t, uHuePath);\n}\n\n// ── Nuclei field ──────────────────────────────────────────────────────────\nvoid nucleiField(vec2 p, float t, out float paletteId, out float valueMod) {\n float accumBias = 0.0;\n float accumValue = 0.0;\n float accumW = 0.0;\n for (int i = 0; i < MAX_NUCLEI; i++) {\n if (i >= uNucleiCount) break;\n vec2 posI = uNucleiPos[i]\n + uNucleiDriftRadius[i] * vec2(\n cos(t * uNucleiDrift * K_NUCLEI + uNucleiDriftPhase[i]),\n sin(t * uNucleiDrift * K_NUCLEI + uNucleiDriftPhase[i] * 1.13)\n );\n vec2 diff = p - posI;\n // Anisotropic Gaussian: rotate diff into the nucleus's local frame\n // (major axis along uNucleiAngle), then scale the major-axis component by\n // 1/elongation so the squared distance describes an ellipse. Defaults\n // 1.0/0.0 reduce to the isotropic dot(diff, diff).\n float ca = cos(uNucleiAngle[i]);\n float sa = sin(uNucleiAngle[i]);\n vec2 local = vec2( ca * diff.x + sa * diff.y,\n -sa * diff.x + ca * diff.y);\n float along = local.x / max(uNucleiElong[i], 0.01);\n float across = local.y;\n float d2 = along * along + across * across;\n float r = max(uNucleiRadius[i], 0.01);\n float w = exp(-uSoftmaxBeta * d2 / (r * r));\n accumBias += w * uNucleiPaletteBias[i];\n accumValue += w * uNucleiValueBias[i];\n accumW += w;\n }\n paletteId = accumBias / max(accumW, 1e-4);\n valueMod = accumValue / max(accumW, 1e-4);\n\n // Palette drift — slow global paletteId breathe between adjacent stops. The\n // rate rides K_PAL (perceptible ~30–60s hue cycle at the default coefficient);\n // the amplitude lifts the authored coefficient into a 0.03..0.06 paletteId band\n // so the palette visibly travels rather than dithering within one stop.\n float palAmp = clamp(uPaletteDrift * 6.0, 0.0, 0.06);\n paletteId += palAmp * sin(t * uPaletteDrift * K_PAL);\n paletteId = clamp(paletteId, 0.0, 1.0);\n}\n", de = "// ── Flow field ────────────────────────────────────────────────────────────\n// Structure-tensor prototype (body defined post-sampleBase in the mediums block).\n// Returns vec3(dir.x, dir.y, coherence) — the minor-eigenvector tangent + A.\nvec3 structureTensorField(vec2 p, float t, vec2 fallbackDir);\n\nvec2 flowField(vec2 p, float t) {\n vec2 dir = vec2(1.0, 0.0);\n if (uFlowPattern == 1) {\n // radial\n dir = normalize(p - uFlowFocal + 1e-4);\n } else if (uFlowPattern == 2) {\n // swirl — tangent to radial\n vec2 rad = normalize(p - uFlowFocal + 1e-4);\n dir = vec2(-rad.y, rad.x);\n } else if (uFlowPattern == 3) {\n // diagonal\n float a = radians(uFlowAngle);\n dir = vec2(cos(a), sin(a));\n } else if (uFlowPattern == 4) {\n // multi — curl-noise driven\n float n = fbm(p * 2.0 + t * 0.02);\n float a = n * 6.2831;\n dir = vec2(cos(a), sin(a));\n } else if (uFlowPattern == 5) {\n // tensor / ETF — the color field's OWN edge-tangent flow (AW.W4.1). The\n // diagonal angle seeds the smooth fallback the coherence-weighted blend\n // relaxes toward in low-structure zones. Returns directly (no curl/cursor\n // post-mix — the tensor field already carries the structure orientation;\n // the cursor warp still drives the underlying color field in domainWarp).\n float a = radians(uFlowAngle);\n vec2 fallback = vec2(cos(a), sin(a));\n return structureTensorField(p, t, fallback).xy;\n }\n // curl — perturb by noise. Radial gets much less curl so rays stay clean.\n if (uFlowCurl > 0.0) {\n float n = fbm(p * 3.0) - 0.5;\n float curlAmt = uFlowCurl;\n if (uFlowPattern == 1) curlAmt *= 0.25; // radial: preserve ray clarity\n if (uFlowPattern == 2) curlAmt *= 0.55; // swirl: allow moderate curl\n float a = atan(dir.y, dir.x) + n * 3.14159 * curlAmt;\n dir = vec2(cos(a), sin(a));\n }\n // cursor influence — swirl around the cursor position\n if (uCursorStrength > 0.001) {\n vec2 toCur = uCursor - p;\n float d = length(toCur);\n float r = max(uCursorRadius, 0.01);\n float w = exp(-(d * d) / (r * r * 0.5));\n // swirl tangent\n vec2 tangent = vec2(-toCur.y, toCur.x) / max(d, 1e-4);\n float a0 = atan(dir.y, dir.x);\n float a1 = atan(tangent.y, tangent.x);\n // blend angle\n float da = a1 - a0;\n da = atan(sin(da), cos(da)); // wrap to [-pi, pi]\n float a = a0 + da * w * uCursorStrength;\n dir = vec2(cos(a), sin(a));\n }\n // AW.W8.1 — velocity-reactive swirl-BURST: a fast flick (uCursorBurst, decaying\n // over ~1s) injects a transient flow bias along the pointer velocity, distinct from\n // the steady attraction above (the stateless swirl is SUPERSEDED by this form). The\n // burst is gated by the master tempo scalar on the CPU side (it decays to 0 under\n // PRM/pause), so this term cannot leak motion under reduced-motion.\n if (uCursorBurst > 0.001) {\n vec2 toCur = uCursor - p;\n float d = length(toCur);\n float r = max(uCursorRadius, 0.01);\n float w = exp(-(d * d) / (r * r * 0.7));\n // Bias the flow toward the velocity direction, scaled by the decaying burst.\n vec2 velDir = uCursorVelocity / max(length(uCursorVelocity), 1e-4);\n float a0 = atan(dir.y, dir.x);\n float a1 = atan(velDir.y, velDir.x);\n float da = atan(sin(a1 - a0), cos(a1 - a0));\n float a = a0 + da * w * uCursorBurst * 0.8;\n dir = vec2(cos(a), sin(a));\n }\n return dir;\n}\n", fe = "// Khronos PBR-Neutral tonemap (hue + saturation preserving)\nvec3 aces(vec3 color) {\n const float startCompression = 0.8 - 0.04;\n const float desaturation = 0.15;\n float x = min(color.r, min(color.g, color.b));\n float offset = x < 0.08 ? x - 6.25 * x * x : 0.04;\n color -= offset;\n float peak = max(color.r, max(color.g, color.b));\n if (peak < startCompression) return clamp(color, 0.0, 1.0);\n float d = 1.0 - startCompression;\n float newPeak = 1.0 - d * d / (peak + d - startCompression);\n color *= newPeak / peak;\n float g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0);\n return clamp(mix(color, newPeak * vec3(1.0), g), 0.0, 1.0);\n}\n", pe = "// ── Curved swept brushstroke primitive ────────────────────────────────────\n// A stroke is a curved spine from A to B with quadratic bulge K (sideways offset\n// at midpoint). Width varies along length via a shape profile. Edge is ragged\n// (bristle-modulated), ends are rounded blobs, inside has streak modulation.\n//\n// For pixel p, we invert an approximate projection onto the curved spine:\n// 1) project p onto straight AB axis -> along0, cross0\n// 2) at parameter along0, spine offset = 4·K·along0·(1-along0) perpendicular\n// 3) cross = cross0 - spineOffset, refine along one iteration\n// Coverage blends paint atop col based on per-stroke color sampled from the\n// underlying base at spine midpoint — so overlapping strokes from different\n// palette regions meet at hard, ragged demarcations.\n\nstruct StrokeHit {\n float coverage; // 0..1\n vec3 color;\n float alongT; // 0..1 along spine, for internal modulation\n float crossN; // cross distance / current half-width, signed\n float edgeN; // distance to edge in half-widths (0 = at edge, 1 = at spine)\n};\n\nStrokeHit noHit() { return StrokeHit(0.0, vec3(0.0), 0.0, 0.0, 0.0); }\n\nvec2 rotateDir(vec2 dir, float angle) {\n float ca = cos(angle), sa = sin(angle);\n return vec2(dir.x * ca - dir.y * sa, dir.x * sa + dir.y * ca);\n}\n\nvec2 safeDir(vec2 dir) {\n float len = length(dir);\n return len > 1e-4 ? dir / len : vec2(1.0, 0.0);\n}\n\n// Shape profile along the stroke. type:\n// 0 tapered — thin-fat-thin (classic brush)\n// 1 load-drag — fat start, tapers to a point\n// 2 dab — ellipse-like, round center\n// 3 even — near-constant, slight end taper\n// 4 comma — van-Gogh atomic dab: ASYMMETRIC taper, fat near one end and\n// drawn to a fine point at the other (the divisionist crescent\n// mark). The width peak sits off-centre (~0.32) so the two ends\n// taper at DIFFERENT rates — a loaded round head + a dragged tail.\nfloat strokeShape(float t, int type) {\n t = clamp(t, 0.0, 1.0);\n if (type == 1) {\n // loaded at t=0, tapering\n return pow(1.0 - t, 0.55) * smoothstep(0.0, 0.08, t);\n } else if (type == 2) {\n // dab / blob\n float d = t - 0.5;\n return exp(-d * d * 12.0);\n } else if (type == 3) {\n // mostly even, slight end softening\n return smoothstep(0.0, 0.08, t) * smoothstep(1.0, 0.92, t) * 0.95 + 0.05;\n } else if (type == 4) {\n // comma / crescent — width peaks at ~0.32 then drags to a fine tail. The two\n // sides of the peak fall at different exponents (a fat round head, a long thin\n // tail), so the mark reads as a directional van-Gogh comma, not a symmetric oval.\n float peak = 0.32;\n float head = smoothstep(0.0, peak, t); // rounded loaded head\n float tail = pow(clamp((1.0 - t) / (1.0 - peak), 0.0, 1.0), 1.6); // dragged thin tail\n return clamp(min(head, tail) * 1.04, 0.0, 1.0);\n }\n // 0 tapered\n return smoothstep(0.0, 0.22, t) * smoothstep(1.0, 0.78, t);\n}\n\n// A single curved stroke.\n// a, b — endpoints\n// halfW — base half-width (world units)\n// bulge — signed perpendicular midpoint offset, in world units\n// shapeType — 0..3\n// bristleFreq — spatial frequency of edge raggedness\n// bristleAmp — 0..0.5 fraction of halfW chewed away at edge extrema\n// streakSeed — uniqueness seed for internal streaks\n// colAtMid — pre-sampled color at midpoint\nStrokeHit curvedStroke(vec2 p, vec2 a, vec2 b, float halfW,\n float bulge, int shapeType,\n float bristleFreq, float bristleAmp,\n float streakSeed, vec3 colAtMid) {\n vec2 ab = b - a;\n float L = length(ab);\n if (L < 1e-5) return noHit();\n vec2 tang = ab / L;\n vec2 norm = vec2(-tang.y, tang.x);\n\n vec2 rel = p - a;\n float along0 = dot(rel, tang) / L; // straight projection 0..1 along AB\n float cross0 = dot(rel, norm); // signed cross\n\n // Spine sideways offset due to bulge (parabolic)\n float bend = 4.0 * bulge * along0 * (1.0 - along0);\n float cross1 = cross0 - bend;\n\n // One refinement step: as the spine bends, the closest-point along shifts.\n // Spine tangent differs from straight tangent; first-order correction:\n float dBend = 4.0 * bulge * (1.0 - 2.0 * along0); // d(bend)/d(along)\n float along1 = along0 + (cross1 * dBend) / (L * (1.0 + dBend * dBend / (L * L)));\n\n // Bristle-ragged edge: half-width gets chewed by 1D noise along the spine\n float edgeNoise = fbm(vec2(along1 * bristleFreq, streakSeed * 7.3)) - 0.5;\n float edgeNoise2 = fbm(vec2(along1 * bristleFreq * 2.3, streakSeed * 3.1 + 17.0)) - 0.5;\n float edgeMod = 1.0 - bristleAmp * (0.6 + 0.4 * edgeNoise) * (0.5 + edgeNoise2);\n\n // Width profile along the stroke\n float shape = strokeShape(along1, shapeType);\n float halfWNow = halfW * shape * edgeMod;\n\n // Inside-segment coverage\n float cov = 0.0;\n if (along1 >= 0.0 && along1 <= 1.0 && halfWNow > 1e-6) {\n float cn = abs(cross1) / halfWNow;\n // fwidth-derived AA: half-width from the field's screen-space gradient so the\n // edge holds ~1px regardless of zoom/DPR (mirrors metaball.frag.ts:252-255).\n float aaC = max(fwidth(cn), 1e-6);\n cov = 1.0 - smoothstep(1.0 - aaC, 1.0 + aaC, cn);\n }\n\n // End-cap blobs (rounded tips, not perpendicular cuts)\n float capA = 0.0, capB = 0.0;\n {\n vec2 capCenterA = a + norm * (bulge * 0.0); // at a\n vec2 capCenterB = a + tang * L + norm * bend * 0.0 + norm * 0.0; // at b; bend is 0 at endpoints\n capCenterB = b;\n float endShapeA = strokeShape(0.02, shapeType);\n float endShapeB = strokeShape(0.98, shapeType);\n float rA = halfW * endShapeA * (0.9 + 0.2 * fbm(vec2(streakSeed * 13.0)));\n float rB = halfW * endShapeB * (0.9 + 0.2 * fbm(vec2(streakSeed * 19.0 + 5.0)));\n float dA = length(p - capCenterA);\n float dB = length(p - capCenterB);\n // fwidth-derived AA on the clean Euclidean cap distances (mirrors metaball.frag.ts:252-255).\n float aaA = max(fwidth(dA), 1e-6);\n float aaB = max(fwidth(dB), 1e-6);\n capA = 1.0 - smoothstep(rA - aaA, rA + aaA, dA);\n capB = 1.0 - smoothstep(rB - aaB, rB + aaB, dB);\n // Only apply caps if we're BEYOND the segment; otherwise the segment wins.\n capA *= (along1 < 0.05) ? 1.0 : 0.0;\n capB *= (along1 > 0.95) ? 1.0 : 0.0;\n }\n\n float coverage = max(cov, max(capA, capB));\n if (coverage < 0.002) return noHit();\n\n // Edge distance (for impasto later): 0 at edge, 1 at spine\n float edgeDist = halfWNow > 1e-6 ? clamp(1.0 - abs(cross1) / halfWNow, 0.0, 1.0) : 0.0;\n\n return StrokeHit(\n coverage,\n colAtMid,\n clamp(along1, 0.0, 1.0),\n halfWNow > 1e-6 ? cross1 / halfWNow : 0.0,\n edgeDist\n );\n}\n\n// The painterly STROKE mediums (those that deposit via paintStrokeLayers/paintOver):\n// oil (3), van-Gogh (5), oil-pastel (6). For these the stroke OVER-composite runs in\n// OKLab so overlapping complementaries mix through a chromatic path, not the linear-\n// RGB grey mud; the smooth/atmospheric pole never reaches paintOver so it pays nothing.\nbool isPainterlyStroke() {\n return uMedium == 3 || uMedium == 5 || uMedium == 6;\n}\n\n// OKLab OVER-composite: lerp L,a,b of the over-color toward the under-color (the\n// Ottosson matrices spliced from procedural-color.glsl.ts — no new color math). Two\n// complementary-hued strokes overlapping transition through OKLab, where the midpoint\n// holds chroma, instead of the linear mix() that desaturates toward grey (slice 8 F2).\nvec3 paintOverOklab(vec3 under, vec3 over, float alpha) {\n vec3 labU = linOklab(under);\n vec3 labO = linOklab(over);\n return max(oklabToLinearSrgb(mix(labU, labO, alpha)), vec3(0.0));\n}\n\n// Composite stroke over col with internal streaking + ACCUMULATE paint HEIGHT.\n// streakAmp — 0..0.2 how much internal streaks darken/lighten\n// impastoAmp — 0..1 paint-thickness contribution (the height the relight reads)\n// hardness — 0..1 how crisp the compositing transition is (1 = razor, 0 = creamy)\n//\n// AW.W4.2 — the fixed-RGB edge rim (the phantom upper-left light) is RETIRED. Each\n// stroke now deposits PAINT HEIGHT into the height accumulator (coverage x per-stroke\n// thickness, perturbed by the bristle/streak fbm for ridges/grooves); mediumOil\n// derives a normal from the accumulated height gradient (dFdx/dFdy) and relights it\n// once with the movable uLightDir source (relightImpasto), in linear before aces().\n//\n// AX.W13 — the internal streak now perturbs HUE + CHROMA in OKLCh (not value-only),\n// so a single stroke carries a small hue gradient (broken color at the ATOM level —\n// adjacent strokes shimmer like real impasto, not flat stamped swatches; slice 8 F5);\n// and the OVER-composite runs in OKLab on the painterly stroke mediums so overlapping\n// complementaries mix as pigment, not the linear-mix grey (slice 8 F2).\n// impastoFloor — the per-stroke height-crown floor: oil's 0.4+0.6*edgeN reads\n// floor=0.4 (the crown tapers to 40% at the stroke edge); the van-Gogh\n// profile passes floor=1.0 so each atomic dab catches a FULL-height\n// glint (scope item 1d). The crown magnitude is floor + (1-floor)*edgeN.\nvoid paintOver(inout vec3 col, inout float height, StrokeHit s,\n float streakFreq, float streakAmp,\n float impastoAmp, float hardness, float streakSeed,\n float impastoFloor) {\n if (s.coverage < 0.002) return;\n float strokeOpacity = clamp(uStrokeAmount, 0.0, 1.0);\n if (strokeOpacity <= 0.001) return;\n vec3 c = s.color;\n\n // Internal streaks — fbm along spine, modulated by cross position. Gives\n // the loaded-brush look: some bristles carry more pigment than others.\n float streakA = fbm(vec2(s.alongT * streakFreq, s.crossN * 2.7 + streakSeed));\n float streakB = fbm(vec2(s.alongT * streakFreq * 0.6 + streakSeed * 3.7, s.crossN * 4.1));\n float streak = 0.6 * (streakA - 0.5) + 0.4 * (streakB - 0.5);\n\n // ── WITHIN-stroke OKLCh broken color (AX.W13, slice 8 F5). The streak fBm perturbs\n // the stroke's HUE + CHROMA along alongT/crossN, seeded per-stroke, so a single\n // stroke reads a small hue gradient (the atom-level broken-color shimmer). Value is\n // carried by the same streak so the loaded-brush light/dark survives. Gated to the\n // painterly stroke mediums + uBrokenColor (the smooth pole keeps the cheap value-only\n // streak). A second decorrelated fBm drives the hue axis so hue ≠ a scaled value.\n float hueStreak = fbm(vec2(s.alongT * streakFreq * 0.45 + streakSeed * 5.3,\n s.crossN * 1.9 + streakSeed * 1.7)) - 0.5;\n if (isPainterlyStroke() && uBrokenColor > 0.001) {\n vec3 lch = oklabToOklch(linOklab(c));\n // ±~10° within-stroke hue swing + ±~9% chroma, scaled by uBrokenColor. Smaller\n // than the per-stroke cell jitter (brokenColorJitter) — this is the intra-stroke\n // gradient layered on top of the per-cell pigment patch.\n lch.z += hueStreak * (20.0 * PI / 180.0) * uBrokenColor;\n lch.y = max(lch.y * (1.0 + (streak) * 0.18 * uBrokenColor), 0.0);\n c = max(oklabToLinearSrgb(oklchToOklab(lch)), vec3(0.0));\n }\n // Value streak (the loaded-brush light/dark across the bristles) — kept for every\n // medium that reaches paintOver (the smooth pole never does). The along-spine value\n // modulation is the loaded-brush light/dark that gives each stroke its OWN directional\n // luminance band — the structure-tensor signal a low-contrast color field (a uniform-hue\n // oil field) has no other source for (the §4.2 anisotropy lift on the flat-palette case).\n c *= 1.0 + streak * streakAmp * 3.2;\n\n // Cross-stroke ridge/valley shading — a catch-light along the spine centre, darkening to\n // the stroke edges. This is the strongest DIRECTIONAL luminance cue: it carves a bright\n // ridge down the stroke's length and dark grooves between adjacent strokes, so a field of\n // parallel strokes reads as parallel light/dark bands (the high-A van-Gogh signature) even\n // where the underlying colour is flat. s.edgeN is 0 at the edge, 1 at the spine.\n float ridgeShade = smoothstep(0.0, 0.55, s.edgeN);\n c *= 1.0 + (ridgeShade - 0.5) * 0.30;\n\n float softLimit = mix(0.35, 0.98, hardness);\n float alpha = smoothstep(0.0, 1.0 - softLimit, s.coverage) * strokeOpacity;\n // OKLab OVER-composite on the painterly stroke mediums (overlap mixes as pigment,\n // not grey); the smooth pole never reaches paintOver, so it keeps the cheap path.\n col = isPainterlyStroke() ? paintOverOklab(col, c, alpha) : mix(col, c, alpha);\n\n // ── Accumulate paint HEIGHT (AW.W4.2). Thickness peaks at the stroke spine and\n // falls toward the impastoFloor at the edge (edgeN: 0=edge, 1=spine), perturbed by\n // the bristle streak so ridges/grooves form. The relight reads the gradient of this\n // field. The van-Gogh profile's floor=1.0 gives every atomic dab a full-height crown.\n float ridge = 0.7 + 0.3 * (streak + 0.5);\n float crown = impastoFloor + (1.0 - impastoFloor) * s.edgeN;\n height += s.coverage * impastoAmp * crown * ridge * alpha;\n}\n\n// AW.W4.2 — relight the accumulated paint height. Derive a normal from the height\n// gradient (dFdx/dFdy — derivatives are in-pattern; fwidth is already used for AA),\n// then apply diffuse + Blinn specular from the movable uLightDir/uLightColor source.\n// LINEAR-space lighting BEFORE aces() (the tonemap/OETF stay locked); thin strokes\n// inherit a low shininess, thick impasto raises it (high-height ridges catch a sharp\n// glint). canvasBase is the tooth height so the canvas weave also catches the light.\nvec3 relightImpasto(vec3 col, float height, float canvasBase) {\n float h = height + canvasBase;\n // Height-gradient normal. heightScale trades relief depth for surface flatness.\n float heightScale = 6.0;\n vec3 N = normalize(vec3(-dFdx(h) * 40.0, -dFdy(h) * 40.0, 1.0 / heightScale));\n vec3 L = normalize(uLightDir);\n vec3 V = vec3(0.0, 0.0, 1.0); // view straight on\n vec3 H = normalize(L + V);\n float diff = max(dot(N, L), 0.0);\n // Thick impasto → high shininess (sharp glint); thin paint → broad soft sheen.\n float shininess = mix(8.0, 64.0, clamp(height * 2.0, 0.0, 1.0));\n float spec = pow(max(dot(N, H), 0.0), shininess);\n // Modulate the lit terms by how much paint sits here (no relief on bare ground).\n float relief = clamp(h * 1.5, 0.0, 1.0);\n vec3 lit = col * (1.0 + uImpasto * relief * (diff - 0.5) * 0.5 * uLightColor);\n // The specular glint is gated to ACCUMULATED-thick paint (height²) — a per-fine-stroke\n // sharp glint on every thin fill dab read as bright isotropic SPECKLE that buried the\n // directional stroke bands (the §4.2 anisotropy / §4.3 slope sink on the low-contrast\n // oil field). Only where real impasto has built up does the raked glint fire, so the\n // highlight tracks the macro ridges, not the fine speckle.\n float specGate = clamp(height, 0.0, 1.0);\n lit += uImpasto * relief * spec * specGate * 0.5 * uLightColor;\n return lit;\n}\n\n// Best-of-9-neighbor placement: sample 3x3 surrounding cells and take the\n// stroke that covers this pixel most. Breaks the grid by per-cell jitter and\n// sparse density via noise thresholding.\n// energyGrade — the SBR energy-grade magnitude (0 = none, the oil/oil-pastel\n// baseline; 1 = the full van-Gogh Starry-Night cascade — long\n// confident strokes in the lights/coherent zones, short dabs in\n// the darks/flat zones). The van-Gogh profile carries 1.0 (the\n// energy grade is a PROFILE field, not a buried uMedium==5 branch).\nStrokeHit bestOil(vec2 p, float cellSize, float lenMul, float halfWMul,\n float jitterAmt, float density, int shapeType,\n float bristleAmp, vec2 flow, float t, float seed,\n float energyGrade) {\n vec2 cell = floor(p / cellSize);\n\n StrokeHit best = noHit();\n // 3x3 neighborhood\n for (int dy = -1; dy <= 1; dy++) {\n for (int dx = -1; dx <= 1; dx++) {\n vec2 cc = cell + vec2(float(dx), float(dy));\n vec2 hh = hash22(cc + seed) - 0.5;\n // Density gate — noise threshold; sparse placement.\n float gate = hash21(cc * 1.7 + seed * 0.3);\n if (gate > density) continue;\n\n vec2 center = (cc + 0.5 + hh * jitterAmt) * cellSize;\n\n // Per-stroke direction source — the AW.W4.1 strokeOrient switch:\n // uStrokeOrient==0 (flow) — the layer-provided hand-authored flowField.\n // uStrokeOrient==1 (tensor) — the structure-tensor minor eigenvector at the\n // cell center (the color field's edge-tangent),\n // so strokes hug the color zones (van Gogh).\n // coh carries the per-cell coherence A for the W4.3 energy grading (long\n // confident strokes where structure is coherent, stubby dabs in flat zones).\n vec2 f;\n float coh = 1.0;\n if (uStrokeOrient == 1) {\n vec3 tf = structureTensorField(center, t, safeDir(flow));\n f = safeDir(tf.xy);\n coh = tf.z;\n } else {\n f = safeDir(flow);\n }\n // Per-stroke deterministic local perturbation so alternate stroke layers stay\n // live. Tensor strokes get FAR less angular jitter (the field already orients them\n // — over-jittering the tensor strokes scatters their directions and the rendered\n // field reads isotropic, the §4.2 anisotropy floor). The flow-oriented (non-tensor)\n // strokes keep the full jitter (they have no per-cell orientation source).\n float jScale = (uStrokeOrient == 1) ? 0.15 : 1.0;\n float angJ = (hash21(cc + seed + 11.0) - 0.5) * 0.9 * jScale; // +/- 0.45/0.07 rad\n float curlScale = (uStrokeOrient == 1) ? 0.30 : 1.0;\n float localCurl = (fbm(center * (2.6 + seed * 0.11) + seed * 1.9) - 0.5) * 0.55 * uFlowCurl * curlScale;\n vec2 dir = rotateDir(f, angJ + localCurl);\n\n // AX.W13 — the SBR energy grade is a PROFILE field (energyGrade), not a buried\n // uMedium==5 branch. The van-Gogh profile passes energyGrade=1.0 (the full\n // Starry-Night cascade); oil/oil-pastel pass 0.0 (uniform length). The grade\n // modulates stroke length by local luminance (bright → long confident strokes,\n // dark → short dabs, the Kolmogorov/Batchelor congruence) AND by coherence\n // (coherent zones → long, flat → stubby), lerped in by energyGrade so a profile\n // can dial the cascade from off (0) to full (1).\n float energy = 1.0;\n if (energyGrade > 0.001) {\n float luma = clamp(dot(sampleBase(center, t), W_LUMA), 0.0, 1.0);\n float graded = mix(0.5, 1.5, luma) * mix(0.6, 1.0, coh);\n energy = mix(1.0, graded, energyGrade);\n }\n\n float lenV = cellSize * lenMul * (0.65 + 0.55 * hash21(cc + seed + 23.0)) * energy;\n float halfW = cellSize * halfWMul * (0.70 + 0.55 * hash21(cc + seed + 41.0));\n float bulge = (hash21(cc + seed + 53.0) - 0.5) * lenV * 0.35;\n\n vec2 a = center - dir * (lenV * 0.5);\n vec2 b = center + dir * (lenV * 0.5);\n\n vec3 colMid = brokenColorJitter(\n sampleBase(center, t),\n hash21(cc + seed + 89.0),\n hash21(cc * 2.3 + seed + 97.0),\n 1.0\n );\n\n StrokeHit h = curvedStroke(p, a, b, halfW, bulge, shapeType,\n 7.0, bristleAmp,\n hash21(cc + seed + 67.0), colMid);\n if (h.coverage > best.coverage) best = h;\n }\n }\n return best;\n}\n", me = "// ── Medium overlays ───────────────────────────────────────────────────────\n\n// A quick re-computation of base color for edge-mask sampling\nvec3 sampleBase(vec2 p, float t) {\n vec2 pw = domainWarp(p, t);\n float id; float vm;\n nucleiField(pw, t, id, vm);\n vec3 c = samplePalette(id);\n c *= 1.0 + uValueVariance * vm;\n return c;\n}\n\n// ── Structure-tensor / edge-tangent-flow (AW.W4.1) ─────────────────────────\n// The keystone of the painterly engine: derive stroke orientation from the COLOR\n// FIELD's OWN structure, not a hand-authored flow pattern. A 3x3 Sobel over\n// luma(sampleBase) yields the gradient (Gx,Gy); the 2x2 structure tensor\n// J = [[Gx², GxGy],[GxGy, Gy²]] eigen-decomposes closed-form into the MINOR\n// eigenvector (the edge-TANGENT — least color change, the stroke direction; the\n// MAJOR eigenvector is the gradient/edge-NORMAL and would make strokes cross the\n// bands) and the coherence A = (λ1-λ2)/(λ1+λ2). Low-coherence (flat) zones relax\n// toward the smooth fallbackDir by (1-A) so tensor noise never reads as jitter.\n// Single-pass small-tap WebGL2 form; the Gaussian-smoothed multi-tap + LIC smear\n// is the AW.W7 WebGPU multi-pass fold. Returns vec3(tangent.x, tangent.y, A).\nvec3 structureTensorField(vec2 p, float t, vec2 fallbackDir) {\n float e = 0.0035; // small-tap neighborhood (edge-mask scale)\n // 3x3 luma samples.\n float l00 = dot(sampleBase(p + vec2(-e, -e), t), W_LUMA);\n float l10 = dot(sampleBase(p + vec2( 0.0, -e), t), W_LUMA);\n float l20 = dot(sampleBase(p + vec2( e, -e), t), W_LUMA);\n float l01 = dot(sampleBase(p + vec2(-e, 0.0), t), W_LUMA);\n float l21 = dot(sampleBase(p + vec2( e, 0.0), t), W_LUMA);\n float l02 = dot(sampleBase(p + vec2(-e, e), t), W_LUMA);\n float l12 = dot(sampleBase(p + vec2( 0.0, e), t), W_LUMA);\n float l22 = dot(sampleBase(p + vec2( e, e), t), W_LUMA);\n // Sobel derivatives.\n float Gx = (l20 + 2.0 * l21 + l22) - (l00 + 2.0 * l01 + l02);\n float Gy = (l02 + 2.0 * l12 + l22) - (l00 + 2.0 * l10 + l20);\n // Structure tensor components.\n float Jxx = Gx * Gx;\n float Jyy = Gy * Gy;\n float Jxy = Gx * Gy;\n // Closed-form 2x2 eigenvalues: λ = 0.5(tr ± sqrt((Jxx-Jyy)² + 4Jxy²)).\n float tr = Jxx + Jyy;\n float disc = sqrt(max((Jxx - Jyy) * (Jxx - Jyy) + 4.0 * Jxy * Jxy, 0.0));\n float lambdaMaj = 0.5 * (tr + disc);\n float lambdaMin = 0.5 * (tr - disc);\n // The MAJOR eigenvector points along the gradient (the edge NORMAL); the MINOR\n // is its perpendicular — the edge TANGENT (the stroke direction). The principal\n // angle is θ = 0.5·atan2(2·Jxy, Jxx-Jyy); this form is robust at Jxy≈0 (where the\n // (λ-Jyy, Jxy) eigenvector formula collapses to a zero vector). The tangent is at\n // θ + 90°. Guard the fully-isotropic (no-gradient) degenerate case → fallback dir.\n vec2 tangent;\n if (disc < 1e-7) {\n tangent = fallbackDir;\n } else {\n float theta = 0.5 * atan(2.0 * Jxy, Jxx - Jyy); // major (gradient) angle\n tangent = vec2(-sin(theta), cos(theta)); // perpendicular = tangent\n }\n // Coherence A in [0,1] — high where one eigenvalue dominates (a strong edge),\n // ~0 in flat zones. Relax toward the smooth fallback by (1 - A).\n float A = (lambdaMaj - lambdaMin) / (lambdaMaj + lambdaMin + 1e-6);\n A = clamp(A, 0.0, 1.0);\n // Coherence-weighted blend: full tensor in coherent zones, smooth in flat ones.\n // Resolve the 180° sign ambiguity toward the fallback so the blend is continuous.\n if (dot(tangent, fallbackDir) < 0.0) tangent = -tangent;\n // The blend weight is a low-power curve (A^0.35) so even MODERATE-coherence zones queue\n // strongly along the tangent (a flowing van-Gogh band reads coherent the way the\n // structure-tensor metric measures it; a linear A under-orients the mid-band and the\n // rendered field reads too isotropic — the §4.2 anisotropy lift). The flat-zone (A≈0)\n // relax to the fallback survives (0^0.35=0), so tensor noise never reads as jitter.\n float blendW = pow(A, 0.28);\n vec2 dir = normalize(mix(fallbackDir, tangent, blendW) + 1e-6);\n return vec3(dir, A);\n}\n\nvec3 mediumWatercolor(vec3 col, vec2 p, float t) {\n // Wet-edge cauliflowers via luma-gradient magnitude\n float eps = 0.004;\n vec3 cx1 = sampleBase(p + vec2(eps, 0.0), t);\n vec3 cx2 = sampleBase(p - vec2(eps, 0.0), t);\n vec3 cy1 = sampleBase(p + vec2(0.0, eps), t);\n vec3 cy2 = sampleBase(p - vec2(0.0, eps), t);\n float gx = dot(cx1 - cx2, W_LUMA);\n float gy = dot(cy1 - cy2, W_LUMA);\n float edge = sqrt(gx * gx + gy * gy) / (2.0 * eps);\n float mask = smoothstep(0.0, 2.5, edge);\n col *= mix(1.0, 0.78, mask * uWetEdge);\n\n // Granulation — pigment settles in paper tooth\n float paper = 0.5 * vnoise(p * 160.0) + 0.5 * vnoise(p * 360.0);\n float pigLoad = 1.0 - dot(col, W_LUMA);\n col *= 1.0 - uGranulation * pigLoad * (paper - 0.5);\n\n // Wash banding — faint horizontal wet gradient\n float band = fbm(vec2(p.x * 1.5, p.y * 0.4));\n col *= 1.0 + 0.04 * (band - 0.5);\n return col;\n}\n\nvec3 mediumPastel(vec3 col, vec2 p, float t) {\n vec2 flow = flowField(p, t);\n vec2 perp = vec2(-flow.y, flow.x);\n float along = dot(p, flow) * uStrokeScale;\n float across = dot(p, perp) * uStrokeScale;\n across += 0.03 * (vnoise(p * 260.0) - 0.5);\n\n float aniso = mix(1.0, 0.18, uStrokeAnisotropy);\n float stroke = fbm(vec2(along * aniso, across));\n col *= mix(1.0, 0.82 + 0.32 * stroke, uStrokeAmount);\n\n // Pastel tooth — tiny high-frequency grain\n float tooth = vnoise(p * 800.0);\n col *= 1.0 - 0.08 * uStrokeAmount * (tooth - 0.5);\n return col;\n}\n", he = "// ── Medium id constants (mirror uniformBridge.ts MEDIUM_ID — the uMedium ladder) ──\n// The StrokeProfile selector (AX.W12 + AX.W13) dispatches on the medium id: oil,\n// van-Gogh, and oil-pastel each AUTHOR a first-class profile in profileFor.\n#define MEDIUM_OIL 3\n#define MEDIUM_CRAYON 4\n#define MEDIUM_VANGOGH 5\n#define MEDIUM_OILPASTEL 6\n\n// ── Crayon — DRY wax pigment on paper tooth (first-class medium, uMedium==4) ──\n// Crayon is not strokes. It's dry pigment crumbs dragged across paper tooth — the\n// DRY tooth-multiply model: anisotropic tooth noise multiplied into the base, a hard\n// SCUMBLE that lets the lower color show through the broken upper layer, and stable\n// OKLCh broken-color pigment patches. NO sheen, NO burnish film — that waxy gloss is\n// the OIL-PASTEL deposition's signature (mediumOilPastel), distinct from dry crayon\n// (AX.W13 slice 8 F1 — the split). Crayon shares the SUBSTRATE (the structure-tensor\n// orientation, the tooth noise, OKLCh brokenColorJitter), not the dispatch body.\nvec3 mediumCrayon(vec3 col, vec2 p, float t) {\n // Orient the deposition along the structure-tensor edge-tangent (the bridge forces\n // tensor for crayon, so read the tensor field directly).\n vec2 flow = structureTensorField(p, t, flowField(p, t)).xy;\n float ang = atan(flow.y, flow.x);\n float ca = cos(-ang), sa = sin(-ang);\n vec2 pr = vec2(p.x * ca - p.y * sa, p.x * sa + p.y * ca);\n\n float aniso = mix(0.45, 0.95, uStrokeAnisotropy);\n float scale = max(uStrokeScale * 1.6, 180.0);\n\n // ── Paper TOOTH height field — the relief the dry pigment rides. Anisotropic\n // (squished along flow, coarse cross-flow). Peaks = ridges, valleys = paper pits.\n float t1 = vnoise(vec2(pr.x * scale * aniso, pr.y * scale));\n float t2 = vnoise(vec2(pr.x * scale * aniso * 0.4, pr.y * scale * 0.4) + 11.0);\n float t3 = vnoise(vec2(pr.x * scale * aniso * 2.1, pr.y * scale * 2.1) + 23.0);\n float tooth = 0.55 * t1 + 0.30 * t2 + 0.15 * t3;\n\n // ── DRY TOOTH-MULTIPLY — the crayon multiplies the tooth relief into the base\n // color (dark in the pits, bright on the ridges). The pressure (uStrokeAmount) sets\n // how hard the multiply bites. This is the DRY model — no pigment build-up, no sheen.\n float pressure = clamp(uStrokeAmount, 0.0, 1.0);\n float bite = mix(0.18, 0.46, pressure); // multiply strength\n vec3 result = col * (1.0 - bite * (0.5 - tooth));\n\n // ── HARD SCUMBLE — a broken upper layer that lets the lower color show through the\n // gaps. A coarse mask gates a HARD (high-contrast) dragged pass so the dry crayon\n // reads as broken pigment crumbs, not a smooth wash. Drag follows the tensor flow.\n float scumbleMask = smoothstep(0.42, 0.78, vnoise(vec2(pr.x * scale * 0.55, pr.y * scale * 0.22) + 7.0));\n float scumbleCoverage = scumbleMask * pressure * 0.7; // hard broken upper layer\n vec3 paper = col * mix(0.80, 0.94, pressure); // the paper showing through\n result = mix(paper, result, scumbleCoverage);\n\n // ── Broken-color pigment: stable wax/pigment patches in OKLCh (W5), not flicker.\n vec2 pigmentCell = floor(pr * max(scale * 0.18, 32.0));\n float pigmentMask = smoothstep(0.28, 0.82, vnoise(pr * scale * 0.21 + 19.0));\n result = brokenColorJitter(\n result,\n hash21(pigmentCell + 17.0),\n hash21(pigmentCell * 2.1 + 31.0),\n 0.45 + 0.55 * pigmentMask\n );\n\n // Crayon is saturation-amplified (the wax chroma punch — but DRY, no sheen).\n result = saturate3(result, 1.08);\n\n return result;\n}\n\n// ── StrokeProfile — the oil-stroke parameter vector (AX.W12, slice 8 F6) ──────\n// The SBR \"stroke = parameter vector\" canon (facet 11): the per-mode if-ladder knobs\n// become struct FIELDS — logic-as-DATA, not an imperative branch. profileFor(medium,\n// mode) populates the profile for a (medium, mode) pair; paintStrokeLayers(profile) runs\n// the four-layer bestOil/paintOver cascade off it. A new medium (the W13 van-Gogh /\n// oil-pastel profiles) AUTHORS a profile entry — it never edits a monolith.\nstruct StrokeProfile {\n int shapeType; // 0 tapered, 1 load-drag, 2 dab, 3 even, 4 comma/crescent\n float bristleAmp; // edge raggedness 0..0.5\n float streakFreq; // internal-streak spatial frequency\n float streakAmp; // internal-streak amplitude\n float impastoAmp; // paint-thickness contribution (the relight reads the height)\n float hardness; // edge-compositing crispness 0..1\n float toothScale; // canvas-tooth (linen weave) spatial frequency\n float toothAmp; // canvas-tooth amplitude\n float pigmentSat; // pigment saturation boost (OKLCh chroma scale)\n float densityBig; // layer-1 (big gestural) placement density gate\n float densityMed; // layer-2 (medium body) placement density gate\n float densitySml; // layer-3 (small dabs) placement density gate\n float energyGrade; // AX.W13 — SBR energy-grade magnitude (0 off; 1 the van-Gogh\n // Starry-Night length cascade). The energy grade is a PROFILE\n // field, NOT a buried uMedium==5 bestOil branch (slice 8 F0).\n float impastoFloor;// AX.W13 — the per-stroke height-crown floor (0.4 oil falloff;\n // 1.0 van-Gogh FULL-height crown so each dab catches its glint).\n float densityFill; // AX.W13 — layer-4 fill-dab density. Oil/oil-pastel fill the\n // bald spots (0.95, full coverage); van-Gogh sets it LOW so the\n // sparse atomic dabs keep their visible inter-stroke ground gaps\n // (the atomicity read — a 0.95 fill would close every gap to a smear).\n float groundFloor; // AX.W13 — the bare-ground darken floor. The gaps between strokes\n // (low accumulated height) multiply toward this floor, so van-Gogh's\n // sparse atomic dabs read over a visibly darker underpainting (the\n // Starry-Night visible ground). Oil/oil-pastel keep 1.0 (no darken).\n};\n\n// The (medium, mode) -> StrokeProfile selector. The if-ladder's knobs are the profile's\n// fields; a new medium adds a CASE here (never an edit to a dispatch body). mode is the\n// uStrokeMode oil-stroke sub-mode (0 oil, 1 palette-knife, 3 brushwork). crayon\n// (uMedium==4) is a PEER tooth-multiply medium that never reaches the stroke cascade.\n// The painterly STROKE mediums are oil (MEDIUM_OIL), van-Gogh (MEDIUM_VANGOGH) and\n// oil-pastel (MEDIUM_OILPASTEL) — each AUTHORS its own profile entry below.\nStrokeProfile profileFor(int medium, int mode) {\n // The oil baseline (mode 0) — balanced modern-abstract/palette-knife hybrid.\n StrokeProfile prof = StrokeProfile(\n 0, // shapeType — tapered\n 0.25, // bristleAmp\n 9.0, // streakFreq\n 0.09, // streakAmp\n 0.9, // impastoAmp\n 0.80, // hardness\n 240.0, // toothScale\n 0.09, // toothAmp\n 1.03, // pigmentSat\n 0.65, // densityBig\n 0.78, // densityMed\n 0.90, // densitySml\n 0.0, // energyGrade — oil is uniform-length (no Starry-Night cascade)\n 0.4, // impastoFloor — oil's 0.4+0.6·edgeN crown falloff\n 0.92, // densityFill — oil fills DENSELY with the directional low-relief fill so the\n // flat-colour regions between the macro strokes are TEXTURED with\n // flow-aligned fine strokes (the §4.2 anisotropy needs every pixel\n // to carry directional gradient; a flat untextured region reads as\n // an isotropic/degenerate tensor and drags mean A down). The fill\n // is now directional + low-streak, so dense coverage textures\n // WITHOUT the prior round-dab speckle.\n 1.0 // groundFloor — oil keeps the full base (no ground darken; fills everywhere)\n );\n\n // ── Van-Gogh — first-class atomic comma/crescent dabs (slice 8 F0). NOT a length\n // modulation of oil: sparse high-contrast SEPARABLE marks with their own asymmetric\n // shape, the full Starry-Night energy cascade (energyGrade=1), and full-height\n // impasto crowns so each dab catches its own glint. The mode sub-modes don't apply\n // (van-Gogh has its own grammar), so this returns early.\n if (medium == MEDIUM_VANGOGH) {\n prof.shapeType = 4; // comma / crescent — the divisionist atomic dab\n prof.bristleAmp = 0.22; // a touch of raggedness on the loaded head\n prof.streakFreq = 16.0; // within-stroke streaks (the loaded comma) — kept in the\n // FFT inertial band (a too-fine streak falls past the §4.3\n // resolved-band edge and reads as steeper, not shallower)\n prof.streakAmp = 0.22;\n prof.impastoAmp = 1.1; // paint per atomic dab — eased off 1.5 so the impasto crown\n // does not push the dab past the PBR-Neutral 0.8 highlight\n // knee (where it compresses + loses the directional contrast\n // the §4.2 anisotropy reads)\n prof.hardness = 0.92; // crisp separable edges (visible inter-stroke gaps)\n prof.toothScale = 220.0;\n prof.toothAmp = 0.06;\n prof.pigmentSat = 0.60; // the van-Gogh chroma — pulled DOWN off the hand-set 1.12\n // so the rendered colorfulness lands inside [55.67, 95.67]\n // (the hand-set punch + the saturated indigo/cobalt/yellow\n // ramp pushed C past the garish ceiling — §4.1 bidirectional)\n // SPARSE-BUT-FLOWING placement — visible inter-stroke canvas gaps (the atomicity read)\n // yet enough directional coverage that the swirl-rows read as coherent flowing bands\n // (the §4.2 anisotropy needs the strokes to dominate the gradient field; too-sparse\n // gates left large flat-ground regions that read isotropic and dragged mean A down).\n prof.densityBig = 0.58;\n prof.densityMed = 0.68;\n prof.densitySml = 0.74;\n prof.energyGrade = 1.0; // the full Starry-Night length cascade (the profile field)\n prof.impastoFloor = 1.0; // FULL-height crowns — every atomic dab catches its glint\n prof.densityFill = 0.30; // a LIGHT directional fill — textures the dark ground with\n // flow-aligned fine strokes so the gaps carry directional\n // gradient (the §4.2 A lift: an untextured dark ground reads\n // isotropic) WHILE staying open enough that the bright atomic\n // dabs still read separable over it (the gap-fraction floor\n // stays satisfied — the fill is fine + dark, not a smear).\n prof.groundFloor = 0.31; // darken the bare-ground gaps to the visible Starry-Night\n // underpainting so the bright impasto dabs read separable —\n // the dab↔ground contrast injects MID-scale energy (the §4.3\n // β-flatten: the PBR-Neutral highlight compression concentrated\n // energy at low-k and steepened β). 0.31 is the balance point:\n // deep enough to hold β in-band, light enough that the textured\n // ground keeps the §4.2 coherence the too-dark 0.26 floor cost.\n return prof;\n }\n\n // ── Oil-pastel — broad smeared directional strokes deposited via the brush engine\n // (slice 8 F1). Creamy soft edges (low hardness), heavy build-up, and a chroma punch.\n // Distinct from the dry-crayon tooth-multiply (mediumCrayon): oil-pastel LAYS strokes.\n if (medium == MEDIUM_OILPASTEL) {\n prof.shapeType = 0; // tapered directional smear — a round dab reads locally\n // isotropic (steep β / low A); the creamy oil-pastel mark\n // is a broad DIRECTIONAL smear, which carries the §4.2\n // coherence + adds the directional mid-band the §4.3 slope\n // needs (the round-dab profile rolled β off to −2.8)\n prof.bristleAmp = 0.10; // smooth creamy edges (waxy pastel, not bristle)\n prof.streakFreq = 18.0; // finer internal smear — adds the mid/high-band the smooth\n // round-dab profile lacked (the β-too-steep fix); the\n // directional along-spine streak flattens the §4.3 slope\n // without crossing the strokes\n prof.streakAmp = 0.20;\n prof.impastoAmp = 0.7; // build-up, but flatter than oil impasto\n prof.hardness = 0.42; // CREAMY — soft compositing, strokes blend on overlap\n prof.toothScale = 280.0;\n prof.toothAmp = 0.12; // the pastel tooth reads stronger\n prof.pigmentSat = 0.80; // the waxy chroma — eased off 1.16 so the rendered C clears\n // the §4.1 band ceiling with margin while the K-M subtractive\n // overlap path (paintOverOklab) keeps the chroma OFF the grey floor\n prof.hardness = 0.58; // creamy but the strokes still REGISTER — a too-soft blend\n // (0.42) smeared the marks into the smooth colour zones and\n // the field rolled off too steep (β≈−2.9, a near-gradient);\n // a crisper deposit keeps the mid/high-band the −5/3 slope needs\n prof.densityBig = 0.62; // broad directional coverage — but NOT over-dense: too many\n // overlapping creamy strokes point in slightly-varied\n // directions and the local §4.2 coherence DROPS (over-dense\n // measured A=0.57 < the 0.62-density A=0.63). The mid-density\n // flow-aligned deposit is the coherence sweet spot.\n prof.densityMed = 0.78;\n prof.densitySml = 0.90;\n prof.energyGrade = 0.80; // a strong Starry-Night length cascade — varies stroke length\n // by luma/coherence so the dab-size energy spreads across\n // scales (the −5/3 cascade the uniform-length 0.0 grade lacked,\n // the §4.3 β-too-steep fix). Not the full van-Gogh 1.0 (oil-\n // pastel keeps its creamy broad character).\n prof.impastoFloor = 0.55; // a softer crown than van-Gogh's full-height glint\n return prof;\n }\n\n // ── Oil sub-modes (mode dispatch on the oil medium).\n if (mode == 1) { // palette knife — razor edges, heavy impasto\n prof.shapeType = 3; // flat, even\n prof.bristleAmp = 0.12;\n prof.streakFreq = 4.0; prof.streakAmp = 0.05;\n prof.impastoAmp = 1.6;\n prof.hardness = 0.95;\n prof.toothAmp = 0.04;\n prof.densityBig = 0.80; prof.densityMed = 0.88; prof.densitySml = 0.70;\n } else if (mode == 3) { // thick brushwork — heavy bristle brush\n prof.shapeType = 0; // tapered\n prof.bristleAmp = 0.32;\n prof.streakFreq = 14.0; prof.streakAmp = 0.14;\n prof.impastoAmp = 1.2;\n prof.hardness = 0.85;\n prof.toothAmp = 0.07;\n }\n return prof;\n}\n\n// The single parameterized four-layer stroke cascade (AX.W12). The four hand-unrolled\n// bestOil/paintOver invocations collapse into ONE body driven by the profile + the\n// uniform-derived per-layer scale/anisotropy multipliers (which are mode-INVARIANT — the\n// per-layer offsets, seeds, and len/wid muls are fixed across modes, so they stay here as\n// the cascade's structure; only the profile's knobs differentiate the medium). The\n// mode passes through for the two mode-special-cased shapes (knife layer-3 dabs +\n// layer-4 fill shape), which are placement details of the cascade, not profile knobs.\nvoid paintStrokeLayers(inout vec3 col, inout float height, StrokeProfile prof,\n int mode, vec2 p, float t) {\n // Scales & multipliers from uniforms (mode-invariant cascade structure).\n float baseScale = max(uStrokeScale * 0.006, 0.008);\n // Three primary layers: big gestural, medium body, small dabs.\n // W-AUR-STUDIO D5 — the −5/3 radii respacing was MEASURED + REVERTED (recorded, not\n // silently struck). The φ-adjacent geometric candidate (2.4 / 1.45 / 0.87, a fixed\n // ≈1.66× step) moved oil-pastel β −2.534 → −2.413 — TOWARD the −5/3 band but NOT into\n // it (floor −1.85), so the keep-iff-into-band gate failed. The single-pass WebGL2 path\n // is the true limiter (HC-aurora §5): the oil-pastel β + the oil/oil-pastel anisotropy\n // residual is the named AY.W-AUR-T5 multi-pass anisotropic-Kuwahara candidate. The\n // hand-set 2.4 / 1.1 / 0.45 stays — it is van-Gogh's landed-band spacing.\n float sBig = baseScale * 2.4;\n float sMed = baseScale * 1.1;\n float sSml = baseScale * 0.45;\n\n // Strokes are elongated HARD (long·thin) so the rendered field reads strongly\n // directional — the §4.2 structure-tensor anisotropy lift. A short fat dab is locally\n // isotropic and drags the measured A down; a long thin stroke that hugs the tensor\n // tangent is what gives Starry Night its A≈0.83. The width muls are pulled tighter to\n // keep the long·thin aspect.\n float lenMulBig = mix(3.0, 5.2, uStrokeAnisotropy);\n float widMulBig = mix(0.42, 0.22, uStrokeAnisotropy);\n float lenMulMed = mix(2.8, 4.8, uStrokeAnisotropy);\n float widMulMed = mix(0.40, 0.21, uStrokeAnisotropy);\n float lenMulSml = mix(2.2, 3.6, uStrokeAnisotropy);\n float widMulSml = mix(0.36, 0.23, uStrokeAnisotropy);\n\n float jitterAmt = 0.75; // large jitter — no grid\n vec2 flow = flowField(p, t);\n\n // Layer 1 — big gestural strokes (sparse, shaping)\n StrokeHit hBig = bestOil(p, sBig, lenMulBig, widMulBig, jitterAmt * 0.55,\n prof.densityBig, prof.shapeType, prof.bristleAmp, flow, t, 1.3,\n prof.energyGrade);\n paintOver(col, height, hBig, prof.streakFreq * 0.7, prof.streakAmp,\n uImpasto * prof.impastoAmp * uStrokeAmount, prof.hardness, 1.3, prof.impastoFloor);\n\n // Layer 2 — medium body strokes\n StrokeHit hMed = bestOil(p + vec2(11.3, 3.7), sMed, lenMulMed, widMulMed,\n jitterAmt, prof.densityMed, prof.shapeType, prof.bristleAmp, flow, t, 2.7,\n prof.energyGrade);\n paintOver(col, height, hMed, prof.streakFreq, prof.streakAmp,\n uImpasto * prof.impastoAmp * uStrokeAmount, prof.hardness, 2.7, prof.impastoFloor);\n\n // Layer 3 — small strokes (more frequent, smaller). Even the knife layer-3 stays the\n // medium's DIRECTIONAL shape, not a round dab — the round-dab sparkle was a fine-scale\n // isotropic speckle that dragged the §4.2 anisotropy down + rolled the §4.3 slope shallow.\n int smlShape = prof.shapeType;\n StrokeHit hSml = bestOil(p + vec2(-5.1, 8.4), sSml, lenMulSml, widMulSml,\n jitterAmt * 0.9, prof.densitySml, smlShape,\n prof.bristleAmp * 0.85, flow, t, 4.1, prof.energyGrade);\n paintOver(col, height, hSml, prof.streakFreq * 1.4, prof.streakAmp * 0.6,\n uImpasto * prof.impastoAmp * 0.45 * uStrokeAmount, prof.hardness, 4.1, prof.impastoFloor);\n\n // Layer 4 — fill strokes (dense, fine) — cover bald spots. The density is\n // PROFILE-driven (prof.densityFill): oil/oil-pastel fill to full coverage; van-Gogh\n // keeps it sparse so the bare-ground gaps between its atomic dabs survive. The fill is\n // ELONGATED + flow-oriented (NOT a round dab) — a dense round-dab fill is locally\n // isotropic and was the dominant §4.2 anisotropy sink; an oriented fine stroke keeps the\n // fill directional so the whole field reads coherent.\n float sFill = baseScale * 0.22;\n float lenMulFill = mix(2.0, 3.2, uStrokeAnisotropy);\n float widMulFill = mix(0.34, 0.24, uStrokeAnisotropy);\n int fillShape = (mode == 1) ? 3 : prof.shapeType; // knife=even; others inherit the medium's directional shape\n StrokeHit hFill = bestOil(p + vec2(3.9, -6.2), sFill, lenMulFill, widMulFill,\n jitterAmt * 0.8, prof.densityFill, fillShape,\n prof.bristleAmp * 0.6, flow, t, 8.9, prof.energyGrade);\n // The fill is a low-relief, low-streak base coat — it covers bald spots WITHOUT adding\n // fine bright speckle (a high-impasto/high-streak fill caught a per-dab glint that read\n // as isotropic noise over the directional macro strokes — the §4.2/§4.3 sink). Its\n // positional jitter is tamed too so the fill strokes stay flow-aligned, not scattered.\n paintOver(col, height, hFill, prof.streakFreq * 1.2, prof.streakAmp * 0.35,\n uImpasto * prof.impastoAmp * 0.18 * uStrokeAmount, prof.hardness * 0.9, 8.9, prof.impastoFloor);\n\n // Optional crosshatch layer\n if (uStrokeLayers == 2) {\n vec2 flow2 = vec2(-flow.y, flow.x);\n StrokeHit hX = bestOil(p + vec2(7.3, -2.1), sMed, lenMulMed * 0.9, widMulMed,\n jitterAmt, prof.densityMed * 0.7, prof.shapeType, prof.bristleAmp, flow2, t, 6.5,\n prof.energyGrade);\n paintOver(col, height, hX, prof.streakFreq, prof.streakAmp * 0.85,\n uImpasto * prof.impastoAmp * 0.55 * uStrokeAmount, prof.hardness, 6.5, prof.impastoFloor);\n }\n}\n\n// The shared stroke-medium SUBSTRATE finish (AX.W12 + AX.W13): paint the four-layer\n// cascade off the profile, then canvas-tooth + relight + saturation. The three painterly\n// STROKE mediums (oil / van-Gogh / oil-pastel) each AUTHOR their own profile and call\n// this — the substrate is shared, the PROFILE (not a dispatch-body fork) differentiates\n// the medium (slice 8 F6). mode passes through for the cascade's mode-special-cased shapes.\nvec3 paintStrokeMedium(vec3 col, vec2 p, float t, StrokeProfile prof, int mode) {\n vec3 result = col;\n // AW.W4.2 — accumulated paint HEIGHT across the stroke layers. The relight reads\n // its gradient for the normal; the canvas tooth seeds the base relief.\n float height = 0.0;\n paintStrokeLayers(result, height, prof, mode, p, t);\n\n // Canvas tooth — linen weave\n float tooth1 = vnoise(p * prof.toothScale);\n float tooth2 = vnoise(p * prof.toothScale * vec2(0.6, 2.4) + 37.0);\n float tooth = (0.6 * tooth1 + 0.4 * tooth2) - 0.5;\n result *= 1.0 + tooth * prof.toothAmp * uCanvasGrain;\n\n // AW.W4.2 — relight the accumulated paint height with the movable uLightDir\n // source (diffuse + Blinn specular, in LINEAR before aces()). The canvas tooth\n // is the base relief term so the weave also catches the raking light.\n float canvasBase = tooth * prof.toothAmp * 0.5;\n result = relightImpasto(result, height, canvasBase);\n\n // AX.W13 — the visible ground. The bare-ground gaps (low accumulated stroke height)\n // multiply toward prof.groundFloor, so van-Gogh's sparse atomic dabs read as separable\n // marks over a darker underpainting (oil/oil-pastel keep groundFloor=1.0 → no-op). The\n // smoothstep keeps the impasto dabs (high height) at full value; only the open ground\n // between them darkens.\n result *= mix(prof.groundFloor, 1.0, smoothstep(0.0, 0.38, height));\n\n // Pigment saturation boost\n result = saturate3(result, prof.pigmentSat);\n\n return result;\n}\n\nvec3 mediumOil(vec3 col, vec2 p, float t) {\n // The oil medium is a thin body: fetch the oil profile (mode-dispatched), paint via\n // the shared stroke substrate. uStrokeMode: 0 oil (gestural), 1 knife, 3 brushwork.\n int mode = uStrokeMode;\n StrokeProfile prof = profileFor(MEDIUM_OIL, mode);\n return paintStrokeMedium(col, p, t, prof, mode);\n}\n\n// AX.W13 — the van-Gogh atomic-stroke medium is now a FIRST-CLASS body, NOT a\n// mediumOil passthrough (slice 8 F0). It composes its OWN vangogh StrokeProfile (the\n// comma/crescent shapeType, sparse high-contrast atomic placement with visible\n// inter-stroke gaps, the full Starry-Night energy-grade cascade as a PROFILE field,\n// and full-height impasto crowns) through the shared stroke substrate. The bridge forces\n// the ETF (tensor) orientation so the swirl-rows queue along the color field's tangent;\n// the within-stroke OKLCh broken color (paintOver) + the OKLab stroke OVER-composite\n// give pigment-true atom-level shimmer. No subject matter — the \"source image\" is the\n// generated nuclei field, so dabs trace its iso-bands (the Starry-Night swirl-rows).\nvec3 mediumVangogh(vec3 col, vec2 p, float t) {\n StrokeProfile prof = profileFor(MEDIUM_VANGOGH, 0);\n return paintStrokeMedium(col, p, t, prof, 0);\n}\n\n// AX.W13 — the oil-pastel medium (uMedium==6) is a DISTINCT stroke-deposition body,\n// split out of the dry-crayon tooth-multiply (mediumCrayon) (slice 8 F1). It DEPOSITS\n// broad smeared directional strokes via the same brush engine with a creamy soft\n// hardness, heavy pigment build-up where strokes overlap, and a chroma punch — the\n// stroke-deposition model that gives oil-pastel its depth and less-uniform read.\n// Shares the SUBSTRATE (the stroke cascade + tooth + relight) with oil/van-Gogh, NOT\n// the dispatch body of dry crayon. The bridge forces tensor orientation.\nvec3 mediumOilPastel(vec3 col, vec2 p, float t) {\n StrokeProfile prof = profileFor(MEDIUM_OILPASTEL, 0);\n return paintStrokeMedium(col, p, t, prof, 0);\n}", A = "\n", ge = `#version 300 es
|
|
11
11
|
precision highp float;
|
|
12
12
|
|
|
13
13
|
in vec2 vUv;
|
|
@@ -257,7 +257,7 @@ vec2 domainWarp(vec2 p, float t) {
|
|
|
257
257
|
return warped;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
` +
|
|
260
|
+
` + ue + A + de + A + "// ── Color utils (OKLCh — W5; the sRGB YIQ hueShift matrix is DELETED) ──────────\nconst vec3 W_LUMA = vec3(0.2126, 0.7152, 0.0722);\n\n// Linear sRGB → OKLab (the cbrt-LMS path; the goo-blob's srgbToOklab takes gamma,\n// here the color field is already linear so we skip the OETF inverse).\nvec3 linOklab(vec3 lin) {\n vec3 lms = LINEAR_SRGB_TO_LMS * lin;\n return LMS_TO_OKLAB * (sign(lms) * pow(abs(lms), vec3(1.0 / 3.0)));\n}\n\n// Broken color (W5) — hue + chroma jitter at fixed PERCEPTUAL lightness, in OKLCh.\n// Broken color is hue variation at constant VALUE, which only OKLCh makes true (the\n// prior YIQ-style sRGB rotation muddied value). This is the seam AW.W4's van-Gogh /\n// oil-pastel per-stroke pigment jitter consumes (the pigment-variation read).\nvec3 brokenColorJitter(vec3 c, float hueSeed, float valueSeed, float strength) {\n float amt = clamp(uBrokenColor * strength, 0.0, 1.0);\n if (amt <= 0.001) return c;\n vec3 lch = oklabToOklch(linOklab(c));\n // ±~16° hue swing + ±~12% chroma swing; L held so value reads constant.\n lch.z += (hueSeed - 0.5) * (32.0 * PI / 180.0) * amt;\n lch.y = max(lch.y * (1.0 + (valueSeed - 0.5) * 0.28 * amt), 0.0);\n return max(oklabToLinearSrgb(oklchToOklab(lch)), vec3(0.0));\n}\n\n// Saturation (W5) — chroma scale at fixed L/H in OKLCh (the perceptual saturation,\n// replacing the luma-mix sRGB form). amt=1 is identity; amt<1 desaturates toward\n// the achromatic axis with NO lightness shift (the muddy-luma-mix grey is gone).\nvec3 saturate3(vec3 c, float amt) {\n vec3 lch = oklabToOklch(linOklab(c));\n lch.y = max(lch.y * amt, 0.0);\n return max(oklabToLinearSrgb(oklchToOklab(lch)), vec3(0.0));\n}\n\n" + fe + A + me + A + pe + A + he + A + "\n// ── Main ──────────────────────────────────────────────────────────────────\nvoid main() {\n // Normalized 0..1 coordinates for nuclei, domain warp, and medium sampling.\n vec2 uv = vUv;\n vec2 pN = uv;\n\n float t = uTime;\n\n // Warp in pN space\n vec2 p_warp = domainWarp(pN, t);\n\n // Composition\n float paletteId; float valueMod;\n nucleiField(p_warp, t, paletteId, valueMod);\n vec3 col = samplePalette(paletteId);\n col *= 1.0 + uValueVariance * valueMod;\n\n // Breath — slow global luminance wobble\n float breath = sin(t * 6.2831 / max(uBreathPeriod, 1.0));\n col *= 1.0 + uBreathDepth * breath * 0.5;\n\n // Medium dispatch (AX.W13 — each is a FIRST-CLASS body, no shared dispatch):\n // 1 pastel · 2 watercolor · 3 oil · 4 crayon (DRY tooth-multiply) ·\n // 5 van-Gogh (atomic comma/crescent dabs) · 6 oil-pastel (stroke deposition).\n // van-Gogh is no longer a mediumOil passthrough; oil-pastel and crayon no longer\n // share a body — they share the SUBSTRATE (the tooth/placement helpers), not the\n // dispatch body (slice 8 F0/F1).\n if (uMedium == 1) col = mediumPastel(col, pN, t);\n else if (uMedium == 2) col = mediumWatercolor(col, pN, t);\n else if (uMedium == 3) col = mediumOil(col, pN, t);\n else if (uMedium == 4) col = mediumCrayon(col, pN, t);\n else if (uMedium == 5) col = mediumVangogh(col, pN, t);\n else if (uMedium == 6) col = mediumOilPastel(col, pN, t);\n\n // Saturation trim\n col = saturate3(col, uSaturation);\n\n // Tonemap + film grain\n col = aces(col);\n float grain = hash21(gl_FragCoord.xy + t * 17.0);\n col += (grain - 0.5) * uPaperGrain;\n\n col = clamp(col * 0.985 + 0.008, 0.0, 1.0);\n\n // MANDATORY OETF — closes the seam (mirrors metaball.frag.ts:278). The whole\n // pipeline above (linear palette LUT, ACES tonemap, grain) runs in LINEAR; without\n // this transfer the aurora ships ~2.2× too dark (linear 0.5 → ~0.215, not ~0.735).\n col = linearToSrgb(col);\n\n // 1-LSB triangular IGN dither in DISPLAY space, AFTER the OETF — breaks 8-bit\n // mid-tone banding on the soft gradient. Texture-free (the canonical Jimenez IGN).\n col += (1.0 / 255.0) * (ign(gl_FragCoord.xy) - 0.5);\n\n // Premultiply operates on DISPLAY-space color (matches the blob: OETF, then premul).\n fragColor = vec4(col * uAlpha, uAlpha);\n}\n", j = "[Aurora]", M = /* @__PURE__ */ "uTime.uPalette.uStopCount.uNucleiCount.uNucleiPos.uNucleiRadius.uNucleiPaletteBias.uNucleiValueBias.uNucleiDriftRadius.uNucleiDriftPhase.uNucleiElong.uNucleiAngle.uSoftmaxBeta.uValueVariance.uWarpAmount.uWarpScale.uWarpDrift.uWarpMode.uNoiseOctaves.uMedium.uHuePath.uFlowPattern.uFlowFocal.uFlowAngle.uFlowCurl.uCursor.uCursorStrength.uCursorRadius.uCursorVelocity.uCursorBurst.uStrokeAmount.uStrokeScale.uStrokeAnisotropy.uStrokeLayers.uStrokeMode.uStrokeOrient.uLightDir.uLightColor.uWetEdge.uGranulation.uImpasto.uBrokenColor.uCanvasGrain.uNucleiDrift.uPaletteDrift.uBreathDepth.uBreathPeriod.uSaturation.uPaperGrain.uAlpha".split(".");
|
|
261
261
|
function N(e, t, n) {
|
|
262
262
|
let r = l(e, e.VERTEX_SHADER, t, j), i = l(e, e.FRAGMENT_SHADER, n, j), o = a(e, r, i, j);
|
|
263
263
|
e.useProgram(o);
|
|
@@ -292,7 +292,7 @@ function N(e, t, n) {
|
|
|
292
292
|
function P(e, t = 8, n) {
|
|
293
293
|
let r = n ?? new Float32Array(t * 3), i = Math.min(e.length, t);
|
|
294
294
|
for (let t = 0; t < i; t++) {
|
|
295
|
-
let [n, i, a] =
|
|
295
|
+
let [n, i, a] = g(e[t]);
|
|
296
296
|
r[t * 3 + 0] = n, r[t * 3 + 1] = i, r[t * 3 + 2] = a;
|
|
297
297
|
}
|
|
298
298
|
for (let e = i; e < t; e++) r[e * 3 + 0] = 0, r[e * 3 + 1] = 0, r[e * 3 + 2] = 0;
|
|
@@ -300,15 +300,15 @@ function P(e, t = 8, n) {
|
|
|
300
300
|
}
|
|
301
301
|
function F(e) {
|
|
302
302
|
if (e.length === 0) return "transparent";
|
|
303
|
-
if (e.length === 1) return
|
|
303
|
+
if (e.length === 1) return y(e[0]);
|
|
304
304
|
let t = e.length;
|
|
305
305
|
return `linear-gradient(135deg, ${e.map((e, n) => {
|
|
306
306
|
let r = Math.round(n / (t - 1) * 100);
|
|
307
|
-
return `${
|
|
307
|
+
return `${y(e)} ${r}%`;
|
|
308
308
|
}).join(", ")})`;
|
|
309
309
|
}
|
|
310
310
|
function I(e) {
|
|
311
|
-
let t = e.replace("#", ""), n = parseInt(t.slice(0, 2), 16), r = parseInt(t.slice(2, 4), 16), i = parseInt(t.slice(4, 6), 16), [a, o, s] =
|
|
311
|
+
let t = e.replace("#", ""), n = parseInt(t.slice(0, 2), 16), r = parseInt(t.slice(2, 4), 16), i = parseInt(t.slice(4, 6), 16), [a, o, s] = ce(n / 255, r / 255, i / 255), [c, l, u] = se(a, o, s);
|
|
312
312
|
return {
|
|
313
313
|
L: c,
|
|
314
314
|
C: l,
|
|
@@ -317,18 +317,18 @@ function I(e) {
|
|
|
317
317
|
}
|
|
318
318
|
var L = [.35, .95];
|
|
319
319
|
function R(e, t = {}) {
|
|
320
|
-
let n = typeof e == "string" ?
|
|
321
|
-
|
|
320
|
+
let n = typeof e == "string" ? b(e) : e, { stopCount: r = 4, harmony: i = "analogous", lightnessSpread: a = .32, chromaFalloff: o = .85, hueSpread: s = 28, lightnessEasing: c = "linear", chromaEasing: l = "bell", temperatureShift: u = 0 } = t, d = Math.max(2, Math.min(8, Math.round(r))), [f, p] = L, m = n.L - a / 2, g = n.L + a / 2;
|
|
321
|
+
m < f && (g += f - m, m = f), g > p && (m -= g - p, g = p), m = Math.max(f, m), g = Math.min(p, g);
|
|
322
322
|
let v = [];
|
|
323
323
|
for (let e = 0; e < d; e++) {
|
|
324
|
-
let t = d === 1 ? 0 : e / (d - 1), r =
|
|
325
|
-
if (l === "bell") f = a + (n.C - a) *
|
|
324
|
+
let t = d === 1 ? 0 : e / (d - 1), r = m + (g - m) * z(t, c), a = n.C * o, f;
|
|
325
|
+
if (l === "bell") f = a + (n.C - a) * _e(t);
|
|
326
326
|
else {
|
|
327
327
|
let e = l === "sine" ? z(t, "sine") : t;
|
|
328
328
|
f = n.C - (n.C - a) * e;
|
|
329
329
|
}
|
|
330
|
-
let p =
|
|
331
|
-
u > 0 && (p =
|
|
330
|
+
let p = _(n.h, i, s, t);
|
|
331
|
+
u > 0 && (p = xe(p, t, u)), v.push(h({
|
|
332
332
|
L: r,
|
|
333
333
|
C: f,
|
|
334
334
|
h: p
|
|
@@ -339,19 +339,19 @@ function R(e, t = {}) {
|
|
|
339
339
|
function z(e, t) {
|
|
340
340
|
return t === "sine" ? .5 - .5 * Math.cos(Math.PI * e) : e;
|
|
341
341
|
}
|
|
342
|
-
function
|
|
342
|
+
function _e(e) {
|
|
343
343
|
return Math.sin(Math.PI * e);
|
|
344
344
|
}
|
|
345
|
-
var
|
|
346
|
-
function
|
|
345
|
+
var ve = 70, ye = 250, be = 22;
|
|
346
|
+
function xe(e, t, n) {
|
|
347
347
|
let r = (t - .5) * 2;
|
|
348
348
|
if (r === 0 || n === 0) return e;
|
|
349
|
-
let i = (
|
|
350
|
-
return ((e + Math.sign(i) * Math.min(Math.abs(r) * n *
|
|
349
|
+
let i = (oe(e, r >= 0 ? ve : ye, 1, "shorter") - e + 540) % 360 - 180;
|
|
350
|
+
return ((e + Math.sign(i) * Math.min(Math.abs(r) * n * be, Math.abs(i))) % 360 + 360) % 360;
|
|
351
351
|
}
|
|
352
352
|
//#endregion
|
|
353
353
|
//#region src/components/custom/aurora/composables/uniformBridge.ts
|
|
354
|
-
var
|
|
354
|
+
var Se = {
|
|
355
355
|
smooth: 0,
|
|
356
356
|
pastel: 1,
|
|
357
357
|
watercolor: 2,
|
|
@@ -359,12 +359,12 @@ var xe = {
|
|
|
359
359
|
crayon: 4,
|
|
360
360
|
vangogh: 5,
|
|
361
361
|
"oil-pastel": 6
|
|
362
|
-
},
|
|
362
|
+
}, Ce = {
|
|
363
363
|
shorter: 0,
|
|
364
364
|
longer: 1,
|
|
365
365
|
increasing: 2,
|
|
366
366
|
decreasing: 3
|
|
367
|
-
},
|
|
367
|
+
}, we = {
|
|
368
368
|
none: 0,
|
|
369
369
|
radial: 1,
|
|
370
370
|
swirl: 2,
|
|
@@ -374,11 +374,11 @@ var xe = {
|
|
|
374
374
|
}, B = {
|
|
375
375
|
flow: 0,
|
|
376
376
|
tensor: 1
|
|
377
|
-
},
|
|
377
|
+
}, Te = {
|
|
378
378
|
fbm: 0,
|
|
379
379
|
cellular: 1,
|
|
380
380
|
hybrid: 2
|
|
381
|
-
},
|
|
381
|
+
}, Ee = {
|
|
382
382
|
oil: 0,
|
|
383
383
|
knife: 1,
|
|
384
384
|
chunky: 3
|
|
@@ -387,19 +387,19 @@ var xe = {
|
|
|
387
387
|
C: .0125,
|
|
388
388
|
h: 77.5
|
|
389
389
|
};
|
|
390
|
-
function Ee(e) {
|
|
391
|
-
return e === void 0 ? _(V.L, V.C, V.h) : Array.isArray(e) ? e : _(e.L, e.C, e.h);
|
|
392
|
-
}
|
|
393
390
|
function De(e) {
|
|
394
|
-
return
|
|
391
|
+
return e === void 0 ? v(V.L, V.C, V.h) : Array.isArray(e) ? e : v(e.L, e.C, e.h);
|
|
395
392
|
}
|
|
396
393
|
function Oe(e) {
|
|
397
|
-
return
|
|
394
|
+
return Se[e.medium];
|
|
398
395
|
}
|
|
399
396
|
function ke(e) {
|
|
397
|
+
return Ee[e.strokeMode];
|
|
398
|
+
}
|
|
399
|
+
function Ae(e) {
|
|
400
400
|
return e.medium === "vangogh" || e.medium === "oil-pastel" || e.medium === "crayon" ? B.tensor : B[e.strokeOrient ?? "flow"];
|
|
401
401
|
}
|
|
402
|
-
function
|
|
402
|
+
function je(e, t, n, r) {
|
|
403
403
|
let i = (e) => 1 - e, a = {
|
|
404
404
|
palette: new Float32Array(24),
|
|
405
405
|
pos: new Float32Array(12),
|
|
@@ -420,13 +420,13 @@ function Ae(e, t, n, r) {
|
|
|
420
420
|
a.pos[e * 2 + 0] = t.x, a.pos[e * 2 + 1] = i(t.y), a.rad[e] = t.radius, a.pb[e] = t.paletteBias, a.vb[e] = t.valueBias, a.dr[e] = t.driftRadius, a.dp[e] = t.driftPhase, a.elong[e] = t.elongation ?? 1, a.angle[e] = -(t.angle ?? 0) * Math.PI / 180;
|
|
421
421
|
}
|
|
422
422
|
for (let e = s; e < 6; e++) a.pos[e * 2 + 0] = 0, a.pos[e * 2 + 1] = 0, a.rad[e] = 0, a.pb[e] = 0, a.vb[e] = 0, a.dr[e] = 0, a.dp[e] = 0, a.elong[e] = 1, a.angle[e] = 0;
|
|
423
|
-
e.uniform2fv(n.uNucleiPos, a.pos), e.uniform1fv(n.uNucleiRadius, a.rad), e.uniform1fv(n.uNucleiPaletteBias, a.pb), e.uniform1fv(n.uNucleiValueBias, a.vb), e.uniform1fv(n.uNucleiDriftRadius, a.dr), e.uniform1fv(n.uNucleiDriftPhase, a.dp), e.uniform1fv(n.uNucleiElong, a.elong), e.uniform1fv(n.uNucleiAngle, a.angle), e.uniform1f(n.uSoftmaxBeta, o.softmaxBeta), e.uniform1f(n.uValueVariance, o.valueVariance), e.uniform1f(n.uWarpAmount, o.warpAmount), e.uniform1f(n.uWarpScale, o.warpScale), e.uniform1f(n.uWarpDrift, o.warpDrift), e.uniform1i(n.uWarpMode,
|
|
423
|
+
e.uniform2fv(n.uNucleiPos, a.pos), e.uniform1fv(n.uNucleiRadius, a.rad), e.uniform1fv(n.uNucleiPaletteBias, a.pb), e.uniform1fv(n.uNucleiValueBias, a.vb), e.uniform1fv(n.uNucleiDriftRadius, a.dr), e.uniform1fv(n.uNucleiDriftPhase, a.dp), e.uniform1fv(n.uNucleiElong, a.elong), e.uniform1fv(n.uNucleiAngle, a.angle), e.uniform1f(n.uSoftmaxBeta, o.softmaxBeta), e.uniform1f(n.uValueVariance, o.valueVariance), e.uniform1f(n.uWarpAmount, o.warpAmount), e.uniform1f(n.uWarpScale, o.warpScale), e.uniform1f(n.uWarpDrift, o.warpDrift), e.uniform1i(n.uWarpMode, Te[o.warpMode]), e.uniform1i(n.uNoiseOctaves, o.noiseOctaves), e.uniform1i(n.uMedium, Oe(o)), e.uniform1i(n.uHuePath, Ce[o.huePath ?? "shorter"]), e.uniform1i(n.uFlowPattern, we[o.flow.pattern]), e.uniform2f(n.uFlowFocal, o.flow.focalX, i(o.flow.focalY)), e.uniform1f(n.uFlowAngle, o.flow.angle), e.uniform1f(n.uFlowCurl, o.flow.curl), e.uniform2f(n.uCursor, r.x, i(r.y)), e.uniform1f(n.uCursorStrength, r.strength), e.uniform1f(n.uCursorRadius, r.radius), e.uniform2f(n.uCursorVelocity, r.velX, -r.velY), e.uniform1f(n.uCursorBurst, r.burst), e.uniform1f(n.uStrokeAmount, o.strokeAmount), e.uniform1f(n.uStrokeScale, o.strokeScale), e.uniform1f(n.uStrokeAnisotropy, o.strokeAnisotropy), e.uniform1i(n.uStrokeLayers, o.strokeLayers), e.uniform1i(n.uStrokeMode, ke(o)), e.uniform1i(n.uStrokeOrient, Ae(o));
|
|
424
424
|
{
|
|
425
425
|
let t = o.lightDir ?? [
|
|
426
426
|
-.5,
|
|
427
427
|
.6,
|
|
428
428
|
.62
|
|
429
|
-
], r =
|
|
429
|
+
], r = De(o.lightColor);
|
|
430
430
|
e.uniform3f(n.uLightDir, t[0], t[1], t[2]), e.uniform3f(n.uLightColor, r[0], r[1], r[2]);
|
|
431
431
|
}
|
|
432
432
|
e.uniform1f(n.uWetEdge, o.wetEdge), e.uniform1f(n.uGranulation, o.granulation), e.uniform1f(n.uImpasto, o.impasto), e.uniform1f(n.uBrokenColor, o.brokenColor), e.uniform1f(n.uCanvasGrain, o.canvasGrain), e.uniform1f(n.uNucleiDrift, o.nucleiDrift), e.uniform1f(n.uPaletteDrift, o.paletteDrift), e.uniform1f(n.uBreathDepth, o.breathDepth), e.uniform1f(n.uBreathPeriod, o.breathPeriod), e.uniform1f(n.uSaturation, o.saturation), e.uniform1f(n.uPaperGrain, o.paperGrain), e.uniform1f(n.uAlpha, o.alpha);
|
|
@@ -434,8 +434,8 @@ function Ae(e, t, n, r) {
|
|
|
434
434
|
}
|
|
435
435
|
//#endregion
|
|
436
436
|
//#region src/components/custom/aurora/composables/cursorModel.ts
|
|
437
|
-
var H = .22,
|
|
438
|
-
function
|
|
437
|
+
var H = .22, Me = .18, Ne = .992, Pe = .96, U = .3;
|
|
438
|
+
function Fe() {
|
|
439
439
|
return {
|
|
440
440
|
x: .5,
|
|
441
441
|
y: .5,
|
|
@@ -449,22 +449,22 @@ function Pe() {
|
|
|
449
449
|
burst: 0
|
|
450
450
|
};
|
|
451
451
|
}
|
|
452
|
-
function
|
|
452
|
+
function Ie(e, t = 1) {
|
|
453
453
|
let n = Math.max(0, Math.min(1, t));
|
|
454
|
-
e.x += (e.targetX - e.x) * H, e.y += (e.targetY - e.y) * H, e.strength += (e.targetStrength - e.strength) *
|
|
454
|
+
e.x += (e.targetX - e.x) * H, e.y += (e.targetY - e.y) * H, e.strength += (e.targetStrength - e.strength) * Me, e.targetStrength *= Ne, e.velX *= (1 - U) * n, e.velY *= (1 - U) * n, e.burst *= Pe * n;
|
|
455
455
|
}
|
|
456
|
-
function
|
|
456
|
+
function Le(e, t, n) {
|
|
457
457
|
let r = .12, i = Math.max(-r, Math.min(r, t)), a = Math.max(-r, Math.min(r, n));
|
|
458
458
|
e.velX += (i - e.velX) * U, e.velY += (a - e.velY) * U;
|
|
459
459
|
let o = Math.hypot(i, a);
|
|
460
460
|
e.burst = Math.min(1, e.burst + o * 4);
|
|
461
461
|
}
|
|
462
|
-
function
|
|
462
|
+
function Re(e) {
|
|
463
463
|
return e.targetStrength > .001 || e.strength > .001 || Math.abs(e.x - e.targetX) > .001 || Math.abs(e.y - e.targetY) > .001;
|
|
464
464
|
}
|
|
465
465
|
//#endregion
|
|
466
466
|
//#region src/components/custom/aurora/composables/frameLoop.ts
|
|
467
|
-
function
|
|
467
|
+
function ze(e) {
|
|
468
468
|
let { gl: t, prog: n, uniforms: r, cursor: i, getConfig: a, getReducedMotion: o } = e, s = (e) => 1 - e;
|
|
469
469
|
function c() {
|
|
470
470
|
return +!o();
|
|
@@ -479,25 +479,25 @@ function Re(e) {
|
|
|
479
479
|
function u() {
|
|
480
480
|
if (o()) return !1;
|
|
481
481
|
let e = a();
|
|
482
|
-
return e.nucleiDrift !== 0 || e.paletteDrift !== 0 || e.breathDepth !== 0 || e.warpDrift !== 0 || e.interactivity?.light && i.burst > .001 ? !0 :
|
|
482
|
+
return e.nucleiDrift !== 0 || e.paletteDrift !== 0 || e.breathDepth !== 0 || e.warpDrift !== 0 || e.interactivity?.light && i.burst > .001 ? !0 : Re(i);
|
|
483
483
|
}
|
|
484
484
|
return {
|
|
485
485
|
frame: (e) => {
|
|
486
|
-
|
|
486
|
+
Ie(i, c()), l(e);
|
|
487
487
|
},
|
|
488
488
|
needsAnimation: u
|
|
489
489
|
};
|
|
490
490
|
}
|
|
491
491
|
//#endregion
|
|
492
492
|
//#region src/components/custom/aurora/composables/runtime.ts
|
|
493
|
-
function
|
|
493
|
+
function Be(e) {
|
|
494
494
|
return e.mode === "capture" ? !0 : e.initStrategy === "eager";
|
|
495
495
|
}
|
|
496
|
-
function
|
|
496
|
+
function Ve(e) {
|
|
497
497
|
return typeof e.preserveDrawingBuffer == "boolean" ? e.preserveDrawingBuffer : e.mode === "capture";
|
|
498
498
|
}
|
|
499
499
|
function W(e, n, r = {}) {
|
|
500
|
-
let i =
|
|
500
|
+
let i = Ve(r), a = n, o = Fe(), c = null, l = s(e, {
|
|
501
501
|
mode: r.mode === "capture" ? "capture" : "live",
|
|
502
502
|
contextAttrs: {
|
|
503
503
|
antialias: !1,
|
|
@@ -506,12 +506,12 @@ function W(e, n, r = {}) {
|
|
|
506
506
|
preserveDrawingBuffer: i
|
|
507
507
|
},
|
|
508
508
|
setup: (n) => {
|
|
509
|
-
let { program: r, vs: i, fs: s, uniforms: u, geometry: d } = N(n,
|
|
509
|
+
let { program: r, vs: i, fs: s, uniforms: u, geometry: d } = N(n, le, ge);
|
|
510
510
|
function f() {
|
|
511
511
|
let i = t(), a = e.getBoundingClientRect(), o = e.parentElement?.getBoundingClientRect(), s = a.width || o?.width || 1, c = a.height || o?.height || 1, l = Math.max(1, Math.round(s * i)), u = Math.max(1, Math.round(c * i));
|
|
512
512
|
(e.width !== l || e.height !== u) && (e.width = l, e.height = u), n.viewport(0, 0, l, u), n.useProgram(r);
|
|
513
513
|
}
|
|
514
|
-
let p =
|
|
514
|
+
let p = je(n, r, u, o), m = ze({
|
|
515
515
|
gl: n,
|
|
516
516
|
prog: r,
|
|
517
517
|
uniforms: u,
|
|
@@ -538,7 +538,7 @@ function W(e, n, r = {}) {
|
|
|
538
538
|
o.targetX = e, o.targetY = t, o.targetStrength = n, l.wake();
|
|
539
539
|
}
|
|
540
540
|
function d(e, t) {
|
|
541
|
-
l.reducedMotion || (
|
|
541
|
+
l.reducedMotion || (Le(o, e, t), l.wake());
|
|
542
542
|
}
|
|
543
543
|
function f() {
|
|
544
544
|
o.targetStrength = 0, l.wake();
|
|
@@ -549,7 +549,7 @@ function W(e, n, r = {}) {
|
|
|
549
549
|
function m(e) {
|
|
550
550
|
l.wake();
|
|
551
551
|
}
|
|
552
|
-
return r.mode !== "capture" &&
|
|
552
|
+
return r.mode !== "capture" && Be(r) && l.arm(), {
|
|
553
553
|
arm: () => l.arm(),
|
|
554
554
|
update: (e) => {
|
|
555
555
|
a = e, c?.(e), l.wake();
|
|
@@ -575,7 +575,7 @@ function G(e) {
|
|
|
575
575
|
}
|
|
576
576
|
//#endregion
|
|
577
577
|
//#region src/components/custom/aurora/composables/useAurora.ts
|
|
578
|
-
function
|
|
578
|
+
function He(e) {
|
|
579
579
|
if (typeof window > "u") return () => {};
|
|
580
580
|
if (typeof window.requestIdleCallback == "function") {
|
|
581
581
|
let t = window.requestIdleCallback(() => e(), { timeout: 2e3 });
|
|
@@ -593,7 +593,7 @@ function Ve(e) {
|
|
|
593
593
|
};
|
|
594
594
|
}
|
|
595
595
|
function K(e, t, n = {}, r = {}) {
|
|
596
|
-
let i = G(t), a = r.renderMode === "css", o = null, s = null, c = null, l = null, u = null, d = null, f = null, p = !1, m =
|
|
596
|
+
let i = G(t), a = r.renderMode === "css", o = null, s = null, c = null, l = null, u = null, d = null, f = null, p = !1, m = D(!1), h = n.mode === "capture" || n.initStrategy === "eager";
|
|
597
597
|
function g() {
|
|
598
598
|
f && o && o.setReducedMotion(f.matches);
|
|
599
599
|
}
|
|
@@ -614,7 +614,7 @@ function K(e, t, n = {}, r = {}) {
|
|
|
614
614
|
_(e);
|
|
615
615
|
return;
|
|
616
616
|
}
|
|
617
|
-
m.value = !0, f = window.matchMedia("(prefers-reduced-motion: reduce)"), f.addEventListener("change", g), s = k(i, (e) => o?.update(e), { deep: !0 }), i().interactivity?.scroll && e.value && (l = k(
|
|
617
|
+
m.value = !0, f = window.matchMedia("(prefers-reduced-motion: reduce)"), f.addEventListener("change", g), s = k(i, (e) => o?.update(e), { deep: !0 }), i().interactivity?.scroll && e.value && (l = k(te({
|
|
618
618
|
target: e,
|
|
619
619
|
trackExit: !0
|
|
620
620
|
}), (e) => {
|
|
@@ -644,7 +644,7 @@ function K(e, t, n = {}, r = {}) {
|
|
|
644
644
|
}, { pauseWhenHidden: !1 });
|
|
645
645
|
let r = d.isIntersecting;
|
|
646
646
|
c = k(r, (e) => {
|
|
647
|
-
!e || p || u || (u =
|
|
647
|
+
!e || p || u || (u = He(() => {
|
|
648
648
|
u = null, r.value && v();
|
|
649
649
|
}));
|
|
650
650
|
}, { immediate: !0 });
|
|
@@ -658,7 +658,7 @@ function K(e, t, n = {}, r = {}) {
|
|
|
658
658
|
renderAt: (e) => o?.renderAt(e),
|
|
659
659
|
pause: () => o?.pause(),
|
|
660
660
|
resume: () => o?.resume(),
|
|
661
|
-
isArmed:
|
|
661
|
+
isArmed: ae(m)
|
|
662
662
|
};
|
|
663
663
|
}
|
|
664
664
|
//#endregion
|
|
@@ -671,17 +671,17 @@ function q(e) {
|
|
|
671
671
|
}
|
|
672
672
|
//#endregion
|
|
673
673
|
//#region src/components/custom/aurora/Aurora.vue
|
|
674
|
-
var
|
|
674
|
+
var Ue = /* @__PURE__ */ e(/* @__PURE__ */ re({
|
|
675
675
|
__name: "Aurora",
|
|
676
676
|
props: {
|
|
677
|
-
config: { default: () =>
|
|
677
|
+
config: { default: () => m },
|
|
678
678
|
runtimeOptions: {},
|
|
679
679
|
onInitError: {},
|
|
680
680
|
renderMode: { default: "auto" },
|
|
681
681
|
opacityCeiling: { default: 1 }
|
|
682
682
|
},
|
|
683
683
|
setup(e, { expose: t }) {
|
|
684
|
-
let n = e, r = q(n.renderMode), i = x(() => Math.max(0, Math.min(1, n.opacityCeiling))), a =
|
|
684
|
+
let n = e, r = q(n.renderMode), i = x(() => Math.max(0, Math.min(1, n.opacityCeiling))), a = D(null), o = K(a, () => n.config, x(() => ({
|
|
685
685
|
...n.runtimeOptions ?? {},
|
|
686
686
|
...n.onInitError ? { onInitError: n.onInitError } : {}
|
|
687
687
|
})).value, { renderMode: r }), s = x(() => F(n.config.palette));
|
|
@@ -696,7 +696,7 @@ var He = /* @__PURE__ */ e(/* @__PURE__ */ ne({
|
|
|
696
696
|
pause: o.pause,
|
|
697
697
|
resume: o.resume,
|
|
698
698
|
isArmed: o.isArmed
|
|
699
|
-
}), (e, t) => (E(),
|
|
699
|
+
}), (e, t) => (E(), ne("div", {
|
|
700
700
|
class: "aurora-root block h-full w-full overflow-hidden",
|
|
701
701
|
style: C({ "--aurora-opacity-ceiling": i.value })
|
|
702
702
|
}, [S("div", {
|
|
@@ -707,13 +707,13 @@ var He = /* @__PURE__ */ e(/* @__PURE__ */ ne({
|
|
|
707
707
|
ref_key: "canvasRef",
|
|
708
708
|
ref: a,
|
|
709
709
|
"aria-hidden": "true",
|
|
710
|
-
class:
|
|
710
|
+
class: ie(["aurora-canvas block h-full w-full", { "aurora-canvas--armed": O(o).isArmed.value }])
|
|
711
711
|
}, null, 2)], 4));
|
|
712
712
|
}
|
|
713
713
|
}), [["__scopeId", "data-v-b51a5fc3"]]);
|
|
714
714
|
//#endregion
|
|
715
715
|
//#region src/components/custom/aurora/composables/useCursorInteraction.ts
|
|
716
|
-
function
|
|
716
|
+
function We(e, t, n) {
|
|
717
717
|
let r = G(t), i = n.hitFraction ?? .35, a = null, o = null, s = null, c = null;
|
|
718
718
|
function l(e, t) {
|
|
719
719
|
let n = t.getBoundingClientRect();
|
|
@@ -851,7 +851,7 @@ var X = {
|
|
|
851
851
|
warpDrift: .008,
|
|
852
852
|
breathDepth: .05
|
|
853
853
|
}
|
|
854
|
-
},
|
|
854
|
+
}, Ge = .61803398875;
|
|
855
855
|
function Q(e, t = "composed") {
|
|
856
856
|
let n = Math.max(1, Math.min(6, Math.round(e))), i = [
|
|
857
857
|
[.33, .33],
|
|
@@ -863,7 +863,7 @@ function Q(e, t = "composed") {
|
|
|
863
863
|
], a = (e) => {
|
|
864
864
|
switch (t) {
|
|
865
865
|
case "scattered": {
|
|
866
|
-
let t = e *
|
|
866
|
+
let t = e * Ge * Math.PI * 2, i = .12 + .3 * Math.sqrt((e + .5) / n);
|
|
867
867
|
return [r(.5 + i * Math.cos(t), .1, .9), r(.5 + i * Math.sin(t), .1, .9)];
|
|
868
868
|
}
|
|
869
869
|
case "centred": {
|
|
@@ -887,14 +887,14 @@ function Q(e, t = "composed") {
|
|
|
887
887
|
};
|
|
888
888
|
});
|
|
889
889
|
}
|
|
890
|
-
function
|
|
890
|
+
function Ke(e) {
|
|
891
891
|
return e < .4 ? "fbm" : e < .75 ? "hybrid" : "cellular";
|
|
892
892
|
}
|
|
893
|
-
function
|
|
893
|
+
function qe(e, t) {
|
|
894
894
|
let n = r(t, 0, 1);
|
|
895
|
-
e.warpAmount = r(Y(.2, .6, n), 0, .6), e.warpScale = r(Y(1, 2.6, n), .5, 3), e.warpMode =
|
|
895
|
+
e.warpAmount = r(Y(.2, .6, n), 0, .6), e.warpScale = r(Y(1, 2.6, n), .5, 3), e.warpMode = Ke(n), e.noiseOctaves = n < .5 ? 3 : n < .85 ? 4 : 5;
|
|
896
896
|
}
|
|
897
|
-
function
|
|
897
|
+
function Je(e, t, n) {
|
|
898
898
|
let i = r(n, 0, 1);
|
|
899
899
|
switch (t) {
|
|
900
900
|
case "watercolor":
|
|
@@ -913,7 +913,7 @@ function qe(e, t, n) {
|
|
|
913
913
|
break;
|
|
914
914
|
}
|
|
915
915
|
}
|
|
916
|
-
function
|
|
916
|
+
function Ye(e = J, t = m) {
|
|
917
917
|
let n = {
|
|
918
918
|
...t,
|
|
919
919
|
palette: t.palette.map((e) => ({ ...e })),
|
|
@@ -924,7 +924,7 @@ function Je(e = J, t = p) {
|
|
|
924
924
|
harmony: e.harmony ?? "analogous",
|
|
925
925
|
stopCount: 4,
|
|
926
926
|
temperatureShift: i === void 0 ? 0 : Y(X.temperatureShift.calm, X.temperatureShift.vivid, i)
|
|
927
|
-
})), i !== void 0 && (n.saturation = r(Y(X.saturation.calm, X.saturation.vivid, i), .6, 1.3), n.valueVariance = r(Y(X.valueVariance.calm, X.valueVariance.vivid, i), 0, .3), n.breathDepth = r(Y(X.breathDepth.calm, X.breathDepth.vivid, i), 0, .15)), e.zones !== void 0 && (n.nuclei = Q(e.zones.count, e.zones.arrangement ?? "composed")), e.noise !== void 0 &&
|
|
927
|
+
})), i !== void 0 && (n.saturation = r(Y(X.saturation.calm, X.saturation.vivid, i), .6, 1.3), n.valueVariance = r(Y(X.valueVariance.calm, X.valueVariance.vivid, i), 0, .3), n.breathDepth = r(Y(X.breathDepth.calm, X.breathDepth.vivid, i), 0, .15)), e.zones !== void 0 && (n.nuclei = Q(e.zones.count, e.zones.arrangement ?? "composed")), e.noise !== void 0 && qe(n, e.noise), e.medium !== void 0 && (n.medium = e.medium.kind, (e.medium.kind === "vangogh" || e.medium.kind === "oil-pastel") && (n.strokeOrient = "tensor"), e.medium.kind !== "smooth" && e.medium.amount !== void 0 && Je(n, e.medium.kind, e.medium.amount)), e.motion !== void 0) {
|
|
928
928
|
let t = Z[e.motion];
|
|
929
929
|
n.nucleiDrift = r(t.nucleiDrift, 0, .05), n.paletteDrift = r(t.paletteDrift, 0, .04), n.warpDrift = r(t.warpDrift, 0, .015), n.breathDepth = r(e.motion === "still" ? 0 : Math.max(n.breathDepth, t.breathDepth), 0, .15);
|
|
930
930
|
}
|
|
@@ -933,10 +933,10 @@ function Je(e = J, t = p) {
|
|
|
933
933
|
function $(e, t, n) {
|
|
934
934
|
return n === t ? 0 : r((e - t) / (n - t), 0, 1);
|
|
935
935
|
}
|
|
936
|
-
function
|
|
936
|
+
function Xe(e) {
|
|
937
937
|
return e.nucleiDrift + e.paletteDrift + e.warpDrift > .001 ? "drifting" : e.breathDepth > .001 ? "breathing" : "still";
|
|
938
938
|
}
|
|
939
|
-
function
|
|
939
|
+
function Ze(e, t) {
|
|
940
940
|
switch (t) {
|
|
941
941
|
case "watercolor": return r(e.wetEdge, 0, 1);
|
|
942
942
|
case "oil":
|
|
@@ -946,13 +946,13 @@ function Xe(e, t) {
|
|
|
946
946
|
case "crayon": return r(e.strokeAmount, 0, 1);
|
|
947
947
|
}
|
|
948
948
|
}
|
|
949
|
-
function
|
|
949
|
+
function Qe(e) {
|
|
950
950
|
let t = e.medium, n = t === "smooth" ? { kind: t } : {
|
|
951
951
|
kind: t,
|
|
952
|
-
amount:
|
|
952
|
+
amount: Ze(e, t)
|
|
953
953
|
};
|
|
954
954
|
return {
|
|
955
|
-
seed: e.palette.length > 0 ?
|
|
955
|
+
seed: e.palette.length > 0 ? y(e.palette[0]) : "#3a7bd5",
|
|
956
956
|
harmony: "analogous",
|
|
957
957
|
colorEnergy: $(e.saturation, X.saturation.calm, X.saturation.vivid),
|
|
958
958
|
zones: {
|
|
@@ -961,8 +961,8 @@ function Ze(e) {
|
|
|
961
961
|
},
|
|
962
962
|
noise: $(e.warpAmount, .2, .6),
|
|
963
963
|
medium: n,
|
|
964
|
-
motion:
|
|
964
|
+
motion: Xe(e)
|
|
965
965
|
};
|
|
966
966
|
}
|
|
967
967
|
//#endregion
|
|
968
|
-
export {
|
|
968
|
+
export { Ue as Aurora, J as DEFAULT_ATOMS, m as DEFAULT_AURORA_CONFIG, f as MAX_NUCLEI, p as MAX_STOPS, d as PAPER_WASH_GROUND, Qe as configToAtoms, W as createAurora, b as cssToOklch, R as deriveAurora, P as flattenPalette, I as hexToOklchStop, Q as nucleiPrior, y as oklchStopToHex, g as oklchToLinear, F as paletteToCssGradient, Ye as resolveAtoms, q as resolveRenderMode, K as useAurora, We as useCursorInteraction };
|