kuinetic 0.1.4 → 0.2.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.
Files changed (124) hide show
  1. package/README.md +93 -10
  2. package/dist/esm/advanced/index.mjs +4727 -0
  3. package/dist/esm/chunk-AEQGC2KM.mjs +189 -0
  4. package/dist/esm/chunk-MQSNMYRP.mjs +9332 -0
  5. package/dist/esm/chunk-RVTYN6NZ.mjs +3446 -0
  6. package/dist/esm/chunk-U43BX5O2.mjs +2984 -0
  7. package/dist/esm/core/index.mjs +16 -9
  8. package/dist/esm/effects/index.mjs +19 -4
  9. package/dist/esm/index.mjs +17 -10
  10. package/dist/kuinetic.advanced.js +5070 -0
  11. package/dist/kuinetic.advanced.min.js +749 -0
  12. package/dist/kuinetic.all.js +15147 -6039
  13. package/dist/kuinetic.all.min.js +2 -0
  14. package/dist/kuinetic.css +2801 -318
  15. package/dist/kuinetic.js +15147 -6038
  16. package/dist/kuinetic.min.css +1 -0
  17. package/dist/kuinetic.min.js +7 -0
  18. package/dist/types/3d/angles.d.ts +30 -0
  19. package/dist/types/3d/flattening.d.ts +49 -0
  20. package/dist/types/3d/index.d.ts +36 -0
  21. package/dist/types/3d/model-3d.d.ts +115 -0
  22. package/dist/types/3d/register.d.ts +26 -0
  23. package/dist/types/3d/webgl.d.ts +24 -0
  24. package/dist/types/advanced/audio.d.ts +322 -0
  25. package/dist/types/advanced/base.d.ts +233 -0
  26. package/dist/types/advanced/camera-3d.d.ts +236 -0
  27. package/dist/types/advanced/fluid-cursor.d.ts +149 -0
  28. package/dist/types/advanced/gl-utils.d.ts +52 -0
  29. package/dist/types/advanced/glsl.d.ts +60 -0
  30. package/dist/types/advanced/index.d.ts +23 -0
  31. package/dist/types/advanced/particles.d.ts +108 -0
  32. package/dist/types/advanced/scenes.d.ts +207 -0
  33. package/dist/types/advanced/shaders.d.ts +972 -0
  34. package/dist/types/browser/boot.d.ts +72 -0
  35. package/dist/types/core/activation.d.ts +227 -2
  36. package/dist/types/core/animator.d.ts +446 -11
  37. package/dist/types/core/attrs.d.ts +9 -0
  38. package/dist/types/core/breakpoints.d.ts +260 -0
  39. package/dist/types/core/bundles.d.ts +71 -0
  40. package/dist/types/core/callback.d.ts +64 -0
  41. package/dist/types/core/capabilities.d.ts +47 -0
  42. package/dist/types/core/channels.d.ts +120 -2
  43. package/dist/types/core/cloak-selectors.d.ts +47 -0
  44. package/dist/types/core/compile.d.ts +207 -2
  45. package/dist/types/core/control.d.ts +218 -0
  46. package/dist/types/core/declarations.d.ts +128 -0
  47. package/dist/types/core/derived/aggregate.d.ts +43 -0
  48. package/dist/types/core/derived/book.d.ts +30 -0
  49. package/dist/types/core/derived/diagnostics.d.ts +51 -0
  50. package/dist/types/core/derived/group-gate.d.ts +62 -0
  51. package/dist/types/core/derived/install.d.ts +110 -0
  52. package/dist/types/core/derived/late-matches.d.ts +19 -0
  53. package/dist/types/core/derived/types.d.ts +108 -0
  54. package/dist/types/core/easing.d.ts +57 -0
  55. package/dist/types/core/element-config.d.ts +47 -6
  56. package/dist/types/core/element-size.d.ts +23 -0
  57. package/dist/types/core/event-sources.d.ts +39 -0
  58. package/dist/types/core/flip.d.ts +34 -1
  59. package/dist/types/core/gesture.d.ts +12 -0
  60. package/dist/types/core/host-facts.d.ts +155 -0
  61. package/dist/types/core/index.d.ts +7 -3
  62. package/dist/types/core/owned-styles.d.ts +129 -2
  63. package/dist/types/core/params.d.ts +48 -0
  64. package/dist/types/core/parse.d.ts +28 -1
  65. package/dist/types/core/register-into.d.ts +29 -0
  66. package/dist/types/core/registry.d.ts +10 -3
  67. package/dist/types/core/repeat.d.ts +91 -0
  68. package/dist/types/core/sequence.d.ts +126 -0
  69. package/dist/types/core/stagger-config.d.ts +199 -0
  70. package/dist/types/core/stagger-keys.d.ts +63 -0
  71. package/dist/types/core/stagger.d.ts +187 -6
  72. package/dist/types/core/style-plan.d.ts +21 -16
  73. package/dist/types/core/target.d.ts +128 -0
  74. package/dist/types/core/threshold-reachability.d.ts +45 -0
  75. package/dist/types/core/time-scale.d.ts +9 -0
  76. package/dist/types/core/toggle-actions.d.ts +141 -0
  77. package/dist/types/core/travel.d.ts +59 -0
  78. package/dist/types/core/types.d.ts +699 -13
  79. package/dist/types/core/unquoted-selectors.d.ts +53 -0
  80. package/dist/types/effects/carousel/drag.d.ts +80 -0
  81. package/dist/types/effects/carousel/index.d.ts +149 -0
  82. package/dist/types/effects/catalog/discrete.d.ts +13 -0
  83. package/dist/types/effects/catalog/interaction-proximity.d.ts +24 -0
  84. package/dist/types/effects/catalog/interaction-reveal.d.ts +248 -0
  85. package/dist/types/effects/catalog/interaction-shared.d.ts +42 -0
  86. package/dist/types/effects/catalog/interaction-states.d.ts +68 -0
  87. package/dist/types/effects/catalog/materials.d.ts +67 -0
  88. package/dist/types/effects/catalog/media-shared.d.ts +3 -2
  89. package/dist/types/effects/catalog/numbers-shared.d.ts +8 -5
  90. package/dist/types/effects/catalog/shared.d.ts +1 -1
  91. package/dist/types/effects/catalog/text-shared.d.ts +86 -10
  92. package/dist/types/effects/catalog/transforms.d.ts +13 -0
  93. package/dist/types/effects/catalog/view-transitions.d.ts +13 -0
  94. package/dist/types/effects/forms/primitives.d.ts +15 -4
  95. package/dist/types/effects/gestures/index.d.ts +53 -0
  96. package/dist/types/effects/index.d.ts +5 -0
  97. package/dist/types/effects/layout/presets.d.ts +35 -0
  98. package/dist/types/effects/motion-path/index.d.ts +35 -0
  99. package/dist/types/effects/scroll-mechanics/params.d.ts +23 -0
  100. package/dist/types/effects/scroll-mechanics/presets.d.ts +51 -0
  101. package/dist/types/effects/scroll-mechanics/section-index.d.ts +47 -0
  102. package/dist/types/effects/shared.d.ts +124 -4
  103. package/dist/types/effects/step-index.d.ts +121 -0
  104. package/dist/types/effects/step-marking.d.ts +41 -33
  105. package/dist/types/effects/svg/icon-parts.d.ts +23 -0
  106. package/dist/types/effects/three-d/flip-parts.d.ts +46 -0
  107. package/dist/types/effects/tween/index.d.ts +57 -0
  108. package/dist/types/effects/tween/properties.d.ts +107 -0
  109. package/dist/types/effects/tween/waypoints.d.ts +106 -0
  110. package/dist/types/showcase/compare.d.ts +4 -0
  111. package/dist/types/showcase/device-frame.d.ts +3 -0
  112. package/dist/types/showcase/hotspots.d.ts +3 -0
  113. package/dist/types/showcase/index.d.ts +30 -0
  114. package/dist/types/showcase/lightbox.d.ts +3 -0
  115. package/dist/types/showcase/media-source.d.ts +13 -0
  116. package/dist/types/showcase/modal-shell.d.ts +22 -0
  117. package/dist/types/showcase/scroll-story.d.ts +4 -0
  118. package/dist/types/showcase/shared.d.ts +47 -0
  119. package/dist/types/showcase/slideshow.d.ts +3 -0
  120. package/dist/types/showcase/slow-mo.d.ts +5 -0
  121. package/package.json +14 -7
  122. package/dist/esm/chunk-7WMNPIOZ.mjs +0 -4883
  123. package/dist/esm/chunk-JT4PZL3A.mjs +0 -773
  124. package/dist/esm/chunk-TJDDIQRG.mjs +0 -1290
@@ -0,0 +1,60 @@
1
+ /**
2
+ * GLSL Shader Sources for kUInetic Advanced Modules
3
+ */
4
+ export declare const FULLSCREEN_QUAD_VS = "#version 300 es\nin vec2 a_position;\nuniform vec2 u_uvOrigin;\nuniform vec2 u_uvScale;\nout vec2 v_uv;\nvoid main() {\n vec2 uv = (a_position + 1.0) * 0.5;\n uv.y = 1.0 - uv.y;\n v_uv = u_uvOrigin + uv * u_uvScale;\n gl_Position = vec4(a_position, 0.0, 1.0);\n}\n";
5
+ export declare const DISPLACE_FS = "#version 300 es\nprecision highp float;\nin vec2 v_uv;\nout vec4 fragColor;\nuniform sampler2D u_image;\nuniform float u_time;\nuniform float u_strength;\nuniform float u_frequency;\nuniform float u_chromatic;\nuniform vec2 u_mouse;\nuniform vec4 u_tint;\nuniform int u_blend;\nuniform float u_duotone;\nuniform vec4 u_color1;\nuniform vec4 u_color2;\nuniform float u_progress;\nuniform float u_audio;\nfloat audioGain() { return 1.0 + clamp(u_audio, 0.0, 1.0); }\nuniform vec4 u_maskBox;\nuniform vec4 u_maskRx;\nuniform vec4 u_maskRy;\nuniform float u_maskAlpha;\nfloat shapeMask() {\n if (u_maskBox.z <= 0.0 || u_maskBox.w <= 0.0) return u_maskAlpha;\n vec2 p = gl_FragCoord.xy - u_maskBox.xy;\n vec2 h = u_maskBox.zw;\n bool left = p.x < 0.0;\n float rx = p.y >= 0.0 ? (left ? u_maskRx.x : u_maskRx.y) : (left ? u_maskRx.w : u_maskRx.z);\n float ry = p.y >= 0.0 ? (left ? u_maskRy.x : u_maskRy.y) : (left ? u_maskRy.w : u_maskRy.z);\n rx = min(rx, h.x);\n ry = min(ry, h.y);\n if (rx <= 0.0 || ry <= 0.0) return u_maskAlpha;\n vec2 q = abs(p) - (h - vec2(rx, ry));\n if (q.x <= 0.0 || q.y <= 0.0) return u_maskAlpha;\n float d = (length(q / vec2(rx, ry)) - 1.0) * min(rx, ry);\n return u_maskAlpha * clamp(0.5 - d, 0.0, 1.0);\n}\n\nvec4 applyBlend(vec4 base, vec4 tint, int mode) {\n // `base` is premultiplied \u2014 `createGLTexture` uploads with `UNPACK_PREMULTIPLY_ALPHA_WEBGL` to\n // match a `premultipliedAlpha: true` context \u2014 while `tint` is straight rgba out of\n // `parseColor`. All four blends are defined on *straight* colour, so every mode takes the same\n // path: un-premultiply, blend, and re-premultiply by this pixel's own coverage.\n //\n // Screen and add were the obvious cases. Run over all four premultiplied components,\n // `screen((0,0,0,0), (1,0,0,1))` is `(1,0,0,1)`, an opaque red pixel where the source had no\n // coverage at all \u2014 `blend: screen tint: red` filled a transparent PNG's surround with a solid\n // red rectangle. Normal and multiply *looked* safe as a plain `base * tint`, because a transparent\n // base does multiply to transparent whatever the tint is. It is the tint's alpha they got wrong:\n // `(1,1,1,1) * (1,0,0,0)` is `(1,0,0,0)`, red at zero coverage, and a premultiplied canvas\n // composites `rgb > a` *additively* \u2014 `tint: rgba(255,0,0,0)`, a fully transparent tint, turned\n // an opaque image red, and any translucent tint bled the same way in proportion.\n //\n // The `min` on `s` is for `main()`'s chromatic sampling, which reads the three colour channels\n // at three offsets and `a` at a fourth: at a transparent PNG's edge that can pair `rgb` from\n // opaque neighbours with `a` from a transparent centre, an input that already breaks the\n // invariant. Clamping the un-premultiplied colour to 1 is what keeps the output inside it.\n //\n // Alpha is `base.a * tint.a` \u2014 the source's coverage, faded by a translucent tint. Whatever the\n // mode, the result satisfies `rgb <= a`, which is the whole contract of a premultiplied pixel.\n float a = base.a * tint.a;\n vec3 s = base.a > 0.0 ? min(base.rgb / base.a, vec3(1.0)) : vec3(0.0);\n vec3 c = mode == 1 ? 1.0 - (1.0 - s) * (1.0 - tint.rgb)\n : mode == 3 ? min(s + tint.rgb, vec3(1.0))\n : s * tint.rgb;\n return vec4(c * a, a);\n}\n\nvoid main() {\n vec2 m = u_mouse;\n float d = distance(v_uv, m);\n float ripple = sin(d * u_frequency - u_time * 3.0) * exp(-d * 4.0);\n float pFactor = u_progress >= 0.0 ? u_progress : 1.0;\n // Audio widens the ripple's throw (up to 2x), leaving its shape and speed alone.\n vec2 disp = normalize(v_uv - m + 0.0001) * ripple * u_strength * 0.05 * pFactor * audioGain();\n\n float cr = texture(u_image, v_uv + disp * (1.0 + u_chromatic)).r;\n float cg = texture(u_image, v_uv + disp).g;\n float cb = texture(u_image, v_uv + disp * (1.0 - u_chromatic)).b;\n float ca = texture(u_image, v_uv + disp).a;\n vec4 color = vec4(cr, cg, cb, ca);\n\n if (u_duotone > 0.5) {\n float lum = dot(color.rgb, vec3(0.299, 0.587, 0.114));\n // `* color.a` because the ramp's two colours are straight rgba and everything downstream of\n // here is premultiplied. Assigning one of them to a transparent pixel's `rgb` left `rgb > 0`\n // with `a == 0`, which a `premultipliedAlpha: true` canvas composites *additively* \u2014 a\n // transparent PNG's surround glowing in `color1` over whatever the page put behind it.\n //\n // `lum` is still read off the premultiplied colour, which is deliberate and unchanged: it\n // makes a half-covered pixel map nearer `color1`, matching how the alpha stencil in\n // `GENERATIVE_FS` reads the same kind of edge.\n color.rgb = mix(u_color1.rgb, u_color2.rgb, lum) * color.a;\n }\n\n fragColor = applyBlend(color, u_tint, u_blend) * shapeMask();\n}\n";
6
+ export declare const FLUID_FS = "#version 300 es\nprecision highp float;\nin vec2 v_uv;\nout vec4 fragColor;\nuniform sampler2D u_image;\nuniform float u_time;\nuniform float u_strength;\nuniform vec2 u_mouse;\nuniform vec4 u_tint;\nuniform float u_progress;\nuniform float u_audio;\nfloat audioGain() { return 1.0 + clamp(u_audio, 0.0, 1.0); }\nuniform vec4 u_maskBox;\nuniform vec4 u_maskRx;\nuniform vec4 u_maskRy;\nuniform float u_maskAlpha;\nfloat shapeMask() {\n if (u_maskBox.z <= 0.0 || u_maskBox.w <= 0.0) return u_maskAlpha;\n vec2 p = gl_FragCoord.xy - u_maskBox.xy;\n vec2 h = u_maskBox.zw;\n bool left = p.x < 0.0;\n float rx = p.y >= 0.0 ? (left ? u_maskRx.x : u_maskRx.y) : (left ? u_maskRx.w : u_maskRx.z);\n float ry = p.y >= 0.0 ? (left ? u_maskRy.x : u_maskRy.y) : (left ? u_maskRy.w : u_maskRy.z);\n rx = min(rx, h.x);\n ry = min(ry, h.y);\n if (rx <= 0.0 || ry <= 0.0) return u_maskAlpha;\n vec2 q = abs(p) - (h - vec2(rx, ry));\n if (q.x <= 0.0 || q.y <= 0.0) return u_maskAlpha;\n float d = (length(q / vec2(rx, ry)) - 1.0) * min(rx, ry);\n return u_maskAlpha * clamp(0.5 - d, 0.0, 1.0);\n}\nconst int KUI_MAX_OCTAVES = 6;\n\n/**\n * Integer avalanche over one lattice coordinate.\n *\n * Deliberately not the `fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453)` idiom. That relies\n * on the low bits of a large sine, which `highp` does not reliably have: on several mobile GPUs\n * neighbouring cells stay correlated and the field bands visibly. GLSL ES 3.00 has real unsigned\n * integer arithmetic, so a proper mix is both better distributed and cheaper.\n */\nuint kuiHash(uvec3 v) {\n uint h = v.x * 0x8da6b343u ^ v.y * 0xd8163841u ^ v.z * 0xcb1ab31fu;\n h ^= h >> 15u; h *= 0x2c1b3c6du;\n h ^= h >> 12u; h *= 0x297a2d39u;\n h ^= h >> 15u;\n return h;\n}\n\n/**\n * A unit gradient vector for one lattice corner.\n *\n * The three components read *non-overlapping* 10-bit fields. Overlapping slices of one hash are\n * correlated with each other, which shows up as gradients clustering around a diagonal and the\n * noise developing a directional grain. The `1e-5` keeps `normalize` defined for the one input\n * that hashes to exactly zero.\n */\nvec3 kuiGradient(uvec3 cell) {\n uint h = kuiHash(cell);\n vec3 g = vec3(\n float((h ) & 1023u),\n float((h >> 10u ) & 1023u),\n float((h >> 20u ) & 1023u)\n ) * (2.0 / 1023.0) - 1.0;\n return normalize(g + vec3(1e-5));\n}\n\n/**\n * One octave of gradient noise, approximately -1..1.\n *\n * The lattice index is biased into unsigned space before hashing: seeds, scroll and a centred UV\n * field all reach negative coordinates, and `uvec3` of a negative `ivec3` wraps \u2014 which is\n * harmless for a hash, but only once every negative input maps somewhere distinct rather than\n * folding onto its positive twin.\n */\nfloat kuiNoise(vec3 p) {\n vec3 i = floor(p);\n vec3 f = p - i;\n vec3 w = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n uvec3 c = uvec3(ivec3(i) + 0x10000);\n float n000 = dot(kuiGradient(c + uvec3(0u, 0u, 0u)), f - vec3(0.0, 0.0, 0.0));\n float n100 = dot(kuiGradient(c + uvec3(1u, 0u, 0u)), f - vec3(1.0, 0.0, 0.0));\n float n010 = dot(kuiGradient(c + uvec3(0u, 1u, 0u)), f - vec3(0.0, 1.0, 0.0));\n float n110 = dot(kuiGradient(c + uvec3(1u, 1u, 0u)), f - vec3(1.0, 1.0, 0.0));\n float n001 = dot(kuiGradient(c + uvec3(0u, 0u, 1u)), f - vec3(0.0, 0.0, 1.0));\n float n101 = dot(kuiGradient(c + uvec3(1u, 0u, 1u)), f - vec3(1.0, 0.0, 1.0));\n float n011 = dot(kuiGradient(c + uvec3(0u, 1u, 1u)), f - vec3(0.0, 1.0, 1.0));\n float n111 = dot(kuiGradient(c + uvec3(1u, 1u, 1u)), f - vec3(1.0, 1.0, 1.0));\n float nx00 = mix(n000, n100, w.x);\n float nx10 = mix(n010, n110, w.x);\n float nx01 = mix(n001, n101, w.x);\n float nx11 = mix(n011, n111, w.x);\n // 2/sqrt(3) \u2014 a 3D gradient-noise octave peaks near sqrt(3)/2, so this brings it to about -1..1.\n return mix(mix(nx00, nx10, w.y), mix(nx01, nx11, w.y), w.z) * 1.1547;\n}\n\n/**\n * Octaves summed and **normalised by the amplitude sum**, approximately -1..1 at every octave count.\n *\n * The division is the whole point of the function. Without it, raising the octave count raises the\n * total amplitude, so the one authored knob that means \"how much fine structure\" would silently be\n * a contrast control as well \u2014 two behaviours behind one parameter, which is the class of bug this\n * repository keeps rediscovering. Gain and lacunarity stay fixed at 0.5 and 2.0: they are the two\n * numbers that make layered noise look wrong in a hundred ways and right in one.\n *\n * The loop bound is a constant with an early break because a loop bound that is itself a uniform\n * forces some drivers to unroll against the worst case anyway, and this way the cost of a low\n * `detail` is genuinely low.\n */\nfloat kuiFbm(vec3 p, int octaves) {\n float amp = 1.0;\n float freq = 1.0;\n float sum = 0.0;\n float norm = 0.0;\n for (int i = 0; i < KUI_MAX_OCTAVES; i++) {\n if (i >= octaves) break;\n sum += amp * kuiNoise(p * freq);\n norm += amp;\n amp *= 0.5;\n freq *= 2.0;\n }\n return sum / max(norm, 1e-5);\n}\nuniform float u_noise;\nuniform float u_seed;\nuniform float u_scale;\nuniform float u_warp;\nuniform int u_detail;\n\n/**\n * A sine's RMS is 0.707. Amplitude-normalised fbm's is about 0.23 \u2014 it reaches its `1.0` peak\n * rarely, which is what makes it look like noise rather than a wave. Handing the programs the raw\n * field would therefore read as the *effect being turned down*, not as it being re-textured, and\n * the first thing anyone trying `noise: 1` would do is reach for `strength`. This brings the two\n * to the same typical magnitude so the amount is a change of texture and nothing else.\n */\nconst float KUI_FBM_SINE_RMS = 3.0;\n\n/**\n * One scalar of animated noise, scaled to stand in for a unit sine at a stated rate.\n *\n * `cycles` and `rate` are the spatial and temporal frequency of the term being replaced, in\n * cycles rather than radians \u2014 each call site divides its own trig coefficients by 2*pi and passes\n * the result, so `noise: 1` lands on the same feature size and the same speed as the wave it took\n * over from. That is the entire mechanism keeping this from being a new look.\n *\n * `phase` decorrelates sibling calls: two components of one flow vector must not be the same\n * field, and offsetting the seed is cheaper than hashing a second one.\n *\n * `u_warp` costs two further fbm evaluations and is skipped entirely when it is zero.\n */\nfloat kuiFilterField(vec2 uv, float t, float cycles, float rate, float phase) {\n vec3 s = vec3(u_seed * 137.31, u_seed * 71.17, u_seed * 29.73) + phase;\n vec3 q = vec3((uv - 0.5) * cycles * max(u_scale, 0.05), t * rate) + s;\n if (u_warp > 0.0) {\n float wx = kuiFbm(q * 0.5 + vec3(5.2, 1.3, 7.1), u_detail);\n float wy = kuiFbm(q * 0.5 + vec3(19.7, 11.1, 3.7), u_detail);\n q.xy += vec2(wx, wy) * u_warp;\n }\n return kuiFbm(q, u_detail) * KUI_FBM_SINE_RMS;\n}\n\nvoid main() {\n vec2 uv = v_uv;\n vec2 m = u_mouse;\n float d = distance(uv, m);\n // Audio scales both halves of the flow \u2014 the ambient drift and the pointer's push \u2014 so a beat\n // reads as the whole surface moving faster, not just around the cursor.\n float gain = audioGain();\n float force = exp(-d * 6.0) * u_strength * gain;\n // The ambient drift. Two perpendicular waves at 10 rad across the box and 1 rad/s, which is\n // 10/2pi = 1.5915 cycles and 1/2pi = 0.1592 cycles a second \u2014 the numbers kuiFilterField needs\n // to put its field at the same size and speed. The drift is the whole of the ambient motion\n // here, so this is the mode where the noise core reads most strongly: a regular cross-hatched\n // sway becomes an irregular current.\n vec2 wave = vec2(sin(u_time + uv.y * 10.0), cos(u_time + uv.x * 10.0));\n if (u_noise > 0.0) {\n wave = mix(wave, vec2(\n kuiFilterField(uv, u_time, 1.5915, 0.1592, 0.0),\n kuiFilterField(uv, u_time, 1.5915, 0.1592, 31.4)\n ), u_noise);\n }\n vec2 flow = wave * 0.02 * gain;\n float pFactor = u_progress >= 0.0 ? u_progress : 1.0;\n vec2 offset = (flow + (uv - m) * force * 0.1) * pFactor;\n fragColor = texture(u_image, uv + offset) * u_tint * shapeMask();\n}\n";
7
+ export declare const LIQUID_FS = "#version 300 es\nprecision highp float;\nin vec2 v_uv;\nout vec4 fragColor;\nuniform sampler2D u_image;\nuniform float u_time;\nuniform float u_strength;\nuniform vec4 u_tint;\nuniform float u_progress;\nuniform float u_audio;\nfloat audioGain() { return 1.0 + clamp(u_audio, 0.0, 1.0); }\nuniform vec4 u_maskBox;\nuniform vec4 u_maskRx;\nuniform vec4 u_maskRy;\nuniform float u_maskAlpha;\nfloat shapeMask() {\n if (u_maskBox.z <= 0.0 || u_maskBox.w <= 0.0) return u_maskAlpha;\n vec2 p = gl_FragCoord.xy - u_maskBox.xy;\n vec2 h = u_maskBox.zw;\n bool left = p.x < 0.0;\n float rx = p.y >= 0.0 ? (left ? u_maskRx.x : u_maskRx.y) : (left ? u_maskRx.w : u_maskRx.z);\n float ry = p.y >= 0.0 ? (left ? u_maskRy.x : u_maskRy.y) : (left ? u_maskRy.w : u_maskRy.z);\n rx = min(rx, h.x);\n ry = min(ry, h.y);\n if (rx <= 0.0 || ry <= 0.0) return u_maskAlpha;\n vec2 q = abs(p) - (h - vec2(rx, ry));\n if (q.x <= 0.0 || q.y <= 0.0) return u_maskAlpha;\n float d = (length(q / vec2(rx, ry)) - 1.0) * min(rx, ry);\n return u_maskAlpha * clamp(0.5 - d, 0.0, 1.0);\n}\nconst int KUI_MAX_OCTAVES = 6;\n\n/**\n * Integer avalanche over one lattice coordinate.\n *\n * Deliberately not the `fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453)` idiom. That relies\n * on the low bits of a large sine, which `highp` does not reliably have: on several mobile GPUs\n * neighbouring cells stay correlated and the field bands visibly. GLSL ES 3.00 has real unsigned\n * integer arithmetic, so a proper mix is both better distributed and cheaper.\n */\nuint kuiHash(uvec3 v) {\n uint h = v.x * 0x8da6b343u ^ v.y * 0xd8163841u ^ v.z * 0xcb1ab31fu;\n h ^= h >> 15u; h *= 0x2c1b3c6du;\n h ^= h >> 12u; h *= 0x297a2d39u;\n h ^= h >> 15u;\n return h;\n}\n\n/**\n * A unit gradient vector for one lattice corner.\n *\n * The three components read *non-overlapping* 10-bit fields. Overlapping slices of one hash are\n * correlated with each other, which shows up as gradients clustering around a diagonal and the\n * noise developing a directional grain. The `1e-5` keeps `normalize` defined for the one input\n * that hashes to exactly zero.\n */\nvec3 kuiGradient(uvec3 cell) {\n uint h = kuiHash(cell);\n vec3 g = vec3(\n float((h ) & 1023u),\n float((h >> 10u ) & 1023u),\n float((h >> 20u ) & 1023u)\n ) * (2.0 / 1023.0) - 1.0;\n return normalize(g + vec3(1e-5));\n}\n\n/**\n * One octave of gradient noise, approximately -1..1.\n *\n * The lattice index is biased into unsigned space before hashing: seeds, scroll and a centred UV\n * field all reach negative coordinates, and `uvec3` of a negative `ivec3` wraps \u2014 which is\n * harmless for a hash, but only once every negative input maps somewhere distinct rather than\n * folding onto its positive twin.\n */\nfloat kuiNoise(vec3 p) {\n vec3 i = floor(p);\n vec3 f = p - i;\n vec3 w = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n uvec3 c = uvec3(ivec3(i) + 0x10000);\n float n000 = dot(kuiGradient(c + uvec3(0u, 0u, 0u)), f - vec3(0.0, 0.0, 0.0));\n float n100 = dot(kuiGradient(c + uvec3(1u, 0u, 0u)), f - vec3(1.0, 0.0, 0.0));\n float n010 = dot(kuiGradient(c + uvec3(0u, 1u, 0u)), f - vec3(0.0, 1.0, 0.0));\n float n110 = dot(kuiGradient(c + uvec3(1u, 1u, 0u)), f - vec3(1.0, 1.0, 0.0));\n float n001 = dot(kuiGradient(c + uvec3(0u, 0u, 1u)), f - vec3(0.0, 0.0, 1.0));\n float n101 = dot(kuiGradient(c + uvec3(1u, 0u, 1u)), f - vec3(1.0, 0.0, 1.0));\n float n011 = dot(kuiGradient(c + uvec3(0u, 1u, 1u)), f - vec3(0.0, 1.0, 1.0));\n float n111 = dot(kuiGradient(c + uvec3(1u, 1u, 1u)), f - vec3(1.0, 1.0, 1.0));\n float nx00 = mix(n000, n100, w.x);\n float nx10 = mix(n010, n110, w.x);\n float nx01 = mix(n001, n101, w.x);\n float nx11 = mix(n011, n111, w.x);\n // 2/sqrt(3) \u2014 a 3D gradient-noise octave peaks near sqrt(3)/2, so this brings it to about -1..1.\n return mix(mix(nx00, nx10, w.y), mix(nx01, nx11, w.y), w.z) * 1.1547;\n}\n\n/**\n * Octaves summed and **normalised by the amplitude sum**, approximately -1..1 at every octave count.\n *\n * The division is the whole point of the function. Without it, raising the octave count raises the\n * total amplitude, so the one authored knob that means \"how much fine structure\" would silently be\n * a contrast control as well \u2014 two behaviours behind one parameter, which is the class of bug this\n * repository keeps rediscovering. Gain and lacunarity stay fixed at 0.5 and 2.0: they are the two\n * numbers that make layered noise look wrong in a hundred ways and right in one.\n *\n * The loop bound is a constant with an early break because a loop bound that is itself a uniform\n * forces some drivers to unroll against the worst case anyway, and this way the cost of a low\n * `detail` is genuinely low.\n */\nfloat kuiFbm(vec3 p, int octaves) {\n float amp = 1.0;\n float freq = 1.0;\n float sum = 0.0;\n float norm = 0.0;\n for (int i = 0; i < KUI_MAX_OCTAVES; i++) {\n if (i >= octaves) break;\n sum += amp * kuiNoise(p * freq);\n norm += amp;\n amp *= 0.5;\n freq *= 2.0;\n }\n return sum / max(norm, 1e-5);\n}\nuniform float u_noise;\nuniform float u_seed;\nuniform float u_scale;\nuniform float u_warp;\nuniform int u_detail;\n\n/**\n * A sine's RMS is 0.707. Amplitude-normalised fbm's is about 0.23 \u2014 it reaches its `1.0` peak\n * rarely, which is what makes it look like noise rather than a wave. Handing the programs the raw\n * field would therefore read as the *effect being turned down*, not as it being re-textured, and\n * the first thing anyone trying `noise: 1` would do is reach for `strength`. This brings the two\n * to the same typical magnitude so the amount is a change of texture and nothing else.\n */\nconst float KUI_FBM_SINE_RMS = 3.0;\n\n/**\n * One scalar of animated noise, scaled to stand in for a unit sine at a stated rate.\n *\n * `cycles` and `rate` are the spatial and temporal frequency of the term being replaced, in\n * cycles rather than radians \u2014 each call site divides its own trig coefficients by 2*pi and passes\n * the result, so `noise: 1` lands on the same feature size and the same speed as the wave it took\n * over from. That is the entire mechanism keeping this from being a new look.\n *\n * `phase` decorrelates sibling calls: two components of one flow vector must not be the same\n * field, and offsetting the seed is cheaper than hashing a second one.\n *\n * `u_warp` costs two further fbm evaluations and is skipped entirely when it is zero.\n */\nfloat kuiFilterField(vec2 uv, float t, float cycles, float rate, float phase) {\n vec3 s = vec3(u_seed * 137.31, u_seed * 71.17, u_seed * 29.73) + phase;\n vec3 q = vec3((uv - 0.5) * cycles * max(u_scale, 0.05), t * rate) + s;\n if (u_warp > 0.0) {\n float wx = kuiFbm(q * 0.5 + vec3(5.2, 1.3, 7.1), u_detail);\n float wy = kuiFbm(q * 0.5 + vec3(19.7, 11.1, 3.7), u_detail);\n q.xy += vec2(wx, wy) * u_warp;\n }\n return kuiFbm(q, u_detail) * KUI_FBM_SINE_RMS;\n}\n\nvoid main() {\n vec2 uv = v_uv;\n // 12 rad and 2 rad/s is 1.9099 cycles at 0.3183 a second; 10 rad and 1.5 rad/s is 1.5915 at\n // 0.2387. The two waves keep their own rates under noise rather than being merged into one\n // field, because the difference between them is what stops the surface reading as a single\n // sheet sliding.\n float w1 = sin(uv.y * 12.0 + u_time * 2.0);\n float w2 = cos(uv.x * 10.0 - u_time * 1.5);\n if (u_noise > 0.0) {\n w1 = mix(w1, kuiFilterField(uv, u_time, 1.9099, 0.3183, 0.0), u_noise);\n w2 = mix(w2, kuiFilterField(uv, u_time, 1.5915, 0.2387, 47.3), u_noise);\n }\n w1 *= 0.015;\n w2 *= 0.015;\n float pFactor = u_progress >= 0.0 ? u_progress : 1.0;\n // Audio deepens the wave (up to 2x) without changing its wavelength or speed.\n vec2 offset = vec2(w1, w2) * u_strength * pFactor * audioGain();\n fragColor = texture(u_image, uv + offset) * u_tint * shapeMask();\n}\n";
8
+ export declare const PARTICLES_FS = "#version 300 es\nprecision highp float;\nin vec2 v_uv;\nout vec4 fragColor;\nuniform sampler2D u_image;\nuniform float u_time;\nuniform float u_strength;\nuniform vec4 u_tint;\nuniform float u_progress;\nuniform float u_audio;\nfloat audioGain() { return 1.0 + clamp(u_audio, 0.0, 1.0); }\nuniform vec4 u_maskBox;\nuniform vec4 u_maskRx;\nuniform vec4 u_maskRy;\nuniform float u_maskAlpha;\nfloat shapeMask() {\n if (u_maskBox.z <= 0.0 || u_maskBox.w <= 0.0) return u_maskAlpha;\n vec2 p = gl_FragCoord.xy - u_maskBox.xy;\n vec2 h = u_maskBox.zw;\n bool left = p.x < 0.0;\n float rx = p.y >= 0.0 ? (left ? u_maskRx.x : u_maskRx.y) : (left ? u_maskRx.w : u_maskRx.z);\n float ry = p.y >= 0.0 ? (left ? u_maskRy.x : u_maskRy.y) : (left ? u_maskRy.w : u_maskRy.z);\n rx = min(rx, h.x);\n ry = min(ry, h.y);\n if (rx <= 0.0 || ry <= 0.0) return u_maskAlpha;\n vec2 q = abs(p) - (h - vec2(rx, ry));\n if (q.x <= 0.0 || q.y <= 0.0) return u_maskAlpha;\n float d = (length(q / vec2(rx, ry)) - 1.0) * min(rx, ry);\n return u_maskAlpha * clamp(0.5 - d, 0.0, 1.0);\n}\nconst int KUI_MAX_OCTAVES = 6;\n\n/**\n * Integer avalanche over one lattice coordinate.\n *\n * Deliberately not the `fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453)` idiom. That relies\n * on the low bits of a large sine, which `highp` does not reliably have: on several mobile GPUs\n * neighbouring cells stay correlated and the field bands visibly. GLSL ES 3.00 has real unsigned\n * integer arithmetic, so a proper mix is both better distributed and cheaper.\n */\nuint kuiHash(uvec3 v) {\n uint h = v.x * 0x8da6b343u ^ v.y * 0xd8163841u ^ v.z * 0xcb1ab31fu;\n h ^= h >> 15u; h *= 0x2c1b3c6du;\n h ^= h >> 12u; h *= 0x297a2d39u;\n h ^= h >> 15u;\n return h;\n}\n\n/**\n * A unit gradient vector for one lattice corner.\n *\n * The three components read *non-overlapping* 10-bit fields. Overlapping slices of one hash are\n * correlated with each other, which shows up as gradients clustering around a diagonal and the\n * noise developing a directional grain. The `1e-5` keeps `normalize` defined for the one input\n * that hashes to exactly zero.\n */\nvec3 kuiGradient(uvec3 cell) {\n uint h = kuiHash(cell);\n vec3 g = vec3(\n float((h ) & 1023u),\n float((h >> 10u ) & 1023u),\n float((h >> 20u ) & 1023u)\n ) * (2.0 / 1023.0) - 1.0;\n return normalize(g + vec3(1e-5));\n}\n\n/**\n * One octave of gradient noise, approximately -1..1.\n *\n * The lattice index is biased into unsigned space before hashing: seeds, scroll and a centred UV\n * field all reach negative coordinates, and `uvec3` of a negative `ivec3` wraps \u2014 which is\n * harmless for a hash, but only once every negative input maps somewhere distinct rather than\n * folding onto its positive twin.\n */\nfloat kuiNoise(vec3 p) {\n vec3 i = floor(p);\n vec3 f = p - i;\n vec3 w = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n uvec3 c = uvec3(ivec3(i) + 0x10000);\n float n000 = dot(kuiGradient(c + uvec3(0u, 0u, 0u)), f - vec3(0.0, 0.0, 0.0));\n float n100 = dot(kuiGradient(c + uvec3(1u, 0u, 0u)), f - vec3(1.0, 0.0, 0.0));\n float n010 = dot(kuiGradient(c + uvec3(0u, 1u, 0u)), f - vec3(0.0, 1.0, 0.0));\n float n110 = dot(kuiGradient(c + uvec3(1u, 1u, 0u)), f - vec3(1.0, 1.0, 0.0));\n float n001 = dot(kuiGradient(c + uvec3(0u, 0u, 1u)), f - vec3(0.0, 0.0, 1.0));\n float n101 = dot(kuiGradient(c + uvec3(1u, 0u, 1u)), f - vec3(1.0, 0.0, 1.0));\n float n011 = dot(kuiGradient(c + uvec3(0u, 1u, 1u)), f - vec3(0.0, 1.0, 1.0));\n float n111 = dot(kuiGradient(c + uvec3(1u, 1u, 1u)), f - vec3(1.0, 1.0, 1.0));\n float nx00 = mix(n000, n100, w.x);\n float nx10 = mix(n010, n110, w.x);\n float nx01 = mix(n001, n101, w.x);\n float nx11 = mix(n011, n111, w.x);\n // 2/sqrt(3) \u2014 a 3D gradient-noise octave peaks near sqrt(3)/2, so this brings it to about -1..1.\n return mix(mix(nx00, nx10, w.y), mix(nx01, nx11, w.y), w.z) * 1.1547;\n}\n\n/**\n * Octaves summed and **normalised by the amplitude sum**, approximately -1..1 at every octave count.\n *\n * The division is the whole point of the function. Without it, raising the octave count raises the\n * total amplitude, so the one authored knob that means \"how much fine structure\" would silently be\n * a contrast control as well \u2014 two behaviours behind one parameter, which is the class of bug this\n * repository keeps rediscovering. Gain and lacunarity stay fixed at 0.5 and 2.0: they are the two\n * numbers that make layered noise look wrong in a hundred ways and right in one.\n *\n * The loop bound is a constant with an early break because a loop bound that is itself a uniform\n * forces some drivers to unroll against the worst case anyway, and this way the cost of a low\n * `detail` is genuinely low.\n */\nfloat kuiFbm(vec3 p, int octaves) {\n float amp = 1.0;\n float freq = 1.0;\n float sum = 0.0;\n float norm = 0.0;\n for (int i = 0; i < KUI_MAX_OCTAVES; i++) {\n if (i >= octaves) break;\n sum += amp * kuiNoise(p * freq);\n norm += amp;\n amp *= 0.5;\n freq *= 2.0;\n }\n return sum / max(norm, 1e-5);\n}\nuniform float u_noise;\nuniform float u_seed;\nuniform float u_scale;\nuniform float u_warp;\nuniform int u_detail;\n\n/**\n * A sine's RMS is 0.707. Amplitude-normalised fbm's is about 0.23 \u2014 it reaches its `1.0` peak\n * rarely, which is what makes it look like noise rather than a wave. Handing the programs the raw\n * field would therefore read as the *effect being turned down*, not as it being re-textured, and\n * the first thing anyone trying `noise: 1` would do is reach for `strength`. This brings the two\n * to the same typical magnitude so the amount is a change of texture and nothing else.\n */\nconst float KUI_FBM_SINE_RMS = 3.0;\n\n/**\n * One scalar of animated noise, scaled to stand in for a unit sine at a stated rate.\n *\n * `cycles` and `rate` are the spatial and temporal frequency of the term being replaced, in\n * cycles rather than radians \u2014 each call site divides its own trig coefficients by 2*pi and passes\n * the result, so `noise: 1` lands on the same feature size and the same speed as the wave it took\n * over from. That is the entire mechanism keeping this from being a new look.\n *\n * `phase` decorrelates sibling calls: two components of one flow vector must not be the same\n * field, and offsetting the seed is cheaper than hashing a second one.\n *\n * `u_warp` costs two further fbm evaluations and is skipped entirely when it is zero.\n */\nfloat kuiFilterField(vec2 uv, float t, float cycles, float rate, float phase) {\n vec3 s = vec3(u_seed * 137.31, u_seed * 71.17, u_seed * 29.73) + phase;\n vec3 q = vec3((uv - 0.5) * cycles * max(u_scale, 0.05), t * rate) + s;\n if (u_warp > 0.0) {\n float wx = kuiFbm(q * 0.5 + vec3(5.2, 1.3, 7.1), u_detail);\n float wy = kuiFbm(q * 0.5 + vec3(19.7, 11.1, 3.7), u_detail);\n q.xy += vec2(wx, wy) * u_warp;\n }\n return kuiFbm(q, u_detail) * KUI_FBM_SINE_RMS;\n}\n\n// No u_mouse here, deliberately. This program declared one from the day it was written and never\n// read it, so particles has never responded to the pointer \u2014 the declaration was a promise the\n// body did not keep, and the compiler dropped it anyway (an unused uniform gets no location, so\n// uploadCoordUniforms was already skipping it and removing it changes no pixel). It is deleted\n// rather than wired because wiring it would restyle a mode that is already in use without anyone\n// asking, which is the same argument FILTER_NOISE_GLSL's doc makes for noise: being opt-in. The\n// pointer gesture that *was* asked for lives on the generative program below, under hover:.\nvoid main() {\n vec2 uv = v_uv;\n vec2 grid = fract(uv * 40.0) - 0.5;\n float dist = length(grid);\n // 100 rad across the box and 5 rad/s is 15.915 cycles at 0.7958 a second. That is by far the\n // highest frequency of the four, and deliberately so: the trig term here is a diagonal travelling\n // stripe, which is the most visible of the four repeats. Under noise the stripe becomes a\n // scintillation, which is the largest change of the set and is the reason the parameter exists\n // rather than the swap being unconditional.\n //\n // The clamp is not damage control. Mapped through KUI_FBM_SINE_RMS the field reaches 0 and 1\n // often, which is what a sine does too \u2014 sin()*0.5+0.5 is arcsine-distributed and spends most of\n // its time at the extremes. Clamping reproduces that; a soft field would give a duller sparkle\n // than the one being replaced.\n //\n // Cost: this is one fbm per pixel, three with warp on, over the whole quad. At detail 6 the\n // finest octave here sits near 509 cycles across the box, which is sub-pixel on a small element\n // and will alias \u2014 high detail belongs on the low-frequency modes.\n float sparkle = sin(u_time * 5.0 + dot(uv, vec2(100.0))) * 0.5 + 0.5;\n if (u_noise > 0.0) {\n float n = clamp(kuiFilterField(uv, u_time, 15.915, 0.7958, 0.0) * 0.5 + 0.5, 0.0, 1.0);\n sparkle = mix(sparkle, n, u_noise);\n }\n vec4 tex = texture(u_image, uv);\n float pFactor = u_progress >= 0.0 ? u_progress : 1.0;\n float dotMask = smoothstep(0.4, 0.2, dist) * u_strength * pFactor;\n // Audio brightens the sparkle (0.3 -> at most 0.6) rather than growing the dots: the mask is\n // already multiplied by an unbounded u_strength and feeds a mix(), so widening it would\n // extrapolate past the texture over a larger and larger area.\n fragColor = mix(tex, tex * u_tint + sparkle * 0.3 * audioGain(), dotMask) * shapeMask();\n}\n";
9
+ export declare const MORPH_FS = "#version 300 es\nprecision highp float;\nin vec2 v_uv;\nout vec4 fragColor;\nuniform sampler2D u_image;\nuniform sampler2D u_image_to;\nuniform float u_time;\nuniform float u_strength;\nuniform float u_progress;\nuniform float u_audio;\nfloat audioGain() { return 1.0 + clamp(u_audio, 0.0, 1.0); }\nuniform vec4 u_maskBox;\nuniform vec4 u_maskRx;\nuniform vec4 u_maskRy;\nuniform float u_maskAlpha;\nfloat shapeMask() {\n if (u_maskBox.z <= 0.0 || u_maskBox.w <= 0.0) return u_maskAlpha;\n vec2 p = gl_FragCoord.xy - u_maskBox.xy;\n vec2 h = u_maskBox.zw;\n bool left = p.x < 0.0;\n float rx = p.y >= 0.0 ? (left ? u_maskRx.x : u_maskRx.y) : (left ? u_maskRx.w : u_maskRx.z);\n float ry = p.y >= 0.0 ? (left ? u_maskRy.x : u_maskRy.y) : (left ? u_maskRy.w : u_maskRy.z);\n rx = min(rx, h.x);\n ry = min(ry, h.y);\n if (rx <= 0.0 || ry <= 0.0) return u_maskAlpha;\n vec2 q = abs(p) - (h - vec2(rx, ry));\n if (q.x <= 0.0 || q.y <= 0.0) return u_maskAlpha;\n float d = (length(q / vec2(rx, ry)) - 1.0) * min(rx, ry);\n return u_maskAlpha * clamp(0.5 - d, 0.0, 1.0);\n}\nconst int KUI_MAX_OCTAVES = 6;\n\n/**\n * Integer avalanche over one lattice coordinate.\n *\n * Deliberately not the `fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453)` idiom. That relies\n * on the low bits of a large sine, which `highp` does not reliably have: on several mobile GPUs\n * neighbouring cells stay correlated and the field bands visibly. GLSL ES 3.00 has real unsigned\n * integer arithmetic, so a proper mix is both better distributed and cheaper.\n */\nuint kuiHash(uvec3 v) {\n uint h = v.x * 0x8da6b343u ^ v.y * 0xd8163841u ^ v.z * 0xcb1ab31fu;\n h ^= h >> 15u; h *= 0x2c1b3c6du;\n h ^= h >> 12u; h *= 0x297a2d39u;\n h ^= h >> 15u;\n return h;\n}\n\n/**\n * A unit gradient vector for one lattice corner.\n *\n * The three components read *non-overlapping* 10-bit fields. Overlapping slices of one hash are\n * correlated with each other, which shows up as gradients clustering around a diagonal and the\n * noise developing a directional grain. The `1e-5` keeps `normalize` defined for the one input\n * that hashes to exactly zero.\n */\nvec3 kuiGradient(uvec3 cell) {\n uint h = kuiHash(cell);\n vec3 g = vec3(\n float((h ) & 1023u),\n float((h >> 10u ) & 1023u),\n float((h >> 20u ) & 1023u)\n ) * (2.0 / 1023.0) - 1.0;\n return normalize(g + vec3(1e-5));\n}\n\n/**\n * One octave of gradient noise, approximately -1..1.\n *\n * The lattice index is biased into unsigned space before hashing: seeds, scroll and a centred UV\n * field all reach negative coordinates, and `uvec3` of a negative `ivec3` wraps \u2014 which is\n * harmless for a hash, but only once every negative input maps somewhere distinct rather than\n * folding onto its positive twin.\n */\nfloat kuiNoise(vec3 p) {\n vec3 i = floor(p);\n vec3 f = p - i;\n vec3 w = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n uvec3 c = uvec3(ivec3(i) + 0x10000);\n float n000 = dot(kuiGradient(c + uvec3(0u, 0u, 0u)), f - vec3(0.0, 0.0, 0.0));\n float n100 = dot(kuiGradient(c + uvec3(1u, 0u, 0u)), f - vec3(1.0, 0.0, 0.0));\n float n010 = dot(kuiGradient(c + uvec3(0u, 1u, 0u)), f - vec3(0.0, 1.0, 0.0));\n float n110 = dot(kuiGradient(c + uvec3(1u, 1u, 0u)), f - vec3(1.0, 1.0, 0.0));\n float n001 = dot(kuiGradient(c + uvec3(0u, 0u, 1u)), f - vec3(0.0, 0.0, 1.0));\n float n101 = dot(kuiGradient(c + uvec3(1u, 0u, 1u)), f - vec3(1.0, 0.0, 1.0));\n float n011 = dot(kuiGradient(c + uvec3(0u, 1u, 1u)), f - vec3(0.0, 1.0, 1.0));\n float n111 = dot(kuiGradient(c + uvec3(1u, 1u, 1u)), f - vec3(1.0, 1.0, 1.0));\n float nx00 = mix(n000, n100, w.x);\n float nx10 = mix(n010, n110, w.x);\n float nx01 = mix(n001, n101, w.x);\n float nx11 = mix(n011, n111, w.x);\n // 2/sqrt(3) \u2014 a 3D gradient-noise octave peaks near sqrt(3)/2, so this brings it to about -1..1.\n return mix(mix(nx00, nx10, w.y), mix(nx01, nx11, w.y), w.z) * 1.1547;\n}\n\n/**\n * Octaves summed and **normalised by the amplitude sum**, approximately -1..1 at every octave count.\n *\n * The division is the whole point of the function. Without it, raising the octave count raises the\n * total amplitude, so the one authored knob that means \"how much fine structure\" would silently be\n * a contrast control as well \u2014 two behaviours behind one parameter, which is the class of bug this\n * repository keeps rediscovering. Gain and lacunarity stay fixed at 0.5 and 2.0: they are the two\n * numbers that make layered noise look wrong in a hundred ways and right in one.\n *\n * The loop bound is a constant with an early break because a loop bound that is itself a uniform\n * forces some drivers to unroll against the worst case anyway, and this way the cost of a low\n * `detail` is genuinely low.\n */\nfloat kuiFbm(vec3 p, int octaves) {\n float amp = 1.0;\n float freq = 1.0;\n float sum = 0.0;\n float norm = 0.0;\n for (int i = 0; i < KUI_MAX_OCTAVES; i++) {\n if (i >= octaves) break;\n sum += amp * kuiNoise(p * freq);\n norm += amp;\n amp *= 0.5;\n freq *= 2.0;\n }\n return sum / max(norm, 1e-5);\n}\nuniform float u_noise;\nuniform float u_seed;\nuniform float u_scale;\nuniform float u_warp;\nuniform int u_detail;\n\n/**\n * A sine's RMS is 0.707. Amplitude-normalised fbm's is about 0.23 \u2014 it reaches its `1.0` peak\n * rarely, which is what makes it look like noise rather than a wave. Handing the programs the raw\n * field would therefore read as the *effect being turned down*, not as it being re-textured, and\n * the first thing anyone trying `noise: 1` would do is reach for `strength`. This brings the two\n * to the same typical magnitude so the amount is a change of texture and nothing else.\n */\nconst float KUI_FBM_SINE_RMS = 3.0;\n\n/**\n * One scalar of animated noise, scaled to stand in for a unit sine at a stated rate.\n *\n * `cycles` and `rate` are the spatial and temporal frequency of the term being replaced, in\n * cycles rather than radians \u2014 each call site divides its own trig coefficients by 2*pi and passes\n * the result, so `noise: 1` lands on the same feature size and the same speed as the wave it took\n * over from. That is the entire mechanism keeping this from being a new look.\n *\n * `phase` decorrelates sibling calls: two components of one flow vector must not be the same\n * field, and offsetting the seed is cheaper than hashing a second one.\n *\n * `u_warp` costs two further fbm evaluations and is skipped entirely when it is zero.\n */\nfloat kuiFilterField(vec2 uv, float t, float cycles, float rate, float phase) {\n vec3 s = vec3(u_seed * 137.31, u_seed * 71.17, u_seed * 29.73) + phase;\n vec3 q = vec3((uv - 0.5) * cycles * max(u_scale, 0.05), t * rate) + s;\n if (u_warp > 0.0) {\n float wx = kuiFbm(q * 0.5 + vec3(5.2, 1.3, 7.1), u_detail);\n float wy = kuiFbm(q * 0.5 + vec3(19.7, 11.1, 3.7), u_detail);\n q.xy += vec2(wx, wy) * u_warp;\n }\n return kuiFbm(q, u_detail) * KUI_FBM_SINE_RMS;\n}\n\nvoid main() {\n vec2 uv = v_uv;\n float progress = u_progress >= 0.0 ? clamp(u_progress, 0.0, 1.0) : clamp(u_strength, 0.0, 1.0);\n // Audio wobbles the crossfade harder. Deliberately not applied to u_progress: the morph's\n // position between the two images belongs to the author (or to scroll), and letting a beat\n // drive it would make the transition jump backwards on every quiet frame.\n //\n // 20 rad and 1 rad/s is 3.1831 cycles at 0.1592 a second. The 0.7071 is the one place the shared\n // RMS gain is wrong by construction: the term being replaced is a *product* of two unit sines,\n // whose RMS is 0.5 rather than a single sine's 0.7071, so matching it means taking the field\n // down by that ratio. Without it the displacement under noise would be about 40% deeper than\n // the one it stands in for \u2014 a visibly stronger morph, not the same one better textured.\n float noise = sin(uv.x * 20.0 + u_time) * cos(uv.y * 20.0 + u_time);\n if (u_noise > 0.0) {\n noise = mix(noise, kuiFilterField(uv, u_time, 3.1831, 0.1592, 0.0) * 0.7071, u_noise);\n }\n noise = noise * 0.05 * audioGain();\n vec4 c1 = texture(u_image, uv + vec2(noise * (1.0 - progress)));\n vec4 c2 = texture(u_image_to, uv - vec2(noise * progress));\n fragColor = mix(c1, c2, smoothstep(0.2, 0.8, progress)) * shapeMask();\n}\n";
10
+ /**
11
+ * The generative program: colour from mathematics rather than from a source image.
12
+ *
13
+ * **Two modes, one program.** `u_maskMode` is the whole difference between them:
14
+ *
15
+ * - `mode: gradient` (`u_maskMode 0`) — no texture is bound and the field fills the element's box.
16
+ * Every program above samples `u_image`, which is why every one of them is a *filter* and why
17
+ * the tier could not produce a background of its own until this one. `shaders.ts` skips the
18
+ * texture requirement for it (see `GENERATIVE_MODES`) and does not hide the host, because hiding
19
+ * an element the author may have put content in is destructive rather than merely useless.
20
+ * - `mode: logo` (`u_maskMode 1` or `2`) — the host `<img>`'s own pixels are the *stencil*, and the
21
+ * field is painted only where the mark is. This is the ordinary filter path with nothing changed:
22
+ * the texture is required, the host is hidden, `object-fit` resolves through `v_uv` exactly as it
23
+ * does for `displace`. What arrives is not a recolour of the image but a generated field wearing
24
+ * its shape.
25
+ *
26
+ * The sampler is declared unconditionally and read only inside the `u_maskMode > 0` branch. In
27
+ * `gradient` nothing is bound to the unit, which WebGL defines as sampling an incomplete texture —
28
+ * `(0, 0, 0, 1)`, no error — and the branch means even that is never reached.
29
+ *
30
+ * Splitting these into two programs would mean a second copy of the noise core, and three
31
+ * near-identical fbm implementations is the state this directory was already dug out of once.
32
+ *
33
+ * **`gradient`'s honest surface is still an element nothing overlaps**, though not for the reason
34
+ * it first shipped with. The shared canvas is one `position: fixed` layer at `z-index: 1` by
35
+ * default (movable page-wide with `--kui-shader-z`, which is a coping knob and not composition).
36
+ * A *positioned* headline laid over a field now paints on top of it rather than vanishing behind
37
+ * it; a static one still does not, because the canvas is `fixed` and it is not. So a full-bleed
38
+ * band, a card face or a footer strip work, and anything needing real interleaving does not.
39
+ * `logo` is the exception that needs nothing lifted: a mark *should* paint over the page, which is
40
+ * exactly what the five filters already do.
41
+ *
42
+ * The parameter split worth understanding: `scale` zooms the field, `frequency` is the rate of the
43
+ * *warping* field that distorts it, and `detail` is how many octaves ride on top. Three knobs that
44
+ * sound similar and are not — one changes how big the shapes are, one how contorted, one how
45
+ * intricate.
46
+ *
47
+ * **The pointer.** This is the one program in the tier built to be a background rather than a
48
+ * filter, and until `hover:` it was also the only one with no pointer response at all — which is
49
+ * awkward, because responding to the pointer is the single thing a shader background can do that a
50
+ * video cannot. `hover: stir` drags the field's sample point around the cursor; `hover: light`
51
+ * raises the colour under it; `both` runs the two together, which they can do without fighting
52
+ * because one acts on the coordinate and the other on the colour that coordinate produced. All of
53
+ * it is off at `hover: none`, the default, where `u_mouse` is never read and the pointer cannot
54
+ * reach a pixel.
55
+ *
56
+ * **Motion.** `motion: evolve` — the default — is the behaviour described above: time as the third
57
+ * noise axis, the pattern changing in place. `drift` and `swirl` add a rigid motion of the domain
58
+ * on top of it, along and about the axis `angle` names.
59
+ */
60
+ export declare const GRADIENT_FS = "#version 300 es\nprecision highp float;\nin vec2 v_uv;\nout vec4 fragColor;\nuniform float u_time;\nuniform float u_strength;\nuniform float u_frequency;\nuniform float u_chromatic;\nuniform float u_iridescence;\nuniform float u_progress;\nuniform float u_seed;\nuniform float u_scale;\nuniform float u_warp;\nuniform float u_grain;\nuniform float u_hue;\nuniform int u_detail;\nuniform int u_bands;\nuniform int u_colorCount;\nuniform vec4 u_colors[5];\nuniform vec4 u_tint;\nuniform sampler2D u_image;\nuniform int u_maskMode;\nuniform vec2 u_mouse;\nuniform int u_hover;\nuniform vec4 u_backdrop;\nuniform float u_angle;\nuniform int u_motion;\nuniform float u_audio;\nfloat audioGain() { return 1.0 + clamp(u_audio, 0.0, 1.0); }\nuniform vec4 u_maskBox;\nuniform vec4 u_maskRx;\nuniform vec4 u_maskRy;\nuniform float u_maskAlpha;\nfloat shapeMask() {\n if (u_maskBox.z <= 0.0 || u_maskBox.w <= 0.0) return u_maskAlpha;\n vec2 p = gl_FragCoord.xy - u_maskBox.xy;\n vec2 h = u_maskBox.zw;\n bool left = p.x < 0.0;\n float rx = p.y >= 0.0 ? (left ? u_maskRx.x : u_maskRx.y) : (left ? u_maskRx.w : u_maskRx.z);\n float ry = p.y >= 0.0 ? (left ? u_maskRy.x : u_maskRy.y) : (left ? u_maskRy.w : u_maskRy.z);\n rx = min(rx, h.x);\n ry = min(ry, h.y);\n if (rx <= 0.0 || ry <= 0.0) return u_maskAlpha;\n vec2 q = abs(p) - (h - vec2(rx, ry));\n if (q.x <= 0.0 || q.y <= 0.0) return u_maskAlpha;\n float d = (length(q / vec2(rx, ry)) - 1.0) * min(rx, ry);\n return u_maskAlpha * clamp(0.5 - d, 0.0, 1.0);\n}\nconst int KUI_MAX_OCTAVES = 6;\n\n/**\n * Integer avalanche over one lattice coordinate.\n *\n * Deliberately not the `fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453)` idiom. That relies\n * on the low bits of a large sine, which `highp` does not reliably have: on several mobile GPUs\n * neighbouring cells stay correlated and the field bands visibly. GLSL ES 3.00 has real unsigned\n * integer arithmetic, so a proper mix is both better distributed and cheaper.\n */\nuint kuiHash(uvec3 v) {\n uint h = v.x * 0x8da6b343u ^ v.y * 0xd8163841u ^ v.z * 0xcb1ab31fu;\n h ^= h >> 15u; h *= 0x2c1b3c6du;\n h ^= h >> 12u; h *= 0x297a2d39u;\n h ^= h >> 15u;\n return h;\n}\n\n/**\n * A unit gradient vector for one lattice corner.\n *\n * The three components read *non-overlapping* 10-bit fields. Overlapping slices of one hash are\n * correlated with each other, which shows up as gradients clustering around a diagonal and the\n * noise developing a directional grain. The `1e-5` keeps `normalize` defined for the one input\n * that hashes to exactly zero.\n */\nvec3 kuiGradient(uvec3 cell) {\n uint h = kuiHash(cell);\n vec3 g = vec3(\n float((h ) & 1023u),\n float((h >> 10u ) & 1023u),\n float((h >> 20u ) & 1023u)\n ) * (2.0 / 1023.0) - 1.0;\n return normalize(g + vec3(1e-5));\n}\n\n/**\n * One octave of gradient noise, approximately -1..1.\n *\n * The lattice index is biased into unsigned space before hashing: seeds, scroll and a centred UV\n * field all reach negative coordinates, and `uvec3` of a negative `ivec3` wraps \u2014 which is\n * harmless for a hash, but only once every negative input maps somewhere distinct rather than\n * folding onto its positive twin.\n */\nfloat kuiNoise(vec3 p) {\n vec3 i = floor(p);\n vec3 f = p - i;\n vec3 w = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n uvec3 c = uvec3(ivec3(i) + 0x10000);\n float n000 = dot(kuiGradient(c + uvec3(0u, 0u, 0u)), f - vec3(0.0, 0.0, 0.0));\n float n100 = dot(kuiGradient(c + uvec3(1u, 0u, 0u)), f - vec3(1.0, 0.0, 0.0));\n float n010 = dot(kuiGradient(c + uvec3(0u, 1u, 0u)), f - vec3(0.0, 1.0, 0.0));\n float n110 = dot(kuiGradient(c + uvec3(1u, 1u, 0u)), f - vec3(1.0, 1.0, 0.0));\n float n001 = dot(kuiGradient(c + uvec3(0u, 0u, 1u)), f - vec3(0.0, 0.0, 1.0));\n float n101 = dot(kuiGradient(c + uvec3(1u, 0u, 1u)), f - vec3(1.0, 0.0, 1.0));\n float n011 = dot(kuiGradient(c + uvec3(0u, 1u, 1u)), f - vec3(0.0, 1.0, 1.0));\n float n111 = dot(kuiGradient(c + uvec3(1u, 1u, 1u)), f - vec3(1.0, 1.0, 1.0));\n float nx00 = mix(n000, n100, w.x);\n float nx10 = mix(n010, n110, w.x);\n float nx01 = mix(n001, n101, w.x);\n float nx11 = mix(n011, n111, w.x);\n // 2/sqrt(3) \u2014 a 3D gradient-noise octave peaks near sqrt(3)/2, so this brings it to about -1..1.\n return mix(mix(nx00, nx10, w.y), mix(nx01, nx11, w.y), w.z) * 1.1547;\n}\n\n/**\n * Octaves summed and **normalised by the amplitude sum**, approximately -1..1 at every octave count.\n *\n * The division is the whole point of the function. Without it, raising the octave count raises the\n * total amplitude, so the one authored knob that means \"how much fine structure\" would silently be\n * a contrast control as well \u2014 two behaviours behind one parameter, which is the class of bug this\n * repository keeps rediscovering. Gain and lacunarity stay fixed at 0.5 and 2.0: they are the two\n * numbers that make layered noise look wrong in a hundred ways and right in one.\n *\n * The loop bound is a constant with an early break because a loop bound that is itself a uniform\n * forces some drivers to unroll against the worst case anyway, and this way the cost of a low\n * `detail` is genuinely low.\n */\nfloat kuiFbm(vec3 p, int octaves) {\n float amp = 1.0;\n float freq = 1.0;\n float sum = 0.0;\n float norm = 0.0;\n for (int i = 0; i < KUI_MAX_OCTAVES; i++) {\n if (i >= octaves) break;\n sum += amp * kuiNoise(p * freq);\n norm += amp;\n amp *= 0.5;\n freq *= 2.0;\n }\n return sum / max(norm, 1e-5);\n}\n\n\n/** `hover:` as bits, so `both` is `stir | light` rather than a third code path. */\nconst int KUI_HOVER_STIR = 1;\nconst int KUI_HOVER_LIGHT = 2;\n\n/** `motion:` \u2014 `0` is `evolve`, which is this program's original behaviour and its default. */\nconst int KUI_MOTION_DRIFT = 1;\nconst int KUI_MOTION_SWIRL = 2;\n\n/**\n * Rotate a point of the field's domain.\n *\n * `v_uv` is y-down, so this matrix \u2014 the ordinary counter-clockwise one \u2014 turns the *picture*\n * clockwise, which is `transform: rotate()`'s direction and therefore the one an author already\n * has an intuition for. Every call site passes `-angle`: rotating the sample point one way is\n * what rotates what you see the other.\n */\nvec2 kuiRotate(vec2 v, float a) {\n float c = cos(a);\n float s = sin(a);\n return vec2(v.x * c - v.y * s, v.x * s + v.y * c);\n}\n\n/**\n * The pair of factors that carry a centred UV offset into a space that is isotropic **on screen**,\n * and back out again by dividing.\n *\n * `v_uv` is normalised per axis, so one unit of `v_uv.x` is the box's whole width and one unit of\n * `v_uv.y` is its whole height \u2014 two different numbers of screen pixels as soon as the box is not\n * square. Anything round or rigid has to leave that space first. Scaling by `(k, 1/k)` for\n * `k = sqrt(halfWidth / halfHeight)` puts both axes in units of `sqrt(width * height)`: a circle\n * stays a circle, a rotation stays a rotation, and the unit tracks the box's overall size rather\n * than one of its two dimensions. On a square box the pair is exactly `(1, 1)` and every user of\n * it degenerates to the plain UV arithmetic it had before.\n *\n * It is a *ratio* of the two half-extents, so it is DPR-invariant \u2014 device pixel ratio scales\n * `u_maskBox.z` and `u_maskBox.w` together and cancels.\n *\n * A zero half-extent is `shapeMask()`'s \"no box measured\" signal and is read the same way here:\n * fall back to `(1, 1)` rather than divide by it.\n */\nvec2 kuiAspectAxes() {\n if (u_maskBox.z > 0.0 && u_maskBox.w > 0.0) {\n float k = sqrt(u_maskBox.z / u_maskBox.w);\n return vec2(k, 1.0 / k);\n }\n return vec2(1.0);\n}\n\n/**\n * Turn a centred UV offset by `a`, rigidly **on screen** rather than in the field's own stretched\n * space.\n *\n * `A^-1 \u00B7 R(a) \u00B7 A`, with `A = diag(kuiAspectAxes())`: leave the per-axis-normalised space, turn\n * there, come back. Rotating without the conjugation turns *and shears* \u2014 on a 160x100 box an\n * authored `angle: 45deg` actually travels at 32 degrees, an error that grows with how far from\n * square the box is. `angle` is documented as `transform: rotate()`'s convention, and this is what\n * makes that true of a wide box and not only of a square one.\n *\n * **Not bit-identical at `a == 0`**, and that matters: `A^-1 \u00B7 I \u00B7 A` is `diag(k * (1/k), (1/k) * k)`,\n * which in IEEE 754 is about an ulp away from the identity per axis rather than exactly it. Every\n * caller must therefore keep its existing exact-zero guard \u2014 the defaults are unchanged because\n * nothing here executes at them, not because this collapses.\n *\n * `scale` is a scalar and commutes with all three matrices, so a zoomed offset can be passed\n * straight in: `A^-1 R A (zoom * d)` is `zoom * (A^-1 R A d)`.\n */\nvec2 kuiRotateScreen(vec2 v, float a) {\n vec2 s = kuiAspectAxes();\n return kuiRotate(v * s, a) / s;\n}\n\n/**\n * How strongly the pointer reaches this fragment: 1 under it, falling to nothing at the edge of a\n * **round** pool.\n *\n * Round is the whole difficulty. `v_uv` and `u_mouse` are both normalised per axis, so the\n * obvious `distance(v_uv, u_mouse)` draws an *ellipse* as soon as the box is not square \u2014 and the\n * boxes this mode is documented for are full-bleed bands, card faces and footer strips, i.e.\n * precisely the wide ones. `kuiAspectAxes()` is the correction, and measuring the offset there\n * makes the pool a circle on screen whose size tracks the box's overall scale rather than one of\n * its two dimensions. On a square box the pair is exactly `(1, 1)` and this degenerates to the\n * plain UV distance `displace` already uses.\n *\n * Gaussian rather than `smoothstep`, because a pool of light has no edge to see.\n */\nfloat kuiHoverFalloff() {\n vec2 d = (v_uv - u_mouse) * kuiAspectAxes();\n return exp(-dot(d, d) * 6.0);\n}\n\n/**\n * One point of the element's UV square, in the field's own frame.\n *\n * A rigid motion \u2014 one rotation and one translation \u2014 so that carrying the pointer through the\n * same call puts it at the same place relative to the field that it occupies on screen. That is\n * what lets `stir` swirl the field about a centre the cursor is actually sitting on, at any\n * `angle`, mid-`drift`, mid-`swirl`.\n *\n * The turn is `kuiRotateScreen`, so it is rigid **on screen** on a box of any shape: `angle: 45deg`\n * points at 45 degrees on a 160x100 band, and `motion: drift` travels along exactly the axis the\n * author named instead of a sheared one.\n *\n * Inert at the defaults by construction: `orient` of zero skips the rotation entirely and\n * `travel` of zero subtracts an exact zero, so `motion: evolve` at `angle: 0deg` computes the\n * same `(v_uv - 0.5) * scale` this program has always computed, bit for bit. **The guard is the\n * whole of that promise** \u2014 `kuiRotateScreen` does not collapse to the identity at zero, it only\n * gets within an ulp, so deleting the guard because \"a rotation is cheap\" would silently move the\n * last bit of every generative pixel already on the web.\n */\nvec2 kuiFieldPoint(vec2 uv, float zoom, float orient, float travel) {\n vec2 v = (uv - 0.5) * zoom;\n if (orient != 0.0) v = kuiRotateScreen(v, -orient);\n v.x -= travel;\n return v;\n}\n\n/**\n * `hover: stir` \u2014 one vortex of the sample point about the pointer.\n *\n * A turn that dies away from the cursor, plus a push of the *sample point* outward, which is the\n * field spiralling inward and is the whole difference between a rotating disc and a stir.\n * `rel / (length(rel) + eps)` rather than `normalize`, so the push fades to nothing at the centre\n * instead of kicking the one pixel under the cursor in whatever direction the epsilon points.\n *\n * Both halves run inside `kuiAspectAxes()`, for the same reason `kuiHoverFalloff` does: the\n * falloff that drives this is already a circle on screen, so a vortex measured in raw UV would be\n * a round pool turning an elliptical gesture \u2014 two different ideas of \"the same distance from the\n * cursor\" in one effect.\n */\nvec2 kuiStirPoint(vec2 pt, vec2 centre, float zoom, float fall) {\n vec2 s = kuiAspectAxes();\n vec2 rel = (pt - centre) * s;\n vec2 turned = kuiRotate(rel, 1.6 * fall) + rel / (length(rel) + 1e-4) * (0.10 * zoom * fall);\n return centre + turned / s;\n}\n\n/**\n * The host image read as a stencil: 1 where the mark is, 0 where the page should show through.\n *\n * Three ways to ask, because a logo file answers the question differently depending on what it is:\n *\n * - `0` \u2014 no stencil. `mode: gradient`; `u_image` is not sampled at all.\n * - `1` (`mask: alpha`) \u2014 the image's own alpha. An SVG or PNG mark on a transparent ground,\n * which is what a logo asset usually is. The texture is uploaded premultiplied, so `a` is the\n * coverage and multiplying all four channels by it stays premultiplied-correct.\n * - `2`/`3` (`mask: luma` / `luma-invert`) \u2014 luminance, for a format with no alpha to read. A\n * JPEG has none, so a black-on-white mark under `alpha` would give an all-opaque stencil and\n * fill the whole rectangle with field. `luma` takes the *bright* pixels as the mark (white on\n * black); `luma-invert` takes the dark ones (black on white, which is the common logo file).\n * Both directions exist because guessing one silently produces a perfect negative of the mark \u2014\n * an image that looks deliberate and is exactly wrong.\n *\n * Luminance is taken from the premultiplied colour, so a transparent pixel is black. Under\n * `luma` that reads as \"not the mark\", which is the sensible answer for a file that has alpha and\n * was asked for a luma stencil anyway. Under `luma-invert` the same pixel reads as *solid* mark \u2014\n * correct for the opaque JPEG the keyword exists for, and wrong for anything with a transparent\n * surround, which should be using `alpha`.\n */\nfloat glyphMask() {\n if (u_maskMode == 0) return 1.0;\n vec4 src = texture(u_image, v_uv);\n if (u_maskMode == 1) return src.a;\n float lum = dot(src.rgb, vec3(0.299, 0.587, 0.114));\n return u_maskMode == 2 ? lum : 1.0 - lum;\n}\n\n/**\n * The palette ramp, interpolated in linear-light rather than straight sRGB.\n *\n * `mix()` between two sRGB triples passes through a desaturated middle \u2014 the classic muddy\n * midpoint of a naive gradient, worst exactly where two saturated brand colours meet. Squaring\n * into approximately linear light, mixing there, and taking the square root back is a two-operation\n * approximation of a gamma-2.0 space that removes almost all of it.\n *\n * `smoothstep` on the segment fraction, not a straight lerp, so the ramp has no visible crease at\n * each stop \u2014 a crease that banding (`u_bands`) would otherwise land on and amplify.\n */\nvec3 kuiPalette(float t) {\n int n = max(u_colorCount, 2);\n float x = clamp(t, 0.0, 1.0) * float(n - 1);\n int i = int(floor(x));\n int j = min(i + 1, n - 1);\n float f = smoothstep(0.0, 1.0, x - float(i));\n vec3 a = u_colors[i].rgb;\n vec3 b = u_colors[j].rgb;\n return sqrt(mix(a * a, b * b, f));\n}\n\n/** Rotate a colour about the grey axis \u2014 Rodrigues' formula, which needs no colour-space change. */\nvec3 kuiHueRotate(vec3 c, float angle) {\n const vec3 axis = vec3(0.57735026);\n float s = sin(angle);\n float k = cos(angle);\n return c * k + cross(axis, c) * s + axis * dot(axis, c) * (1.0 - k);\n}\n\n/** Per-pixel film grain. Quantised in time so it flickers at ~60 steps a second rather than per frame. */\nfloat kuiGrain(vec2 fragCoord, float t) {\n uint h = kuiHash(uvec3(uvec2(fragCoord), uint(int(t * 60.0) + 0x10000)));\n return float(h & 0xffffu) / 65535.0 - 0.5;\n}\n\nvoid main() {\n // Centred and aspect-free: the field is authored in the element's own UV space, so the same\n // parameters give the same picture whatever size the box is.\n float zoom = max(u_scale, 0.05);\n vec3 seed = vec3(u_seed * 137.31, u_seed * 71.17, u_seed * 29.73);\n float t = u_time;\n float pFactor = u_progress >= 0.0 ? u_progress : 1.0;\n\n // Orientation and travel, as one rigid motion of the domain. `angle` names the field's axis;\n // `motion: swirl` turns that axis over time; `motion: drift` slides along it. Both are *added\n // to* the third-axis evolution below rather than replacing it, so a drifting field is still\n // changing as it goes rather than being one frozen picture towed across the box.\n float spin = u_motion == KUI_MOTION_SWIRL ? t * 0.2 : 0.0;\n float orient = u_angle + spin;\n // Multiplied by the zoom, so `drift` crosses the box at the same visual rate whatever `scale`\n // is. Without it, zooming in would slow the travel down \u2014 one knob quietly moving two things,\n // which is the bug kuiFbm's `/norm` divisor exists to avoid on the octave count.\n float travel = u_motion == KUI_MOTION_DRIFT ? t * 0.12 * zoom : 0.0;\n\n vec2 p = kuiFieldPoint(v_uv, zoom, orient, travel);\n // One evaluation, shared by both gestures, and skipped entirely when there is no pointer\n // response authored.\n float hoverFall = u_hover != 0 ? kuiHoverFalloff() : 0.0;\n\n // Domain warp: a second field displaces the sample point of the first. This is the whole\n // difference between concentric fbm contours and something that reads as liquid.\n //\n // **This displacement is deliberately NOT aspect-corrected, and it is the one thing here that\n // is not.** `vec2(wx, wy)` is isotropic in the field's own normalised space, which means it is\n // anisotropic on screen \u2014 the same flaw `kuiRotateScreen` exists to fix for `angle`, `motion`\n // and the `stir` vortex. It is left alone because `warp` is *shipped and in use*: multiplying\n // it through `kuiAspectAxes()` would restyle every live page that authored one, which is a\n // price a correctness argument does not get to charge. The rule the rest of this program follows\n // and this line does not: **orientation and the pointer vortex are screen-true; the domain warp\n // is field-true.** If you are here to \"finish the job\", that is a behaviour change to real\n // sites and it needs the owner, not a tidy-up commit.\n vec2 q = p;\n if (u_warp > 0.0) {\n float wf = u_frequency * 0.1;\n float wx = kuiFbm(vec3(p * wf, t * 0.35) + seed, u_detail);\n float wy = kuiFbm(vec3(p * wf + 5.2, t * 0.35 + 1.3) + seed, u_detail);\n q += vec2(wx, wy) * u_warp;\n }\n\n // `hover: stir` \u2014 the cursor dragged through the colour, like stirring thick paint.\n //\n // Applied here, last, to the finished sample point: the gesture is dragging paint that is\n // already there, not re-warping the field before it exists. It also means `stir` works at the\n // library defaults, where `warp` is 0 and there is no domain warp to ride on.\n //\n // A vortex, which is one gesture and not two \u2014 see `kuiStirPoint`, which also explains why both\n // halves of it are measured in the same screen-isotropic space as the falloff that drives them.\n //\n // On `mode: logo` this cannot touch the mark's edges: `glyphMask()` samples `u_image` at the\n // undistorted `v_uv` and multiplies the finished colour, so stirring `q` swirls the fill and\n // leaves the stencil exactly where it was.\n if ((u_hover & KUI_HOVER_STIR) != 0) {\n vec2 pm = kuiFieldPoint(u_mouse, zoom, orient, travel);\n q = kuiStirPoint(q, pm, zoom, hoverFall);\n }\n\n // Audio and progress scale the field's *amplitude*, leaving its shape and rate alone \u2014 so a\n // beat swells the contrast and a scroll at 0 leaves a flat single colour rather than a jump cut.\n float field = kuiFbm(vec3(q, t * 0.25) + seed, u_detail);\n field *= u_strength * pFactor * audioGain();\n float v = clamp(field * 0.5 + 0.5, 0.0, 1.0);\n\n // Iridescence wraps the ramp back on itself, so one palette reads as many sheens across the box.\n float wrapped = u_iridescence > 0.0 ? fract(v * u_iridescence) : v;\n\n // Posterise. Divided by bands-1 so the top step reaches the end of the ramp; a plain /bands\n // would stop one step short and quietly desaturate the brightest colour.\n if (u_bands > 0) {\n float steps = float(u_bands);\n wrapped = floor(wrapped * steps) / max(steps - 1.0, 1.0);\n }\n\n // Fringing offsets the *ramp position* per channel rather than re-sampling the field three\n // times: three palette lookups are nearly free, three more fbm evaluations would triple the\n // program's cost for a decorative edge.\n vec3 color;\n if (u_chromatic > 0.0) {\n float d = u_chromatic * 0.04;\n color = vec3(\n kuiPalette(wrapped + d).r,\n kuiPalette(wrapped).g,\n kuiPalette(wrapped - d).b\n );\n } else {\n color = kuiPalette(wrapped);\n }\n\n if (u_hue != 0.0) color = kuiHueRotate(color, u_hue);\n\n // `hover: light` \u2014 a torch held over the surface.\n //\n // A multiply rather than a blend toward white. Multiplying preserves the hue and *raises* the\n // saturation, so what appears under the pointer is the author's own colour becoming vivid;\n // mixing toward white would wash the palette out at exactly the point the eye is on. The gain is\n // 1.0 in the far field, which is what makes this a highlight rather than a restyling \u2014 a palette\n // already at the top of the range has nowhere to go, and that is the honest behaviour of a torch.\n if ((u_hover & KUI_HOVER_LIGHT) != 0) color *= 1.0 + 0.7 * hoverFall;\n\n if (u_grain > 0.0) color += kuiGrain(gl_FragCoord.xy, t) * u_grain * 0.25;\n\n // Opaque before masking, and premultiplied trivially because alpha is 1 \u2014 the element's own\n // shape and its ancestors' opacity arrive through shapeMask(), the mark's shape through\n // glyphMask(), and both multiply all four channels, which is the right operation on\n // premultiplied colour and is what leaves the page visible around a logo.\n float sm = shapeMask();\n float gm = glyphMask();\n vec4 px = vec4(clamp(color, 0.0, 1.0), 1.0) * u_tint * sm * gm;\n\n // `backdrop` \u2014 the colour the field is composited *over*, inside the element's own shape.\n //\n // Premultiplied \"over\", because everything in this program already is. `u_tint.a * gm` is the\n // field's coverage before the shape mask, so the backdrop is pulled through the same `sm` as the\n // field rather than being masked twice \u2014 which would otherwise thin the backdrop along an\n // antialiased rounded corner.\n //\n // What it is *for*: `mode: logo`'s plate. Outside the mark the coverage is zero and the backdrop\n // is all there is, so a mark gets a solid ground instead of the page showing through. A bare\n // `gradient` is opaque, so there this is a no-op by construction \u2014 visible only through a `tint`\n // whose alpha is below 1. That is a property of an opaque field, not an oversight.\n if (u_backdrop.a > 0.0) {\n px += vec4(u_backdrop.rgb * u_backdrop.a, u_backdrop.a) * sm * (1.0 - u_tint.a * gm);\n }\n fragColor = px;\n}\n";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * kUInetic Advanced Modules
3
+ *
4
+ * Modular extensions outside of kUInetic core:
5
+ * - Shared WebGL2 Shader System
6
+ * - Interactive Particle Dissolve & Mesh Emitters
7
+ * - Interactive Liquid Cursor Trails
8
+ * - 3D Virtual Camera & Depth Projection
9
+ * - Cinematic Scene & Timeline Choreography
10
+ */
11
+ import type { Registry } from '../core/registry.js';
12
+ import type { Animator } from '../core/animator.js';
13
+ export { registerShaders, prepareShaders, SHADER_PARAMETERS, SHADERS_PRIMITIVE, SHADERS_PRESETS, } from './shaders.js';
14
+ export { registerScenes, prepareScene, SCENE_PARAMETERS, SCENE_PRIMITIVES, SCENE_PRESETS, type SceneOptions, type StepConfig, type StepRecord, type TransitionValue, } from './scenes.js';
15
+ export { registerCamera, prepareCameraScene, CAMERA_PARAMETERS, CAMERA_PRIMITIVES, CAMERA_PRESETS, type CameraOptions, type CameraLayer, } from './camera-3d.js';
16
+ export { registerParticles, prepareParticles, PARTICLE_PARAMETERS, PARTICLE_PRIMITIVES, PARTICLE_PRESETS, type ParticleOptions, } from './particles.js';
17
+ export { registerFluidCursor, prepareFluidCursor, FLUID_PARAMETERS, FLUID_PRIMITIVES, FLUID_PRESETS, type FluidTrailOptions, } from './fluid-cursor.js';
18
+ export { registerAudio, prepareAudioSource, AUDIO_PARAMETERS, AUDIO_PRIMITIVES, AUDIO_PRESETS, AudioSourceController, type AudioBands, type AudioSourceOptions, } from './audio.js';
19
+ export { type AdvancedEnv, type ResolvedEnv, type EffectInstanceOptions, } from './base.js';
20
+ /**
21
+ * Register all advanced modules into a kUInetic Registry or Animator instance.
22
+ */
23
+ export declare function registerAdvanced(target: unknown): Registry | Animator;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * kUInetic DOM Particle Dissolve & Mesh Emitter Module
3
+ */
4
+ import type { EffectInstance, EffectParams, PrepareContext, Preset, Primitive } from '../core/types.js';
5
+ import type { Registry } from '../core/registry.js';
6
+ import type { Animator } from '../core/animator.js';
7
+ import { type AdvancedEnv, type WindowLike, type DocumentLike, type RafFunction, type CafFunction, clamp } from './base.js';
8
+ export { clamp };
9
+ export type ParticleParamAccessor = EffectParams | {
10
+ num?: (name: string, fallback?: number) => number;
11
+ text?: (name: string, fallback?: string) => string;
12
+ };
13
+ export interface Particle {
14
+ x: number;
15
+ y: number;
16
+ originX: number;
17
+ originY: number;
18
+ vx: number;
19
+ vy: number;
20
+ radius: number;
21
+ color: string;
22
+ }
23
+ export interface ParticleOptions {
24
+ count?: number;
25
+ radius?: number;
26
+ color?: string;
27
+ }
28
+ /**
29
+ * Pure physics step for a single particle.
30
+ */
31
+ export declare function stepParticle(p: Particle, mouseX: number, mouseY: number, repelRadius: number): void;
32
+ export declare function spawnParticlesGrid(count: number, color: string, width: number, height: number): Particle[];
33
+ export declare class ParticleEmitter {
34
+ element: HTMLElement;
35
+ options: ParticleOptions;
36
+ env: AdvancedEnv;
37
+ window: Window | WindowLike | null;
38
+ document: Document | DocumentLike | null;
39
+ createCanvas: () => HTMLCanvasElement | null;
40
+ raf: RafFunction;
41
+ caf: CafFunction;
42
+ canvas: HTMLCanvasElement | null;
43
+ ctx: CanvasRenderingContext2D | null;
44
+ particles: Particle[];
45
+ mouse: {
46
+ x: number;
47
+ y: number;
48
+ };
49
+ rafId: number | null;
50
+ isListening: boolean;
51
+ dpr: number;
52
+ /**
53
+ * The host's `position`, and what it was before this module needed a positioning context.
54
+ *
55
+ * The hand-rolled pair this replaced read back only the value, never the priority, so an
56
+ * author's `position: relative !important` came back as a plain declaration on teardown. The
57
+ * canvas's own `cssText` needs no ledger — that element is created and removed here.
58
+ *
59
+ * Shared with every other controller writing to this element — see `createAdvancedLedgers`.
60
+ */
61
+ private ledgers;
62
+ constructor(element: HTMLElement, options?: ParticleOptions, env?: AdvancedEnv);
63
+ syncDimensions(): number;
64
+ setupPosition(): void;
65
+ init(): boolean;
66
+ spawnParticles(width: number, height: number): void;
67
+ resizeObserver: ResizeObserver | null;
68
+ bindEvents(): void;
69
+ unbindEvents(): void;
70
+ onResize(): void;
71
+ onMouseMove(e: MouseEvent | {
72
+ clientX: number;
73
+ clientY: number;
74
+ }): void;
75
+ onMouseLeave(): void;
76
+ startLoop(): void;
77
+ stopLoop(): void;
78
+ update(): boolean;
79
+ draw(): void;
80
+ /** `try`/`finally` for the reason `camera-3d.ts`'s `destroy()` documents. */
81
+ destroy(): void;
82
+ private removeCanvas;
83
+ }
84
+ export declare function prepareParticles(el: Element, params: EffectParams, ctx?: PrepareContext | null): EffectInstance;
85
+ export declare const PARTICLE_PARAMETERS: {
86
+ count: {
87
+ type: "number";
88
+ default: string;
89
+ minimum: number;
90
+ maximum: number;
91
+ cssProperty: string;
92
+ };
93
+ radius: {
94
+ type: "number";
95
+ default: string;
96
+ minimum: number;
97
+ maximum: number;
98
+ cssProperty: string;
99
+ };
100
+ color: {
101
+ type: "color";
102
+ default: string;
103
+ cssProperty: string;
104
+ };
105
+ };
106
+ export declare const PARTICLE_PRIMITIVES: Primitive[];
107
+ export declare const PARTICLE_PRESETS: Preset[];
108
+ export declare function registerParticles(target: unknown): Registry | Animator;
@@ -0,0 +1,207 @@
1
+ /**
2
+ * kUInetic Cinematic Scene & Timeline Choreography Module
3
+ */
4
+ import type { EffectInstance, EffectParams, PrepareContext, Preset, Primitive } from '../core/types.js';
5
+ import type { Registry } from '../core/registry.js';
6
+ import type { Animator } from '../core/animator.js';
7
+ import { type AdvancedEnv, type WindowLike, type RafFunction, type CafFunction, clamp, lerp } from './base.js';
8
+ export { clamp, lerp };
9
+ export type SceneParamAccessor = EffectParams | {
10
+ num?: (name: string, fallback?: number) => number;
11
+ text?: (name: string, fallback?: string) => string;
12
+ };
13
+ export interface TransitionValue {
14
+ from: number;
15
+ to: number;
16
+ unit: string;
17
+ }
18
+ export interface StepConfig {
19
+ range: [number, number];
20
+ opacity?: TransitionValue | null;
21
+ x?: TransitionValue | null;
22
+ y?: TransitionValue | null;
23
+ scale?: TransitionValue | null;
24
+ }
25
+ export interface StepRecord {
26
+ element: HTMLElement;
27
+ config: StepConfig;
28
+ }
29
+ export interface SceneOptions {
30
+ /**
31
+ * Which clock drives `progress`. The two keywords, and nothing else.
32
+ *
33
+ * This carried a `| string` arm, which defeated the union entirely. `SCENE_PARAMETERS.progress`
34
+ * is a `keyword` spec with a closed list, so `core/params.ts` already rejects `progress:tiem`
35
+ * before `prepare` runs — but the *type* said any string was a mode, so a typo written in
36
+ * library code type-checked and then silently behaved as `scroll`, because every read of this
37
+ * field is `=== 'time'`. The same spirit as `ValueParamSpec` declaring `keywords?: never`: where
38
+ * the schema closes a set, the type has to close it too, or one of the two is lying.
39
+ *
40
+ * {@link prepareScene} is what makes the narrowing honest rather than asserted — it maps the
41
+ * authored text onto these two rather than casting it.
42
+ */
43
+ progress?: 'scroll' | 'time';
44
+ duration?: number;
45
+ }
46
+ /**
47
+ * Read a step's progress window off its raw `data-kui`.
48
+ *
49
+ * Each end defaults independently — `from:` to 0, `to:` to 1 — so `scene-step to:0.4` means "from
50
+ * the start until 40%" and `scene-step from:0.6` means "from 60% to the end", with neither needing
51
+ * the other spelled out. The single-string form this replaced could not express that: a range
52
+ * missing one end was not a range at all, so the whole value fell back to the full `0..1` and the
53
+ * author was told nothing.
54
+ *
55
+ * Unreadable text can no longer reach here. `from:` and `to:` are declared `number` parameters
56
+ * bounded to 0..1 (see {@link SCENE_STEP_PARAMETERS}), so `core/params.ts` rejects anything else
57
+ * and substitutes the declared default *with a warning naming the parameter*, before `prepare`
58
+ * runs. {@link NUM} matching the core's own pattern is what keeps that true for this second read
59
+ * of the same attribute.
60
+ *
61
+ * @param kui - The step element's raw `data-kui` text.
62
+ * @returns `[start, end]`, each either an authored 0..1 value or its declared default.
63
+ */
64
+ export declare function parseWindow(kui: string): [number, number];
65
+ /**
66
+ * Parse a transition expression like "0->1", "0->100px", or "40px->0px".
67
+ */
68
+ export declare function parseTransitionValue(str?: string | null): TransitionValue | null;
69
+ /**
70
+ * Parse a child element's `data-kui` attribute into scene step transitions.
71
+ */
72
+ export declare function parseChildStep(child: Element | null): StepConfig;
73
+ export declare class SceneController {
74
+ container: HTMLElement;
75
+ options: SceneOptions;
76
+ env: AdvancedEnv;
77
+ window: Window | WindowLike | null;
78
+ raf: RafFunction;
79
+ caf: CafFunction;
80
+ steps: StepRecord[];
81
+ progress: number;
82
+ rafId: number | null;
83
+ isListening: boolean;
84
+ timeStart: number;
85
+ /**
86
+ * One ledger per step element, each opened at that step's first write and shared with every
87
+ * other controller writing to the same element — see `createAdvancedLedgers`.
88
+ *
89
+ * Steps are collected during `prepare`, which can be a long way before the scene ever scrolls
90
+ * into view. Recording opacity and transform *there* meant destroy restored the markup as it
91
+ * looked at preparation time, discarding anything the author wrote in between.
92
+ *
93
+ * Sharing is also what keeps two *legitimate* owners of one element honest. A nested `scene` is
94
+ * no longer one of them — `prepareScene` scopes its scan with {@link ownedDescendants}, so an
95
+ * outer scene stops at an inner one instead of claiming its steps — but an element authored as
96
+ * both a `scene-step` and a `camera-layer` still has two, because those are different kinds of
97
+ * host, and so does any controller constructed directly rather than through `prepare`.
98
+ */
99
+ private ledgers;
100
+ constructor(container: HTMLElement, options?: SceneOptions, env?: AdvancedEnv);
101
+ addStep(element: HTMLElement, config: StepConfig): void;
102
+ start(onComplete?: () => void): void;
103
+ stop(): void;
104
+ startTimeLoop(onComplete?: () => void): void;
105
+ onScroll(): void;
106
+ calculateProgress(): void;
107
+ updateAll(): void;
108
+ updateElement(el: HTMLElement, cfg: StepConfig, globalProgress: number): void;
109
+ /** `try`/`finally` for the reason `camera-3d.ts`'s `destroy()` documents. */
110
+ destroy(): void;
111
+ }
112
+ export declare function prepareScene(el: Element, params: EffectParams, ctx?: PrepareContext | null): EffectInstance;
113
+ /**
114
+ * `scene`'s own parameters.
115
+ *
116
+ * There used to be a third, `name`, and it did nothing at all. It was declared `text`, read into
117
+ * `SceneOptions.name` by `prepareScene`, carried on every controller — and never read again by
118
+ * anything: no branch, no diagnostic, no selector. Nor could its `cssProperty` save it, because
119
+ * `resolveParams` drops every `text` parameter before the stylesheet by design (`core/params.ts`
120
+ * — a `text` value reaching CSS is the injection surface that rule exists to close), so
121
+ * `--kui-scene-name` was never written either. A parameter that is declared, validated, plumbed
122
+ * and inert is a documented lie: it tells an author the scene can be named and then ignores the
123
+ * name. Deleted rather than wired up, because nothing in the module ever wanted it.
124
+ */
125
+ export declare const SCENE_PARAMETERS: {
126
+ progress: {
127
+ type: "keyword";
128
+ default: string;
129
+ keywords: string[];
130
+ cssProperty: string;
131
+ };
132
+ duration: {
133
+ type: "number";
134
+ default: string;
135
+ minimum: number;
136
+ maximum: number;
137
+ cssProperty: string;
138
+ };
139
+ };
140
+ /**
141
+ * `scene-step`'s own parameters.
142
+ *
143
+ * Every one of these is read straight off the raw `data-kui` attribute by {@link parseChildStep},
144
+ * which the *parent* `scene` runs over its descendants — not by this primitive's `prepare`, which
145
+ * does nothing. Declaring them is what lets the compiler accept `from:0 to:0.4 opacity:0->1` on a
146
+ * step instead of warning "unknown parameter" once "unknown effect" is fixed, exactly as
147
+ * `camera-layer`'s `z` does in `camera-3d.ts`.
148
+ *
149
+ * `from`/`to` are real `number`s and are therefore genuinely validated — bounded to 0..1, rejected
150
+ * with a diagnostic naming the parameter, and defaulted per end. The four transition parameters
151
+ * cannot be: `0->1` and `40px->0px` are a pair grammar this module owns and no `ScalarParamType`
152
+ * describes, so they are `text`, which `core/params.ts` accepts unconditionally. `parseTransitionValue`
153
+ * returning `null` for unreadable text is all the safety they have, and a typo in one is still
154
+ * silent. Splitting them the way the window was split would be the fix, and is deliberately not
155
+ * done here — it is a second grammar change, and this one is already breaking.
156
+ */
157
+ export declare const SCENE_STEP_PARAMETERS: {
158
+ from: {
159
+ type: "number";
160
+ default: string;
161
+ minimum: number;
162
+ maximum: number;
163
+ cssProperty: string;
164
+ };
165
+ to: {
166
+ type: "number";
167
+ default: string;
168
+ minimum: number;
169
+ maximum: number;
170
+ cssProperty: string;
171
+ };
172
+ opacity: {
173
+ type: "text";
174
+ default: string;
175
+ cssProperty: string;
176
+ };
177
+ x: {
178
+ type: "text";
179
+ default: string;
180
+ cssProperty: string;
181
+ };
182
+ y: {
183
+ type: "text";
184
+ default: string;
185
+ cssProperty: string;
186
+ };
187
+ scale: {
188
+ type: "text";
189
+ default: string;
190
+ cssProperty: string;
191
+ };
192
+ };
193
+ /**
194
+ * `scene-step` never runs on its own: `prepareScene`'s `[data-kui*="scene-step"]` scan reads each
195
+ * step's window and transitions directly off its attribute, and the parent `SceneController` is
196
+ * what writes `opacity`/`transform` to it every frame (`updateElement`). Without a registered
197
+ * primitive for the name, every `scene-step` element compiled as an unrecognised effect — the
198
+ * scene still animated it correctly, but the console warned "unknown effect scene-step" once per
199
+ * step. This primitive exists only so the name resolves; it deliberately does nothing of its own.
200
+ *
201
+ * The same shape as `prepareCameraLayer` in `camera-3d.ts`, and fixed second because the key its
202
+ * window used to be spelled with, `at:`, is owned by `core/parse.ts` — see {@link FROM_REGEX}.
203
+ */
204
+ export declare function prepareSceneStep(): EffectInstance;
205
+ export declare const SCENE_PRIMITIVES: Primitive[];
206
+ export declare const SCENE_PRESETS: Preset[];
207
+ export declare function registerScenes(target: unknown): Registry | Animator;