kuinetic 0.1.3 → 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.
- package/README.md +93 -10
- package/dist/esm/advanced/index.mjs +4727 -0
- package/dist/esm/chunk-AEQGC2KM.mjs +189 -0
- package/dist/esm/chunk-MQSNMYRP.mjs +9332 -0
- package/dist/esm/chunk-RVTYN6NZ.mjs +3446 -0
- package/dist/esm/chunk-U43BX5O2.mjs +2984 -0
- package/dist/esm/core/index.mjs +16 -9
- package/dist/esm/effects/index.mjs +19 -4
- package/dist/esm/index.mjs +17 -10
- package/dist/kuinetic.advanced.js +5070 -0
- package/dist/kuinetic.advanced.min.js +749 -0
- package/dist/kuinetic.all.js +15218 -5595
- package/dist/kuinetic.all.min.js +2 -0
- package/dist/kuinetic.css +2900 -360
- package/dist/kuinetic.js +15217 -5593
- package/dist/kuinetic.min.css +1 -0
- package/dist/kuinetic.min.js +7 -0
- package/dist/types/3d/angles.d.ts +30 -0
- package/dist/types/3d/flattening.d.ts +49 -0
- package/dist/types/3d/index.d.ts +36 -0
- package/dist/types/3d/model-3d.d.ts +115 -0
- package/dist/types/3d/register.d.ts +26 -0
- package/dist/types/3d/webgl.d.ts +24 -0
- package/dist/types/advanced/audio.d.ts +322 -0
- package/dist/types/advanced/base.d.ts +233 -0
- package/dist/types/advanced/camera-3d.d.ts +236 -0
- package/dist/types/advanced/fluid-cursor.d.ts +149 -0
- package/dist/types/advanced/gl-utils.d.ts +52 -0
- package/dist/types/advanced/glsl.d.ts +60 -0
- package/dist/types/advanced/index.d.ts +23 -0
- package/dist/types/advanced/particles.d.ts +108 -0
- package/dist/types/advanced/scenes.d.ts +207 -0
- package/dist/types/advanced/shaders.d.ts +972 -0
- package/dist/types/browser/boot.d.ts +72 -0
- package/dist/types/core/activation.d.ts +227 -2
- package/dist/types/core/animator.d.ts +446 -11
- package/dist/types/core/attrs.d.ts +9 -0
- package/dist/types/core/breakpoints.d.ts +260 -0
- package/dist/types/core/bundles.d.ts +71 -0
- package/dist/types/core/callback.d.ts +64 -0
- package/dist/types/core/capabilities.d.ts +47 -0
- package/dist/types/core/channels.d.ts +120 -2
- package/dist/types/core/cloak-selectors.d.ts +47 -0
- package/dist/types/core/compile.d.ts +207 -2
- package/dist/types/core/control.d.ts +218 -0
- package/dist/types/core/declarations.d.ts +128 -0
- package/dist/types/core/derived/aggregate.d.ts +43 -0
- package/dist/types/core/derived/book.d.ts +30 -0
- package/dist/types/core/derived/diagnostics.d.ts +51 -0
- package/dist/types/core/derived/group-gate.d.ts +62 -0
- package/dist/types/core/derived/install.d.ts +110 -0
- package/dist/types/core/derived/late-matches.d.ts +19 -0
- package/dist/types/core/derived/types.d.ts +108 -0
- package/dist/types/core/easing.d.ts +57 -0
- package/dist/types/core/element-config.d.ts +47 -6
- package/dist/types/core/element-size.d.ts +23 -0
- package/dist/types/core/event-sources.d.ts +39 -0
- package/dist/types/core/flip.d.ts +34 -1
- package/dist/types/core/gesture.d.ts +12 -0
- package/dist/types/core/host-facts.d.ts +155 -0
- package/dist/types/core/index.d.ts +7 -3
- package/dist/types/core/owned-styles.d.ts +129 -2
- package/dist/types/core/params.d.ts +48 -0
- package/dist/types/core/parse.d.ts +28 -1
- package/dist/types/core/register-into.d.ts +29 -0
- package/dist/types/core/registry.d.ts +10 -3
- package/dist/types/core/repeat.d.ts +91 -0
- package/dist/types/core/sequence.d.ts +126 -0
- package/dist/types/core/stagger-config.d.ts +199 -0
- package/dist/types/core/stagger-keys.d.ts +63 -0
- package/dist/types/core/stagger.d.ts +187 -6
- package/dist/types/core/style-plan.d.ts +21 -16
- package/dist/types/core/target.d.ts +128 -0
- package/dist/types/core/threshold-reachability.d.ts +45 -0
- package/dist/types/core/time-scale.d.ts +9 -0
- package/dist/types/core/toggle-actions.d.ts +141 -0
- package/dist/types/core/travel.d.ts +59 -0
- package/dist/types/core/types.d.ts +705 -13
- package/dist/types/core/unquoted-selectors.d.ts +53 -0
- package/dist/types/effects/carousel/drag.d.ts +80 -0
- package/dist/types/effects/carousel/index.d.ts +149 -0
- package/dist/types/effects/catalog/background-media.d.ts +152 -0
- package/dist/types/effects/catalog/discrete.d.ts +13 -0
- package/dist/types/effects/catalog/interaction-proximity.d.ts +24 -0
- package/dist/types/effects/catalog/interaction-reveal.d.ts +248 -0
- package/dist/types/effects/catalog/interaction-shared.d.ts +42 -0
- package/dist/types/effects/catalog/interaction-states.d.ts +68 -0
- package/dist/types/effects/catalog/materials.d.ts +67 -0
- package/dist/types/effects/catalog/media-shared.d.ts +3 -2
- package/dist/types/effects/catalog/numbers-shared.d.ts +8 -5
- package/dist/types/effects/catalog/shared.d.ts +2 -2
- package/dist/types/effects/catalog/text-shared.d.ts +92 -10
- package/dist/types/effects/catalog/transforms.d.ts +13 -0
- package/dist/types/effects/catalog/view-transitions.d.ts +13 -0
- package/dist/types/effects/forms/primitives.d.ts +15 -4
- package/dist/types/effects/gestures/index.d.ts +53 -0
- package/dist/types/effects/index.d.ts +5 -0
- package/dist/types/effects/layout/presets.d.ts +35 -0
- package/dist/types/effects/motion-path/index.d.ts +35 -0
- package/dist/types/effects/scroll-mechanics/params.d.ts +23 -0
- package/dist/types/effects/scroll-mechanics/presets.d.ts +51 -0
- package/dist/types/effects/scroll-mechanics/section-index.d.ts +47 -0
- package/dist/types/effects/shared.d.ts +162 -2
- package/dist/types/effects/step-index.d.ts +121 -0
- package/dist/types/effects/step-marking.d.ts +41 -33
- package/dist/types/effects/svg/icon-parts.d.ts +23 -0
- package/dist/types/effects/three-d/flip-parts.d.ts +46 -0
- package/dist/types/effects/tween/index.d.ts +57 -0
- package/dist/types/effects/tween/properties.d.ts +107 -0
- package/dist/types/effects/tween/waypoints.d.ts +106 -0
- package/dist/types/showcase/compare.d.ts +4 -0
- package/dist/types/showcase/device-frame.d.ts +3 -0
- package/dist/types/showcase/hotspots.d.ts +3 -0
- package/dist/types/showcase/index.d.ts +30 -0
- package/dist/types/showcase/lightbox.d.ts +3 -0
- package/dist/types/showcase/media-source.d.ts +13 -0
- package/dist/types/showcase/modal-shell.d.ts +22 -0
- package/dist/types/showcase/scroll-story.d.ts +4 -0
- package/dist/types/showcase/shared.d.ts +47 -0
- package/dist/types/showcase/slideshow.d.ts +3 -0
- package/dist/types/showcase/slow-mo.d.ts +5 -0
- package/package.json +14 -7
- package/dist/esm/chunk-5DON3UFQ.mjs +0 -4380
- package/dist/esm/chunk-JT4PZL3A.mjs +0 -773
- package/dist/esm/chunk-QUVFODSQ.mjs +0 -1278
|
@@ -0,0 +1,4727 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createStyleClaim
|
|
3
|
+
} from "../chunk-AEQGC2KM.mjs";
|
|
4
|
+
|
|
5
|
+
// src/advanced/glsl.ts
|
|
6
|
+
var AUDIO_GAIN_GLSL = `uniform float u_audio;
|
|
7
|
+
float audioGain() { return 1.0 + clamp(u_audio, 0.0, 1.0); }
|
|
8
|
+
`;
|
|
9
|
+
var SHAPE_MASK_GLSL = `uniform vec4 u_maskBox;
|
|
10
|
+
uniform vec4 u_maskRx;
|
|
11
|
+
uniform vec4 u_maskRy;
|
|
12
|
+
uniform float u_maskAlpha;
|
|
13
|
+
float shapeMask() {
|
|
14
|
+
if (u_maskBox.z <= 0.0 || u_maskBox.w <= 0.0) return u_maskAlpha;
|
|
15
|
+
vec2 p = gl_FragCoord.xy - u_maskBox.xy;
|
|
16
|
+
vec2 h = u_maskBox.zw;
|
|
17
|
+
bool left = p.x < 0.0;
|
|
18
|
+
float rx = p.y >= 0.0 ? (left ? u_maskRx.x : u_maskRx.y) : (left ? u_maskRx.w : u_maskRx.z);
|
|
19
|
+
float ry = p.y >= 0.0 ? (left ? u_maskRy.x : u_maskRy.y) : (left ? u_maskRy.w : u_maskRy.z);
|
|
20
|
+
rx = min(rx, h.x);
|
|
21
|
+
ry = min(ry, h.y);
|
|
22
|
+
if (rx <= 0.0 || ry <= 0.0) return u_maskAlpha;
|
|
23
|
+
vec2 q = abs(p) - (h - vec2(rx, ry));
|
|
24
|
+
if (q.x <= 0.0 || q.y <= 0.0) return u_maskAlpha;
|
|
25
|
+
float d = (length(q / vec2(rx, ry)) - 1.0) * min(rx, ry);
|
|
26
|
+
return u_maskAlpha * clamp(0.5 - d, 0.0, 1.0);
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
29
|
+
var NOISE_GLSL = `const int KUI_MAX_OCTAVES = 6;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Integer avalanche over one lattice coordinate.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately not the \`fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453)\` idiom. That relies
|
|
35
|
+
* on the low bits of a large sine, which \`highp\` does not reliably have: on several mobile GPUs
|
|
36
|
+
* neighbouring cells stay correlated and the field bands visibly. GLSL ES 3.00 has real unsigned
|
|
37
|
+
* integer arithmetic, so a proper mix is both better distributed and cheaper.
|
|
38
|
+
*/
|
|
39
|
+
uint kuiHash(uvec3 v) {
|
|
40
|
+
uint h = v.x * 0x8da6b343u ^ v.y * 0xd8163841u ^ v.z * 0xcb1ab31fu;
|
|
41
|
+
h ^= h >> 15u; h *= 0x2c1b3c6du;
|
|
42
|
+
h ^= h >> 12u; h *= 0x297a2d39u;
|
|
43
|
+
h ^= h >> 15u;
|
|
44
|
+
return h;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A unit gradient vector for one lattice corner.
|
|
49
|
+
*
|
|
50
|
+
* The three components read *non-overlapping* 10-bit fields. Overlapping slices of one hash are
|
|
51
|
+
* correlated with each other, which shows up as gradients clustering around a diagonal and the
|
|
52
|
+
* noise developing a directional grain. The \`1e-5\` keeps \`normalize\` defined for the one input
|
|
53
|
+
* that hashes to exactly zero.
|
|
54
|
+
*/
|
|
55
|
+
vec3 kuiGradient(uvec3 cell) {
|
|
56
|
+
uint h = kuiHash(cell);
|
|
57
|
+
vec3 g = vec3(
|
|
58
|
+
float((h ) & 1023u),
|
|
59
|
+
float((h >> 10u ) & 1023u),
|
|
60
|
+
float((h >> 20u ) & 1023u)
|
|
61
|
+
) * (2.0 / 1023.0) - 1.0;
|
|
62
|
+
return normalize(g + vec3(1e-5));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* One octave of gradient noise, approximately -1..1.
|
|
67
|
+
*
|
|
68
|
+
* The lattice index is biased into unsigned space before hashing: seeds, scroll and a centred UV
|
|
69
|
+
* field all reach negative coordinates, and \`uvec3\` of a negative \`ivec3\` wraps \u2014 which is
|
|
70
|
+
* harmless for a hash, but only once every negative input maps somewhere distinct rather than
|
|
71
|
+
* folding onto its positive twin.
|
|
72
|
+
*/
|
|
73
|
+
float kuiNoise(vec3 p) {
|
|
74
|
+
vec3 i = floor(p);
|
|
75
|
+
vec3 f = p - i;
|
|
76
|
+
vec3 w = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
|
|
77
|
+
uvec3 c = uvec3(ivec3(i) + 0x10000);
|
|
78
|
+
float n000 = dot(kuiGradient(c + uvec3(0u, 0u, 0u)), f - vec3(0.0, 0.0, 0.0));
|
|
79
|
+
float n100 = dot(kuiGradient(c + uvec3(1u, 0u, 0u)), f - vec3(1.0, 0.0, 0.0));
|
|
80
|
+
float n010 = dot(kuiGradient(c + uvec3(0u, 1u, 0u)), f - vec3(0.0, 1.0, 0.0));
|
|
81
|
+
float n110 = dot(kuiGradient(c + uvec3(1u, 1u, 0u)), f - vec3(1.0, 1.0, 0.0));
|
|
82
|
+
float n001 = dot(kuiGradient(c + uvec3(0u, 0u, 1u)), f - vec3(0.0, 0.0, 1.0));
|
|
83
|
+
float n101 = dot(kuiGradient(c + uvec3(1u, 0u, 1u)), f - vec3(1.0, 0.0, 1.0));
|
|
84
|
+
float n011 = dot(kuiGradient(c + uvec3(0u, 1u, 1u)), f - vec3(0.0, 1.0, 1.0));
|
|
85
|
+
float n111 = dot(kuiGradient(c + uvec3(1u, 1u, 1u)), f - vec3(1.0, 1.0, 1.0));
|
|
86
|
+
float nx00 = mix(n000, n100, w.x);
|
|
87
|
+
float nx10 = mix(n010, n110, w.x);
|
|
88
|
+
float nx01 = mix(n001, n101, w.x);
|
|
89
|
+
float nx11 = mix(n011, n111, w.x);
|
|
90
|
+
// 2/sqrt(3) \u2014 a 3D gradient-noise octave peaks near sqrt(3)/2, so this brings it to about -1..1.
|
|
91
|
+
return mix(mix(nx00, nx10, w.y), mix(nx01, nx11, w.y), w.z) * 1.1547;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Octaves summed and **normalised by the amplitude sum**, approximately -1..1 at every octave count.
|
|
96
|
+
*
|
|
97
|
+
* The division is the whole point of the function. Without it, raising the octave count raises the
|
|
98
|
+
* total amplitude, so the one authored knob that means "how much fine structure" would silently be
|
|
99
|
+
* a contrast control as well \u2014 two behaviours behind one parameter, which is the class of bug this
|
|
100
|
+
* repository keeps rediscovering. Gain and lacunarity stay fixed at 0.5 and 2.0: they are the two
|
|
101
|
+
* numbers that make layered noise look wrong in a hundred ways and right in one.
|
|
102
|
+
*
|
|
103
|
+
* The loop bound is a constant with an early break because a loop bound that is itself a uniform
|
|
104
|
+
* forces some drivers to unroll against the worst case anyway, and this way the cost of a low
|
|
105
|
+
* \`detail\` is genuinely low.
|
|
106
|
+
*/
|
|
107
|
+
float kuiFbm(vec3 p, int octaves) {
|
|
108
|
+
float amp = 1.0;
|
|
109
|
+
float freq = 1.0;
|
|
110
|
+
float sum = 0.0;
|
|
111
|
+
float norm = 0.0;
|
|
112
|
+
for (int i = 0; i < KUI_MAX_OCTAVES; i++) {
|
|
113
|
+
if (i >= octaves) break;
|
|
114
|
+
sum += amp * kuiNoise(p * freq);
|
|
115
|
+
norm += amp;
|
|
116
|
+
amp *= 0.5;
|
|
117
|
+
freq *= 2.0;
|
|
118
|
+
}
|
|
119
|
+
return sum / max(norm, 1e-5);
|
|
120
|
+
}
|
|
121
|
+
`;
|
|
122
|
+
var FILTER_NOISE_GLSL = `uniform float u_noise;
|
|
123
|
+
uniform float u_seed;
|
|
124
|
+
uniform float u_scale;
|
|
125
|
+
uniform float u_warp;
|
|
126
|
+
uniform int u_detail;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A sine's RMS is 0.707. Amplitude-normalised fbm's is about 0.23 \u2014 it reaches its \`1.0\` peak
|
|
130
|
+
* rarely, which is what makes it look like noise rather than a wave. Handing the programs the raw
|
|
131
|
+
* field would therefore read as the *effect being turned down*, not as it being re-textured, and
|
|
132
|
+
* the first thing anyone trying \`noise: 1\` would do is reach for \`strength\`. This brings the two
|
|
133
|
+
* to the same typical magnitude so the amount is a change of texture and nothing else.
|
|
134
|
+
*/
|
|
135
|
+
const float KUI_FBM_SINE_RMS = 3.0;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* One scalar of animated noise, scaled to stand in for a unit sine at a stated rate.
|
|
139
|
+
*
|
|
140
|
+
* \`cycles\` and \`rate\` are the spatial and temporal frequency of the term being replaced, in
|
|
141
|
+
* cycles rather than radians \u2014 each call site divides its own trig coefficients by 2*pi and passes
|
|
142
|
+
* the result, so \`noise: 1\` lands on the same feature size and the same speed as the wave it took
|
|
143
|
+
* over from. That is the entire mechanism keeping this from being a new look.
|
|
144
|
+
*
|
|
145
|
+
* \`phase\` decorrelates sibling calls: two components of one flow vector must not be the same
|
|
146
|
+
* field, and offsetting the seed is cheaper than hashing a second one.
|
|
147
|
+
*
|
|
148
|
+
* \`u_warp\` costs two further fbm evaluations and is skipped entirely when it is zero.
|
|
149
|
+
*/
|
|
150
|
+
float kuiFilterField(vec2 uv, float t, float cycles, float rate, float phase) {
|
|
151
|
+
vec3 s = vec3(u_seed * 137.31, u_seed * 71.17, u_seed * 29.73) + phase;
|
|
152
|
+
vec3 q = vec3((uv - 0.5) * cycles * max(u_scale, 0.05), t * rate) + s;
|
|
153
|
+
if (u_warp > 0.0) {
|
|
154
|
+
float wx = kuiFbm(q * 0.5 + vec3(5.2, 1.3, 7.1), u_detail);
|
|
155
|
+
float wy = kuiFbm(q * 0.5 + vec3(19.7, 11.1, 3.7), u_detail);
|
|
156
|
+
q.xy += vec2(wx, wy) * u_warp;
|
|
157
|
+
}
|
|
158
|
+
return kuiFbm(q, u_detail) * KUI_FBM_SINE_RMS;
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
var FULLSCREEN_QUAD_VS = `#version 300 es
|
|
162
|
+
in vec2 a_position;
|
|
163
|
+
uniform vec2 u_uvOrigin;
|
|
164
|
+
uniform vec2 u_uvScale;
|
|
165
|
+
out vec2 v_uv;
|
|
166
|
+
void main() {
|
|
167
|
+
vec2 uv = (a_position + 1.0) * 0.5;
|
|
168
|
+
uv.y = 1.0 - uv.y;
|
|
169
|
+
v_uv = u_uvOrigin + uv * u_uvScale;
|
|
170
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
171
|
+
}
|
|
172
|
+
`;
|
|
173
|
+
var DISPLACE_FS = `#version 300 es
|
|
174
|
+
precision highp float;
|
|
175
|
+
in vec2 v_uv;
|
|
176
|
+
out vec4 fragColor;
|
|
177
|
+
uniform sampler2D u_image;
|
|
178
|
+
uniform float u_time;
|
|
179
|
+
uniform float u_strength;
|
|
180
|
+
uniform float u_frequency;
|
|
181
|
+
uniform float u_chromatic;
|
|
182
|
+
uniform vec2 u_mouse;
|
|
183
|
+
uniform vec4 u_tint;
|
|
184
|
+
uniform int u_blend;
|
|
185
|
+
uniform float u_duotone;
|
|
186
|
+
uniform vec4 u_color1;
|
|
187
|
+
uniform vec4 u_color2;
|
|
188
|
+
uniform float u_progress;
|
|
189
|
+
${AUDIO_GAIN_GLSL}${SHAPE_MASK_GLSL}
|
|
190
|
+
vec4 applyBlend(vec4 base, vec4 tint, int mode) {
|
|
191
|
+
// \`base\` is premultiplied \u2014 \`createGLTexture\` uploads with \`UNPACK_PREMULTIPLY_ALPHA_WEBGL\` to
|
|
192
|
+
// match a \`premultipliedAlpha: true\` context \u2014 while \`tint\` is straight rgba out of
|
|
193
|
+
// \`parseColor\`. All four blends are defined on *straight* colour, so every mode takes the same
|
|
194
|
+
// path: un-premultiply, blend, and re-premultiply by this pixel's own coverage.
|
|
195
|
+
//
|
|
196
|
+
// Screen and add were the obvious cases. Run over all four premultiplied components,
|
|
197
|
+
// \`screen((0,0,0,0), (1,0,0,1))\` is \`(1,0,0,1)\`, an opaque red pixel where the source had no
|
|
198
|
+
// coverage at all \u2014 \`blend: screen tint: red\` filled a transparent PNG's surround with a solid
|
|
199
|
+
// red rectangle. Normal and multiply *looked* safe as a plain \`base * tint\`, because a transparent
|
|
200
|
+
// base does multiply to transparent whatever the tint is. It is the tint's alpha they got wrong:
|
|
201
|
+
// \`(1,1,1,1) * (1,0,0,0)\` is \`(1,0,0,0)\`, red at zero coverage, and a premultiplied canvas
|
|
202
|
+
// composites \`rgb > a\` *additively* \u2014 \`tint: rgba(255,0,0,0)\`, a fully transparent tint, turned
|
|
203
|
+
// an opaque image red, and any translucent tint bled the same way in proportion.
|
|
204
|
+
//
|
|
205
|
+
// The \`min\` on \`s\` is for \`main()\`'s chromatic sampling, which reads the three colour channels
|
|
206
|
+
// at three offsets and \`a\` at a fourth: at a transparent PNG's edge that can pair \`rgb\` from
|
|
207
|
+
// opaque neighbours with \`a\` from a transparent centre, an input that already breaks the
|
|
208
|
+
// invariant. Clamping the un-premultiplied colour to 1 is what keeps the output inside it.
|
|
209
|
+
//
|
|
210
|
+
// Alpha is \`base.a * tint.a\` \u2014 the source's coverage, faded by a translucent tint. Whatever the
|
|
211
|
+
// mode, the result satisfies \`rgb <= a\`, which is the whole contract of a premultiplied pixel.
|
|
212
|
+
float a = base.a * tint.a;
|
|
213
|
+
vec3 s = base.a > 0.0 ? min(base.rgb / base.a, vec3(1.0)) : vec3(0.0);
|
|
214
|
+
vec3 c = mode == 1 ? 1.0 - (1.0 - s) * (1.0 - tint.rgb)
|
|
215
|
+
: mode == 3 ? min(s + tint.rgb, vec3(1.0))
|
|
216
|
+
: s * tint.rgb;
|
|
217
|
+
return vec4(c * a, a);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
void main() {
|
|
221
|
+
vec2 m = u_mouse;
|
|
222
|
+
float d = distance(v_uv, m);
|
|
223
|
+
float ripple = sin(d * u_frequency - u_time * 3.0) * exp(-d * 4.0);
|
|
224
|
+
float pFactor = u_progress >= 0.0 ? u_progress : 1.0;
|
|
225
|
+
// Audio widens the ripple's throw (up to 2x), leaving its shape and speed alone.
|
|
226
|
+
vec2 disp = normalize(v_uv - m + 0.0001) * ripple * u_strength * 0.05 * pFactor * audioGain();
|
|
227
|
+
|
|
228
|
+
float cr = texture(u_image, v_uv + disp * (1.0 + u_chromatic)).r;
|
|
229
|
+
float cg = texture(u_image, v_uv + disp).g;
|
|
230
|
+
float cb = texture(u_image, v_uv + disp * (1.0 - u_chromatic)).b;
|
|
231
|
+
float ca = texture(u_image, v_uv + disp).a;
|
|
232
|
+
vec4 color = vec4(cr, cg, cb, ca);
|
|
233
|
+
|
|
234
|
+
if (u_duotone > 0.5) {
|
|
235
|
+
float lum = dot(color.rgb, vec3(0.299, 0.587, 0.114));
|
|
236
|
+
// \`* color.a\` because the ramp's two colours are straight rgba and everything downstream of
|
|
237
|
+
// here is premultiplied. Assigning one of them to a transparent pixel's \`rgb\` left \`rgb > 0\`
|
|
238
|
+
// with \`a == 0\`, which a \`premultipliedAlpha: true\` canvas composites *additively* \u2014 a
|
|
239
|
+
// transparent PNG's surround glowing in \`color1\` over whatever the page put behind it.
|
|
240
|
+
//
|
|
241
|
+
// \`lum\` is still read off the premultiplied colour, which is deliberate and unchanged: it
|
|
242
|
+
// makes a half-covered pixel map nearer \`color1\`, matching how the alpha stencil in
|
|
243
|
+
// \`GENERATIVE_FS\` reads the same kind of edge.
|
|
244
|
+
color.rgb = mix(u_color1.rgb, u_color2.rgb, lum) * color.a;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
fragColor = applyBlend(color, u_tint, u_blend) * shapeMask();
|
|
248
|
+
}
|
|
249
|
+
`;
|
|
250
|
+
var FLUID_FS = `#version 300 es
|
|
251
|
+
precision highp float;
|
|
252
|
+
in vec2 v_uv;
|
|
253
|
+
out vec4 fragColor;
|
|
254
|
+
uniform sampler2D u_image;
|
|
255
|
+
uniform float u_time;
|
|
256
|
+
uniform float u_strength;
|
|
257
|
+
uniform vec2 u_mouse;
|
|
258
|
+
uniform vec4 u_tint;
|
|
259
|
+
uniform float u_progress;
|
|
260
|
+
${AUDIO_GAIN_GLSL}${SHAPE_MASK_GLSL}${NOISE_GLSL}${FILTER_NOISE_GLSL}
|
|
261
|
+
void main() {
|
|
262
|
+
vec2 uv = v_uv;
|
|
263
|
+
vec2 m = u_mouse;
|
|
264
|
+
float d = distance(uv, m);
|
|
265
|
+
// Audio scales both halves of the flow \u2014 the ambient drift and the pointer's push \u2014 so a beat
|
|
266
|
+
// reads as the whole surface moving faster, not just around the cursor.
|
|
267
|
+
float gain = audioGain();
|
|
268
|
+
float force = exp(-d * 6.0) * u_strength * gain;
|
|
269
|
+
// The ambient drift. Two perpendicular waves at 10 rad across the box and 1 rad/s, which is
|
|
270
|
+
// 10/2pi = 1.5915 cycles and 1/2pi = 0.1592 cycles a second \u2014 the numbers kuiFilterField needs
|
|
271
|
+
// to put its field at the same size and speed. The drift is the whole of the ambient motion
|
|
272
|
+
// here, so this is the mode where the noise core reads most strongly: a regular cross-hatched
|
|
273
|
+
// sway becomes an irregular current.
|
|
274
|
+
vec2 wave = vec2(sin(u_time + uv.y * 10.0), cos(u_time + uv.x * 10.0));
|
|
275
|
+
if (u_noise > 0.0) {
|
|
276
|
+
wave = mix(wave, vec2(
|
|
277
|
+
kuiFilterField(uv, u_time, 1.5915, 0.1592, 0.0),
|
|
278
|
+
kuiFilterField(uv, u_time, 1.5915, 0.1592, 31.4)
|
|
279
|
+
), u_noise);
|
|
280
|
+
}
|
|
281
|
+
vec2 flow = wave * 0.02 * gain;
|
|
282
|
+
float pFactor = u_progress >= 0.0 ? u_progress : 1.0;
|
|
283
|
+
vec2 offset = (flow + (uv - m) * force * 0.1) * pFactor;
|
|
284
|
+
fragColor = texture(u_image, uv + offset) * u_tint * shapeMask();
|
|
285
|
+
}
|
|
286
|
+
`;
|
|
287
|
+
var LIQUID_FS = `#version 300 es
|
|
288
|
+
precision highp float;
|
|
289
|
+
in vec2 v_uv;
|
|
290
|
+
out vec4 fragColor;
|
|
291
|
+
uniform sampler2D u_image;
|
|
292
|
+
uniform float u_time;
|
|
293
|
+
uniform float u_strength;
|
|
294
|
+
uniform vec4 u_tint;
|
|
295
|
+
uniform float u_progress;
|
|
296
|
+
${AUDIO_GAIN_GLSL}${SHAPE_MASK_GLSL}${NOISE_GLSL}${FILTER_NOISE_GLSL}
|
|
297
|
+
void main() {
|
|
298
|
+
vec2 uv = v_uv;
|
|
299
|
+
// 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
|
|
300
|
+
// 0.2387. The two waves keep their own rates under noise rather than being merged into one
|
|
301
|
+
// field, because the difference between them is what stops the surface reading as a single
|
|
302
|
+
// sheet sliding.
|
|
303
|
+
float w1 = sin(uv.y * 12.0 + u_time * 2.0);
|
|
304
|
+
float w2 = cos(uv.x * 10.0 - u_time * 1.5);
|
|
305
|
+
if (u_noise > 0.0) {
|
|
306
|
+
w1 = mix(w1, kuiFilterField(uv, u_time, 1.9099, 0.3183, 0.0), u_noise);
|
|
307
|
+
w2 = mix(w2, kuiFilterField(uv, u_time, 1.5915, 0.2387, 47.3), u_noise);
|
|
308
|
+
}
|
|
309
|
+
w1 *= 0.015;
|
|
310
|
+
w2 *= 0.015;
|
|
311
|
+
float pFactor = u_progress >= 0.0 ? u_progress : 1.0;
|
|
312
|
+
// Audio deepens the wave (up to 2x) without changing its wavelength or speed.
|
|
313
|
+
vec2 offset = vec2(w1, w2) * u_strength * pFactor * audioGain();
|
|
314
|
+
fragColor = texture(u_image, uv + offset) * u_tint * shapeMask();
|
|
315
|
+
}
|
|
316
|
+
`;
|
|
317
|
+
var PARTICLES_FS = `#version 300 es
|
|
318
|
+
precision highp float;
|
|
319
|
+
in vec2 v_uv;
|
|
320
|
+
out vec4 fragColor;
|
|
321
|
+
uniform sampler2D u_image;
|
|
322
|
+
uniform float u_time;
|
|
323
|
+
uniform float u_strength;
|
|
324
|
+
uniform vec4 u_tint;
|
|
325
|
+
uniform float u_progress;
|
|
326
|
+
${AUDIO_GAIN_GLSL}${SHAPE_MASK_GLSL}${NOISE_GLSL}${FILTER_NOISE_GLSL}
|
|
327
|
+
// No u_mouse here, deliberately. This program declared one from the day it was written and never
|
|
328
|
+
// read it, so particles has never responded to the pointer \u2014 the declaration was a promise the
|
|
329
|
+
// body did not keep, and the compiler dropped it anyway (an unused uniform gets no location, so
|
|
330
|
+
// uploadCoordUniforms was already skipping it and removing it changes no pixel). It is deleted
|
|
331
|
+
// rather than wired because wiring it would restyle a mode that is already in use without anyone
|
|
332
|
+
// asking, which is the same argument FILTER_NOISE_GLSL's doc makes for noise: being opt-in. The
|
|
333
|
+
// pointer gesture that *was* asked for lives on the generative program below, under hover:.
|
|
334
|
+
void main() {
|
|
335
|
+
vec2 uv = v_uv;
|
|
336
|
+
vec2 grid = fract(uv * 40.0) - 0.5;
|
|
337
|
+
float dist = length(grid);
|
|
338
|
+
// 100 rad across the box and 5 rad/s is 15.915 cycles at 0.7958 a second. That is by far the
|
|
339
|
+
// highest frequency of the four, and deliberately so: the trig term here is a diagonal travelling
|
|
340
|
+
// stripe, which is the most visible of the four repeats. Under noise the stripe becomes a
|
|
341
|
+
// scintillation, which is the largest change of the set and is the reason the parameter exists
|
|
342
|
+
// rather than the swap being unconditional.
|
|
343
|
+
//
|
|
344
|
+
// The clamp is not damage control. Mapped through KUI_FBM_SINE_RMS the field reaches 0 and 1
|
|
345
|
+
// often, which is what a sine does too \u2014 sin()*0.5+0.5 is arcsine-distributed and spends most of
|
|
346
|
+
// its time at the extremes. Clamping reproduces that; a soft field would give a duller sparkle
|
|
347
|
+
// than the one being replaced.
|
|
348
|
+
//
|
|
349
|
+
// Cost: this is one fbm per pixel, three with warp on, over the whole quad. At detail 6 the
|
|
350
|
+
// finest octave here sits near 509 cycles across the box, which is sub-pixel on a small element
|
|
351
|
+
// and will alias \u2014 high detail belongs on the low-frequency modes.
|
|
352
|
+
float sparkle = sin(u_time * 5.0 + dot(uv, vec2(100.0))) * 0.5 + 0.5;
|
|
353
|
+
if (u_noise > 0.0) {
|
|
354
|
+
float n = clamp(kuiFilterField(uv, u_time, 15.915, 0.7958, 0.0) * 0.5 + 0.5, 0.0, 1.0);
|
|
355
|
+
sparkle = mix(sparkle, n, u_noise);
|
|
356
|
+
}
|
|
357
|
+
vec4 tex = texture(u_image, uv);
|
|
358
|
+
float pFactor = u_progress >= 0.0 ? u_progress : 1.0;
|
|
359
|
+
float dotMask = smoothstep(0.4, 0.2, dist) * u_strength * pFactor;
|
|
360
|
+
// Audio brightens the sparkle (0.3 -> at most 0.6) rather than growing the dots: the mask is
|
|
361
|
+
// already multiplied by an unbounded u_strength and feeds a mix(), so widening it would
|
|
362
|
+
// extrapolate past the texture over a larger and larger area.
|
|
363
|
+
fragColor = mix(tex, tex * u_tint + sparkle * 0.3 * audioGain(), dotMask) * shapeMask();
|
|
364
|
+
}
|
|
365
|
+
`;
|
|
366
|
+
var MORPH_FS = `#version 300 es
|
|
367
|
+
precision highp float;
|
|
368
|
+
in vec2 v_uv;
|
|
369
|
+
out vec4 fragColor;
|
|
370
|
+
uniform sampler2D u_image;
|
|
371
|
+
uniform sampler2D u_image_to;
|
|
372
|
+
uniform float u_time;
|
|
373
|
+
uniform float u_strength;
|
|
374
|
+
uniform float u_progress;
|
|
375
|
+
${AUDIO_GAIN_GLSL}${SHAPE_MASK_GLSL}${NOISE_GLSL}${FILTER_NOISE_GLSL}
|
|
376
|
+
void main() {
|
|
377
|
+
vec2 uv = v_uv;
|
|
378
|
+
float progress = u_progress >= 0.0 ? clamp(u_progress, 0.0, 1.0) : clamp(u_strength, 0.0, 1.0);
|
|
379
|
+
// Audio wobbles the crossfade harder. Deliberately not applied to u_progress: the morph's
|
|
380
|
+
// position between the two images belongs to the author (or to scroll), and letting a beat
|
|
381
|
+
// drive it would make the transition jump backwards on every quiet frame.
|
|
382
|
+
//
|
|
383
|
+
// 20 rad and 1 rad/s is 3.1831 cycles at 0.1592 a second. The 0.7071 is the one place the shared
|
|
384
|
+
// RMS gain is wrong by construction: the term being replaced is a *product* of two unit sines,
|
|
385
|
+
// whose RMS is 0.5 rather than a single sine's 0.7071, so matching it means taking the field
|
|
386
|
+
// down by that ratio. Without it the displacement under noise would be about 40% deeper than
|
|
387
|
+
// the one it stands in for \u2014 a visibly stronger morph, not the same one better textured.
|
|
388
|
+
float noise = sin(uv.x * 20.0 + u_time) * cos(uv.y * 20.0 + u_time);
|
|
389
|
+
if (u_noise > 0.0) {
|
|
390
|
+
noise = mix(noise, kuiFilterField(uv, u_time, 3.1831, 0.1592, 0.0) * 0.7071, u_noise);
|
|
391
|
+
}
|
|
392
|
+
noise = noise * 0.05 * audioGain();
|
|
393
|
+
vec4 c1 = texture(u_image, uv + vec2(noise * (1.0 - progress)));
|
|
394
|
+
vec4 c2 = texture(u_image_to, uv - vec2(noise * progress));
|
|
395
|
+
fragColor = mix(c1, c2, smoothstep(0.2, 0.8, progress)) * shapeMask();
|
|
396
|
+
}
|
|
397
|
+
`;
|
|
398
|
+
var GRADIENT_FS = `#version 300 es
|
|
399
|
+
precision highp float;
|
|
400
|
+
in vec2 v_uv;
|
|
401
|
+
out vec4 fragColor;
|
|
402
|
+
uniform float u_time;
|
|
403
|
+
uniform float u_strength;
|
|
404
|
+
uniform float u_frequency;
|
|
405
|
+
uniform float u_chromatic;
|
|
406
|
+
uniform float u_iridescence;
|
|
407
|
+
uniform float u_progress;
|
|
408
|
+
uniform float u_seed;
|
|
409
|
+
uniform float u_scale;
|
|
410
|
+
uniform float u_warp;
|
|
411
|
+
uniform float u_grain;
|
|
412
|
+
uniform float u_hue;
|
|
413
|
+
uniform int u_detail;
|
|
414
|
+
uniform int u_bands;
|
|
415
|
+
uniform int u_colorCount;
|
|
416
|
+
uniform vec4 u_colors[5];
|
|
417
|
+
uniform vec4 u_tint;
|
|
418
|
+
uniform sampler2D u_image;
|
|
419
|
+
uniform int u_maskMode;
|
|
420
|
+
uniform vec2 u_mouse;
|
|
421
|
+
uniform int u_hover;
|
|
422
|
+
uniform vec4 u_backdrop;
|
|
423
|
+
uniform float u_angle;
|
|
424
|
+
uniform int u_motion;
|
|
425
|
+
${AUDIO_GAIN_GLSL}${SHAPE_MASK_GLSL}${NOISE_GLSL}
|
|
426
|
+
|
|
427
|
+
/** \`hover:\` as bits, so \`both\` is \`stir | light\` rather than a third code path. */
|
|
428
|
+
const int KUI_HOVER_STIR = 1;
|
|
429
|
+
const int KUI_HOVER_LIGHT = 2;
|
|
430
|
+
|
|
431
|
+
/** \`motion:\` \u2014 \`0\` is \`evolve\`, which is this program's original behaviour and its default. */
|
|
432
|
+
const int KUI_MOTION_DRIFT = 1;
|
|
433
|
+
const int KUI_MOTION_SWIRL = 2;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Rotate a point of the field's domain.
|
|
437
|
+
*
|
|
438
|
+
* \`v_uv\` is y-down, so this matrix \u2014 the ordinary counter-clockwise one \u2014 turns the *picture*
|
|
439
|
+
* clockwise, which is \`transform: rotate()\`'s direction and therefore the one an author already
|
|
440
|
+
* has an intuition for. Every call site passes \`-angle\`: rotating the sample point one way is
|
|
441
|
+
* what rotates what you see the other.
|
|
442
|
+
*/
|
|
443
|
+
vec2 kuiRotate(vec2 v, float a) {
|
|
444
|
+
float c = cos(a);
|
|
445
|
+
float s = sin(a);
|
|
446
|
+
return vec2(v.x * c - v.y * s, v.x * s + v.y * c);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* The pair of factors that carry a centred UV offset into a space that is isotropic **on screen**,
|
|
451
|
+
* and back out again by dividing.
|
|
452
|
+
*
|
|
453
|
+
* \`v_uv\` is normalised per axis, so one unit of \`v_uv.x\` is the box's whole width and one unit of
|
|
454
|
+
* \`v_uv.y\` is its whole height \u2014 two different numbers of screen pixels as soon as the box is not
|
|
455
|
+
* square. Anything round or rigid has to leave that space first. Scaling by \`(k, 1/k)\` for
|
|
456
|
+
* \`k = sqrt(halfWidth / halfHeight)\` puts both axes in units of \`sqrt(width * height)\`: a circle
|
|
457
|
+
* stays a circle, a rotation stays a rotation, and the unit tracks the box's overall size rather
|
|
458
|
+
* than one of its two dimensions. On a square box the pair is exactly \`(1, 1)\` and every user of
|
|
459
|
+
* it degenerates to the plain UV arithmetic it had before.
|
|
460
|
+
*
|
|
461
|
+
* It is a *ratio* of the two half-extents, so it is DPR-invariant \u2014 device pixel ratio scales
|
|
462
|
+
* \`u_maskBox.z\` and \`u_maskBox.w\` together and cancels.
|
|
463
|
+
*
|
|
464
|
+
* A zero half-extent is \`shapeMask()\`'s "no box measured" signal and is read the same way here:
|
|
465
|
+
* fall back to \`(1, 1)\` rather than divide by it.
|
|
466
|
+
*/
|
|
467
|
+
vec2 kuiAspectAxes() {
|
|
468
|
+
if (u_maskBox.z > 0.0 && u_maskBox.w > 0.0) {
|
|
469
|
+
float k = sqrt(u_maskBox.z / u_maskBox.w);
|
|
470
|
+
return vec2(k, 1.0 / k);
|
|
471
|
+
}
|
|
472
|
+
return vec2(1.0);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Turn a centred UV offset by \`a\`, rigidly **on screen** rather than in the field's own stretched
|
|
477
|
+
* space.
|
|
478
|
+
*
|
|
479
|
+
* \`A^-1 \xB7 R(a) \xB7 A\`, with \`A = diag(kuiAspectAxes())\`: leave the per-axis-normalised space, turn
|
|
480
|
+
* there, come back. Rotating without the conjugation turns *and shears* \u2014 on a 160x100 box an
|
|
481
|
+
* authored \`angle: 45deg\` actually travels at 32 degrees, an error that grows with how far from
|
|
482
|
+
* square the box is. \`angle\` is documented as \`transform: rotate()\`'s convention, and this is what
|
|
483
|
+
* makes that true of a wide box and not only of a square one.
|
|
484
|
+
*
|
|
485
|
+
* **Not bit-identical at \`a == 0\`**, and that matters: \`A^-1 \xB7 I \xB7 A\` is \`diag(k * (1/k), (1/k) * k)\`,
|
|
486
|
+
* which in IEEE 754 is about an ulp away from the identity per axis rather than exactly it. Every
|
|
487
|
+
* caller must therefore keep its existing exact-zero guard \u2014 the defaults are unchanged because
|
|
488
|
+
* nothing here executes at them, not because this collapses.
|
|
489
|
+
*
|
|
490
|
+
* \`scale\` is a scalar and commutes with all three matrices, so a zoomed offset can be passed
|
|
491
|
+
* straight in: \`A^-1 R A (zoom * d)\` is \`zoom * (A^-1 R A d)\`.
|
|
492
|
+
*/
|
|
493
|
+
vec2 kuiRotateScreen(vec2 v, float a) {
|
|
494
|
+
vec2 s = kuiAspectAxes();
|
|
495
|
+
return kuiRotate(v * s, a) / s;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* How strongly the pointer reaches this fragment: 1 under it, falling to nothing at the edge of a
|
|
500
|
+
* **round** pool.
|
|
501
|
+
*
|
|
502
|
+
* Round is the whole difficulty. \`v_uv\` and \`u_mouse\` are both normalised per axis, so the
|
|
503
|
+
* obvious \`distance(v_uv, u_mouse)\` draws an *ellipse* as soon as the box is not square \u2014 and the
|
|
504
|
+
* boxes this mode is documented for are full-bleed bands, card faces and footer strips, i.e.
|
|
505
|
+
* precisely the wide ones. \`kuiAspectAxes()\` is the correction, and measuring the offset there
|
|
506
|
+
* makes the pool a circle on screen whose size tracks the box's overall scale rather than one of
|
|
507
|
+
* its two dimensions. On a square box the pair is exactly \`(1, 1)\` and this degenerates to the
|
|
508
|
+
* plain UV distance \`displace\` already uses.
|
|
509
|
+
*
|
|
510
|
+
* Gaussian rather than \`smoothstep\`, because a pool of light has no edge to see.
|
|
511
|
+
*/
|
|
512
|
+
float kuiHoverFalloff() {
|
|
513
|
+
vec2 d = (v_uv - u_mouse) * kuiAspectAxes();
|
|
514
|
+
return exp(-dot(d, d) * 6.0);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* One point of the element's UV square, in the field's own frame.
|
|
519
|
+
*
|
|
520
|
+
* A rigid motion \u2014 one rotation and one translation \u2014 so that carrying the pointer through the
|
|
521
|
+
* same call puts it at the same place relative to the field that it occupies on screen. That is
|
|
522
|
+
* what lets \`stir\` swirl the field about a centre the cursor is actually sitting on, at any
|
|
523
|
+
* \`angle\`, mid-\`drift\`, mid-\`swirl\`.
|
|
524
|
+
*
|
|
525
|
+
* The turn is \`kuiRotateScreen\`, so it is rigid **on screen** on a box of any shape: \`angle: 45deg\`
|
|
526
|
+
* points at 45 degrees on a 160x100 band, and \`motion: drift\` travels along exactly the axis the
|
|
527
|
+
* author named instead of a sheared one.
|
|
528
|
+
*
|
|
529
|
+
* Inert at the defaults by construction: \`orient\` of zero skips the rotation entirely and
|
|
530
|
+
* \`travel\` of zero subtracts an exact zero, so \`motion: evolve\` at \`angle: 0deg\` computes the
|
|
531
|
+
* same \`(v_uv - 0.5) * scale\` this program has always computed, bit for bit. **The guard is the
|
|
532
|
+
* whole of that promise** \u2014 \`kuiRotateScreen\` does not collapse to the identity at zero, it only
|
|
533
|
+
* gets within an ulp, so deleting the guard because "a rotation is cheap" would silently move the
|
|
534
|
+
* last bit of every generative pixel already on the web.
|
|
535
|
+
*/
|
|
536
|
+
vec2 kuiFieldPoint(vec2 uv, float zoom, float orient, float travel) {
|
|
537
|
+
vec2 v = (uv - 0.5) * zoom;
|
|
538
|
+
if (orient != 0.0) v = kuiRotateScreen(v, -orient);
|
|
539
|
+
v.x -= travel;
|
|
540
|
+
return v;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* \`hover: stir\` \u2014 one vortex of the sample point about the pointer.
|
|
545
|
+
*
|
|
546
|
+
* A turn that dies away from the cursor, plus a push of the *sample point* outward, which is the
|
|
547
|
+
* field spiralling inward and is the whole difference between a rotating disc and a stir.
|
|
548
|
+
* \`rel / (length(rel) + eps)\` rather than \`normalize\`, so the push fades to nothing at the centre
|
|
549
|
+
* instead of kicking the one pixel under the cursor in whatever direction the epsilon points.
|
|
550
|
+
*
|
|
551
|
+
* Both halves run inside \`kuiAspectAxes()\`, for the same reason \`kuiHoverFalloff\` does: the
|
|
552
|
+
* falloff that drives this is already a circle on screen, so a vortex measured in raw UV would be
|
|
553
|
+
* a round pool turning an elliptical gesture \u2014 two different ideas of "the same distance from the
|
|
554
|
+
* cursor" in one effect.
|
|
555
|
+
*/
|
|
556
|
+
vec2 kuiStirPoint(vec2 pt, vec2 centre, float zoom, float fall) {
|
|
557
|
+
vec2 s = kuiAspectAxes();
|
|
558
|
+
vec2 rel = (pt - centre) * s;
|
|
559
|
+
vec2 turned = kuiRotate(rel, 1.6 * fall) + rel / (length(rel) + 1e-4) * (0.10 * zoom * fall);
|
|
560
|
+
return centre + turned / s;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* The host image read as a stencil: 1 where the mark is, 0 where the page should show through.
|
|
565
|
+
*
|
|
566
|
+
* Three ways to ask, because a logo file answers the question differently depending on what it is:
|
|
567
|
+
*
|
|
568
|
+
* - \`0\` \u2014 no stencil. \`mode: gradient\`; \`u_image\` is not sampled at all.
|
|
569
|
+
* - \`1\` (\`mask: alpha\`) \u2014 the image's own alpha. An SVG or PNG mark on a transparent ground,
|
|
570
|
+
* which is what a logo asset usually is. The texture is uploaded premultiplied, so \`a\` is the
|
|
571
|
+
* coverage and multiplying all four channels by it stays premultiplied-correct.
|
|
572
|
+
* - \`2\`/\`3\` (\`mask: luma\` / \`luma-invert\`) \u2014 luminance, for a format with no alpha to read. A
|
|
573
|
+
* JPEG has none, so a black-on-white mark under \`alpha\` would give an all-opaque stencil and
|
|
574
|
+
* fill the whole rectangle with field. \`luma\` takes the *bright* pixels as the mark (white on
|
|
575
|
+
* black); \`luma-invert\` takes the dark ones (black on white, which is the common logo file).
|
|
576
|
+
* Both directions exist because guessing one silently produces a perfect negative of the mark \u2014
|
|
577
|
+
* an image that looks deliberate and is exactly wrong.
|
|
578
|
+
*
|
|
579
|
+
* Luminance is taken from the premultiplied colour, so a transparent pixel is black. Under
|
|
580
|
+
* \`luma\` that reads as "not the mark", which is the sensible answer for a file that has alpha and
|
|
581
|
+
* was asked for a luma stencil anyway. Under \`luma-invert\` the same pixel reads as *solid* mark \u2014
|
|
582
|
+
* correct for the opaque JPEG the keyword exists for, and wrong for anything with a transparent
|
|
583
|
+
* surround, which should be using \`alpha\`.
|
|
584
|
+
*/
|
|
585
|
+
float glyphMask() {
|
|
586
|
+
if (u_maskMode == 0) return 1.0;
|
|
587
|
+
vec4 src = texture(u_image, v_uv);
|
|
588
|
+
if (u_maskMode == 1) return src.a;
|
|
589
|
+
float lum = dot(src.rgb, vec3(0.299, 0.587, 0.114));
|
|
590
|
+
return u_maskMode == 2 ? lum : 1.0 - lum;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* The palette ramp, interpolated in linear-light rather than straight sRGB.
|
|
595
|
+
*
|
|
596
|
+
* \`mix()\` between two sRGB triples passes through a desaturated middle \u2014 the classic muddy
|
|
597
|
+
* midpoint of a naive gradient, worst exactly where two saturated brand colours meet. Squaring
|
|
598
|
+
* into approximately linear light, mixing there, and taking the square root back is a two-operation
|
|
599
|
+
* approximation of a gamma-2.0 space that removes almost all of it.
|
|
600
|
+
*
|
|
601
|
+
* \`smoothstep\` on the segment fraction, not a straight lerp, so the ramp has no visible crease at
|
|
602
|
+
* each stop \u2014 a crease that banding (\`u_bands\`) would otherwise land on and amplify.
|
|
603
|
+
*/
|
|
604
|
+
vec3 kuiPalette(float t) {
|
|
605
|
+
int n = max(u_colorCount, 2);
|
|
606
|
+
float x = clamp(t, 0.0, 1.0) * float(n - 1);
|
|
607
|
+
int i = int(floor(x));
|
|
608
|
+
int j = min(i + 1, n - 1);
|
|
609
|
+
float f = smoothstep(0.0, 1.0, x - float(i));
|
|
610
|
+
vec3 a = u_colors[i].rgb;
|
|
611
|
+
vec3 b = u_colors[j].rgb;
|
|
612
|
+
return sqrt(mix(a * a, b * b, f));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Rotate a colour about the grey axis \u2014 Rodrigues' formula, which needs no colour-space change. */
|
|
616
|
+
vec3 kuiHueRotate(vec3 c, float angle) {
|
|
617
|
+
const vec3 axis = vec3(0.57735026);
|
|
618
|
+
float s = sin(angle);
|
|
619
|
+
float k = cos(angle);
|
|
620
|
+
return c * k + cross(axis, c) * s + axis * dot(axis, c) * (1.0 - k);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Per-pixel film grain. Quantised in time so it flickers at ~60 steps a second rather than per frame. */
|
|
624
|
+
float kuiGrain(vec2 fragCoord, float t) {
|
|
625
|
+
uint h = kuiHash(uvec3(uvec2(fragCoord), uint(int(t * 60.0) + 0x10000)));
|
|
626
|
+
return float(h & 0xffffu) / 65535.0 - 0.5;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
void main() {
|
|
630
|
+
// Centred and aspect-free: the field is authored in the element's own UV space, so the same
|
|
631
|
+
// parameters give the same picture whatever size the box is.
|
|
632
|
+
float zoom = max(u_scale, 0.05);
|
|
633
|
+
vec3 seed = vec3(u_seed * 137.31, u_seed * 71.17, u_seed * 29.73);
|
|
634
|
+
float t = u_time;
|
|
635
|
+
float pFactor = u_progress >= 0.0 ? u_progress : 1.0;
|
|
636
|
+
|
|
637
|
+
// Orientation and travel, as one rigid motion of the domain. \`angle\` names the field's axis;
|
|
638
|
+
// \`motion: swirl\` turns that axis over time; \`motion: drift\` slides along it. Both are *added
|
|
639
|
+
// to* the third-axis evolution below rather than replacing it, so a drifting field is still
|
|
640
|
+
// changing as it goes rather than being one frozen picture towed across the box.
|
|
641
|
+
float spin = u_motion == KUI_MOTION_SWIRL ? t * 0.2 : 0.0;
|
|
642
|
+
float orient = u_angle + spin;
|
|
643
|
+
// Multiplied by the zoom, so \`drift\` crosses the box at the same visual rate whatever \`scale\`
|
|
644
|
+
// is. Without it, zooming in would slow the travel down \u2014 one knob quietly moving two things,
|
|
645
|
+
// which is the bug kuiFbm's \`/norm\` divisor exists to avoid on the octave count.
|
|
646
|
+
float travel = u_motion == KUI_MOTION_DRIFT ? t * 0.12 * zoom : 0.0;
|
|
647
|
+
|
|
648
|
+
vec2 p = kuiFieldPoint(v_uv, zoom, orient, travel);
|
|
649
|
+
// One evaluation, shared by both gestures, and skipped entirely when there is no pointer
|
|
650
|
+
// response authored.
|
|
651
|
+
float hoverFall = u_hover != 0 ? kuiHoverFalloff() : 0.0;
|
|
652
|
+
|
|
653
|
+
// Domain warp: a second field displaces the sample point of the first. This is the whole
|
|
654
|
+
// difference between concentric fbm contours and something that reads as liquid.
|
|
655
|
+
//
|
|
656
|
+
// **This displacement is deliberately NOT aspect-corrected, and it is the one thing here that
|
|
657
|
+
// is not.** \`vec2(wx, wy)\` is isotropic in the field's own normalised space, which means it is
|
|
658
|
+
// anisotropic on screen \u2014 the same flaw \`kuiRotateScreen\` exists to fix for \`angle\`, \`motion\`
|
|
659
|
+
// and the \`stir\` vortex. It is left alone because \`warp\` is *shipped and in use*: multiplying
|
|
660
|
+
// it through \`kuiAspectAxes()\` would restyle every live page that authored one, which is a
|
|
661
|
+
// price a correctness argument does not get to charge. The rule the rest of this program follows
|
|
662
|
+
// and this line does not: **orientation and the pointer vortex are screen-true; the domain warp
|
|
663
|
+
// is field-true.** If you are here to "finish the job", that is a behaviour change to real
|
|
664
|
+
// sites and it needs the owner, not a tidy-up commit.
|
|
665
|
+
vec2 q = p;
|
|
666
|
+
if (u_warp > 0.0) {
|
|
667
|
+
float wf = u_frequency * 0.1;
|
|
668
|
+
float wx = kuiFbm(vec3(p * wf, t * 0.35) + seed, u_detail);
|
|
669
|
+
float wy = kuiFbm(vec3(p * wf + 5.2, t * 0.35 + 1.3) + seed, u_detail);
|
|
670
|
+
q += vec2(wx, wy) * u_warp;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// \`hover: stir\` \u2014 the cursor dragged through the colour, like stirring thick paint.
|
|
674
|
+
//
|
|
675
|
+
// Applied here, last, to the finished sample point: the gesture is dragging paint that is
|
|
676
|
+
// already there, not re-warping the field before it exists. It also means \`stir\` works at the
|
|
677
|
+
// library defaults, where \`warp\` is 0 and there is no domain warp to ride on.
|
|
678
|
+
//
|
|
679
|
+
// A vortex, which is one gesture and not two \u2014 see \`kuiStirPoint\`, which also explains why both
|
|
680
|
+
// halves of it are measured in the same screen-isotropic space as the falloff that drives them.
|
|
681
|
+
//
|
|
682
|
+
// On \`mode: logo\` this cannot touch the mark's edges: \`glyphMask()\` samples \`u_image\` at the
|
|
683
|
+
// undistorted \`v_uv\` and multiplies the finished colour, so stirring \`q\` swirls the fill and
|
|
684
|
+
// leaves the stencil exactly where it was.
|
|
685
|
+
if ((u_hover & KUI_HOVER_STIR) != 0) {
|
|
686
|
+
vec2 pm = kuiFieldPoint(u_mouse, zoom, orient, travel);
|
|
687
|
+
q = kuiStirPoint(q, pm, zoom, hoverFall);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Audio and progress scale the field's *amplitude*, leaving its shape and rate alone \u2014 so a
|
|
691
|
+
// beat swells the contrast and a scroll at 0 leaves a flat single colour rather than a jump cut.
|
|
692
|
+
float field = kuiFbm(vec3(q, t * 0.25) + seed, u_detail);
|
|
693
|
+
field *= u_strength * pFactor * audioGain();
|
|
694
|
+
float v = clamp(field * 0.5 + 0.5, 0.0, 1.0);
|
|
695
|
+
|
|
696
|
+
// Iridescence wraps the ramp back on itself, so one palette reads as many sheens across the box.
|
|
697
|
+
float wrapped = u_iridescence > 0.0 ? fract(v * u_iridescence) : v;
|
|
698
|
+
|
|
699
|
+
// Posterise. Divided by bands-1 so the top step reaches the end of the ramp; a plain /bands
|
|
700
|
+
// would stop one step short and quietly desaturate the brightest colour.
|
|
701
|
+
if (u_bands > 0) {
|
|
702
|
+
float steps = float(u_bands);
|
|
703
|
+
wrapped = floor(wrapped * steps) / max(steps - 1.0, 1.0);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Fringing offsets the *ramp position* per channel rather than re-sampling the field three
|
|
707
|
+
// times: three palette lookups are nearly free, three more fbm evaluations would triple the
|
|
708
|
+
// program's cost for a decorative edge.
|
|
709
|
+
vec3 color;
|
|
710
|
+
if (u_chromatic > 0.0) {
|
|
711
|
+
float d = u_chromatic * 0.04;
|
|
712
|
+
color = vec3(
|
|
713
|
+
kuiPalette(wrapped + d).r,
|
|
714
|
+
kuiPalette(wrapped).g,
|
|
715
|
+
kuiPalette(wrapped - d).b
|
|
716
|
+
);
|
|
717
|
+
} else {
|
|
718
|
+
color = kuiPalette(wrapped);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (u_hue != 0.0) color = kuiHueRotate(color, u_hue);
|
|
722
|
+
|
|
723
|
+
// \`hover: light\` \u2014 a torch held over the surface.
|
|
724
|
+
//
|
|
725
|
+
// A multiply rather than a blend toward white. Multiplying preserves the hue and *raises* the
|
|
726
|
+
// saturation, so what appears under the pointer is the author's own colour becoming vivid;
|
|
727
|
+
// mixing toward white would wash the palette out at exactly the point the eye is on. The gain is
|
|
728
|
+
// 1.0 in the far field, which is what makes this a highlight rather than a restyling \u2014 a palette
|
|
729
|
+
// already at the top of the range has nowhere to go, and that is the honest behaviour of a torch.
|
|
730
|
+
if ((u_hover & KUI_HOVER_LIGHT) != 0) color *= 1.0 + 0.7 * hoverFall;
|
|
731
|
+
|
|
732
|
+
if (u_grain > 0.0) color += kuiGrain(gl_FragCoord.xy, t) * u_grain * 0.25;
|
|
733
|
+
|
|
734
|
+
// Opaque before masking, and premultiplied trivially because alpha is 1 \u2014 the element's own
|
|
735
|
+
// shape and its ancestors' opacity arrive through shapeMask(), the mark's shape through
|
|
736
|
+
// glyphMask(), and both multiply all four channels, which is the right operation on
|
|
737
|
+
// premultiplied colour and is what leaves the page visible around a logo.
|
|
738
|
+
float sm = shapeMask();
|
|
739
|
+
float gm = glyphMask();
|
|
740
|
+
vec4 px = vec4(clamp(color, 0.0, 1.0), 1.0) * u_tint * sm * gm;
|
|
741
|
+
|
|
742
|
+
// \`backdrop\` \u2014 the colour the field is composited *over*, inside the element's own shape.
|
|
743
|
+
//
|
|
744
|
+
// Premultiplied "over", because everything in this program already is. \`u_tint.a * gm\` is the
|
|
745
|
+
// field's coverage before the shape mask, so the backdrop is pulled through the same \`sm\` as the
|
|
746
|
+
// field rather than being masked twice \u2014 which would otherwise thin the backdrop along an
|
|
747
|
+
// antialiased rounded corner.
|
|
748
|
+
//
|
|
749
|
+
// What it is *for*: \`mode: logo\`'s plate. Outside the mark the coverage is zero and the backdrop
|
|
750
|
+
// is all there is, so a mark gets a solid ground instead of the page showing through. A bare
|
|
751
|
+
// \`gradient\` is opaque, so there this is a no-op by construction \u2014 visible only through a \`tint\`
|
|
752
|
+
// whose alpha is below 1. That is a property of an opaque field, not an oversight.
|
|
753
|
+
if (u_backdrop.a > 0.0) {
|
|
754
|
+
px += vec4(u_backdrop.rgb * u_backdrop.a, u_backdrop.a) * sm * (1.0 - u_tint.a * gm);
|
|
755
|
+
}
|
|
756
|
+
fragColor = px;
|
|
757
|
+
}
|
|
758
|
+
`;
|
|
759
|
+
|
|
760
|
+
// src/advanced/base.ts
|
|
761
|
+
function clamp(val, min, max) {
|
|
762
|
+
return Math.min(Math.max(val, min), max);
|
|
763
|
+
}
|
|
764
|
+
function styleOf(ledgers, el) {
|
|
765
|
+
if (!el || typeof el.hasAttribute !== "function") return null;
|
|
766
|
+
if (!el.style) return null;
|
|
767
|
+
return ledgers.style(el);
|
|
768
|
+
}
|
|
769
|
+
function createAdvancedLedgers(host) {
|
|
770
|
+
const claim = createStyleClaim();
|
|
771
|
+
return {
|
|
772
|
+
style: (el) => claim.style(el),
|
|
773
|
+
/*
|
|
774
|
+
* One element's teardown must not cost the rest of the subtree's.
|
|
775
|
+
*
|
|
776
|
+
* This loop is the *only* thing that ever gives a found element back — a `camera-layer`, a
|
|
777
|
+
* `scene-step` — because nothing else holds a claim on it. `createStyleLedger.restore()` ends
|
|
778
|
+
* in `removeProperty`/`setProperty`/`removeAttribute` calls straight against the author's
|
|
779
|
+
* element, and any of those can throw on a page that patched the CSSOM, froze a node, or runs a
|
|
780
|
+
* `MutationObserver` that throws in the same task. An unguarded loop therefore turned one such
|
|
781
|
+
* element into *every element after it in the claim set*, host included, keeping this library's
|
|
782
|
+
* inline transforms permanently: `openClaimBook.leave` drops the claim before the restore that
|
|
783
|
+
* threw, so those elements were never coming back — no later owner can be the last one out of
|
|
784
|
+
* an entry the failed sweep already deleted, and `createEffectInstance.destroy()` latches
|
|
785
|
+
* `isDestroyed` before `opts.destroy()`, so a second `destroy()` cannot retry either.
|
|
786
|
+
*
|
|
787
|
+
* The failures are collected rather than swallowed: `destroy()` throwing is contained upstream
|
|
788
|
+
* (`js-effect-preparer.ts` warns and moves on; the animator's `runQuietly` swallows), and an
|
|
789
|
+
* element the library genuinely could not hand back is worth saying so about. Aggregated so the
|
|
790
|
+
* first failure does not hide the fifth.
|
|
791
|
+
*/
|
|
792
|
+
restore() {
|
|
793
|
+
const failures = [];
|
|
794
|
+
const release = (el) => {
|
|
795
|
+
try {
|
|
796
|
+
claim.release(el);
|
|
797
|
+
} catch (error) {
|
|
798
|
+
failures.push(error);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
for (const el of claim.elements()) {
|
|
802
|
+
if (el !== host) release(el);
|
|
803
|
+
}
|
|
804
|
+
release(host);
|
|
805
|
+
if (failures.length > 0) {
|
|
806
|
+
throw new AggregateError(
|
|
807
|
+
failures,
|
|
808
|
+
`kuinetic: ${failures.length} element(s) could not be given back`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
elements: () => claim.elements()
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
var effectNamePatterns = /* @__PURE__ */ new Map();
|
|
816
|
+
function effectNamePattern(name) {
|
|
817
|
+
let pattern = effectNamePatterns.get(name);
|
|
818
|
+
if (!pattern) {
|
|
819
|
+
pattern = new RegExp(String.raw`(?:^|,)\s*${name}(?=[\s,]|$)`);
|
|
820
|
+
effectNamePatterns.set(name, pattern);
|
|
821
|
+
}
|
|
822
|
+
return pattern;
|
|
823
|
+
}
|
|
824
|
+
function declaresEffect(el, name) {
|
|
825
|
+
if (typeof el.getAttribute !== "function") return false;
|
|
826
|
+
const attr = el.getAttribute("data-kui");
|
|
827
|
+
return Boolean(attr) && effectNamePattern(name).test(attr);
|
|
828
|
+
}
|
|
829
|
+
function isNearestHostOfKind(host, el, hostEffect) {
|
|
830
|
+
let node = el?.parentElement;
|
|
831
|
+
while (node && node !== host) {
|
|
832
|
+
if (declaresEffect(node, hostEffect)) return false;
|
|
833
|
+
node = node.parentElement;
|
|
834
|
+
}
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
function ownedDescendants(host, hostEffect, descendantEffect) {
|
|
838
|
+
if (typeof host.querySelectorAll !== "function") return [];
|
|
839
|
+
const owned = [];
|
|
840
|
+
for (const el of host.querySelectorAll(`[data-kui*="${descendantEffect}"]`)) {
|
|
841
|
+
if (declaresEffect(el, descendantEffect) && isNearestHostOfKind(host, el, hostEffect)) owned.push(el);
|
|
842
|
+
}
|
|
843
|
+
return owned;
|
|
844
|
+
}
|
|
845
|
+
function lerp(start, end, progress) {
|
|
846
|
+
return start + (end - start) * progress;
|
|
847
|
+
}
|
|
848
|
+
function defaultRaf(win, fn) {
|
|
849
|
+
if (win && typeof win.requestAnimationFrame === "function") {
|
|
850
|
+
return win.requestAnimationFrame(fn);
|
|
851
|
+
}
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
function defaultCaf(win, id) {
|
|
855
|
+
if (win && typeof win.cancelAnimationFrame === "function" && id != null) {
|
|
856
|
+
win.cancelAnimationFrame(id);
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
function defaultSetTimer(fn, ms) {
|
|
861
|
+
if (typeof globalThis.setTimeout !== "function") return null;
|
|
862
|
+
return globalThis.setTimeout(fn, ms);
|
|
863
|
+
}
|
|
864
|
+
function defaultClearTimer(id) {
|
|
865
|
+
if (typeof globalThis.clearTimeout === "function" && id != null) {
|
|
866
|
+
globalThis.clearTimeout(id);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function resolveWindow(ctx, env) {
|
|
870
|
+
if (ctx?.win !== void 0) return ctx.win;
|
|
871
|
+
if (env.window !== void 0) return env.window;
|
|
872
|
+
return typeof window !== "undefined" ? window : null;
|
|
873
|
+
}
|
|
874
|
+
function resolveDocument(ctx, env) {
|
|
875
|
+
if (ctx?.doc !== void 0) return ctx.doc;
|
|
876
|
+
if (env.document !== void 0) return env.document;
|
|
877
|
+
return typeof document !== "undefined" ? document : null;
|
|
878
|
+
}
|
|
879
|
+
var defaultCanvasFactories = /* @__PURE__ */ new WeakMap();
|
|
880
|
+
var noDocumentCanvas = () => null;
|
|
881
|
+
function resolveCreateCanvas(c, env, doc) {
|
|
882
|
+
if (c?.createCanvas) return c.createCanvas;
|
|
883
|
+
if (env.createCanvas) return env.createCanvas;
|
|
884
|
+
if (!doc) return noDocumentCanvas;
|
|
885
|
+
const cached = defaultCanvasFactories.get(doc);
|
|
886
|
+
if (cached) return cached;
|
|
887
|
+
const factory = () => typeof doc.createElement === "function" ? doc.createElement("canvas") : null;
|
|
888
|
+
defaultCanvasFactories.set(doc, factory);
|
|
889
|
+
return factory;
|
|
890
|
+
}
|
|
891
|
+
function resolveRaf(c, env, win) {
|
|
892
|
+
if (c?.raf) return c.raf;
|
|
893
|
+
if (env.raf) return env.raf;
|
|
894
|
+
return (fn) => defaultRaf(win, fn);
|
|
895
|
+
}
|
|
896
|
+
function resolveCaf(c, env, win) {
|
|
897
|
+
if (c?.caf) return c.caf;
|
|
898
|
+
if (env.caf) return env.caf;
|
|
899
|
+
return (id) => defaultCaf(win, id);
|
|
900
|
+
}
|
|
901
|
+
function resolveTimers(c, env) {
|
|
902
|
+
if (c?.setTimer && c.clearTimer) return [c.setTimer, c.clearTimer];
|
|
903
|
+
if (env.setTimer && env.clearTimer) return [env.setTimer, env.clearTimer];
|
|
904
|
+
return [defaultSetTimer, defaultClearTimer];
|
|
905
|
+
}
|
|
906
|
+
function resolveEnv(ctx = null, env = {}) {
|
|
907
|
+
const c = ctx;
|
|
908
|
+
const win = resolveWindow(c, env);
|
|
909
|
+
const doc = resolveDocument(c, env);
|
|
910
|
+
const [setTimer, clearTimer] = resolveTimers(c, env);
|
|
911
|
+
return {
|
|
912
|
+
window: win,
|
|
913
|
+
document: doc,
|
|
914
|
+
createCanvas: resolveCreateCanvas(c, env, doc),
|
|
915
|
+
raf: resolveRaf(c, env, win),
|
|
916
|
+
caf: resolveCaf(c, env, win),
|
|
917
|
+
setTimer,
|
|
918
|
+
clearTimer
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
function isReducedMotion(ctx) {
|
|
922
|
+
if (!ctx) return false;
|
|
923
|
+
const rm = ctx.reducedMotion;
|
|
924
|
+
if (rm === true) return true;
|
|
925
|
+
if (typeof rm === "object" && rm && rm.enabled === true) return true;
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
function createEffectInstance(opts = {}) {
|
|
929
|
+
let isDestroyed = false;
|
|
930
|
+
let isFinished = false;
|
|
931
|
+
let resolveFinished;
|
|
932
|
+
const finishedPromise = new Promise((resolve) => {
|
|
933
|
+
resolveFinished = resolve;
|
|
934
|
+
});
|
|
935
|
+
return {
|
|
936
|
+
continuous: Boolean(opts.continuous),
|
|
937
|
+
activate() {
|
|
938
|
+
if (isDestroyed) return;
|
|
939
|
+
if (typeof opts.activate === "function") opts.activate();
|
|
940
|
+
},
|
|
941
|
+
cancel() {
|
|
942
|
+
if (isDestroyed) return;
|
|
943
|
+
if (typeof opts.cancel === "function") opts.cancel();
|
|
944
|
+
if (!isFinished && resolveFinished) {
|
|
945
|
+
isFinished = true;
|
|
946
|
+
resolveFinished();
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
finish() {
|
|
950
|
+
if (isDestroyed) return;
|
|
951
|
+
if (typeof opts.finish === "function") opts.finish();
|
|
952
|
+
if (!isFinished && resolveFinished) {
|
|
953
|
+
isFinished = true;
|
|
954
|
+
resolveFinished();
|
|
955
|
+
}
|
|
956
|
+
},
|
|
957
|
+
finished: opts.continuous ? Promise.resolve() : finishedPromise,
|
|
958
|
+
/*
|
|
959
|
+
* `finally`, because `destroy()` is the one call that cannot be made twice.
|
|
960
|
+
*
|
|
961
|
+
* `cancel()` and `finish()` throwing leave the instance alive, so a later call still settles
|
|
962
|
+
* `finished`. `destroy()` latches `isDestroyed` first — deliberately, so that a teardown which
|
|
963
|
+
* threw partway cannot be re-entered and a later `activate()` cannot resurrect a destroyed
|
|
964
|
+
* effect through ledgers that rejoin on write (`owned-styles.ts`). The cost of that latch was
|
|
965
|
+
* that a throwing `opts.destroy()` also skipped the resolve below, and every subsequent call
|
|
966
|
+
* returned at the guard: a non-continuous instance's `finished` then never settled at all, so
|
|
967
|
+
* anything awaiting it waited for the life of the page. The teardown still throws — the caller
|
|
968
|
+
* is told — but the promise it advertises is honoured on the way out.
|
|
969
|
+
*/
|
|
970
|
+
destroy() {
|
|
971
|
+
if (isDestroyed) return;
|
|
972
|
+
isDestroyed = true;
|
|
973
|
+
try {
|
|
974
|
+
if (typeof opts.destroy === "function") opts.destroy();
|
|
975
|
+
} finally {
|
|
976
|
+
if (!isFinished && resolveFinished) {
|
|
977
|
+
isFinished = true;
|
|
978
|
+
resolveFinished();
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
function createInertInstance(destroy) {
|
|
985
|
+
let isDestroyed = false;
|
|
986
|
+
return {
|
|
987
|
+
continuous: false,
|
|
988
|
+
activate() {
|
|
989
|
+
},
|
|
990
|
+
cancel() {
|
|
991
|
+
},
|
|
992
|
+
finish() {
|
|
993
|
+
},
|
|
994
|
+
finished: Promise.resolve(),
|
|
995
|
+
destroy() {
|
|
996
|
+
if (isDestroyed) return;
|
|
997
|
+
isDestroyed = true;
|
|
998
|
+
if (typeof destroy === "function") destroy();
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function toList(val) {
|
|
1003
|
+
return Array.isArray(val) ? val : [val];
|
|
1004
|
+
}
|
|
1005
|
+
function hasRegistrarShape(value) {
|
|
1006
|
+
if (!value || typeof value !== "object") return false;
|
|
1007
|
+
const v = value;
|
|
1008
|
+
return typeof v.registerPrimitives === "function" && typeof v.registerPresets === "function";
|
|
1009
|
+
}
|
|
1010
|
+
function asRegistry(target) {
|
|
1011
|
+
if (!target || typeof target !== "object") return void 0;
|
|
1012
|
+
if (hasRegistrarShape(target)) return target;
|
|
1013
|
+
const host = target;
|
|
1014
|
+
if (hasRegistrarShape(host.registry)) return host.registry;
|
|
1015
|
+
return void 0;
|
|
1016
|
+
}
|
|
1017
|
+
function registerInto(target, primitives, presets, name = "module") {
|
|
1018
|
+
const reg = asRegistry(target);
|
|
1019
|
+
if (!reg) {
|
|
1020
|
+
throw new Error(`kuinetic: register${name} requires a Registry or Animator instance`);
|
|
1021
|
+
}
|
|
1022
|
+
reg.registerPrimitives(toList(primitives));
|
|
1023
|
+
reg.registerPresets(toList(presets));
|
|
1024
|
+
return target;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// src/advanced/gl-utils.ts
|
|
1028
|
+
function compileShader(gl, type, source) {
|
|
1029
|
+
const shader = gl.createShader(type);
|
|
1030
|
+
if (!shader) return null;
|
|
1031
|
+
gl.shaderSource(shader, source);
|
|
1032
|
+
gl.compileShader(shader);
|
|
1033
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
1034
|
+
gl.deleteShader(shader);
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
return shader;
|
|
1038
|
+
}
|
|
1039
|
+
function createProgram(gl, vsSource, fsSource) {
|
|
1040
|
+
const vs = compileShader(gl, gl.VERTEX_SHADER, vsSource);
|
|
1041
|
+
const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
|
1042
|
+
if (!vs || !fs) {
|
|
1043
|
+
if (vs) gl.deleteShader(vs);
|
|
1044
|
+
if (fs) gl.deleteShader(fs);
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
const program = gl.createProgram();
|
|
1048
|
+
if (!program) {
|
|
1049
|
+
gl.deleteShader(vs);
|
|
1050
|
+
gl.deleteShader(fs);
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
gl.attachShader(program, vs);
|
|
1054
|
+
gl.attachShader(program, fs);
|
|
1055
|
+
gl.linkProgram(program);
|
|
1056
|
+
gl.deleteShader(vs);
|
|
1057
|
+
gl.deleteShader(fs);
|
|
1058
|
+
if (typeof gl.getProgramParameter === "function" && !gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
1059
|
+
gl.deleteProgram(program);
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
return program;
|
|
1063
|
+
}
|
|
1064
|
+
function extractLocations(gl, program) {
|
|
1065
|
+
if (!program) return null;
|
|
1066
|
+
return {
|
|
1067
|
+
program,
|
|
1068
|
+
a_position: gl.getAttribLocation(program, "a_position"),
|
|
1069
|
+
u_image: gl.getUniformLocation(program, "u_image"),
|
|
1070
|
+
u_image_to: gl.getUniformLocation(program, "u_image_to"),
|
|
1071
|
+
u_uvOrigin: gl.getUniformLocation(program, "u_uvOrigin"),
|
|
1072
|
+
u_uvScale: gl.getUniformLocation(program, "u_uvScale"),
|
|
1073
|
+
u_maskBox: gl.getUniformLocation(program, "u_maskBox"),
|
|
1074
|
+
u_maskRx: gl.getUniformLocation(program, "u_maskRx"),
|
|
1075
|
+
u_maskRy: gl.getUniformLocation(program, "u_maskRy"),
|
|
1076
|
+
u_maskAlpha: gl.getUniformLocation(program, "u_maskAlpha"),
|
|
1077
|
+
u_time: gl.getUniformLocation(program, "u_time"),
|
|
1078
|
+
u_strength: gl.getUniformLocation(program, "u_strength"),
|
|
1079
|
+
u_frequency: gl.getUniformLocation(program, "u_frequency"),
|
|
1080
|
+
u_chromatic: gl.getUniformLocation(program, "u_chromatic"),
|
|
1081
|
+
u_iridescence: gl.getUniformLocation(program, "u_iridescence"),
|
|
1082
|
+
u_mouse: gl.getUniformLocation(program, "u_mouse"),
|
|
1083
|
+
u_tint: gl.getUniformLocation(program, "u_tint"),
|
|
1084
|
+
u_color1: gl.getUniformLocation(program, "u_color1"),
|
|
1085
|
+
u_color2: gl.getUniformLocation(program, "u_color2"),
|
|
1086
|
+
u_duotone: gl.getUniformLocation(program, "u_duotone"),
|
|
1087
|
+
u_blend: gl.getUniformLocation(program, "u_blend"),
|
|
1088
|
+
u_progress: gl.getUniformLocation(program, "u_progress"),
|
|
1089
|
+
u_seed: gl.getUniformLocation(program, "u_seed"),
|
|
1090
|
+
u_scale: gl.getUniformLocation(program, "u_scale"),
|
|
1091
|
+
u_warp: gl.getUniformLocation(program, "u_warp"),
|
|
1092
|
+
u_grain: gl.getUniformLocation(program, "u_grain"),
|
|
1093
|
+
u_hue: gl.getUniformLocation(program, "u_hue"),
|
|
1094
|
+
u_detail: gl.getUniformLocation(program, "u_detail"),
|
|
1095
|
+
u_bands: gl.getUniformLocation(program, "u_bands"),
|
|
1096
|
+
u_colorCount: gl.getUniformLocation(program, "u_colorCount"),
|
|
1097
|
+
u_colors: gl.getUniformLocation(program, "u_colors"),
|
|
1098
|
+
// Which stencil the generative program applies: 0 none (`gradient`), 1 alpha, 2 luma,
|
|
1099
|
+
// 3 inverted luma. See `glyphMask()` in `glsl.ts`.
|
|
1100
|
+
u_maskMode: gl.getUniformLocation(program, "u_maskMode"),
|
|
1101
|
+
u_hover: gl.getUniformLocation(program, "u_hover"),
|
|
1102
|
+
u_backdrop: gl.getUniformLocation(program, "u_backdrop"),
|
|
1103
|
+
u_angle: gl.getUniformLocation(program, "u_angle"),
|
|
1104
|
+
u_motion: gl.getUniformLocation(program, "u_motion")
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
function disposeGLResources(gl, quadBuffer, programs) {
|
|
1108
|
+
if (!gl) return;
|
|
1109
|
+
if (quadBuffer && gl.deleteBuffer) gl.deleteBuffer(quadBuffer);
|
|
1110
|
+
for (const info of Object.values(programs)) {
|
|
1111
|
+
const prog = info && typeof info === "object" && "program" in info ? info.program : info;
|
|
1112
|
+
if (prog && gl.deleteProgram) gl.deleteProgram(prog);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
function sourceExtent(source) {
|
|
1116
|
+
const s = source;
|
|
1117
|
+
const w = Number(s.naturalWidth ?? s.videoWidth ?? s.width ?? 0);
|
|
1118
|
+
const h = Number(s.naturalHeight ?? s.videoHeight ?? s.height ?? 0);
|
|
1119
|
+
return [Number.isFinite(w) ? w : 0, Number.isFinite(h) ? h : 0];
|
|
1120
|
+
}
|
|
1121
|
+
function exceedsTextureLimit(gl, source) {
|
|
1122
|
+
if (typeof gl.getParameter !== "function") return false;
|
|
1123
|
+
const limit = Number(gl.getParameter(gl.MAX_TEXTURE_SIZE));
|
|
1124
|
+
if (!Number.isFinite(limit) || limit <= 0) return false;
|
|
1125
|
+
const [w, h] = sourceExtent(source);
|
|
1126
|
+
return w > limit || h > limit;
|
|
1127
|
+
}
|
|
1128
|
+
function drainGLErrors(gl) {
|
|
1129
|
+
if (typeof gl.getError !== "function") return;
|
|
1130
|
+
for (let i = 0; i < 8; i += 1) {
|
|
1131
|
+
if (Number(gl.getError()) === 0) return;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function glRaisedError(gl) {
|
|
1135
|
+
if (typeof gl.getError !== "function") return false;
|
|
1136
|
+
const code = gl.getError();
|
|
1137
|
+
return typeof code === "number" && code !== 0;
|
|
1138
|
+
}
|
|
1139
|
+
function createGLTexture(gl, source) {
|
|
1140
|
+
if (exceedsTextureLimit(gl, source)) return null;
|
|
1141
|
+
const tex = gl.createTexture();
|
|
1142
|
+
if (!tex) return null;
|
|
1143
|
+
drainGLErrors(gl);
|
|
1144
|
+
try {
|
|
1145
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1146
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1147
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1148
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1149
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1150
|
+
if (typeof gl.pixelStorei === "function") gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
|
|
1151
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
1152
|
+
if (glRaisedError(gl)) {
|
|
1153
|
+
if (gl.deleteTexture) gl.deleteTexture(tex);
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
return tex;
|
|
1157
|
+
} catch {
|
|
1158
|
+
if (gl.deleteTexture) gl.deleteTexture(tex);
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// src/advanced/audio.ts
|
|
1164
|
+
var AUDIO_CHANNELS = [
|
|
1165
|
+
"--kui-audio-bass",
|
|
1166
|
+
"--kui-audio-mid",
|
|
1167
|
+
"--kui-audio-treble",
|
|
1168
|
+
"--kui-audio-level",
|
|
1169
|
+
"--kui-audio"
|
|
1170
|
+
];
|
|
1171
|
+
var AUDIO_BAND_PROPERTIES = {
|
|
1172
|
+
bass: "--kui-audio-bass",
|
|
1173
|
+
mid: "--kui-audio-mid",
|
|
1174
|
+
treble: "--kui-audio-treble",
|
|
1175
|
+
level: "--kui-audio-level"
|
|
1176
|
+
};
|
|
1177
|
+
var AUDIO_BANDS = Object.keys(AUDIO_BAND_PROPERTIES);
|
|
1178
|
+
function parseBandValue(raw) {
|
|
1179
|
+
if (!raw) return null;
|
|
1180
|
+
const val = parseFloat(raw);
|
|
1181
|
+
return Number.isFinite(val) ? clamp(val, 0, 1) : null;
|
|
1182
|
+
}
|
|
1183
|
+
function parseAudioBand(raw) {
|
|
1184
|
+
if (!raw) return null;
|
|
1185
|
+
const name = raw.trim();
|
|
1186
|
+
return Object.hasOwn(AUDIO_BAND_PROPERTIES, name) ? name : null;
|
|
1187
|
+
}
|
|
1188
|
+
function readAudioBand(el, band) {
|
|
1189
|
+
const prop = AUDIO_BAND_PROPERTIES[band];
|
|
1190
|
+
const fromInline = parseBandValue(el.style?.getPropertyValue?.(prop));
|
|
1191
|
+
if (fromInline !== null) return fromInline;
|
|
1192
|
+
const view = el.ownerDocument?.defaultView;
|
|
1193
|
+
return parseBandValue(view?.getComputedStyle?.(el).getPropertyValue(prop)) ?? 0;
|
|
1194
|
+
}
|
|
1195
|
+
function computeFrequencyBands(data, sampleRate) {
|
|
1196
|
+
const binCount = data.length;
|
|
1197
|
+
if (binCount === 0) return { bass: 0, mid: 0, treble: 0, level: 0 };
|
|
1198
|
+
const nyquist = sampleRate > 0 ? sampleRate / 2 : 22050;
|
|
1199
|
+
const hzPerBin = nyquist / binCount;
|
|
1200
|
+
const getAverage = (minHz, maxHz) => {
|
|
1201
|
+
const startBin = Math.max(0, Math.floor(minHz / hzPerBin));
|
|
1202
|
+
const endBin = Math.min(binCount, Math.max(startBin + 1, Math.ceil(maxHz / hzPerBin)));
|
|
1203
|
+
let sum = 0;
|
|
1204
|
+
let count = 0;
|
|
1205
|
+
for (let i = startBin; i < endBin; i++) {
|
|
1206
|
+
sum += data[i];
|
|
1207
|
+
count++;
|
|
1208
|
+
}
|
|
1209
|
+
return count > 0 ? sum / (count * 255) : 0;
|
|
1210
|
+
};
|
|
1211
|
+
let totalSum = 0;
|
|
1212
|
+
for (let i = 0; i < binCount; i++) totalSum += data[i];
|
|
1213
|
+
const level = totalSum / (binCount * 255);
|
|
1214
|
+
return {
|
|
1215
|
+
bass: clamp(getAverage(20, 250), 0, 1),
|
|
1216
|
+
mid: clamp(getAverage(250, 4e3), 0, 1),
|
|
1217
|
+
treble: clamp(getAverage(4e3, 16e3), 0, 1),
|
|
1218
|
+
level: clamp(level, 0, 1)
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
var DEFAULT_MEDIA_SELECTOR = "audio, video";
|
|
1222
|
+
function isMediaElement(el, win) {
|
|
1223
|
+
const injected = win?.HTMLMediaElement;
|
|
1224
|
+
const ctor = injected ?? (typeof HTMLMediaElement !== "undefined" ? HTMLMediaElement : null);
|
|
1225
|
+
return ctor ? el instanceof ctor : false;
|
|
1226
|
+
}
|
|
1227
|
+
function findMediaElement(el, authored, win, doc) {
|
|
1228
|
+
if (isMediaElement(el, win)) return el;
|
|
1229
|
+
const local = el.querySelector?.(authored ?? DEFAULT_MEDIA_SELECTOR);
|
|
1230
|
+
if (local) return local;
|
|
1231
|
+
if (!authored) return null;
|
|
1232
|
+
return doc?.querySelector?.(authored) || null;
|
|
1233
|
+
}
|
|
1234
|
+
function safeDisconnect(node) {
|
|
1235
|
+
if (!node || typeof node.disconnect !== "function") return;
|
|
1236
|
+
try {
|
|
1237
|
+
node.disconnect();
|
|
1238
|
+
} catch {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
function safeDisconnectFrom(node, destination) {
|
|
1243
|
+
if (!node || typeof node.disconnect !== "function" || !destination) return;
|
|
1244
|
+
try {
|
|
1245
|
+
node.disconnect(destination);
|
|
1246
|
+
} catch {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
var FFT_SIZES = [32, 64, 128, 256, 512, 1024, 2048];
|
|
1251
|
+
function snapFftSize(requested) {
|
|
1252
|
+
let best = FFT_SIZES[0];
|
|
1253
|
+
for (const size of FFT_SIZES) {
|
|
1254
|
+
if (Math.abs(size - requested) < Math.abs(best - requested)) best = size;
|
|
1255
|
+
}
|
|
1256
|
+
return best;
|
|
1257
|
+
}
|
|
1258
|
+
var GESTURE_EVENTS = ["pointerdown", "keydown", "touchend"];
|
|
1259
|
+
var sharedGraphs = /* @__PURE__ */ new WeakMap();
|
|
1260
|
+
function createMutedSink(ctx) {
|
|
1261
|
+
if (typeof ctx.createGain !== "function") return null;
|
|
1262
|
+
try {
|
|
1263
|
+
const gain = ctx.createGain();
|
|
1264
|
+
if (gain.gain) gain.gain.value = 0;
|
|
1265
|
+
gain.connect(ctx.destination);
|
|
1266
|
+
return gain;
|
|
1267
|
+
} catch {
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
function acquireSharedGraph(win) {
|
|
1272
|
+
const existing = sharedGraphs.get(win);
|
|
1273
|
+
if (existing) {
|
|
1274
|
+
existing.consumers++;
|
|
1275
|
+
return existing;
|
|
1276
|
+
}
|
|
1277
|
+
const AudioCtxCtor = win.AudioContext || win.webkitAudioContext;
|
|
1278
|
+
if (!AudioCtxCtor) return null;
|
|
1279
|
+
try {
|
|
1280
|
+
const ctx = new AudioCtxCtor();
|
|
1281
|
+
const graph = {
|
|
1282
|
+
ctx,
|
|
1283
|
+
sources: /* @__PURE__ */ new WeakMap(),
|
|
1284
|
+
sink: createMutedSink(ctx),
|
|
1285
|
+
consumers: 1,
|
|
1286
|
+
ownsMediaOutput: false
|
|
1287
|
+
};
|
|
1288
|
+
sharedGraphs.set(win, graph);
|
|
1289
|
+
return graph;
|
|
1290
|
+
} catch {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
function releaseSharedGraph(win, graph) {
|
|
1295
|
+
graph.consumers--;
|
|
1296
|
+
if (graph.consumers > 0 || graph.ownsMediaOutput) return;
|
|
1297
|
+
sharedGraphs.delete(win);
|
|
1298
|
+
const closing = graph.ctx.close?.();
|
|
1299
|
+
if (closing) closing.catch(() => void 0);
|
|
1300
|
+
}
|
|
1301
|
+
function mediaSourceFor(graph, mediaEl) {
|
|
1302
|
+
const existing = graph.sources.get(mediaEl);
|
|
1303
|
+
if (existing) return existing;
|
|
1304
|
+
if (typeof graph.ctx.createMediaElementSource !== "function") return null;
|
|
1305
|
+
try {
|
|
1306
|
+
const source = graph.ctx.createMediaElementSource(mediaEl);
|
|
1307
|
+
source.connect(graph.ctx.destination);
|
|
1308
|
+
graph.sources.set(mediaEl, source);
|
|
1309
|
+
graph.ownsMediaOutput = true;
|
|
1310
|
+
return source;
|
|
1311
|
+
} catch {
|
|
1312
|
+
return null;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
var AudioSourceController = class {
|
|
1316
|
+
element;
|
|
1317
|
+
options;
|
|
1318
|
+
env;
|
|
1319
|
+
window;
|
|
1320
|
+
document;
|
|
1321
|
+
raf;
|
|
1322
|
+
caf;
|
|
1323
|
+
audioCtx = null;
|
|
1324
|
+
analyser = null;
|
|
1325
|
+
sourceNode = null;
|
|
1326
|
+
stream = null;
|
|
1327
|
+
freqData = null;
|
|
1328
|
+
rafId = null;
|
|
1329
|
+
isActive = false;
|
|
1330
|
+
/** The window-wide graph this instance is currently counted against. */
|
|
1331
|
+
graph = null;
|
|
1332
|
+
/**
|
|
1333
|
+
* Whether `sourceNode` belongs to the shared graph rather than to this instance.
|
|
1334
|
+
*
|
|
1335
|
+
* True for `source: media` — the node feeds the page's speakers as well as this analyser, so
|
|
1336
|
+
* teardown may only cut its edge to *this* analyser. False for `source: mic`, where the node and
|
|
1337
|
+
* its stream are this instance's alone and must be released in full.
|
|
1338
|
+
*/
|
|
1339
|
+
sourceIsShared = false;
|
|
1340
|
+
/**
|
|
1341
|
+
* The author's inline custom properties, and what this module replaced them with.
|
|
1342
|
+
*
|
|
1343
|
+
* Not a snapshot taken here: the ledger for `element` is opened at the first write in
|
|
1344
|
+
* `updateFrame`, so what it restores is whatever the author had at that instant. The field it
|
|
1345
|
+
* replaced captured all five properties in this constructor and restored them on destroy, which
|
|
1346
|
+
* meant an author who wrote `--kui-audio-bass` any time after `prepare` had that write silently
|
|
1347
|
+
* reverted to a value from before their own.
|
|
1348
|
+
*
|
|
1349
|
+
* Shared with every other controller writing to this element — see `createAdvancedLedgers`. Two
|
|
1350
|
+
* `audio-source` effects on one element (`audio-reactive` plus `audio-mic`, say) would otherwise
|
|
1351
|
+
* open a ledger each, and the second would capture the first's `0.000` as the author's value.
|
|
1352
|
+
*/
|
|
1353
|
+
ledgers;
|
|
1354
|
+
/**
|
|
1355
|
+
* Which microphone request is the live one.
|
|
1356
|
+
*
|
|
1357
|
+
* `getUserMedia` cannot be cancelled — the permission prompt belongs to the browser, and the
|
|
1358
|
+
* promise settles whenever the visitor answers it. Every `stop()` bumps this, so a request the
|
|
1359
|
+
* visitor grants *after* the effect was cancelled resolves into a handler that no longer
|
|
1360
|
+
* recognises its own generation and stops the tracks instead of connecting them. Without it,
|
|
1361
|
+
* cancelling while the prompt is open and then granting leaves the microphone recording until
|
|
1362
|
+
* the element is eventually destroyed.
|
|
1363
|
+
*/
|
|
1364
|
+
micRequestToken = 0;
|
|
1365
|
+
/** Detaches the one-shot gesture listeners armed by {@link armGestureResume}. */
|
|
1366
|
+
releaseGestureResume = null;
|
|
1367
|
+
constructor(element, options = {}, env = {}) {
|
|
1368
|
+
this.element = element;
|
|
1369
|
+
this.options = options;
|
|
1370
|
+
this.env = env;
|
|
1371
|
+
const resolved = resolveEnv(null, env);
|
|
1372
|
+
this.window = resolved.window;
|
|
1373
|
+
this.document = resolved.document;
|
|
1374
|
+
this.raf = resolved.raf;
|
|
1375
|
+
this.caf = resolved.caf;
|
|
1376
|
+
this.ledgers = createAdvancedLedgers(element);
|
|
1377
|
+
}
|
|
1378
|
+
connectMicSource(graph, analyser, win) {
|
|
1379
|
+
const nav = win?.navigator;
|
|
1380
|
+
if (!nav?.mediaDevices?.getUserMedia) return;
|
|
1381
|
+
const token = ++this.micRequestToken;
|
|
1382
|
+
nav.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
|
|
1383
|
+
if (token !== this.micRequestToken || this.analyser !== analyser) {
|
|
1384
|
+
for (const track of stream.getTracks()) track.stop();
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
this.stream = stream;
|
|
1388
|
+
this.sourceNode = graph.ctx.createMediaStreamSource(stream);
|
|
1389
|
+
this.sourceIsShared = false;
|
|
1390
|
+
this.sourceNode.connect(analyser);
|
|
1391
|
+
if (this.resumeContext()) this.startLoopIfReady();
|
|
1392
|
+
}).catch(() => void 0);
|
|
1393
|
+
}
|
|
1394
|
+
connectMediaSource(graph, analyser, win) {
|
|
1395
|
+
const doc = this.document;
|
|
1396
|
+
try {
|
|
1397
|
+
const mediaEl = findMediaElement(this.element, this.options.target || null, win, doc);
|
|
1398
|
+
if (!mediaEl) return;
|
|
1399
|
+
const source = mediaSourceFor(graph, mediaEl);
|
|
1400
|
+
if (!source) return;
|
|
1401
|
+
source.connect(analyser);
|
|
1402
|
+
this.sourceNode = source;
|
|
1403
|
+
this.sourceIsShared = true;
|
|
1404
|
+
} catch {
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* This instance's analyser, already on a live path to the destination.
|
|
1410
|
+
*
|
|
1411
|
+
* @returns The analyser, or `null` when the context refused to build one.
|
|
1412
|
+
*/
|
|
1413
|
+
buildAnalyser(graph) {
|
|
1414
|
+
try {
|
|
1415
|
+
const analyser = graph.ctx.createAnalyser();
|
|
1416
|
+
analyser.fftSize = snapFftSize(this.options.fftSize || 256);
|
|
1417
|
+
analyser.smoothingTimeConstant = this.options.smoothing ?? 0.8;
|
|
1418
|
+
if (graph.sink) analyser.connect(graph.sink);
|
|
1419
|
+
return analyser;
|
|
1420
|
+
} catch {
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Join the window's audio graph and give this instance an analyser on it.
|
|
1426
|
+
*
|
|
1427
|
+
* `this.analyser`, not `this.audioCtx`, is the "already initialised" test, and nothing is
|
|
1428
|
+
* recorded on `this` until the analyser exists. Assigning the context first is what left a
|
|
1429
|
+
* half-built instance behind — an open context, no analyser, and a loop writing zeros — whenever
|
|
1430
|
+
* `createAnalyser` or `fftSize` threw.
|
|
1431
|
+
*/
|
|
1432
|
+
initAudio() {
|
|
1433
|
+
if (this.analyser) return true;
|
|
1434
|
+
const win = this.window;
|
|
1435
|
+
if (!win) return false;
|
|
1436
|
+
const graph = acquireSharedGraph(win);
|
|
1437
|
+
if (!graph) return false;
|
|
1438
|
+
const analyser = this.buildAnalyser(graph);
|
|
1439
|
+
if (!analyser) {
|
|
1440
|
+
releaseSharedGraph(win, graph);
|
|
1441
|
+
return false;
|
|
1442
|
+
}
|
|
1443
|
+
this.graph = graph;
|
|
1444
|
+
this.audioCtx = graph.ctx;
|
|
1445
|
+
this.analyser = analyser;
|
|
1446
|
+
this.freqData = new Uint8Array(new ArrayBuffer(analyser.frequencyBinCount));
|
|
1447
|
+
if (this.options.source === "mic") this.connectMicSource(graph, analyser, win);
|
|
1448
|
+
else this.connectMediaSource(graph, analyser, win);
|
|
1449
|
+
return true;
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Go live — but only run a frame loop once there is something for it to read.
|
|
1453
|
+
*
|
|
1454
|
+
* The loop used to start unconditionally, and both ways that can be wrong were reachable:
|
|
1455
|
+
*
|
|
1456
|
+
* - **Initialisation failed.** No Web Audio, or an `AudioContext` the browser refused to build.
|
|
1457
|
+
* `initAudio()` is only ever called from here and here early-returns on `isActive`, so there is
|
|
1458
|
+
* no retry path at all — the loop sampled nothing, wrote the same `0.000` forever and could
|
|
1459
|
+
* never recover.
|
|
1460
|
+
* - **The context is suspended.** `defaultActivation` is `'load'`, so this normally runs before
|
|
1461
|
+
* the visitor has touched anything and the context starts suspended. The gesture retry below
|
|
1462
|
+
* is the fix for *that*, but the loop does not have to run while it waits: a suspended context
|
|
1463
|
+
* fills zeros, and {@link GESTURE_EVENTS} is `pointerdown`/`keydown`/`touchend` — a visitor who
|
|
1464
|
+
* only ever scrolls with a wheel fires none of them, so "while it waits" was the rest of the
|
|
1465
|
+
* session at 60fps.
|
|
1466
|
+
*
|
|
1467
|
+
* - **There is nothing to read.** `initAudio()` answers `true` as soon as the analyser exists,
|
|
1468
|
+
* and the analyser can exist with nothing connected to it: `audio-source target:#missing`, or a
|
|
1469
|
+
* page with no media under the element, leaves `connectMediaSource` with no node to attach;
|
|
1470
|
+
* `audio-mic` leaves `connectMicSource` waiting on a permission prompt the visitor may close,
|
|
1471
|
+
* refuse, or never answer. A loop over a disconnected analyser is the first case again — the
|
|
1472
|
+
* same `0.000` at 60fps, for the life of the page, with nothing coming to change it.
|
|
1473
|
+
*
|
|
1474
|
+
* One frame of silence covers all three, and it is all any of them needs: nothing is going to
|
|
1475
|
+
* change these five values until a resume or a stream lands, and writing them once still says
|
|
1476
|
+
* "there is a driver on this element and it is silent" to a consumer that would otherwise inherit
|
|
1477
|
+
* an ancestor's live band. The loop is then started by whichever arrives last — the `resume()`
|
|
1478
|
+
* that succeeds, or the microphone the visitor grants — see {@link startLoopIfReady}.
|
|
1479
|
+
*/
|
|
1480
|
+
start() {
|
|
1481
|
+
if (this.isActive) return;
|
|
1482
|
+
this.isActive = true;
|
|
1483
|
+
if (!this.initAudio() || !this.resumeContext() || !this.sourceNode) {
|
|
1484
|
+
this.updateFrame();
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
this.startLoop();
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Wake a context the browser suspended for autoplay policy.
|
|
1491
|
+
*
|
|
1492
|
+
* `defaultActivation` is `'load'`, so the context is very often constructed before the visitor
|
|
1493
|
+
* has touched anything, and a context built outside a user gesture starts `suspended`. A bare
|
|
1494
|
+
* `resume()` there is rejected or left pending — WebKit is strict about it, and iOS is this
|
|
1495
|
+
* repo's main audience — after which `getByteFrequencyData` fills zeros and all five properties
|
|
1496
|
+
* sit at `0.000` for the rest of the session. `on:click` only worked by luck, because it happens
|
|
1497
|
+
* to run inside a gesture.
|
|
1498
|
+
*
|
|
1499
|
+
* So: try immediately, and arm a one-shot retry on the visitor's first gesture. The `on:click`
|
|
1500
|
+
* path still succeeds on the first attempt and the listeners are never needed.
|
|
1501
|
+
*
|
|
1502
|
+
* @returns `true` when the context is already live and {@link start} may run its loop now,
|
|
1503
|
+
* `false` when a resume has been arranged and the loop is that resume's job to start.
|
|
1504
|
+
*/
|
|
1505
|
+
resumeContext() {
|
|
1506
|
+
const ctx = this.audioCtx;
|
|
1507
|
+
if (!ctx || ctx.state !== "suspended") return true;
|
|
1508
|
+
ctx.resume().then(() => this.startLoopIfReady()).catch(() => void 0);
|
|
1509
|
+
this.armGestureResume();
|
|
1510
|
+
return false;
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Start the loop, unless the effect was torn down while the context was still waking up — or
|
|
1514
|
+
* there is still nothing connected for it to read.
|
|
1515
|
+
*
|
|
1516
|
+
* `resume()` settles whenever the browser gets to it, which can be long after a `cancel()` — the
|
|
1517
|
+
* same race {@link micRequestToken} closes for `getUserMedia`, and with the same consequence if
|
|
1518
|
+
* it is left open: a resume landing after teardown would start a frame loop on a stopped
|
|
1519
|
+
* instance, and nothing would ever come back to stop it.
|
|
1520
|
+
*
|
|
1521
|
+
* The source check is the other half of the gate {@link start} applies: a resume that lands
|
|
1522
|
+
* before the microphone is granted, or on an element with no media to bind, has nothing to start
|
|
1523
|
+
* a loop over. When the stream does arrive, `connectMicSource` comes back through here.
|
|
1524
|
+
*/
|
|
1525
|
+
startLoopIfReady() {
|
|
1526
|
+
if (this.isActive && this.sourceNode) this.startLoop();
|
|
1527
|
+
}
|
|
1528
|
+
armGestureResume() {
|
|
1529
|
+
const win = this.window;
|
|
1530
|
+
if (this.releaseGestureResume || !win?.addEventListener || !win.removeEventListener) return;
|
|
1531
|
+
const onGesture = () => {
|
|
1532
|
+
this.releaseGestureResume?.();
|
|
1533
|
+
this.audioCtx?.resume().then(() => this.startLoopIfReady()).catch(() => void 0);
|
|
1534
|
+
};
|
|
1535
|
+
this.releaseGestureResume = () => {
|
|
1536
|
+
this.releaseGestureResume = null;
|
|
1537
|
+
for (const type of GESTURE_EVENTS) win.removeEventListener?.(type, onGesture);
|
|
1538
|
+
};
|
|
1539
|
+
for (const type of GESTURE_EVENTS) win.addEventListener(type, onGesture, { passive: true });
|
|
1540
|
+
}
|
|
1541
|
+
startLoop() {
|
|
1542
|
+
if (this.rafId) return;
|
|
1543
|
+
const tick = () => {
|
|
1544
|
+
this.updateFrame();
|
|
1545
|
+
if (this.isActive) {
|
|
1546
|
+
this.rafId = this.raf(tick);
|
|
1547
|
+
} else {
|
|
1548
|
+
this.rafId = null;
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
this.rafId = this.raf(tick);
|
|
1552
|
+
}
|
|
1553
|
+
updateFrame() {
|
|
1554
|
+
const style = styleOf(this.ledgers, this.element);
|
|
1555
|
+
if (!style) return;
|
|
1556
|
+
let bands = { bass: 0, mid: 0, treble: 0, level: 0 };
|
|
1557
|
+
if (this.analyser && this.freqData && this.audioCtx) {
|
|
1558
|
+
this.analyser.getByteFrequencyData(this.freqData);
|
|
1559
|
+
bands = computeFrequencyBands(this.freqData, this.audioCtx.sampleRate);
|
|
1560
|
+
}
|
|
1561
|
+
const level = bands.level.toFixed(3);
|
|
1562
|
+
this.writeChannel(style, "--kui-audio-bass", bands.bass.toFixed(3));
|
|
1563
|
+
this.writeChannel(style, "--kui-audio-mid", bands.mid.toFixed(3));
|
|
1564
|
+
this.writeChannel(style, "--kui-audio-treble", bands.treble.toFixed(3));
|
|
1565
|
+
this.writeChannel(style, "--kui-audio-level", level);
|
|
1566
|
+
this.writeChannel(style, "--kui-audio", level);
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* One channel, written only when it would actually change anything.
|
|
1570
|
+
*
|
|
1571
|
+
* These are unregistered custom properties, so they inherit: five unconditional `setProperty`
|
|
1572
|
+
* calls per frame per instance dirty the whole subtree's style 60 times a second even on a page
|
|
1573
|
+
* whose audio is silent or whose context never resumed, where all five values are the identical
|
|
1574
|
+
* `0.000`.
|
|
1575
|
+
*
|
|
1576
|
+
* The comparison reads the live inline value rather than a remembered one on purpose. A cache
|
|
1577
|
+
* would go stale the moment the author wrote the property themselves — this module would agree
|
|
1578
|
+
* with its own record, skip the write, and look dead. Reading the CSSOM's own map is a lookup,
|
|
1579
|
+
* not a style resolution, so it costs nothing a `setProperty` would not already cost.
|
|
1580
|
+
*
|
|
1581
|
+
* `claim()` first and unconditionally, because the very first frame can legitimately skip its
|
|
1582
|
+
* write (the author happened to have written the same string), and the ledger still has to have
|
|
1583
|
+
* captured what was there to restore it.
|
|
1584
|
+
*
|
|
1585
|
+
* @complexity O(1).
|
|
1586
|
+
*/
|
|
1587
|
+
writeChannel(style, property, value) {
|
|
1588
|
+
style.claim(property);
|
|
1589
|
+
if (this.element.style.getPropertyValue(property) === value) return;
|
|
1590
|
+
style.set(property, value);
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Give back everything this instance owns: the frame loop, its analyser, the microphone.
|
|
1594
|
+
*
|
|
1595
|
+
* This is what `cancel()` and `finish()` reach, and it is deliberately a full release of the
|
|
1596
|
+
* *instance's* half of the graph. A cancelled `audio-mic` that only stopped its loop left the
|
|
1597
|
+
* stream connected and the recording indicator lit until the element was eventually destroyed —
|
|
1598
|
+
* the visitor stopped the effect and the microphone stayed on.
|
|
1599
|
+
*
|
|
1600
|
+
* What it deliberately does **not** do is touch the page's own audio or the author's styles.
|
|
1601
|
+
* The shared media source keeps its edge to the destination (see {@link SharedAudioGraph}), and
|
|
1602
|
+
* the ledger is left as it is because `EffectInstance.cancel` means "stop where it is, leaving
|
|
1603
|
+
* the element mid-effect" — unwinding the five custom properties here would snap every rule
|
|
1604
|
+
* reading `var(--kui-audio-bass, 0)` back to its fallback on a pause, which is not what the
|
|
1605
|
+
* other five modules in this directory do. `destroy()` is where the styles go back.
|
|
1606
|
+
*
|
|
1607
|
+
* Idempotent, and safe on a controller that was never started — every step below no-ops on an
|
|
1608
|
+
* empty graph. It runs unguarded for that reason: the old `isActive` early-return meant a
|
|
1609
|
+
* controller cancelled twice, or destroyed without ever activating, skipped the parts that had
|
|
1610
|
+
* nothing to do with the loop.
|
|
1611
|
+
*/
|
|
1612
|
+
stop() {
|
|
1613
|
+
this.isActive = false;
|
|
1614
|
+
this.micRequestToken++;
|
|
1615
|
+
if (this.rafId) {
|
|
1616
|
+
this.caf(this.rafId);
|
|
1617
|
+
this.rafId = null;
|
|
1618
|
+
}
|
|
1619
|
+
this.releaseGestureResume?.();
|
|
1620
|
+
this.disconnectAudioNodes();
|
|
1621
|
+
}
|
|
1622
|
+
disconnectAudioNodes() {
|
|
1623
|
+
if (this.stream) {
|
|
1624
|
+
for (const track of this.stream.getTracks()) track.stop();
|
|
1625
|
+
this.stream = null;
|
|
1626
|
+
}
|
|
1627
|
+
const analyser = this.analyser;
|
|
1628
|
+
if (this.sourceIsShared) safeDisconnectFrom(this.sourceNode, analyser);
|
|
1629
|
+
else safeDisconnect(this.sourceNode);
|
|
1630
|
+
this.sourceNode = null;
|
|
1631
|
+
this.sourceIsShared = false;
|
|
1632
|
+
safeDisconnect(analyser);
|
|
1633
|
+
this.analyser = null;
|
|
1634
|
+
this.freqData = null;
|
|
1635
|
+
this.audioCtx = null;
|
|
1636
|
+
if (this.graph) {
|
|
1637
|
+
releaseSharedGraph(this.window, this.graph);
|
|
1638
|
+
this.graph = null;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Everything `stop()` gives back, plus the author's inline styles.
|
|
1643
|
+
*
|
|
1644
|
+
* `try`/`finally` because the restore must not be hostage to the graph teardown: the animator
|
|
1645
|
+
* swallows a throwing `instance.destroy()` (`runQuietly`), and an element this module reached on
|
|
1646
|
+
* its own — a `camera-layer`, a `scene-step` — is in no ledger the animator will ever restore.
|
|
1647
|
+
* A single throw on the way out would otherwise leave the library's values on it permanently.
|
|
1648
|
+
*/
|
|
1649
|
+
destroy() {
|
|
1650
|
+
try {
|
|
1651
|
+
this.stop();
|
|
1652
|
+
} finally {
|
|
1653
|
+
this.ledgers.restore();
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
function prepareAudioSource(el, params, ctx) {
|
|
1658
|
+
if (isReducedMotion(ctx)) return createInertInstance();
|
|
1659
|
+
const resolvedEnv = resolveEnv(ctx);
|
|
1660
|
+
const rawSource = params.text ? params.text("source", "media") : "media";
|
|
1661
|
+
const source = rawSource === "mic" ? "mic" : "media";
|
|
1662
|
+
const target = params.text ? params.text("target", "") : "";
|
|
1663
|
+
const smoothing = clamp(params.num ? params.num("smoothing", 0.8) : 0.8, 0, 0.99);
|
|
1664
|
+
const fftSize = snapFftSize(clamp(params.num ? params.num("fft", 256) : 256, 32, 2048));
|
|
1665
|
+
const controller = new AudioSourceController(
|
|
1666
|
+
el,
|
|
1667
|
+
{ source, target, smoothing, fftSize },
|
|
1668
|
+
resolvedEnv
|
|
1669
|
+
);
|
|
1670
|
+
return createEffectInstance({
|
|
1671
|
+
continuous: true,
|
|
1672
|
+
activate() {
|
|
1673
|
+
controller.start();
|
|
1674
|
+
},
|
|
1675
|
+
cancel() {
|
|
1676
|
+
controller.stop();
|
|
1677
|
+
},
|
|
1678
|
+
finish() {
|
|
1679
|
+
controller.stop();
|
|
1680
|
+
},
|
|
1681
|
+
destroy() {
|
|
1682
|
+
controller.destroy();
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
var AUDIO_PARAMETERS = {
|
|
1687
|
+
source: { type: "keyword", default: "media", keywords: ["media", "mic"], cssProperty: "--kui-audio-source" },
|
|
1688
|
+
/**
|
|
1689
|
+
* Which media element to analyse — the library's one spelling for "this effect names another
|
|
1690
|
+
* element", declared exactly as every other primitive that owns the key declares it — see
|
|
1691
|
+
* `compile.ts`'s `liftTarget` for the current set — down to the shared `--kui-target`.
|
|
1692
|
+
*
|
|
1693
|
+
* It was `media:`, which was both a second word for `target:`'s job and a live footgun:
|
|
1694
|
+
* `compile.ts`'s `liftTarget` only leaves `target:` in a primitive's own parameters when that
|
|
1695
|
+
* primitive declares it, and otherwise *relocates the whole effect* onto the elements the
|
|
1696
|
+
* selector matched. So `audio-source target:#track` used to move the analyser onto the `<audio>`
|
|
1697
|
+
* element itself — which happens to work, silently writing the five custom properties on the
|
|
1698
|
+
* wrong element. Declaring the key here claims it and closes that.
|
|
1699
|
+
*
|
|
1700
|
+
* No `scope: SCOPE_PARAM` alongside it, unlike its peers: this resolves its own two-step search
|
|
1701
|
+
* (`findMediaElement` — the host's own subtree first, the document only for an authored
|
|
1702
|
+
* selector), and importing core's shared declaration would be a value import from `src/effects`
|
|
1703
|
+
* into a tier whose bundle weight is measured (see `base.ts`'s `asRegistry`).
|
|
1704
|
+
*/
|
|
1705
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" },
|
|
1706
|
+
smoothing: { type: "number", default: "0.8", minimum: 0, maximum: 0.99, cssProperty: "--kui-audio-smoothing" },
|
|
1707
|
+
fft: { type: "number", default: "256", minimum: 32, maximum: 2048, cssProperty: "--kui-audio-fft" }
|
|
1708
|
+
};
|
|
1709
|
+
var AUDIO_PRIMITIVES = [
|
|
1710
|
+
{
|
|
1711
|
+
id: "audio-source",
|
|
1712
|
+
renderer: "javascript",
|
|
1713
|
+
// The five custom properties `updateFrame` writes, named exactly. `[]` said this primitive
|
|
1714
|
+
// touched nothing, so the registry's conflict detection could not see a second source writing
|
|
1715
|
+
// the same variables and let the two race, last frame wins.
|
|
1716
|
+
channels: [...AUDIO_CHANNELS],
|
|
1717
|
+
parameters: AUDIO_PARAMETERS,
|
|
1718
|
+
supportedTimelines: ["time"],
|
|
1719
|
+
// `manual` alongside the existing three (same additive shape as `interaction-proximity.ts`
|
|
1720
|
+
// and `interaction.ts`): `on:load` (the default) leaves `resumeContext`'s one-shot
|
|
1721
|
+
// page-gesture listener as the fallback, but an author who wants the AudioContext to wake on
|
|
1722
|
+
// one specific, deliberate gesture — a "start visualiser" button, not the visitor's next
|
|
1723
|
+
// incidental tap anywhere on the page — can now write `on:manual` and call `.play()` from
|
|
1724
|
+
// that button's own handler, which still lands inside the same user gesture the browser
|
|
1725
|
+
// requires.
|
|
1726
|
+
supportedActivations: ["load", "enter", "click", "manual"],
|
|
1727
|
+
defaultActivation: "load",
|
|
1728
|
+
perfClass: "continuous",
|
|
1729
|
+
reducedMotion: "disable",
|
|
1730
|
+
prepare: prepareAudioSource
|
|
1731
|
+
}
|
|
1732
|
+
];
|
|
1733
|
+
var AUDIO_PRESETS = [
|
|
1734
|
+
{ name: "audio-source", primitive: "audio-source" },
|
|
1735
|
+
{ name: "audio-reactive", primitive: "audio-source" },
|
|
1736
|
+
{ name: "audio-mic", primitive: "audio-source", params: { source: "mic" } }
|
|
1737
|
+
];
|
|
1738
|
+
function registerAudio(target) {
|
|
1739
|
+
return registerInto(target, AUDIO_PRIMITIVES[0], AUDIO_PRESETS, "AudioSource");
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// src/advanced/shaders.ts
|
|
1743
|
+
function parseHex(raw) {
|
|
1744
|
+
if (!/^[0-9a-fA-F]{3,8}$/.test(raw)) return null;
|
|
1745
|
+
if (raw.length === 3 || raw.length === 4) {
|
|
1746
|
+
const a = raw.length === 4 ? parseInt(raw[3] + raw[3], 16) / 255 : 1;
|
|
1747
|
+
return [parseInt(raw[0] + raw[0], 16) / 255, parseInt(raw[1] + raw[1], 16) / 255, parseInt(raw[2] + raw[2], 16) / 255, a];
|
|
1748
|
+
}
|
|
1749
|
+
if (raw.length === 6 || raw.length === 8) {
|
|
1750
|
+
const a = raw.length === 8 ? parseInt(raw.slice(6, 8), 16) / 255 : 1;
|
|
1751
|
+
return [parseInt(raw.slice(0, 2), 16) / 255, parseInt(raw.slice(2, 4), 16) / 255, parseInt(raw.slice(4, 6), 16) / 255, a];
|
|
1752
|
+
}
|
|
1753
|
+
return null;
|
|
1754
|
+
}
|
|
1755
|
+
var RGBA_REGEX = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/i;
|
|
1756
|
+
function parseRgbFunctional(val) {
|
|
1757
|
+
const match = RGBA_REGEX.exec(val);
|
|
1758
|
+
if (!match || !match[1] || !match[2] || !match[3]) return null;
|
|
1759
|
+
const r = Math.min(255, Math.max(0, parseInt(match[1], 10))) / 255;
|
|
1760
|
+
const g = Math.min(255, Math.max(0, parseInt(match[2], 10))) / 255;
|
|
1761
|
+
const b = Math.min(255, Math.max(0, parseInt(match[3], 10))) / 255;
|
|
1762
|
+
const a = match[4] !== void 0 ? Math.min(1, Math.max(0, parseFloat(match[4]))) : 1;
|
|
1763
|
+
return [r, g, b, a];
|
|
1764
|
+
}
|
|
1765
|
+
var NAMED_COLORS = {
|
|
1766
|
+
transparent: [0, 0, 0, 0],
|
|
1767
|
+
black: [0, 0, 0, 1],
|
|
1768
|
+
white: [1, 1, 1, 1],
|
|
1769
|
+
red: [1, 0, 0, 1],
|
|
1770
|
+
green: [0, 0.5, 0, 1],
|
|
1771
|
+
blue: [0, 0, 1, 1],
|
|
1772
|
+
yellow: [1, 1, 0, 1],
|
|
1773
|
+
cyan: [0, 1, 1, 1],
|
|
1774
|
+
magenta: [1, 0, 1, 1],
|
|
1775
|
+
gray: [0.5, 0.5, 0.5, 1],
|
|
1776
|
+
grey: [0.5, 0.5, 0.5, 1],
|
|
1777
|
+
orange: [1, 0.647, 0, 1],
|
|
1778
|
+
purple: [0.5, 0, 0.5, 1]
|
|
1779
|
+
};
|
|
1780
|
+
var colorContexts = /* @__PURE__ */ new WeakMap();
|
|
1781
|
+
function colorContextFor(createCanvas) {
|
|
1782
|
+
const cached = colorContexts.get(createCanvas);
|
|
1783
|
+
if (cached !== void 0) return cached;
|
|
1784
|
+
let ctx = null;
|
|
1785
|
+
try {
|
|
1786
|
+
const canvas = createCanvas();
|
|
1787
|
+
if (canvas) {
|
|
1788
|
+
canvas.width = 1;
|
|
1789
|
+
canvas.height = 1;
|
|
1790
|
+
ctx = canvas.getContext?.("2d", { willReadFrequently: true }) ?? null;
|
|
1791
|
+
}
|
|
1792
|
+
} catch {
|
|
1793
|
+
ctx = null;
|
|
1794
|
+
}
|
|
1795
|
+
if (ctx) colorContexts.set(createCanvas, ctx);
|
|
1796
|
+
return ctx;
|
|
1797
|
+
}
|
|
1798
|
+
function parseColorWithCanvas(val, createCanvas) {
|
|
1799
|
+
try {
|
|
1800
|
+
const ctx = colorContextFor(createCanvas);
|
|
1801
|
+
if (!ctx) return null;
|
|
1802
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
1803
|
+
ctx.fillStyle = "#000000";
|
|
1804
|
+
ctx.fillStyle = val;
|
|
1805
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
1806
|
+
const data = ctx.getImageData(0, 0, 1, 1).data;
|
|
1807
|
+
return [data[0] / 255, data[1] / 255, data[2] / 255, data[3] / 255];
|
|
1808
|
+
} catch {
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
function parseColor(str, createCanvas = resolveEnv(null).createCanvas) {
|
|
1813
|
+
if (!str) return [1, 1, 1, 1];
|
|
1814
|
+
const val = str.trim();
|
|
1815
|
+
if (val.startsWith("#")) {
|
|
1816
|
+
const hex = parseHex(val.slice(1));
|
|
1817
|
+
if (hex) return hex;
|
|
1818
|
+
}
|
|
1819
|
+
const rgb = parseRgbFunctional(val);
|
|
1820
|
+
if (rgb) return rgb;
|
|
1821
|
+
const lower = val.toLowerCase();
|
|
1822
|
+
if (lower in NAMED_COLORS) return NAMED_COLORS[lower];
|
|
1823
|
+
const canvasColor = parseColorWithCanvas(val, createCanvas);
|
|
1824
|
+
if (canvasColor) return canvasColor;
|
|
1825
|
+
return [1, 1, 1, 1];
|
|
1826
|
+
}
|
|
1827
|
+
var ANGLE_UNITS = {
|
|
1828
|
+
deg: Math.PI / 180,
|
|
1829
|
+
grad: Math.PI / 200,
|
|
1830
|
+
rad: 1,
|
|
1831
|
+
turn: Math.PI * 2
|
|
1832
|
+
};
|
|
1833
|
+
function parseAngleRadians(raw) {
|
|
1834
|
+
if (!raw) return 0;
|
|
1835
|
+
const match = /^([+-]?(?:\d+(?:\.\d+)?|\.\d+))(deg|grad|rad|turn)?$/i.exec(raw.trim());
|
|
1836
|
+
if (!match) return 0;
|
|
1837
|
+
const value = parseFloat(match[1]);
|
|
1838
|
+
if (!Number.isFinite(value)) return 0;
|
|
1839
|
+
return value * (ANGLE_UNITS[(match[2] ?? "deg").toLowerCase()] ?? ANGLE_UNITS.deg);
|
|
1840
|
+
}
|
|
1841
|
+
var PALETTE_SIZE = 5;
|
|
1842
|
+
var DEFAULT_PALETTE = [
|
|
1843
|
+
[0.09, 0.11, 0.25, 1],
|
|
1844
|
+
[0.45, 0.29, 0.78, 1]
|
|
1845
|
+
];
|
|
1846
|
+
function buildPalette(authored, createCanvas) {
|
|
1847
|
+
const set = authored.filter((raw) => raw.trim() !== "").slice(0, PALETTE_SIZE);
|
|
1848
|
+
const stops = set.length >= 2 ? set.map((raw) => parseColor(raw, createCanvas)) : [...DEFAULT_PALETTE];
|
|
1849
|
+
const palette = new Float32Array(PALETTE_SIZE * 4);
|
|
1850
|
+
for (let i = 0; i < PALETTE_SIZE; i += 1) {
|
|
1851
|
+
const stop = stops[Math.min(i, stops.length - 1)];
|
|
1852
|
+
palette.set(stop, i * 4);
|
|
1853
|
+
}
|
|
1854
|
+
return { palette, colorCount: stops.length };
|
|
1855
|
+
}
|
|
1856
|
+
function cacheLocalLocations(gl, locs, program) {
|
|
1857
|
+
if (locs.u_audio === void 0) locs.u_audio = gl.getUniformLocation(program, "u_audio");
|
|
1858
|
+
if (locs.u_noise === void 0) locs.u_noise = gl.getUniformLocation(program, "u_noise");
|
|
1859
|
+
return locs;
|
|
1860
|
+
}
|
|
1861
|
+
function resolveLocations(gl, progInfo) {
|
|
1862
|
+
if (!progInfo) return null;
|
|
1863
|
+
if ("program" in progInfo && progInfo.program) return cacheLocalLocations(gl, progInfo, progInfo.program);
|
|
1864
|
+
const p = progInfo;
|
|
1865
|
+
const g = (n) => gl.getUniformLocation(p, n);
|
|
1866
|
+
return {
|
|
1867
|
+
u_time: g("u_time"),
|
|
1868
|
+
u_strength: g("u_strength"),
|
|
1869
|
+
u_frequency: g("u_frequency"),
|
|
1870
|
+
u_chromatic: g("u_chromatic"),
|
|
1871
|
+
u_iridescence: g("u_iridescence"),
|
|
1872
|
+
u_mouse: g("u_mouse"),
|
|
1873
|
+
u_uvOrigin: g("u_uvOrigin"),
|
|
1874
|
+
u_uvScale: g("u_uvScale"),
|
|
1875
|
+
u_tint: g("u_tint"),
|
|
1876
|
+
u_blend: g("u_blend"),
|
|
1877
|
+
u_duotone: g("u_duotone"),
|
|
1878
|
+
u_color1: g("u_color1"),
|
|
1879
|
+
u_color2: g("u_color2"),
|
|
1880
|
+
u_image: g("u_image"),
|
|
1881
|
+
u_image_to: g("u_image_to"),
|
|
1882
|
+
u_progress: g("u_progress"),
|
|
1883
|
+
u_audio: g("u_audio"),
|
|
1884
|
+
u_noise: g("u_noise"),
|
|
1885
|
+
u_maskBox: g("u_maskBox"),
|
|
1886
|
+
u_maskRx: g("u_maskRx"),
|
|
1887
|
+
u_maskRy: g("u_maskRy"),
|
|
1888
|
+
u_maskAlpha: g("u_maskAlpha"),
|
|
1889
|
+
u_seed: g("u_seed"),
|
|
1890
|
+
u_scale: g("u_scale"),
|
|
1891
|
+
u_warp: g("u_warp"),
|
|
1892
|
+
u_grain: g("u_grain"),
|
|
1893
|
+
u_hue: g("u_hue"),
|
|
1894
|
+
u_detail: g("u_detail"),
|
|
1895
|
+
u_bands: g("u_bands"),
|
|
1896
|
+
u_colorCount: g("u_colorCount"),
|
|
1897
|
+
u_colors: g("u_colors"),
|
|
1898
|
+
// `u_maskMode` was missing from this branch while `extractLocations` looked it up, so an
|
|
1899
|
+
// external caller handing this module a bare `WebGLProgram` got a generative draw with no
|
|
1900
|
+
// stencil. Latent — everything inside this file goes through `extractLocations` — but the two
|
|
1901
|
+
// lists are supposed to answer the same question, and one of them was a stop short.
|
|
1902
|
+
u_maskMode: g("u_maskMode"),
|
|
1903
|
+
u_hover: g("u_hover"),
|
|
1904
|
+
u_backdrop: g("u_backdrop"),
|
|
1905
|
+
u_angle: g("u_angle"),
|
|
1906
|
+
u_motion: g("u_motion")
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
var NO_MASK_BOX = [0, 0, 0, 0];
|
|
1910
|
+
var NO_BACKDROP = [0, 0, 0, 0];
|
|
1911
|
+
function uploadMaskUniforms(gl, locs, opt) {
|
|
1912
|
+
if (locs.u_maskBox) gl.uniform4fv(locs.u_maskBox, opt.maskBox ?? NO_MASK_BOX);
|
|
1913
|
+
if (locs.u_maskRx) gl.uniform4fv(locs.u_maskRx, opt.maskRx ?? NO_MASK_BOX);
|
|
1914
|
+
if (locs.u_maskRy) gl.uniform4fv(locs.u_maskRy, opt.maskRy ?? NO_MASK_BOX);
|
|
1915
|
+
uploadFloat(gl, locs.u_maskAlpha, opt.maskAlpha ?? 1);
|
|
1916
|
+
}
|
|
1917
|
+
function uploadFloat(gl, loc, val) {
|
|
1918
|
+
if (loc && val !== void 0) gl.uniform1f(loc, val);
|
|
1919
|
+
}
|
|
1920
|
+
function uploadInt(gl, loc, val) {
|
|
1921
|
+
if (loc && val !== void 0) gl.uniform1i(loc, Math.round(val));
|
|
1922
|
+
}
|
|
1923
|
+
function uploadFieldSamplingUniforms(gl, locs, opt) {
|
|
1924
|
+
uploadFloat(gl, locs.u_seed, opt.seed ?? 0);
|
|
1925
|
+
uploadFloat(gl, locs.u_scale, opt.scale ?? 1);
|
|
1926
|
+
uploadFloat(gl, locs.u_warp, opt.warp ?? 0);
|
|
1927
|
+
uploadFloat(gl, locs.u_noise, opt.noise ?? 0);
|
|
1928
|
+
}
|
|
1929
|
+
function uploadFieldTextureUniforms(gl, locs, opt) {
|
|
1930
|
+
uploadFloat(gl, locs.u_grain, opt.grain ?? 0);
|
|
1931
|
+
uploadFloat(gl, locs.u_hue, opt.hue ?? 0);
|
|
1932
|
+
uploadInt(gl, locs.u_detail, opt.detail ?? 3);
|
|
1933
|
+
uploadInt(gl, locs.u_bands, opt.bands ?? 0);
|
|
1934
|
+
}
|
|
1935
|
+
function uploadFieldBranchUniforms(gl, locs, opt) {
|
|
1936
|
+
uploadInt(gl, locs.u_maskMode, opt.maskMode ?? 0);
|
|
1937
|
+
uploadInt(gl, locs.u_hover, opt.hover ?? 0);
|
|
1938
|
+
uploadFloat(gl, locs.u_angle, opt.angle ?? 0);
|
|
1939
|
+
uploadInt(gl, locs.u_motion, opt.motion ?? 0);
|
|
1940
|
+
}
|
|
1941
|
+
function uploadFieldUniforms(gl, locs, opt) {
|
|
1942
|
+
uploadFieldSamplingUniforms(gl, locs, opt);
|
|
1943
|
+
uploadFieldTextureUniforms(gl, locs, opt);
|
|
1944
|
+
uploadFieldBranchUniforms(gl, locs, opt);
|
|
1945
|
+
if (locs.u_backdrop) gl.uniform4fv(locs.u_backdrop, opt.backdropRgba ?? NO_BACKDROP);
|
|
1946
|
+
}
|
|
1947
|
+
function uploadCoordUniforms(gl, locs, opt) {
|
|
1948
|
+
if (locs.u_mouse && opt.localMouse) gl.uniform2f(locs.u_mouse, opt.localMouse.x, opt.localMouse.y);
|
|
1949
|
+
if (locs.u_uvOrigin && opt.uvOrigin) gl.uniform2f(locs.u_uvOrigin, opt.uvOrigin[0], opt.uvOrigin[1]);
|
|
1950
|
+
if (locs.u_uvScale && opt.uvScale) gl.uniform2f(locs.u_uvScale, opt.uvScale[0], opt.uvScale[1]);
|
|
1951
|
+
}
|
|
1952
|
+
function uploadUniforms(gl, progInfo, opt) {
|
|
1953
|
+
const locs = resolveLocations(gl, progInfo);
|
|
1954
|
+
if (!locs) return;
|
|
1955
|
+
const timeVal = opt.time !== void 0 ? opt.time * (opt.speed ?? 1) : void 0;
|
|
1956
|
+
uploadFloat(gl, locs.u_time, timeVal);
|
|
1957
|
+
uploadFloat(gl, locs.u_strength, opt.strength);
|
|
1958
|
+
uploadFloat(gl, locs.u_frequency, opt.frequency);
|
|
1959
|
+
uploadFloat(gl, locs.u_chromatic, opt.chromatic);
|
|
1960
|
+
uploadFloat(gl, locs.u_iridescence, opt.iridescence);
|
|
1961
|
+
uploadFloat(gl, locs.u_progress, opt.progress ?? -1);
|
|
1962
|
+
uploadFloat(gl, locs.u_audio, opt.audio ?? 0);
|
|
1963
|
+
uploadCoordUniforms(gl, locs, opt);
|
|
1964
|
+
uploadMaskUniforms(gl, locs, opt);
|
|
1965
|
+
uploadFieldUniforms(gl, locs, opt);
|
|
1966
|
+
}
|
|
1967
|
+
function uploadDuotoneUniforms(gl, locs, opt) {
|
|
1968
|
+
if (!opt.isDuotone) return;
|
|
1969
|
+
if (locs.u_color1 && opt.c1Rgba) gl.uniform4fv(locs.u_color1, opt.c1Rgba);
|
|
1970
|
+
if (locs.u_color2 && opt.c2Rgba) gl.uniform4fv(locs.u_color2, opt.c2Rgba);
|
|
1971
|
+
}
|
|
1972
|
+
function uploadColorUniforms(gl, progInfo, opt) {
|
|
1973
|
+
const locs = resolveLocations(gl, progInfo);
|
|
1974
|
+
if (!locs) return;
|
|
1975
|
+
if (locs.u_tint && opt.tintRgba) gl.uniform4fv(locs.u_tint, opt.tintRgba);
|
|
1976
|
+
if (locs.u_blend && opt.blendMode !== void 0) gl.uniform1i(locs.u_blend, opt.blendMode);
|
|
1977
|
+
if (locs.u_duotone) gl.uniform1f(locs.u_duotone, opt.isDuotone ? 1 : 0);
|
|
1978
|
+
uploadDuotoneUniforms(gl, locs, opt);
|
|
1979
|
+
if (locs.u_colors && opt.palette) gl.uniform4fv(locs.u_colors, opt.palette);
|
|
1980
|
+
uploadInt(gl, locs.u_colorCount, opt.colorCount);
|
|
1981
|
+
}
|
|
1982
|
+
function isRectVisible(rect, winHeight) {
|
|
1983
|
+
if (rect.width <= 0 || rect.height <= 0) return false;
|
|
1984
|
+
return rect.bottom >= 0 && rect.top <= winHeight;
|
|
1985
|
+
}
|
|
1986
|
+
function getScissorEnv(win) {
|
|
1987
|
+
return {
|
|
1988
|
+
height: win?.innerHeight ?? 800,
|
|
1989
|
+
dpr: Math.min(win?.devicePixelRatio ?? 1, 2)
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
function numOr(raw, fallback = 0) {
|
|
1993
|
+
const val = parseFloat(raw ?? "");
|
|
1994
|
+
return Number.isFinite(val) ? val : fallback;
|
|
1995
|
+
}
|
|
1996
|
+
function cssStyleOf(el, win) {
|
|
1997
|
+
const gcs = win?.getComputedStyle;
|
|
1998
|
+
if (typeof gcs !== "function") return null;
|
|
1999
|
+
try {
|
|
2000
|
+
return gcs.call(win, el);
|
|
2001
|
+
} catch {
|
|
2002
|
+
return null;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
function boxOf(rect) {
|
|
2006
|
+
return { left: rect.left, top: rect.top, right: rect.left + rect.width, bottom: rect.top + rect.height };
|
|
2007
|
+
}
|
|
2008
|
+
function intersectBox(a, b) {
|
|
2009
|
+
return {
|
|
2010
|
+
left: Math.max(a.left, b.left),
|
|
2011
|
+
top: Math.max(a.top, b.top),
|
|
2012
|
+
right: Math.min(a.right, b.right),
|
|
2013
|
+
bottom: Math.min(a.bottom, b.bottom)
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
function insetBox(box, by) {
|
|
2017
|
+
return {
|
|
2018
|
+
left: box.left + by.left,
|
|
2019
|
+
top: box.top + by.top,
|
|
2020
|
+
right: box.right - by.right,
|
|
2021
|
+
bottom: box.bottom - by.bottom
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
function borderWidthsOf(cs) {
|
|
2025
|
+
return {
|
|
2026
|
+
left: numOr(cs.borderLeftWidth),
|
|
2027
|
+
top: numOr(cs.borderTopWidth),
|
|
2028
|
+
right: numOr(cs.borderRightWidth),
|
|
2029
|
+
bottom: numOr(cs.borderBottomWidth)
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
function contentBoxOf(rect, cs) {
|
|
2033
|
+
const border = boxOf(rect);
|
|
2034
|
+
if (!cs) return border;
|
|
2035
|
+
const edges = borderWidthsOf(cs);
|
|
2036
|
+
return insetBox(border, {
|
|
2037
|
+
left: edges.left + numOr(cs.paddingLeft),
|
|
2038
|
+
top: edges.top + numOr(cs.paddingTop),
|
|
2039
|
+
right: edges.right + numOr(cs.paddingRight),
|
|
2040
|
+
bottom: edges.bottom + numOr(cs.paddingBottom)
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
function fittedSize(fit, bw, bh, natural) {
|
|
2044
|
+
const [iw, ih] = natural;
|
|
2045
|
+
if (fit === "cover") {
|
|
2046
|
+
const s = Math.max(bw / iw, bh / ih);
|
|
2047
|
+
return [iw * s, ih * s];
|
|
2048
|
+
}
|
|
2049
|
+
if (fit === "none") return [iw, ih];
|
|
2050
|
+
if (fit === "contain" || fit === "scale-down") {
|
|
2051
|
+
const s = Math.min(bw / iw, bh / ih, fit === "scale-down" ? 1 : Infinity);
|
|
2052
|
+
return [iw * s, ih * s];
|
|
2053
|
+
}
|
|
2054
|
+
return [bw, bh];
|
|
2055
|
+
}
|
|
2056
|
+
var POSITION_KEYWORDS = { left: 0, top: 0, center: 0.5, right: 1, bottom: 1 };
|
|
2057
|
+
function positionOffset(token, free) {
|
|
2058
|
+
const keyword = POSITION_KEYWORDS[token];
|
|
2059
|
+
if (keyword !== void 0) return free * keyword;
|
|
2060
|
+
if (token.endsWith("%")) return free * (numOr(token) / 100);
|
|
2061
|
+
return numOr(token, free * 0.5);
|
|
2062
|
+
}
|
|
2063
|
+
function paintBoxOf(content, natural, cs) {
|
|
2064
|
+
if (!natural || !cs) return content;
|
|
2065
|
+
const bw = content.right - content.left;
|
|
2066
|
+
const bh = content.bottom - content.top;
|
|
2067
|
+
if (bw <= 0 || bh <= 0) return content;
|
|
2068
|
+
const [dw, dh] = fittedSize(cs.objectFit || "fill", bw, bh, natural);
|
|
2069
|
+
const tokens = (cs.objectPosition || "50% 50%").trim().split(/\s+/);
|
|
2070
|
+
const left = content.left + positionOffset(tokens[0] ?? "50%", bw - dw);
|
|
2071
|
+
const top = content.top + positionOffset(tokens[1] ?? "50%", bh - dh);
|
|
2072
|
+
return { left, top, right: left + dw, bottom: top + dh };
|
|
2073
|
+
}
|
|
2074
|
+
function radiusPart(token, basis) {
|
|
2075
|
+
if (!token) return 0;
|
|
2076
|
+
return token.endsWith("%") ? Math.max(0, basis * (numOr(token) / 100)) : Math.max(0, numOr(token));
|
|
2077
|
+
}
|
|
2078
|
+
function cornerRadius(raw, bw, bh) {
|
|
2079
|
+
const parts = (raw ?? "").trim().split(/\s+/);
|
|
2080
|
+
return [radiusPart(parts[0], bw), radiusPart(parts[1] ?? parts[0], bh)];
|
|
2081
|
+
}
|
|
2082
|
+
function radiiOf(cs, rect) {
|
|
2083
|
+
if (!cs) return [0, 0, 0, 0, 0, 0, 0, 0];
|
|
2084
|
+
const w = rect.width, h = rect.height;
|
|
2085
|
+
const tl = cornerRadius(cs.borderTopLeftRadius, w, h);
|
|
2086
|
+
const tr = cornerRadius(cs.borderTopRightRadius, w, h);
|
|
2087
|
+
const br = cornerRadius(cs.borderBottomRightRadius, w, h);
|
|
2088
|
+
const bl = cornerRadius(cs.borderBottomLeftRadius, w, h);
|
|
2089
|
+
return [tl[0], tl[1], tr[0], tr[1], br[0], br[1], bl[0], bl[1]];
|
|
2090
|
+
}
|
|
2091
|
+
function naturalSizeOf(el) {
|
|
2092
|
+
const { naturalWidth: w, naturalHeight: h } = el;
|
|
2093
|
+
if (typeof w !== "number" || typeof h !== "number" || w <= 0 || h <= 0) return null;
|
|
2094
|
+
return [w, h];
|
|
2095
|
+
}
|
|
2096
|
+
function isClipping(cs) {
|
|
2097
|
+
for (const overflow of [cs.overflowX, cs.overflowY]) {
|
|
2098
|
+
if (overflow && overflow !== "visible") return true;
|
|
2099
|
+
}
|
|
2100
|
+
return false;
|
|
2101
|
+
}
|
|
2102
|
+
var ANCESTOR_WALK_LIMIT = 12;
|
|
2103
|
+
function isContainingBlock(cs) {
|
|
2104
|
+
if (cs.position && cs.position !== "static") return true;
|
|
2105
|
+
return [cs.transform, cs.filter, cs.perspective].some((v) => v && v !== "none");
|
|
2106
|
+
}
|
|
2107
|
+
function clipsSubject(subject, cs) {
|
|
2108
|
+
if (subject === "fixed") return false;
|
|
2109
|
+
if (subject === "absolute") return isContainingBlock(cs);
|
|
2110
|
+
return true;
|
|
2111
|
+
}
|
|
2112
|
+
function narrowByAncestor(acc, node, win) {
|
|
2113
|
+
const cs = cssStyleOf(node, win);
|
|
2114
|
+
if (!cs) return;
|
|
2115
|
+
const opacity = numOr(cs.opacity, 1);
|
|
2116
|
+
if (opacity < 1) acc.alpha *= Math.max(0, opacity);
|
|
2117
|
+
if (clipsSubject(acc.subject, cs)) {
|
|
2118
|
+
const rect = isClipping(cs) ? node.getBoundingClientRect?.() : null;
|
|
2119
|
+
if (rect) acc.clip = intersectBox(acc.clip, insetBox(boxOf(rect), borderWidthsOf(cs)));
|
|
2120
|
+
acc.subject = cs.position || "static";
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
function walkAncestors(el, win, start, subject) {
|
|
2124
|
+
const acc = { clip: start, alpha: 1, subject };
|
|
2125
|
+
let node = el.parentElement ?? null;
|
|
2126
|
+
for (let depth = 0; node && depth < ANCESTOR_WALK_LIMIT; depth++) {
|
|
2127
|
+
narrowByAncestor(acc, node, win);
|
|
2128
|
+
node = node.parentElement ?? null;
|
|
2129
|
+
}
|
|
2130
|
+
return acc;
|
|
2131
|
+
}
|
|
2132
|
+
function measureElementGeometry(el, win, fullBox = false, hostAlpha) {
|
|
2133
|
+
const rect = el.getBoundingClientRect();
|
|
2134
|
+
const { height } = getScissorEnv(win);
|
|
2135
|
+
if (!isRectVisible(rect, height)) return null;
|
|
2136
|
+
const view = win ?? null;
|
|
2137
|
+
const cs = cssStyleOf(el, view);
|
|
2138
|
+
const border = boxOf(rect);
|
|
2139
|
+
const content = fullBox ? border : contentBoxOf(rect, cs);
|
|
2140
|
+
const walked = walkAncestors(el, view, content, cs?.position || "static");
|
|
2141
|
+
const own = Math.max(0, Math.min(1, hostAlpha ?? numOr(cs?.opacity, 1)));
|
|
2142
|
+
return {
|
|
2143
|
+
border,
|
|
2144
|
+
paint: fullBox ? border : paintBoxOf(content, naturalSizeOf(el), cs),
|
|
2145
|
+
clip: walked.clip,
|
|
2146
|
+
radii: radiiOf(cs, rect),
|
|
2147
|
+
alpha: walked.alpha * own,
|
|
2148
|
+
hostAlpha: own
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
function maskUniformsOf(geom, canvasHeight, dpr) {
|
|
2152
|
+
const b = geom.border;
|
|
2153
|
+
const r = geom.radii;
|
|
2154
|
+
return {
|
|
2155
|
+
maskBox: [
|
|
2156
|
+
(b.left + b.right) * 0.5 * dpr,
|
|
2157
|
+
canvasHeight - (b.top + b.bottom) * 0.5 * dpr,
|
|
2158
|
+
(b.right - b.left) * 0.5 * dpr,
|
|
2159
|
+
(b.bottom - b.top) * 0.5 * dpr
|
|
2160
|
+
],
|
|
2161
|
+
maskRx: [r[0] * dpr, r[2] * dpr, r[4] * dpr, r[6] * dpr],
|
|
2162
|
+
maskRy: [r[1] * dpr, r[3] * dpr, r[5] * dpr, r[7] * dpr]
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
function resolveDrawBox(geom, canvas, dpr) {
|
|
2166
|
+
const paintW = geom.paint.right - geom.paint.left;
|
|
2167
|
+
const paintH = geom.paint.bottom - geom.paint.top;
|
|
2168
|
+
if (paintW <= 0 || paintH <= 0) return null;
|
|
2169
|
+
const view = { left: 0, top: 0, right: canvas.width / dpr, bottom: canvas.height / dpr };
|
|
2170
|
+
const draw = intersectBox(intersectBox(geom.paint, geom.clip), view);
|
|
2171
|
+
const drawW = draw.right - draw.left;
|
|
2172
|
+
const drawH = draw.bottom - draw.top;
|
|
2173
|
+
if (drawW <= 0 || drawH <= 0) return null;
|
|
2174
|
+
const sx = Math.round(draw.left * dpr);
|
|
2175
|
+
const sy = Math.round(canvas.height - draw.bottom * dpr);
|
|
2176
|
+
const sw = Math.min(canvas.width - sx, Math.round(drawW * dpr));
|
|
2177
|
+
const sh = Math.min(canvas.height - sy, Math.round(drawH * dpr));
|
|
2178
|
+
if (sw <= 0 || sh <= 0) return null;
|
|
2179
|
+
const b = geom.border;
|
|
2180
|
+
return {
|
|
2181
|
+
sx,
|
|
2182
|
+
sy,
|
|
2183
|
+
sw,
|
|
2184
|
+
sh,
|
|
2185
|
+
rect: { left: b.left, top: b.top, width: b.right - b.left, height: b.bottom - b.top },
|
|
2186
|
+
uvOrigin: [(draw.left - geom.paint.left) / paintW, (draw.top - geom.paint.top) / paintH],
|
|
2187
|
+
uvScale: [drawW / paintW, drawH / paintH],
|
|
2188
|
+
alpha: geom.alpha,
|
|
2189
|
+
...maskUniformsOf(geom, canvas.height, dpr)
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
function applyDrawBox(gl, canvas, geom, dpr) {
|
|
2193
|
+
const box = resolveDrawBox(geom, canvas, dpr);
|
|
2194
|
+
if (!box) return null;
|
|
2195
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
2196
|
+
gl.viewport(box.sx, box.sy, box.sw, box.sh);
|
|
2197
|
+
gl.scissor(box.sx, box.sy, box.sw, box.sh);
|
|
2198
|
+
return box;
|
|
2199
|
+
}
|
|
2200
|
+
function bindShaderTexture(gl, progInfo, texture, toTexture) {
|
|
2201
|
+
if (!texture) return;
|
|
2202
|
+
const locs = resolveLocations(gl, progInfo);
|
|
2203
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
2204
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
2205
|
+
if (locs?.u_image) gl.uniform1i(locs.u_image, 0);
|
|
2206
|
+
if (locs?.u_image_to) {
|
|
2207
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
2208
|
+
gl.bindTexture(gl.TEXTURE_2D, toTexture || texture);
|
|
2209
|
+
gl.uniform1i(locs.u_image_to, 1);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
function getProgram(info) {
|
|
2213
|
+
if (!info) return null;
|
|
2214
|
+
return "program" in info && info.program ? info.program : info;
|
|
2215
|
+
}
|
|
2216
|
+
function computeMousePos(renderer, rect) {
|
|
2217
|
+
if (renderer.hasPointer === false) return { x: 0.5, y: 0.5 };
|
|
2218
|
+
const mx = renderer.mouse.x - rect.left;
|
|
2219
|
+
const my = renderer.mouse.y - rect.top;
|
|
2220
|
+
return {
|
|
2221
|
+
x: clamp(mx / rect.width, 0, 1),
|
|
2222
|
+
y: clamp(my / rect.height, 0, 1)
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
function bindQuadAttrib(gl, quadBuffer, info) {
|
|
2226
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
|
2227
|
+
const locs = info;
|
|
2228
|
+
if (locs?.a_position !== void 0 && locs.a_position !== -1) {
|
|
2229
|
+
gl.enableVertexAttribArray(locs.a_position);
|
|
2230
|
+
gl.vertexAttribPointer(locs.a_position, 2, gl.FLOAT, false, 0, 0);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
function drawTargets(renderer) {
|
|
2234
|
+
const { gl, canvas, quadBuffer } = renderer;
|
|
2235
|
+
return gl && canvas && quadBuffer ? { gl, canvas } : null;
|
|
2236
|
+
}
|
|
2237
|
+
function drawElementQuad(renderer, el, opt = {}) {
|
|
2238
|
+
const target = drawTargets(renderer);
|
|
2239
|
+
if (!target) return false;
|
|
2240
|
+
const { gl, canvas } = target;
|
|
2241
|
+
const mode = opt.mode ?? "displace";
|
|
2242
|
+
const info = renderer.programs[mode];
|
|
2243
|
+
if (info === void 0) return false;
|
|
2244
|
+
const program = getProgram(info);
|
|
2245
|
+
if (!program) return false;
|
|
2246
|
+
const geom = opt.geometry !== void 0 ? opt.geometry : measureElementGeometry(el, renderer.window, GENERATIVE_MODES.has(mode));
|
|
2247
|
+
if (!geom) return false;
|
|
2248
|
+
const scissor = applyDrawBox(gl, canvas, geom, getScissorEnv(renderer.window).dpr);
|
|
2249
|
+
if (!scissor) return false;
|
|
2250
|
+
const localMouse = computeMousePos(renderer, scissor.rect);
|
|
2251
|
+
gl.useProgram(program);
|
|
2252
|
+
bindShaderTexture(gl, info, opt.texture, opt.toTexture);
|
|
2253
|
+
uploadUniforms(gl, info, {
|
|
2254
|
+
...opt,
|
|
2255
|
+
localMouse,
|
|
2256
|
+
uvOrigin: scissor.uvOrigin,
|
|
2257
|
+
uvScale: scissor.uvScale,
|
|
2258
|
+
maskBox: scissor.maskBox,
|
|
2259
|
+
maskRx: scissor.maskRx,
|
|
2260
|
+
maskRy: scissor.maskRy,
|
|
2261
|
+
maskAlpha: scissor.alpha
|
|
2262
|
+
});
|
|
2263
|
+
uploadColorUniforms(gl, info, opt);
|
|
2264
|
+
bindQuadAttrib(gl, renderer.quadBuffer, info);
|
|
2265
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
2266
|
+
return true;
|
|
2267
|
+
}
|
|
2268
|
+
var GENERATIVE_MODES = /* @__PURE__ */ new Set(["gradient"]);
|
|
2269
|
+
var SHADER_MODES = ["displace", "fluid", "liquid", "particles", "morph", "gradient", "logo"];
|
|
2270
|
+
var PROGRAM_SOURCES = [
|
|
2271
|
+
[["displace"], DISPLACE_FS],
|
|
2272
|
+
[["fluid"], FLUID_FS],
|
|
2273
|
+
[["liquid"], LIQUID_FS],
|
|
2274
|
+
[["particles"], PARTICLES_FS],
|
|
2275
|
+
[["morph"], MORPH_FS],
|
|
2276
|
+
[["gradient", "logo"], GRADIENT_FS]
|
|
2277
|
+
];
|
|
2278
|
+
var MASK_MODES = { alpha: 1, luma: 2, "luma-invert": 3 };
|
|
2279
|
+
var HOVER_MODES = { none: 0, stir: 1, light: 2, both: 3 };
|
|
2280
|
+
var MOTION_MODES = { evolve: 0, drift: 1, swirl: 2 };
|
|
2281
|
+
function readShaderMode(params) {
|
|
2282
|
+
return "keyword" in params && typeof params.keyword === "function" ? params.keyword("mode") : params.text("mode", "displace");
|
|
2283
|
+
}
|
|
2284
|
+
function readMaskMode(params, mode) {
|
|
2285
|
+
return mode === "logo" ? MASK_MODES[params.text("mask", "alpha")] ?? MASK_MODES.alpha : 0;
|
|
2286
|
+
}
|
|
2287
|
+
function readDuotoneColors(c1Text, c2Text, createCanvas) {
|
|
2288
|
+
return {
|
|
2289
|
+
isDuotone: Boolean(c1Text && c2Text),
|
|
2290
|
+
c1Rgba: parseColor(c1Text || "#000000", createCanvas),
|
|
2291
|
+
c2Rgba: parseColor(c2Text || "#ffffff", createCanvas)
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
function extractShaderOptions(params, createCanvas) {
|
|
2295
|
+
const mode = readShaderMode(params);
|
|
2296
|
+
const c1Text = params.text("color1", "");
|
|
2297
|
+
const c2Text = params.text("color2", "");
|
|
2298
|
+
const backdropText = params.text("backdrop", "").trim();
|
|
2299
|
+
const blendMap = { normal: 0, screen: 1, multiply: 2, add: 3 };
|
|
2300
|
+
const { palette, colorCount } = buildPalette([
|
|
2301
|
+
c1Text,
|
|
2302
|
+
c2Text,
|
|
2303
|
+
params.text("color3", ""),
|
|
2304
|
+
params.text("color4", ""),
|
|
2305
|
+
params.text("color5", "")
|
|
2306
|
+
], createCanvas);
|
|
2307
|
+
return {
|
|
2308
|
+
mode,
|
|
2309
|
+
seed: params.num("seed", 0),
|
|
2310
|
+
scale: params.num("scale", 1),
|
|
2311
|
+
warp: params.num("warp", 0),
|
|
2312
|
+
noise: params.num("noise", 0),
|
|
2313
|
+
grain: params.num("grain", 0),
|
|
2314
|
+
hue: parseAngleRadians(params.text("hue", "0deg")),
|
|
2315
|
+
detail: params.num("detail", 3),
|
|
2316
|
+
bands: params.num("bands", 0),
|
|
2317
|
+
palette,
|
|
2318
|
+
colorCount,
|
|
2319
|
+
maskMode: readMaskMode(params, mode),
|
|
2320
|
+
hover: HOVER_MODES[params.text("hover", "none")] ?? 0,
|
|
2321
|
+
angle: parseAngleRadians(params.text("angle", "0deg")),
|
|
2322
|
+
motion: MOTION_MODES[params.text("motion", "evolve")] ?? 0,
|
|
2323
|
+
// Read through the raw string rather than straight into `parseColor`, which answers opaque
|
|
2324
|
+
// white for an empty input — the fallback that makes an unset `color1` harmless would make an
|
|
2325
|
+
// unset `backdrop` a white plate behind every logo on the page.
|
|
2326
|
+
backdropRgba: backdropText ? parseColor(backdropText, createCanvas) : [0, 0, 0, 0],
|
|
2327
|
+
strength: params.num("strength", 0.5),
|
|
2328
|
+
speed: params.num("speed", 1),
|
|
2329
|
+
frequency: params.num("frequency", 10),
|
|
2330
|
+
chromatic: params.num("chromatic", 0),
|
|
2331
|
+
iridescence: params.num("iridescence", 0),
|
|
2332
|
+
tintRgba: parseColor(params.text("tint", "#ffffff"), createCanvas),
|
|
2333
|
+
...readDuotoneColors(c1Text, c2Text, createCanvas),
|
|
2334
|
+
blendMode: blendMap[params.text("blend", "normal")] ?? 0,
|
|
2335
|
+
to: params.text("to", ""),
|
|
2336
|
+
progress: params.num ? params.num("progress", -1) : -1,
|
|
2337
|
+
// Compared against the one enabling word rather than "not `off`", so — exactly as with
|
|
2338
|
+
// `audioBand` below — a caller passing an unvalidated accessor, or an author who typed
|
|
2339
|
+
// `scrub: scrol`, cannot turn the bridge on by accident.
|
|
2340
|
+
scrubsFromScroll: params.text("scrub", "off") === "scroll",
|
|
2341
|
+
// `parseAudioBand` answers `null` for `off`, for an empty string, and for any word that is not
|
|
2342
|
+
// a band, so a caller passing an unvalidated accessor cannot turn the feature on by accident.
|
|
2343
|
+
audioBand: parseAudioBand(params.text("audio", "off"))
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
function parseProgressValue(raw) {
|
|
2347
|
+
if (!raw) return null;
|
|
2348
|
+
const val = parseFloat(raw);
|
|
2349
|
+
return Number.isFinite(val) ? clamp(val, 0, 1) : null;
|
|
2350
|
+
}
|
|
2351
|
+
function readElementProgress(el) {
|
|
2352
|
+
const fromInline = parseProgressValue(el.style?.getPropertyValue?.("--kui-progress"));
|
|
2353
|
+
if (fromInline !== null) return fromInline;
|
|
2354
|
+
const view = el.ownerDocument?.defaultView;
|
|
2355
|
+
const fromComputed = parseProgressValue(view?.getComputedStyle?.(el).getPropertyValue("--kui-progress"));
|
|
2356
|
+
return fromComputed !== null ? fromComputed : -1;
|
|
2357
|
+
}
|
|
2358
|
+
var nextShaderId = 0;
|
|
2359
|
+
function invokeIsolated(fn) {
|
|
2360
|
+
try {
|
|
2361
|
+
fn();
|
|
2362
|
+
} catch {
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
function syncCanvasDimensions(canvas, win, dpr) {
|
|
2367
|
+
const vw = win?.innerWidth ?? 1e3;
|
|
2368
|
+
const vh = win?.innerHeight ?? 800;
|
|
2369
|
+
const cw = Math.round(vw * dpr);
|
|
2370
|
+
const ch = Math.round(vh * dpr);
|
|
2371
|
+
if (canvas.width !== cw || canvas.height !== ch) {
|
|
2372
|
+
canvas.width = cw;
|
|
2373
|
+
canvas.height = ch;
|
|
2374
|
+
}
|
|
2375
|
+
const style = canvas.style;
|
|
2376
|
+
if (!style) return;
|
|
2377
|
+
const cssW = `${vw}px`;
|
|
2378
|
+
const cssH = `${vh}px`;
|
|
2379
|
+
if (style.width !== cssW) style.width = cssW;
|
|
2380
|
+
if (style.height !== cssH) style.height = cssH;
|
|
2381
|
+
}
|
|
2382
|
+
var DEFAULT_SHADER_Z_INDEX = 1;
|
|
2383
|
+
var SHADER_Z_PROPERTY = "--kui-shader-z";
|
|
2384
|
+
function readShaderZIndex(doc, win) {
|
|
2385
|
+
const root = doc?.documentElement;
|
|
2386
|
+
if (!root) return DEFAULT_SHADER_Z_INDEX;
|
|
2387
|
+
const inline = parseZIndexValue(root.style?.getPropertyValue?.(SHADER_Z_PROPERTY));
|
|
2388
|
+
return inline ?? readComputedZIndex(root, win) ?? DEFAULT_SHADER_Z_INDEX;
|
|
2389
|
+
}
|
|
2390
|
+
function readComputedZIndex(root, win) {
|
|
2391
|
+
return parseZIndexValue(win?.getComputedStyle?.(root)?.getPropertyValue?.(SHADER_Z_PROPERTY));
|
|
2392
|
+
}
|
|
2393
|
+
function parseZIndexValue(raw) {
|
|
2394
|
+
const trimmed = raw?.trim();
|
|
2395
|
+
if (!trimmed) return null;
|
|
2396
|
+
const n = Number(trimmed);
|
|
2397
|
+
return Number.isFinite(n) ? Math.trunc(n) : null;
|
|
2398
|
+
}
|
|
2399
|
+
var SharedShaderRenderer = class {
|
|
2400
|
+
env;
|
|
2401
|
+
window;
|
|
2402
|
+
document;
|
|
2403
|
+
createCanvas;
|
|
2404
|
+
raf;
|
|
2405
|
+
caf;
|
|
2406
|
+
canvas = null;
|
|
2407
|
+
gl = null;
|
|
2408
|
+
quadBuffer = null;
|
|
2409
|
+
rafId = null;
|
|
2410
|
+
drawCalls = /* @__PURE__ */ new Map();
|
|
2411
|
+
contextCallbacks = /* @__PURE__ */ new Map();
|
|
2412
|
+
/**
|
|
2413
|
+
* One style-reading pass per registered instance — scroll progress and, when the author asked
|
|
2414
|
+
* for one, an audio band — run in full before any instance's draw call this frame. See
|
|
2415
|
+
* `renderFrame` for why the ordering, not just the reads themselves, is the point.
|
|
2416
|
+
*/
|
|
2417
|
+
inputReaders = /* @__PURE__ */ new Map();
|
|
2418
|
+
programs = {};
|
|
2419
|
+
refCount = 0;
|
|
2420
|
+
mouse = { x: 0, y: 0 };
|
|
2421
|
+
/** Cleared until a real `pointermove` lands. See {@link computeMousePos}. */
|
|
2422
|
+
hasPointer = false;
|
|
2423
|
+
isContextLost = false;
|
|
2424
|
+
/**
|
|
2425
|
+
* Bumped every time this renderer's GL objects stop being valid, i.e. on each context loss.
|
|
2426
|
+
*
|
|
2427
|
+
* It exists because "is this handle still real?" has no other answer. A `WebGLTexture` from a lost
|
|
2428
|
+
* context is an ordinary live JavaScript object — binding it raises `INVALID_OPERATION` and
|
|
2429
|
+
* samples nothing, and neither the draw nor `drawElementQuad`'s return value can tell.
|
|
2430
|
+
*
|
|
2431
|
+
* The hole it closes: `cancel()`/`finish()` unregister an instance, which takes its entry out of
|
|
2432
|
+
* `contextCallbacks` with it, so an *inactive* instance is never told the context went away. Its
|
|
2433
|
+
* texture holders kept answering with pre-loss handles, and `acquireRenderer` short-circuits on
|
|
2434
|
+
* re-activation because the renderer itself was never destroyed — so a hover, a hover-out, a
|
|
2435
|
+
* context loss and a second hover drew a blank replica over a hidden image, permanently.
|
|
2436
|
+
*
|
|
2437
|
+
* Stamping the holders rather than keeping every acquired instance registered for context events:
|
|
2438
|
+
* the stamp is checked where the stale handle is actually read, so it holds however the instance
|
|
2439
|
+
* came to miss the event, and it costs two compares per holder per frame. See
|
|
2440
|
+
* {@link initTextureHolder}.
|
|
2441
|
+
*/
|
|
2442
|
+
contextGeneration = 0;
|
|
2443
|
+
mapKey;
|
|
2444
|
+
onPointer;
|
|
2445
|
+
onContextLost;
|
|
2446
|
+
onContextRestored;
|
|
2447
|
+
constructor(env = {}) {
|
|
2448
|
+
this.env = env;
|
|
2449
|
+
const r = resolveEnv(null, env);
|
|
2450
|
+
this.window = r.window;
|
|
2451
|
+
this.document = r.document;
|
|
2452
|
+
this.createCanvas = r.createCanvas;
|
|
2453
|
+
this.raf = r.raf;
|
|
2454
|
+
this.caf = r.caf;
|
|
2455
|
+
this.mapKey = r.document ?? r.window ?? fallbackShaderKey;
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Whether `destroy()` has run. A destroyed renderer never comes back to life.
|
|
2459
|
+
*
|
|
2460
|
+
* `destroy()` un-maps itself, so the object an already-prepared instance is still holding is no
|
|
2461
|
+
* longer the one `getSharedShaderRenderer` hands out. Letting `acquire()` re-`init()` that object
|
|
2462
|
+
* therefore built a *second* full-viewport canvas and WebGL2 context in one document — and then
|
|
2463
|
+
* a third, because the next element to be prepared found nothing in the map and made its own,
|
|
2464
|
+
* and whichever of them tore down next deleted the shared document key from under the live one.
|
|
2465
|
+
* Cycling shader elements on an SPA route walked straight into the browser's 8-16 context cap.
|
|
2466
|
+
*
|
|
2467
|
+
* So a teardown is final, and `RendererRef` is what gets an orphaned instance back onto the live
|
|
2468
|
+
* renderer.
|
|
2469
|
+
*/
|
|
2470
|
+
isDestroyed = false;
|
|
2471
|
+
acquire() {
|
|
2472
|
+
if (this.isDestroyed) return false;
|
|
2473
|
+
if (this.canvas && this.isContextLost) {
|
|
2474
|
+
this.refCount++;
|
|
2475
|
+
return true;
|
|
2476
|
+
}
|
|
2477
|
+
if (this.gl && !this.isContextLost) {
|
|
2478
|
+
this.refCount++;
|
|
2479
|
+
return true;
|
|
2480
|
+
}
|
|
2481
|
+
const ok = this.init();
|
|
2482
|
+
if (ok) this.refCount++;
|
|
2483
|
+
return ok;
|
|
2484
|
+
}
|
|
2485
|
+
release() {
|
|
2486
|
+
if (this.refCount > 0) this.refCount--;
|
|
2487
|
+
if (this.refCount === 0) this.destroy();
|
|
2488
|
+
}
|
|
2489
|
+
init() {
|
|
2490
|
+
if (!this.document?.body?.appendChild) return false;
|
|
2491
|
+
this.canvas = this.createCanvas();
|
|
2492
|
+
if (!this.canvas) return false;
|
|
2493
|
+
this.canvas.className = "kui-shader-canvas";
|
|
2494
|
+
this.canvas.style.cssText = `position:fixed;top:0;left:0;pointer-events:none;z-index:${readShaderZIndex(this.document, this.window)};`;
|
|
2495
|
+
this.gl = this.canvas.getContext ? this.canvas.getContext("webgl2", { alpha: true, premultipliedAlpha: true }) : null;
|
|
2496
|
+
if (!this.gl) return false;
|
|
2497
|
+
syncCanvasDimensions(this.canvas, this.window, getScissorEnv(this.window).dpr);
|
|
2498
|
+
this.document.body.appendChild(this.canvas);
|
|
2499
|
+
if (!this.initQuad()) return this.abandonInit();
|
|
2500
|
+
this.initPrograms();
|
|
2501
|
+
this.bindEvents();
|
|
2502
|
+
this.startLoop();
|
|
2503
|
+
return true;
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Undo a half-built `init()` and report it as a failure.
|
|
2507
|
+
*
|
|
2508
|
+
* Not `destroy()`: nothing has acquired this renderer yet, so there is no teardown to broadcast,
|
|
2509
|
+
* and marking it `isDestroyed` would retire an object that is free to try again the next time
|
|
2510
|
+
* something acquires it. Under transient memory pressure that retry is the whole point — the
|
|
2511
|
+
* alternative is a permanently dead renderer for a `createBuffer` that would succeed a second
|
|
2512
|
+
* later. The canvas goes because it is already in the document by this point; leaving it would
|
|
2513
|
+
* stack one invisible full-viewport canvas per attempt.
|
|
2514
|
+
*/
|
|
2515
|
+
abandonInit() {
|
|
2516
|
+
disposeGLResources(this.gl, this.quadBuffer, this.programs);
|
|
2517
|
+
this.canvas?.remove();
|
|
2518
|
+
this.canvas = null;
|
|
2519
|
+
this.gl = null;
|
|
2520
|
+
this.quadBuffer = null;
|
|
2521
|
+
this.programs = {};
|
|
2522
|
+
return false;
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* Allocate and fill the one static buffer every draw in this module reads its vertices from.
|
|
2526
|
+
*
|
|
2527
|
+
* Returns whether it worked, and the caller has to care. `gl.createBuffer()` returns `null` under
|
|
2528
|
+
* memory pressure rather than throwing, and so does nothing else downstream: `vertexAttribPointer`
|
|
2529
|
+
* and `drawArrays` against a null binding only *set* a GL error, and `drawElementQuad` used to
|
|
2530
|
+
* return `true` regardless. The visible result was the worst possible one — every affected
|
|
2531
|
+
* element's `opacity` driven to 0 by `hideBehindRenderer` with nothing drawn over it, so the page
|
|
2532
|
+
* lost its images rather than losing an effect. A renderer that cannot allocate this is not a
|
|
2533
|
+
* renderer, so it fails here instead.
|
|
2534
|
+
*
|
|
2535
|
+
* `bufferData` is guarded too, and only for a host that throws rather than raising `OUT_OF_MEMORY`
|
|
2536
|
+
* through the error queue. Reading the queue is deliberately not done: `getError` is a
|
|
2537
|
+
* synchronous round-trip to the GPU process, and the allocation failure it would catch is already
|
|
2538
|
+
* caught by the null above.
|
|
2539
|
+
*/
|
|
2540
|
+
initQuad() {
|
|
2541
|
+
const gl = this.gl;
|
|
2542
|
+
if (!gl) return false;
|
|
2543
|
+
this.quadBuffer = gl.createBuffer();
|
|
2544
|
+
if (!this.quadBuffer) return false;
|
|
2545
|
+
try {
|
|
2546
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
|
|
2547
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
|
|
2548
|
+
} catch {
|
|
2549
|
+
if (gl.deleteBuffer) gl.deleteBuffer(this.quadBuffer);
|
|
2550
|
+
this.quadBuffer = null;
|
|
2551
|
+
return false;
|
|
2552
|
+
}
|
|
2553
|
+
return true;
|
|
2554
|
+
}
|
|
2555
|
+
initPrograms() {
|
|
2556
|
+
const gl = this.gl;
|
|
2557
|
+
if (!gl) return;
|
|
2558
|
+
this.programs = {};
|
|
2559
|
+
for (const [modes, fs] of PROGRAM_SOURCES) {
|
|
2560
|
+
const prog = createProgram(gl, FULLSCREEN_QUAD_VS, fs);
|
|
2561
|
+
if (!prog) continue;
|
|
2562
|
+
const locs = extractLocations(gl, prog);
|
|
2563
|
+
for (const mode of modes) this.programs[mode] = locs;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
bindEvents() {
|
|
2567
|
+
this.onPointer = (e) => {
|
|
2568
|
+
this.mouse.x = e.clientX;
|
|
2569
|
+
this.mouse.y = e.clientY;
|
|
2570
|
+
this.hasPointer = true;
|
|
2571
|
+
};
|
|
2572
|
+
this.window?.addEventListener?.("pointermove", this.onPointer, { passive: true });
|
|
2573
|
+
if (this.canvas) {
|
|
2574
|
+
this.onContextLost = (e) => {
|
|
2575
|
+
if (e && typeof e.preventDefault === "function") e.preventDefault();
|
|
2576
|
+
this.isContextLost = true;
|
|
2577
|
+
this.contextGeneration++;
|
|
2578
|
+
this.stopLoop();
|
|
2579
|
+
for (const cb of this.contextCallbacks.values()) invokeIsolated(() => cb.onLost());
|
|
2580
|
+
};
|
|
2581
|
+
this.onContextRestored = () => {
|
|
2582
|
+
this.isContextLost = false;
|
|
2583
|
+
const ok = this.initQuad();
|
|
2584
|
+
this.initPrograms();
|
|
2585
|
+
for (const cb of this.contextCallbacks.values()) invokeIsolated(() => cb.onRestored());
|
|
2586
|
+
if (ok) this.startLoop();
|
|
2587
|
+
};
|
|
2588
|
+
this.canvas.addEventListener("webglcontextlost", this.onContextLost, false);
|
|
2589
|
+
this.canvas.addEventListener("webglcontextrestored", this.onContextRestored, false);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
createTexture(source) {
|
|
2593
|
+
return this.gl ? createGLTexture(this.gl, source) : null;
|
|
2594
|
+
}
|
|
2595
|
+
register(id, fn, callbacks, readInputs) {
|
|
2596
|
+
this.drawCalls.set(id, fn);
|
|
2597
|
+
if (callbacks) this.contextCallbacks.set(id, callbacks);
|
|
2598
|
+
if (readInputs) this.inputReaders.set(id, readInputs);
|
|
2599
|
+
if (!this.isContextLost) this.startLoop();
|
|
2600
|
+
}
|
|
2601
|
+
unregister(id) {
|
|
2602
|
+
this.drawCalls.delete(id);
|
|
2603
|
+
this.contextCallbacks.delete(id);
|
|
2604
|
+
this.inputReaders.delete(id);
|
|
2605
|
+
}
|
|
2606
|
+
startLoop() {
|
|
2607
|
+
if (this.rafId || this.isContextLost) return;
|
|
2608
|
+
const tick = (ts) => {
|
|
2609
|
+
this.renderFrame(ts * 1e-3);
|
|
2610
|
+
if (this.drawCalls.size > 0 && !this.isContextLost) {
|
|
2611
|
+
this.rafId = this.raf(tick);
|
|
2612
|
+
} else {
|
|
2613
|
+
this.rafId = null;
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
this.rafId = this.raf(tick);
|
|
2617
|
+
}
|
|
2618
|
+
stopLoop() {
|
|
2619
|
+
if (this.rafId) {
|
|
2620
|
+
this.caf(this.rafId);
|
|
2621
|
+
this.rafId = null;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
renderFrame(timeSeconds) {
|
|
2625
|
+
const gl = this.gl;
|
|
2626
|
+
if (!gl || !this.canvas || this.isContextLost) return;
|
|
2627
|
+
const dpr = Math.min(this.window?.devicePixelRatio ?? 1, 2);
|
|
2628
|
+
syncCanvasDimensions(this.canvas, this.window, dpr);
|
|
2629
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
2630
|
+
gl.clearColor(0, 0, 0, 0);
|
|
2631
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
2632
|
+
for (const readInputs of this.inputReaders.values()) invokeIsolated(readInputs);
|
|
2633
|
+
this.runDrawCalls(timeSeconds);
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Every registered instance's draw, in registration order, each one isolated from its neighbours.
|
|
2637
|
+
*
|
|
2638
|
+
* The snapshot is here because a draw call may unregister itself — the `catch` below does — and
|
|
2639
|
+
* mutating `drawCalls` under a live iterator would skip its neighbour. But a snapshot of the
|
|
2640
|
+
* *entries* must not become a snapshot of the *context*, which is why the `gl` is re-read per
|
|
2641
|
+
* iteration rather than taken from `renderFrame`'s. `destroy()` nulls `gl` and deletes the
|
|
2642
|
+
* programs, so a draw call that dropped the last reference mid-frame left every remaining entry
|
|
2643
|
+
* in the snapshot drawing through a disposed context, on a renderer already `isDestroyed` with
|
|
2644
|
+
* its `drawCalls` cleared. The same re-read covers a context lost mid-frame, where `gl` survives
|
|
2645
|
+
* but the programs and buffers do not.
|
|
2646
|
+
*
|
|
2647
|
+
* @param timeSeconds - This frame's timestamp, passed to every draw call.
|
|
2648
|
+
* @complexity O(n) in registered instances.
|
|
2649
|
+
*/
|
|
2650
|
+
runDrawCalls(timeSeconds) {
|
|
2651
|
+
for (const [id, drawCall] of Array.from(this.drawCalls.entries())) {
|
|
2652
|
+
const live = this.gl;
|
|
2653
|
+
if (!live || this.isContextLost) return;
|
|
2654
|
+
try {
|
|
2655
|
+
drawCall(live, timeSeconds);
|
|
2656
|
+
} catch {
|
|
2657
|
+
const callbacks = this.contextCallbacks.get(id);
|
|
2658
|
+
if (callbacks) invokeIsolated(() => callbacks.onLost());
|
|
2659
|
+
this.unregister(id);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
destroy() {
|
|
2664
|
+
if (this.isDestroyed) return;
|
|
2665
|
+
this.isDestroyed = true;
|
|
2666
|
+
this.stopLoop();
|
|
2667
|
+
if (this.window?.removeEventListener && this.onPointer) {
|
|
2668
|
+
this.window.removeEventListener("pointermove", this.onPointer);
|
|
2669
|
+
}
|
|
2670
|
+
if (this.canvas) {
|
|
2671
|
+
if (this.onContextLost) this.canvas.removeEventListener("webglcontextlost", this.onContextLost);
|
|
2672
|
+
if (this.onContextRestored) this.canvas.removeEventListener("webglcontextrestored", this.onContextRestored);
|
|
2673
|
+
this.canvas.remove();
|
|
2674
|
+
this.canvas = null;
|
|
2675
|
+
}
|
|
2676
|
+
disposeGLResources(this.gl, this.quadBuffer, this.programs);
|
|
2677
|
+
this.gl = null;
|
|
2678
|
+
this.quadBuffer = null;
|
|
2679
|
+
this.isContextLost = false;
|
|
2680
|
+
this.drawCalls.clear();
|
|
2681
|
+
this.contextCallbacks.clear();
|
|
2682
|
+
this.inputReaders.clear();
|
|
2683
|
+
this.programs = {};
|
|
2684
|
+
this.refCount = 0;
|
|
2685
|
+
if (shaderRenderers.get(this.mapKey) === this) shaderRenderers.delete(this.mapKey);
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
var shaderRenderers = /* @__PURE__ */ new WeakMap();
|
|
2689
|
+
var fallbackShaderKey = {};
|
|
2690
|
+
function getSharedShaderRenderer(env) {
|
|
2691
|
+
const resolved = resolveEnv(null, env);
|
|
2692
|
+
const key = resolved.document ?? resolved.window ?? fallbackShaderKey;
|
|
2693
|
+
let renderer = shaderRenderers.get(key);
|
|
2694
|
+
if (!renderer || renderer.isDestroyed) {
|
|
2695
|
+
renderer = new SharedShaderRenderer(env);
|
|
2696
|
+
shaderRenderers.set(key, renderer);
|
|
2697
|
+
}
|
|
2698
|
+
return renderer;
|
|
2699
|
+
}
|
|
2700
|
+
function createRendererRef(env) {
|
|
2701
|
+
let current = getSharedShaderRenderer(env);
|
|
2702
|
+
return {
|
|
2703
|
+
get() {
|
|
2704
|
+
if (current.isDestroyed) current = getSharedShaderRenderer(env);
|
|
2705
|
+
return current;
|
|
2706
|
+
}
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
function initTextureHolder(getSource, host) {
|
|
2710
|
+
let tex = null;
|
|
2711
|
+
let owner = null;
|
|
2712
|
+
let generation = -1;
|
|
2713
|
+
const dropIfStale = (renderer) => {
|
|
2714
|
+
if (tex && (owner !== renderer || generation !== renderer.contextGeneration)) tex = null;
|
|
2715
|
+
};
|
|
2716
|
+
return {
|
|
2717
|
+
get() {
|
|
2718
|
+
const renderer = host.renderer;
|
|
2719
|
+
dropIfStale(renderer);
|
|
2720
|
+
if (!tex && !renderer.isContextLost) {
|
|
2721
|
+
const src = getSource();
|
|
2722
|
+
if (src) {
|
|
2723
|
+
tex = renderer.createTexture(src);
|
|
2724
|
+
owner = renderer;
|
|
2725
|
+
generation = renderer.contextGeneration;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
return tex;
|
|
2729
|
+
},
|
|
2730
|
+
peek() {
|
|
2731
|
+
dropIfStale(host.renderer);
|
|
2732
|
+
return tex;
|
|
2733
|
+
},
|
|
2734
|
+
reset() {
|
|
2735
|
+
tex = null;
|
|
2736
|
+
}
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2739
|
+
function createShaderTextureHolders(htmlEl, options, host) {
|
|
2740
|
+
const isImg = (t) => t instanceof HTMLImageElement && t.complete && t.naturalWidth > 0;
|
|
2741
|
+
const texHolder = initTextureHolder(() => isImg(htmlEl) ? htmlEl : null, host);
|
|
2742
|
+
const toHolder = initTextureHolder(() => {
|
|
2743
|
+
const doc = host.renderer.document;
|
|
2744
|
+
if (!options.to || !doc) return null;
|
|
2745
|
+
try {
|
|
2746
|
+
const toEl = doc.querySelector?.(options.to);
|
|
2747
|
+
return isImg(toEl) ? toEl : null;
|
|
2748
|
+
} catch {
|
|
2749
|
+
return null;
|
|
2750
|
+
}
|
|
2751
|
+
}, host);
|
|
2752
|
+
return { texHolder, toHolder };
|
|
2753
|
+
}
|
|
2754
|
+
function cleanupShaderTextures(texHolder, toHolder, renderer) {
|
|
2755
|
+
const tex = texHolder.peek(), toTex = toHolder.peek();
|
|
2756
|
+
if (tex && renderer.gl) renderer.gl.deleteTexture(tex);
|
|
2757
|
+
if (toTex && toTex !== tex && renderer.gl) renderer.gl.deleteTexture(toTex);
|
|
2758
|
+
texHolder.reset();
|
|
2759
|
+
toHolder.reset();
|
|
2760
|
+
renderer.release();
|
|
2761
|
+
}
|
|
2762
|
+
function hideBehindRenderer(el, state) {
|
|
2763
|
+
if (state.hiddenByRenderer) return;
|
|
2764
|
+
const style = styleOf(state.ledgers, el);
|
|
2765
|
+
if (!style) return;
|
|
2766
|
+
style.set("opacity", "0");
|
|
2767
|
+
state.hiddenByRenderer = true;
|
|
2768
|
+
}
|
|
2769
|
+
function restoreInstanceOpacity(el, state) {
|
|
2770
|
+
if (!state.hiddenByRenderer) return;
|
|
2771
|
+
state.hiddenByRenderer = false;
|
|
2772
|
+
const style = styleOf(state.ledgers, el);
|
|
2773
|
+
if (style) style.set("opacity", style.peek("opacity") ?? "");
|
|
2774
|
+
}
|
|
2775
|
+
var SHADER_TEXTURE_GRACE_MS = 5e3;
|
|
2776
|
+
function acquireRenderer(info, held) {
|
|
2777
|
+
if (held && !info.renderer.isDestroyed) return true;
|
|
2778
|
+
const live = info.ref.get();
|
|
2779
|
+
if (!live.acquire()) return false;
|
|
2780
|
+
info.renderer = live;
|
|
2781
|
+
info.tex.reset();
|
|
2782
|
+
info.to.reset();
|
|
2783
|
+
return true;
|
|
2784
|
+
}
|
|
2785
|
+
function instanceProgress(info) {
|
|
2786
|
+
const authored = info.opt.progress;
|
|
2787
|
+
if (authored !== void 0 && authored >= 0) return authored;
|
|
2788
|
+
return info.opt.scrubsFromScroll ? readElementProgress(info.el) : -1;
|
|
2789
|
+
}
|
|
2790
|
+
function createShaderDrawCall(info, state, isGenerative, onFailure) {
|
|
2791
|
+
return (_gl, time) => {
|
|
2792
|
+
let drew = false;
|
|
2793
|
+
try {
|
|
2794
|
+
const texture = info.tex.get();
|
|
2795
|
+
drew = (!!texture || isGenerative) && drawElementQuad(info.renderer, info.el, {
|
|
2796
|
+
...info.opt,
|
|
2797
|
+
texture,
|
|
2798
|
+
toTexture: info.to.get(),
|
|
2799
|
+
time,
|
|
2800
|
+
progress: state.progress,
|
|
2801
|
+
audio: state.audio,
|
|
2802
|
+
geometry: state.geometry
|
|
2803
|
+
});
|
|
2804
|
+
} catch {
|
|
2805
|
+
onFailure();
|
|
2806
|
+
restoreInstanceOpacity(info.el, state);
|
|
2807
|
+
info.renderer.unregister(info.id);
|
|
2808
|
+
return;
|
|
2809
|
+
}
|
|
2810
|
+
if (drew && !isGenerative) hideBehindRenderer(info.el, state);
|
|
2811
|
+
else if (!isGenerative) restoreInstanceOpacity(info.el, state);
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
function createGraceTransition(grace, deactivate, isAcquired) {
|
|
2815
|
+
return () => {
|
|
2816
|
+
deactivate();
|
|
2817
|
+
if (isAcquired()) grace.arm();
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
function createGraceTimer(info, onExpire, isBusy) {
|
|
2821
|
+
let handle = null;
|
|
2822
|
+
let generation = 0;
|
|
2823
|
+
const cancel = () => {
|
|
2824
|
+
generation += 1;
|
|
2825
|
+
const cancelled = handle;
|
|
2826
|
+
handle = null;
|
|
2827
|
+
if (cancelled !== null) info.clearTimer(cancelled);
|
|
2828
|
+
};
|
|
2829
|
+
return {
|
|
2830
|
+
cancel,
|
|
2831
|
+
arm() {
|
|
2832
|
+
cancel();
|
|
2833
|
+
const armedGeneration = generation;
|
|
2834
|
+
const armedHandle = info.setTimer(() => {
|
|
2835
|
+
if (generation !== armedGeneration) return;
|
|
2836
|
+
generation += 1;
|
|
2837
|
+
handle = null;
|
|
2838
|
+
if (!isBusy()) onExpire();
|
|
2839
|
+
}, SHADER_TEXTURE_GRACE_MS);
|
|
2840
|
+
if (generation === armedGeneration) handle = armedHandle;
|
|
2841
|
+
}
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
function createShaderInstance(info) {
|
|
2845
|
+
const state = {
|
|
2846
|
+
// Shared per element with every other writer, advanced or core — see `createAdvancedLedgers`.
|
|
2847
|
+
// A private `createLedgerSet` here captured whatever a CSS effect on the same element had
|
|
2848
|
+
// already written to `opacity` as the author's own value, and handed it back on teardown.
|
|
2849
|
+
ledgers: createAdvancedLedgers(info.el),
|
|
2850
|
+
hiddenByRenderer: false,
|
|
2851
|
+
progress: -1,
|
|
2852
|
+
audio: 0,
|
|
2853
|
+
geometry: null,
|
|
2854
|
+
hostAlpha: 1
|
|
2855
|
+
};
|
|
2856
|
+
let isActive = false, isAcquired = false;
|
|
2857
|
+
const isGenerative = GENERATIVE_MODES.has(info.opt.mode);
|
|
2858
|
+
const readInputs = () => {
|
|
2859
|
+
state.progress = instanceProgress(info);
|
|
2860
|
+
state.audio = info.opt.audioBand ? readAudioBand(info.el, info.opt.audioBand) : 0;
|
|
2861
|
+
const cached = state.hiddenByRenderer ? state.hostAlpha : void 0;
|
|
2862
|
+
state.geometry = measureElementGeometry(info.el, info.renderer.window, isGenerative, cached);
|
|
2863
|
+
if (state.geometry?.hostAlpha !== void 0) state.hostAlpha = state.geometry.hostAlpha;
|
|
2864
|
+
};
|
|
2865
|
+
const restoreState = () => {
|
|
2866
|
+
if (!isActive) return;
|
|
2867
|
+
isActive = false;
|
|
2868
|
+
info.renderer.unregister(info.id);
|
|
2869
|
+
restoreInstanceOpacity(info.el, state);
|
|
2870
|
+
};
|
|
2871
|
+
const releaseResources = () => {
|
|
2872
|
+
if (!isAcquired) return;
|
|
2873
|
+
isAcquired = false;
|
|
2874
|
+
cleanupShaderTextures(info.tex, info.to, info.renderer);
|
|
2875
|
+
};
|
|
2876
|
+
const grace = createGraceTimer(info, releaseResources, () => isActive);
|
|
2877
|
+
const enterGrace = createGraceTransition(grace, () => {
|
|
2878
|
+
isActive = false;
|
|
2879
|
+
}, () => isAcquired);
|
|
2880
|
+
const drawCall = createShaderDrawCall(info, state, isGenerative, enterGrace);
|
|
2881
|
+
const suspend = () => {
|
|
2882
|
+
if (!isActive) return;
|
|
2883
|
+
try {
|
|
2884
|
+
restoreState();
|
|
2885
|
+
} finally {
|
|
2886
|
+
enterGrace();
|
|
2887
|
+
}
|
|
2888
|
+
};
|
|
2889
|
+
return createEffectInstance({
|
|
2890
|
+
continuous: true,
|
|
2891
|
+
activate() {
|
|
2892
|
+
if (isActive) return;
|
|
2893
|
+
if (!acquireRenderer(info, isAcquired)) return;
|
|
2894
|
+
grace.cancel();
|
|
2895
|
+
isAcquired = true;
|
|
2896
|
+
isActive = true;
|
|
2897
|
+
info.renderer.register(info.id, drawCall, {
|
|
2898
|
+
onLost() {
|
|
2899
|
+
info.tex.reset();
|
|
2900
|
+
info.to.reset();
|
|
2901
|
+
restoreInstanceOpacity(info.el, state);
|
|
2902
|
+
},
|
|
2903
|
+
onRestored() {
|
|
2904
|
+
}
|
|
2905
|
+
}, readInputs);
|
|
2906
|
+
info.renderer.startLoop();
|
|
2907
|
+
},
|
|
2908
|
+
// Unregister and un-hide, then hold the textures and the renderer reference for
|
|
2909
|
+
// {@link SHADER_TEXTURE_GRACE_MS} before giving them up.
|
|
2910
|
+
//
|
|
2911
|
+
// This is the grace-timer half of the trade `e49d517` set up and left open, taken deliberately
|
|
2912
|
+
// rather than closed as a tidy-up. That commit chose to retain until `destroy()` because
|
|
2913
|
+
// `release()` tears down the canvas, the context and all six linked programs, so dropping
|
|
2914
|
+
// `refCount` to zero on every hover-out makes the next hover pay a rebuild and six recompiles —
|
|
2915
|
+
// a stall worse, on a hover effect, than holding a texture roughly the size of the decoded
|
|
2916
|
+
// image the browser already has. It named a grace timer as the way to get the memory back
|
|
2917
|
+
// without the stall, and this is it. A give-back inside the window costs nothing: the instance
|
|
2918
|
+
// never let go, so `acquireRenderer` short-circuits and the texture holders still have their
|
|
2919
|
+
// handles. Past it the memory goes back, and a later `activate()` rebuilds against a live
|
|
2920
|
+
// renderer — `RendererRef.get()` re-resolves once the old one reports `isDestroyed`.
|
|
2921
|
+
//
|
|
2922
|
+
// A later audit read the original retention as an oversight and an agent "fixed" it; that was
|
|
2923
|
+
// reverted in `eccb8e5`, because releasing on *every* give-back with no window is the stall,
|
|
2924
|
+
// not the fix. The window is the whole difference.
|
|
2925
|
+
cancel: suspend,
|
|
2926
|
+
finish: suspend,
|
|
2927
|
+
destroy() {
|
|
2928
|
+
restoreState();
|
|
2929
|
+
grace.cancel();
|
|
2930
|
+
releaseResources();
|
|
2931
|
+
state.ledgers.restore();
|
|
2932
|
+
}
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
var STATIC_BAKE_MAX = 640;
|
|
2936
|
+
function bakeExtent(rect, canvas, dpr) {
|
|
2937
|
+
if (!Number.isFinite(rect.width) || !Number.isFinite(rect.height)) return null;
|
|
2938
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
2939
|
+
const longest = Math.max(rect.width, rect.height) * dpr;
|
|
2940
|
+
const budget = Math.min(STATIC_BAKE_MAX, canvas.width, canvas.height);
|
|
2941
|
+
const factor = longest > budget ? budget / longest : dpr;
|
|
2942
|
+
const w = Math.max(1, Math.round(rect.width * factor));
|
|
2943
|
+
const h = Math.max(1, Math.round(rect.height * factor));
|
|
2944
|
+
return w <= canvas.width && h <= canvas.height ? [w, h] : null;
|
|
2945
|
+
}
|
|
2946
|
+
function readCanvasCorner(canvas, w, h, createCanvas) {
|
|
2947
|
+
const out = createCanvas();
|
|
2948
|
+
const out2d = out?.getContext?.("2d");
|
|
2949
|
+
if (!out || !out2d) return null;
|
|
2950
|
+
out.width = w;
|
|
2951
|
+
out.height = h;
|
|
2952
|
+
try {
|
|
2953
|
+
out2d.drawImage(canvas, 0, 0, w, h, 0, 0, w, h);
|
|
2954
|
+
return out.toDataURL("image/png");
|
|
2955
|
+
} catch {
|
|
2956
|
+
return null;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
function bakeStaticFrame(renderer, el, options, createCanvas) {
|
|
2960
|
+
const gl = renderer.gl;
|
|
2961
|
+
const canvas = renderer.canvas;
|
|
2962
|
+
if (!gl || !canvas || typeof el.getBoundingClientRect !== "function") return null;
|
|
2963
|
+
const dpr = getScissorEnv(renderer.window).dpr;
|
|
2964
|
+
const extent = bakeExtent(el.getBoundingClientRect(), canvas, dpr);
|
|
2965
|
+
if (!extent) return null;
|
|
2966
|
+
const [w, h] = extent;
|
|
2967
|
+
const box = { left: 0, top: 0, right: w / dpr, bottom: h / dpr };
|
|
2968
|
+
const drew = drawElementQuad(renderer, el, {
|
|
2969
|
+
...options,
|
|
2970
|
+
time: 0,
|
|
2971
|
+
audio: 0,
|
|
2972
|
+
texture: null,
|
|
2973
|
+
geometry: { border: box, paint: box, clip: box, radii: [0, 0, 0, 0, 0, 0, 0, 0], alpha: 1 }
|
|
2974
|
+
});
|
|
2975
|
+
if (!drew) return null;
|
|
2976
|
+
const url = readCanvasCorner(canvas, w, h, createCanvas);
|
|
2977
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
2978
|
+
gl.scissor(0, canvas.height - h, w, h);
|
|
2979
|
+
gl.clearColor(0, 0, 0, 0);
|
|
2980
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
2981
|
+
return url;
|
|
2982
|
+
}
|
|
2983
|
+
function prepareReducedMotion(el, options, ctx) {
|
|
2984
|
+
if (!GENERATIVE_MODES.has(options.mode)) return createInertInstance();
|
|
2985
|
+
const hostDoc = el?.ownerDocument;
|
|
2986
|
+
const env = resolveEnv(ctx, hostDoc ? { document: hostDoc, window: hostDoc.defaultView } : void 0);
|
|
2987
|
+
const renderer = getSharedShaderRenderer(env);
|
|
2988
|
+
if (!renderer.acquire()) return createInertInstance();
|
|
2989
|
+
const ledgers = createAdvancedLedgers(el);
|
|
2990
|
+
nextShaderId += 1;
|
|
2991
|
+
const id = `kui-shader-static-${nextShaderId}`;
|
|
2992
|
+
let released = false;
|
|
2993
|
+
const release = () => {
|
|
2994
|
+
if (released) return;
|
|
2995
|
+
released = true;
|
|
2996
|
+
renderer.unregister(id);
|
|
2997
|
+
renderer.release();
|
|
2998
|
+
};
|
|
2999
|
+
renderer.register(id, () => {
|
|
3000
|
+
const url = bakeStaticFrame(renderer, el, options, env.createCanvas);
|
|
3001
|
+
if (!url) return;
|
|
3002
|
+
const style = styleOf(ledgers, el);
|
|
3003
|
+
if (style) {
|
|
3004
|
+
style.set("background-image", `url("${url}")`);
|
|
3005
|
+
style.set("background-size", "100% 100%");
|
|
3006
|
+
style.set("background-repeat", "no-repeat");
|
|
3007
|
+
}
|
|
3008
|
+
release();
|
|
3009
|
+
});
|
|
3010
|
+
renderer.startLoop();
|
|
3011
|
+
return createInertInstance(() => {
|
|
3012
|
+
release();
|
|
3013
|
+
ledgers.restore();
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
function prepareShaders(el, params, ctx) {
|
|
3017
|
+
const hostDoc = el?.ownerDocument;
|
|
3018
|
+
const resolvedEnv = resolveEnv(ctx, hostDoc ? { document: hostDoc, window: hostDoc.defaultView } : void 0);
|
|
3019
|
+
const options = extractShaderOptions(params, resolvedEnv.createCanvas);
|
|
3020
|
+
if (!SHADER_MODES.includes(options.mode)) return createInertInstance();
|
|
3021
|
+
if (isReducedMotion(ctx)) return prepareReducedMotion(el, options, ctx);
|
|
3022
|
+
const ref = createRendererRef(resolvedEnv);
|
|
3023
|
+
nextShaderId += 1;
|
|
3024
|
+
const id = `kui-shader-instance-${nextShaderId}`;
|
|
3025
|
+
const info = {
|
|
3026
|
+
id,
|
|
3027
|
+
el,
|
|
3028
|
+
opt: options,
|
|
3029
|
+
renderer: ref.get(),
|
|
3030
|
+
ref,
|
|
3031
|
+
setTimer: resolvedEnv.setTimer,
|
|
3032
|
+
clearTimer: resolvedEnv.clearTimer
|
|
3033
|
+
};
|
|
3034
|
+
const holders = createShaderTextureHolders(el, options, info);
|
|
3035
|
+
info.tex = holders.texHolder;
|
|
3036
|
+
info.to = holders.toHolder;
|
|
3037
|
+
return createShaderInstance(info);
|
|
3038
|
+
}
|
|
3039
|
+
var SHADER_PARAMETERS = {
|
|
3040
|
+
mode: { type: "keyword", default: "displace", keywords: [...SHADER_MODES], cssProperty: "--kui-shader-mode" },
|
|
3041
|
+
strength: { type: "number", default: "0.5", minimum: 0, maximum: 10, cssProperty: "--kui-shader-strength" },
|
|
3042
|
+
speed: { type: "number", default: "1.0", minimum: 0, maximum: 10, cssProperty: "--kui-shader-speed" },
|
|
3043
|
+
frequency: { type: "number", default: "10.0", minimum: 0.1, maximum: 50, cssProperty: "--kui-shader-frequency" },
|
|
3044
|
+
chromatic: { type: "number", default: "0.0", minimum: 0, maximum: 1, cssProperty: "--kui-shader-chromatic" },
|
|
3045
|
+
/**
|
|
3046
|
+
* How far the colour ramp is wrapped back on itself — the shimmer count.
|
|
3047
|
+
*
|
|
3048
|
+
* Ranged 0..8 rather than the 0..1 it carried before `mode: gradient` existed. The old bound was
|
|
3049
|
+
* right for a mix amount and wrong for a wrap count, and it cost nothing to police because **no
|
|
3050
|
+
* fragment program declared `u_iridescence` at all** — the value was extracted and uploaded to a
|
|
3051
|
+
* location that was `null` in all five programs. It reaches a shader for the first time here.
|
|
3052
|
+
*/
|
|
3053
|
+
iridescence: { type: "number", default: "0.0", minimum: 0, maximum: 8, cssProperty: "--kui-shader-iridescence" },
|
|
3054
|
+
tint: { type: "color", default: "#ffffff", cssProperty: "--kui-shader-tint" },
|
|
3055
|
+
/*
|
|
3056
|
+
* `color1`/`color2` keep their original meaning — the duotone pair `mode: displace` remaps a
|
|
3057
|
+
* photo through — and `mode: gradient` reads the same two as the first stops of its palette.
|
|
3058
|
+
* `color3`..`color5` extend that palette without touching the duotone contract.
|
|
3059
|
+
*
|
|
3060
|
+
* Five separate `color` parameters rather than one comma-separated list: the attribute grammar
|
|
3061
|
+
* splits effect segments on top-level commas, so a list would have to be quoted, and the only
|
|
3062
|
+
* type that could carry it (`text`) validates nothing — a mistyped stop would silently become
|
|
3063
|
+
* white through `parseColor`'s fallback, which is the worst failure a colour input can have.
|
|
3064
|
+
*/
|
|
3065
|
+
color1: { type: "color", default: "", cssProperty: "--kui-shader-color1" },
|
|
3066
|
+
color2: { type: "color", default: "", cssProperty: "--kui-shader-color2" },
|
|
3067
|
+
color3: { type: "color", default: "", cssProperty: "--kui-shader-color3" },
|
|
3068
|
+
color4: { type: "color", default: "", cssProperty: "--kui-shader-color4" },
|
|
3069
|
+
color5: { type: "color", default: "", cssProperty: "--kui-shader-color5" },
|
|
3070
|
+
/*
|
|
3071
|
+
* The generative field's own controls. Every one of these is read by this module's JavaScript and
|
|
3072
|
+
* by no stylesheet, so — as `ParamSpecBase.cssProperty` warns — setting the custom property
|
|
3073
|
+
* directly does nothing at all. Widening one means widening it here.
|
|
3074
|
+
*/
|
|
3075
|
+
/** Which pattern. The same seed gives the same image on every load, which is the point of it. */
|
|
3076
|
+
seed: { type: "number", default: "0", minimum: 0, maximum: 9999, integer: true, cssProperty: "--kui-shader-seed" },
|
|
3077
|
+
/** How zoomed the field is. Larger means smaller, more numerous shapes. */
|
|
3078
|
+
scale: { type: "number", default: "1", minimum: 0.05, maximum: 20, cssProperty: "--kui-shader-scale" },
|
|
3079
|
+
/**
|
|
3080
|
+
* How many octaves of detail ride on the base shapes.
|
|
3081
|
+
*
|
|
3082
|
+
* Named `detail` rather than the reference tools' "definition" — the plainer word for the same
|
|
3083
|
+
* idea. Capped at 6, which is `KUI_MAX_OCTAVES` in the shader; a seventh would be accepted here
|
|
3084
|
+
* and silently ignored there.
|
|
3085
|
+
*/
|
|
3086
|
+
detail: { type: "number", default: "3", minimum: 1, maximum: 6, integer: true, cssProperty: "--kui-shader-detail" },
|
|
3087
|
+
/** How far a second field distorts the first. The difference between contour rings and liquid. */
|
|
3088
|
+
warp: { type: "number", default: "0", minimum: 0, maximum: 2, cssProperty: "--kui-shader-warp" },
|
|
3089
|
+
/**
|
|
3090
|
+
* How much of `fluid`, `liquid`, `particles` and `morph`'s motion comes from real noise.
|
|
3091
|
+
*
|
|
3092
|
+
* Those four shipped with a single trigonometric term standing in for noise, which bands and
|
|
3093
|
+
* repeats visibly. This crossfades that term into the same `kuiFbm` field `mode: gradient`
|
|
3094
|
+
* generates from — `0` is the shipped picture exactly, `1` is the field on its own, and the
|
|
3095
|
+
* values between are a genuine dial rather than a rounded switch.
|
|
3096
|
+
*
|
|
3097
|
+
* **It is off by default and must stay that way.** Four modes are already in use; turning this
|
|
3098
|
+
* on for them without being asked would restyle live pages. See `FILTER_NOISE_GLSL` in
|
|
3099
|
+
* `glsl.ts` for why it is a parameter of its own rather than `warp` doing double duty.
|
|
3100
|
+
*
|
|
3101
|
+
* Above `0` it is also what gives `seed`, `scale`, `warp` and `detail` a meaning on these four
|
|
3102
|
+
* modes; below it they are read and uploaded but nothing samples them. `mode: displace`'s ripple
|
|
3103
|
+
* is a deliberate radial wave rather than fake noise, and `gradient`/`logo` are already nothing
|
|
3104
|
+
* but the noise core, so all three ignore this.
|
|
3105
|
+
*/
|
|
3106
|
+
noise: { type: "number", default: "0", minimum: 0, maximum: 1, cssProperty: "--kui-shader-noise" },
|
|
3107
|
+
/** Posterise the ramp into this many steps. `0` is off, as with `chromatic` and `iridescence`. */
|
|
3108
|
+
bands: { type: "number", default: "0", minimum: 0, maximum: 32, integer: true, cssProperty: "--kui-shader-bands" },
|
|
3109
|
+
/** Film grain amount. */
|
|
3110
|
+
grain: { type: "number", default: "0", minimum: 0, maximum: 1, cssProperty: "--kui-shader-grain" },
|
|
3111
|
+
/**
|
|
3112
|
+
* Hue rotation, as a real angle (`hue: 30deg`, `hue: 0.25turn`).
|
|
3113
|
+
*
|
|
3114
|
+
* An `angle` rather than a normalised scalar, matching `carousel`'s `tilt:` and this repository's
|
|
3115
|
+
* standing rejection of normalised ranges where a CSS unit exists. A bare `hue: 30` is accepted
|
|
3116
|
+
* too — `core/params.ts` normalises it to `30deg`.
|
|
3117
|
+
*/
|
|
3118
|
+
hue: { type: "angle", default: "0deg", cssProperty: "--kui-shader-hue" },
|
|
3119
|
+
/**
|
|
3120
|
+
* Which part of `mode: logo`'s host image is the mark. Ignored by every other mode.
|
|
3121
|
+
*
|
|
3122
|
+
* `alpha` for the usual transparent-ground SVG or PNG. `luma` and `luma-invert` are for a format
|
|
3123
|
+
* that has no alpha to read — a JPEG under `alpha` gives an all-opaque stencil and fills the
|
|
3124
|
+
* whole rectangle. `luma` takes the bright pixels as the mark, `luma-invert` the dark ones; both
|
|
3125
|
+
* directions are spelled out because guessing produces a perfect negative of the mark, which
|
|
3126
|
+
* looks deliberate and is exactly wrong.
|
|
3127
|
+
*/
|
|
3128
|
+
mask: { type: "keyword", default: "alpha", keywords: ["alpha", "luma", "luma-invert"], cssProperty: "--kui-shader-mask" },
|
|
3129
|
+
/**
|
|
3130
|
+
* How `mode: gradient` and `mode: logo` answer the pointer. Ignored by the five filter modes.
|
|
3131
|
+
*
|
|
3132
|
+
* Being *responsive to the pointer* is the one thing a shader background does that a video of a
|
|
3133
|
+
* shader cannot, and these two modes — the two built to stand in for a video background — were
|
|
3134
|
+
* the only ones in the tier with no pointer response at all. `displace` and `fluid` have read
|
|
3135
|
+
* `u_mouse` since they were written.
|
|
3136
|
+
*
|
|
3137
|
+
* - `stir` — the cursor drags the colour flow around it, like stirring thick paint. It moves the
|
|
3138
|
+
* field's *sample point*, so on `mode: logo` the mark's edges do not move: `glyphMask()` reads
|
|
3139
|
+
* the host image at the undistorted coordinate and multiplies the finished colour, which means
|
|
3140
|
+
* the fill swirls inside a stencil that stays exactly where it was.
|
|
3141
|
+
* - `light` — the cursor is a torch held over the surface. Nothing moves; the colour under the
|
|
3142
|
+
* pointer is multiplied up, which raises its saturation rather than washing it toward white.
|
|
3143
|
+
* - `both` — the two together. They compose cleanly because they act at different stages, one on
|
|
3144
|
+
* the coordinate and one on the colour that coordinate produced.
|
|
3145
|
+
*
|
|
3146
|
+
* `none` by default, and `none` means the shader never reads `u_mouse` at all — not that it
|
|
3147
|
+
* reads it and multiplies by zero. Every page already running these modes renders what it always
|
|
3148
|
+
* rendered, byte for byte.
|
|
3149
|
+
*
|
|
3150
|
+
* With no pointer yet — the state of every page on load, every phone, and every screenshot —
|
|
3151
|
+
* `computeMousePos` answers the element's centre, so `light` rests as a centred glow and `stir`
|
|
3152
|
+
* as a centred swirl. Both are compositions someone might have authored on purpose, which is the
|
|
3153
|
+
* bar a resting state has to clear.
|
|
3154
|
+
*/
|
|
3155
|
+
hover: { type: "keyword", default: "none", keywords: ["none", "stir", "light", "both"], cssProperty: "--kui-shader-hover" },
|
|
3156
|
+
/**
|
|
3157
|
+
* A colour painted behind the generative field, inside the element's own shape.
|
|
3158
|
+
*
|
|
3159
|
+
* What it is for is `mode: logo`: outside the mark the field has no coverage, so this is the
|
|
3160
|
+
* plate the mark sits on instead of the page showing through.
|
|
3161
|
+
*
|
|
3162
|
+
* On a bare `mode: gradient` it is **invisible**, and that is arithmetic rather than an
|
|
3163
|
+
* oversight — the field is opaque, so there is nothing behind it to see. It becomes visible
|
|
3164
|
+
* there through a `tint` whose alpha is below 1, which is how a field is made to sit *on* a page
|
|
3165
|
+
* colour rather than replace it. Unset by default, and read through the raw parameter string
|
|
3166
|
+
* rather than `parseColor`, whose empty-input fallback is opaque white.
|
|
3167
|
+
*/
|
|
3168
|
+
backdrop: { type: "color", default: "", cssProperty: "--kui-shader-backdrop" },
|
|
3169
|
+
/**
|
|
3170
|
+
* The generative field's orientation: the axis it runs along.
|
|
3171
|
+
*
|
|
3172
|
+
* `transform: rotate()`'s convention — clockwise-positive, `0deg` pointing right — because that
|
|
3173
|
+
* is the one an author already has. It turns the field's whole domain, so it is visible at the
|
|
3174
|
+
* library defaults rather than only in company with another parameter, and it is the axis
|
|
3175
|
+
* `motion: drift` travels along. One idea with two consequences, not two behaviours: the field
|
|
3176
|
+
* has an axis, this names it, and `drift` slides along the thing it named.
|
|
3177
|
+
*
|
|
3178
|
+
* An `angle` rather than a normalised scalar, matching `hue` above and this repository's
|
|
3179
|
+
* standing rejection of 0..1 ranges where a CSS unit exists. A bare `angle: 30` is accepted —
|
|
3180
|
+
* `core/params.ts` normalises it to `30deg`.
|
|
3181
|
+
*/
|
|
3182
|
+
angle: { type: "angle", default: "0deg", cssProperty: "--kui-shader-angle" },
|
|
3183
|
+
/**
|
|
3184
|
+
* How the generative field moves. Ignored by the five filter modes.
|
|
3185
|
+
*
|
|
3186
|
+
* - `evolve` — the field changes *in place*. Time is the noise function's third axis rather than
|
|
3187
|
+
* a translation of a 2D field, which is why it reads as the pattern genuinely evolving instead
|
|
3188
|
+
* of sliding past. See `NOISE_GLSL` in `glsl.ts`.
|
|
3189
|
+
* - `drift` — it also travels along `angle`, at a rate proportional to `scale` so the visual
|
|
3190
|
+
* speed is the same at any zoom.
|
|
3191
|
+
* - `swirl` — it also turns about the element's centre.
|
|
3192
|
+
*
|
|
3193
|
+
* **`evolve` exists so that the default can be inert.** `drift` would have been the natural
|
|
3194
|
+
* default name, and it is what this parameter's brief asked for — but `drift` is a new look, and
|
|
3195
|
+
* defaulting to it would restyle every page already running `gradient` or `logo` on the next
|
|
3196
|
+
* release. That is the same trap `noise:` above is written around, and the reason `hover:`
|
|
3197
|
+
* defaults to `none`. So the shipped behaviour keeps the default and gets an accurate name, and
|
|
3198
|
+
* the two new ones are opt-in. `drift` and `swirl` add to `evolve` rather than replacing it, so
|
|
3199
|
+
* a travelling field is still changing as it goes rather than one frozen picture towed across
|
|
3200
|
+
* the box.
|
|
3201
|
+
*/
|
|
3202
|
+
motion: { type: "keyword", default: "evolve", keywords: ["evolve", "drift", "swirl"], cssProperty: "--kui-shader-motion" },
|
|
3203
|
+
blend: { type: "keyword", default: "normal", keywords: ["normal", "screen", "multiply", "add"], cssProperty: "--kui-shader-blend" },
|
|
3204
|
+
to: { type: "text", default: "", cssProperty: "--kui-shader-to" },
|
|
3205
|
+
/**
|
|
3206
|
+
* A fixed scrub position, 0..1. `-1` is the unauthored sentinel and means "no scrub".
|
|
3207
|
+
*
|
|
3208
|
+
* `--kui-shader-progress`, **not** `--kui-progress`. Every authored parameter reaches
|
|
3209
|
+
* `element.style` as its custom property — `core/compile.ts`'s `buildPlan` runs `resolveParams`
|
|
3210
|
+
* over JS-rendered entries too — so while this pointed at `--kui-progress` an authored
|
|
3211
|
+
* `progress: 0.4` wrote the scroll primitives' own channel onto the element: it raced
|
|
3212
|
+
* `scroll-progress` when the two were composed, and it inherited down to every descendant
|
|
3213
|
+
* shader and pinned timeline underneath. `test/carousel-3d.test.ts` already carries a
|
|
3214
|
+
* regression test that a primitive must not publish `--kui-progress` meaning something else.
|
|
3215
|
+
* Nothing is lost by renaming — this module reads the authored value from the parameter, never
|
|
3216
|
+
* back off the property.
|
|
3217
|
+
*/
|
|
3218
|
+
progress: { type: "number", default: "-1", minimum: -1, maximum: 1, cssProperty: "--kui-shader-progress" },
|
|
3219
|
+
/**
|
|
3220
|
+
* Whether this shader is scrubbed by `--kui-progress` — the property `scroll-progress`,
|
|
3221
|
+
* `timeline: pin` and the rest of the scroll family publish.
|
|
3222
|
+
*
|
|
3223
|
+
* Opt-in, and `off` by default, for the same reason `audio:` below is: the value arrives as a
|
|
3224
|
+
* custom property written by some *other* primitive, and `--kui-progress` is an ordinary
|
|
3225
|
+
* inheriting custom property, so a shader that read one unasked changed its own output the
|
|
3226
|
+
* moment an unrelated `scroll-progress` appeared anywhere above it in the tree. That was not a
|
|
3227
|
+
* subtle drift — at the top of a scroll range the published value is `0`, every program
|
|
3228
|
+
* multiplies its effect by it, and the shader rendered an untouched copy of the source image.
|
|
3229
|
+
* Dead, silently, with no way for the author to decline.
|
|
3230
|
+
*
|
|
3231
|
+
* `keyword`, so the list is closed and a typo warns instead of quietly doing nothing.
|
|
3232
|
+
* `--kui-shader-scrub` is a name read by this module's JavaScript and by no stylesheet, so
|
|
3233
|
+
* setting the custom property directly does nothing; the value itself is read from
|
|
3234
|
+
* `--kui-progress`, which is a different contract entirely.
|
|
3235
|
+
*/
|
|
3236
|
+
scrub: { type: "keyword", default: "off", keywords: ["off", "scroll"], cssProperty: "--kui-shader-scrub" },
|
|
3237
|
+
/**
|
|
3238
|
+
* Which `audio-source` band drives this shader, if any.
|
|
3239
|
+
*
|
|
3240
|
+
* Opt-in, and `off` by default: the value arrives as a custom property written by some *other*
|
|
3241
|
+
* primitive, so a shader that read one unasked would change its own output the moment an
|
|
3242
|
+
* unrelated `audio-source` appeared anywhere above it in the tree.
|
|
3243
|
+
*
|
|
3244
|
+
* `keyword`, so the list is closed — and `--kui-shader-audio` is a name, read by this module's
|
|
3245
|
+
* JavaScript and by no stylesheet, so setting the custom property in CSS does nothing. The
|
|
3246
|
+
* band values themselves are read from `--kui-audio-*`, which is a different contract entirely
|
|
3247
|
+
* (`audio.ts`'s `AUDIO_BAND_PROPERTIES`).
|
|
3248
|
+
*/
|
|
3249
|
+
audio: { type: "keyword", default: "off", keywords: ["off", ...AUDIO_BANDS], cssProperty: "--kui-shader-audio" }
|
|
3250
|
+
};
|
|
3251
|
+
var SHADERS_PRIMITIVE = {
|
|
3252
|
+
id: "shaders",
|
|
3253
|
+
renderer: "javascript",
|
|
3254
|
+
channels: ["opacity"],
|
|
3255
|
+
parameters: SHADER_PARAMETERS,
|
|
3256
|
+
supportedTimelines: ["time", "view", "scroll"],
|
|
3257
|
+
supportedActivations: ["load", "enter", "hover", "click"],
|
|
3258
|
+
defaultActivation: "load",
|
|
3259
|
+
perfClass: "continuous",
|
|
3260
|
+
reducedMotion: "disable",
|
|
3261
|
+
prepare: prepareShaders
|
|
3262
|
+
};
|
|
3263
|
+
var SHADERS_PRESETS = [
|
|
3264
|
+
{ name: "shaders", primitive: "shaders" },
|
|
3265
|
+
{ name: "shader-displace", primitive: "shaders", params: { mode: "displace" } },
|
|
3266
|
+
{ name: "shader-fluid", primitive: "shaders", params: { mode: "fluid" } },
|
|
3267
|
+
{ name: "fluid-pointer", primitive: "shaders", params: { mode: "fluid" } },
|
|
3268
|
+
{ name: "shader-liquid", primitive: "shaders", params: { mode: "liquid" } },
|
|
3269
|
+
{ name: "liquid-distort", primitive: "shaders", params: { mode: "liquid" } },
|
|
3270
|
+
{ name: "shader-particles", primitive: "shaders", params: { mode: "particles" } },
|
|
3271
|
+
{ name: "particle-field", primitive: "shaders", params: { mode: "particles" } },
|
|
3272
|
+
{ name: "shader-morph", primitive: "shaders", params: { mode: "morph" } },
|
|
3273
|
+
{ name: "image-morph", primitive: "shaders", params: { mode: "morph" } },
|
|
3274
|
+
/*
|
|
3275
|
+
* The generative family. One fragment program, four sets of defaults — a preset here is a
|
|
3276
|
+
* starting point in the same parameter space, not a separate shader.
|
|
3277
|
+
*
|
|
3278
|
+
* These are the only presets in the catalog that work on an element with no image in it, and
|
|
3279
|
+
* their honest surface is still an element nothing overlaps — but for a different reason than it
|
|
3280
|
+
* used to be. The shared canvas now defaults to `z-index: 1`, so a positioned headline laid over
|
|
3281
|
+
* one of these paints on top of it, where before the headline vanished behind it. Better, and
|
|
3282
|
+
* still not composition: `border-radius`, ancestor clipping and ancestor opacity are reproduced
|
|
3283
|
+
* in the fragment shader, but the replica's place in the stacking order is the *canvas's*, not
|
|
3284
|
+
* the element's. `--kui-shader-z` moves that one canvas and every replica on it together. See
|
|
3285
|
+
* `docs/advanced-modules.md`.
|
|
3286
|
+
*/
|
|
3287
|
+
{ name: "shader-gradient", primitive: "shaders", params: { mode: "gradient" } },
|
|
3288
|
+
{ name: "gradient-liquid", primitive: "shaders", params: { mode: "gradient", warp: "1.1", detail: "4", speed: "0.4" } },
|
|
3289
|
+
{ name: "gradient-bands", primitive: "shaders", params: { mode: "gradient", bands: "8", warp: "0.6", speed: "0.3" } },
|
|
3290
|
+
// No palette on purpose: with fewer than two colours set the ramp falls back to its default pair,
|
|
3291
|
+
// and the sheen comes from wrapping it rather than from the stops themselves.
|
|
3292
|
+
{ name: "gradient-holo", primitive: "shaders", params: { mode: "gradient", iridescence: "3", chromatic: "0.5", warp: "0.4", speed: "0.5" } },
|
|
3293
|
+
/*
|
|
3294
|
+
* `logo` is the one generative mode with no stacking caveat at all. It replaces an `<img>`
|
|
3295
|
+
* exactly as the five filters do — the host image is the stencil, not the picture — so a mark
|
|
3296
|
+
* painting over the page is what an author wants rather than the limitation above.
|
|
3297
|
+
*
|
|
3298
|
+
* `<img src="mark.svg" data-kui="shader-logo">` and nothing else: the asset already carries the
|
|
3299
|
+
* shape, `object-fit` already decides where it sits, and the element is already the right size.
|
|
3300
|
+
*/
|
|
3301
|
+
{ name: "shader-logo", primitive: "shaders", params: { mode: "logo" } },
|
|
3302
|
+
{ name: "logo-gradient", primitive: "shaders", params: { mode: "logo", warp: "0.8", detail: "4", scale: "1.6", speed: "0.4" } }
|
|
3303
|
+
];
|
|
3304
|
+
function registerShaders(target) {
|
|
3305
|
+
return registerInto(target, SHADERS_PRIMITIVE, SHADERS_PRESETS, "Shaders");
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
// src/advanced/scenes.ts
|
|
3309
|
+
var NUM = String.raw`-?(?:\d+(?:\.\d+)?|\.\d+)`;
|
|
3310
|
+
var FROM_REGEX = new RegExp(String.raw`\bfrom:(${NUM})`);
|
|
3311
|
+
var TO_REGEX = new RegExp(String.raw`\bto:(${NUM})`);
|
|
3312
|
+
var OPACITY_REGEX = /\bopacity:([0-9.]+)->([0-9.]+)/;
|
|
3313
|
+
var X_REGEX = /\bx:([+-]?[0-9.]+[a-z%]*)->([+-]?[0-9.]+[a-z%]*)/i;
|
|
3314
|
+
var Y_REGEX = /\by:([+-]?[0-9.]+[a-z%]*)->([+-]?[0-9.]+[a-z%]*)/i;
|
|
3315
|
+
var SCALE_REGEX = /\bscale:([0-9.]+)->([0-9.]+)/;
|
|
3316
|
+
function parseWindow(kui) {
|
|
3317
|
+
return [readEnd(FROM_REGEX, kui, 0), readEnd(TO_REGEX, kui, 1)];
|
|
3318
|
+
}
|
|
3319
|
+
function readEnd(pattern, kui, fallback) {
|
|
3320
|
+
const match = pattern.exec(kui);
|
|
3321
|
+
if (!match) return fallback;
|
|
3322
|
+
const value = parseFloat(match[1]);
|
|
3323
|
+
return value >= 0 && value <= 1 ? value : fallback;
|
|
3324
|
+
}
|
|
3325
|
+
function extractUnit(str) {
|
|
3326
|
+
let i = str.length - 1;
|
|
3327
|
+
while (i >= 0 && (str[i] >= "a" && str[i] <= "z" || str[i] >= "A" && str[i] <= "Z" || str[i] === "%")) {
|
|
3328
|
+
i--;
|
|
3329
|
+
}
|
|
3330
|
+
return str.slice(i + 1);
|
|
3331
|
+
}
|
|
3332
|
+
function parseTransitionValue(str) {
|
|
3333
|
+
if (!str) return null;
|
|
3334
|
+
const arrowIdx = str.indexOf("->");
|
|
3335
|
+
if (arrowIdx === -1) return null;
|
|
3336
|
+
const fromStr = str.slice(0, arrowIdx).trim();
|
|
3337
|
+
const toStr = str.slice(arrowIdx + 2).trim();
|
|
3338
|
+
const fromNum = parseFloat(fromStr);
|
|
3339
|
+
const toNum = parseFloat(toStr);
|
|
3340
|
+
if (Number.isNaN(fromNum) || Number.isNaN(toNum)) return null;
|
|
3341
|
+
const unit = extractUnit(fromStr) || extractUnit(toStr);
|
|
3342
|
+
return { from: fromNum, to: toNum, unit };
|
|
3343
|
+
}
|
|
3344
|
+
function parseChildStep(child) {
|
|
3345
|
+
const kui = child?.getAttribute ? child.getAttribute("data-kui") || "" : "";
|
|
3346
|
+
const range = parseWindow(kui);
|
|
3347
|
+
const opMatch = OPACITY_REGEX.exec(kui);
|
|
3348
|
+
const opacity = opMatch ? parseTransitionValue(`${opMatch[1]}->${opMatch[2]}`) : null;
|
|
3349
|
+
const xMatch = X_REGEX.exec(kui);
|
|
3350
|
+
const x = xMatch ? parseTransitionValue(`${xMatch[1]}->${xMatch[2]}`) : null;
|
|
3351
|
+
const yMatch = Y_REGEX.exec(kui);
|
|
3352
|
+
const y = yMatch ? parseTransitionValue(`${yMatch[1]}->${yMatch[2]}`) : null;
|
|
3353
|
+
const scMatch = SCALE_REGEX.exec(kui);
|
|
3354
|
+
const scale = scMatch ? parseTransitionValue(`${scMatch[1]}->${scMatch[2]}`) : null;
|
|
3355
|
+
return { range, opacity, x, y, scale };
|
|
3356
|
+
}
|
|
3357
|
+
function buildStepTransforms(cfg, localT) {
|
|
3358
|
+
const transforms = [];
|
|
3359
|
+
if (cfg.x) {
|
|
3360
|
+
transforms.push(`translateX(${lerp(cfg.x.from, cfg.x.to, localT)}${cfg.x.unit})`);
|
|
3361
|
+
}
|
|
3362
|
+
if (cfg.y) {
|
|
3363
|
+
transforms.push(`translateY(${lerp(cfg.y.from, cfg.y.to, localT)}${cfg.y.unit})`);
|
|
3364
|
+
}
|
|
3365
|
+
if (cfg.scale) {
|
|
3366
|
+
transforms.push(`scale(${lerp(cfg.scale.from, cfg.scale.to, localT)})`);
|
|
3367
|
+
}
|
|
3368
|
+
return transforms.join(" ");
|
|
3369
|
+
}
|
|
3370
|
+
var FALLBACK_VIEWPORT_HEIGHT = 800;
|
|
3371
|
+
var SceneController = class {
|
|
3372
|
+
container;
|
|
3373
|
+
options;
|
|
3374
|
+
env;
|
|
3375
|
+
window;
|
|
3376
|
+
raf;
|
|
3377
|
+
caf;
|
|
3378
|
+
steps = [];
|
|
3379
|
+
progress = 0;
|
|
3380
|
+
rafId = null;
|
|
3381
|
+
isListening = false;
|
|
3382
|
+
timeStart = 0;
|
|
3383
|
+
/**
|
|
3384
|
+
* One ledger per step element, each opened at that step's first write and shared with every
|
|
3385
|
+
* other controller writing to the same element — see `createAdvancedLedgers`.
|
|
3386
|
+
*
|
|
3387
|
+
* Steps are collected during `prepare`, which can be a long way before the scene ever scrolls
|
|
3388
|
+
* into view. Recording opacity and transform *there* meant destroy restored the markup as it
|
|
3389
|
+
* looked at preparation time, discarding anything the author wrote in between.
|
|
3390
|
+
*
|
|
3391
|
+
* Sharing is also what keeps two *legitimate* owners of one element honest. A nested `scene` is
|
|
3392
|
+
* no longer one of them — `prepareScene` scopes its scan with {@link ownedDescendants}, so an
|
|
3393
|
+
* outer scene stops at an inner one instead of claiming its steps — but an element authored as
|
|
3394
|
+
* both a `scene-step` and a `camera-layer` still has two, because those are different kinds of
|
|
3395
|
+
* host, and so does any controller constructed directly rather than through `prepare`.
|
|
3396
|
+
*/
|
|
3397
|
+
ledgers;
|
|
3398
|
+
constructor(container, options = {}, env = {}) {
|
|
3399
|
+
this.container = container;
|
|
3400
|
+
this.options = options;
|
|
3401
|
+
this.env = env;
|
|
3402
|
+
const resolved = resolveEnv(null, env);
|
|
3403
|
+
this.window = resolved.window;
|
|
3404
|
+
this.raf = resolved.raf;
|
|
3405
|
+
this.caf = resolved.caf;
|
|
3406
|
+
this.ledgers = createAdvancedLedgers(container);
|
|
3407
|
+
this.onScroll = this.onScroll.bind(this);
|
|
3408
|
+
}
|
|
3409
|
+
addStep(element, config) {
|
|
3410
|
+
this.steps.push({ element, config });
|
|
3411
|
+
}
|
|
3412
|
+
start(onComplete) {
|
|
3413
|
+
if (this.isListening) return;
|
|
3414
|
+
if (!this.window && this.options.progress !== "time") return;
|
|
3415
|
+
this.isListening = true;
|
|
3416
|
+
if (this.options.progress === "time") {
|
|
3417
|
+
this.startTimeLoop(onComplete);
|
|
3418
|
+
} else {
|
|
3419
|
+
if (this.window?.addEventListener) {
|
|
3420
|
+
this.window.addEventListener("scroll", this.onScroll, { passive: true });
|
|
3421
|
+
this.window.addEventListener("resize", this.onScroll, { passive: true });
|
|
3422
|
+
}
|
|
3423
|
+
this.onScroll();
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
stop() {
|
|
3427
|
+
if (!this.isListening) return;
|
|
3428
|
+
if (this.window?.removeEventListener) {
|
|
3429
|
+
this.window.removeEventListener("scroll", this.onScroll);
|
|
3430
|
+
this.window.removeEventListener("resize", this.onScroll);
|
|
3431
|
+
}
|
|
3432
|
+
if (this.rafId) this.caf(this.rafId);
|
|
3433
|
+
this.rafId = null;
|
|
3434
|
+
this.isListening = false;
|
|
3435
|
+
}
|
|
3436
|
+
startTimeLoop(onComplete) {
|
|
3437
|
+
const duration = this.options.duration || 1e3;
|
|
3438
|
+
const now = () => this.window?.performance?.now ? this.window.performance.now() : Date.now();
|
|
3439
|
+
this.timeStart = now();
|
|
3440
|
+
const tick = () => {
|
|
3441
|
+
if (!this.isListening) {
|
|
3442
|
+
this.rafId = null;
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
const elapsed = now() - this.timeStart;
|
|
3446
|
+
this.progress = clamp(elapsed / duration, 0, 1);
|
|
3447
|
+
this.updateAll();
|
|
3448
|
+
if (this.progress < 1) {
|
|
3449
|
+
this.rafId = this.raf(tick);
|
|
3450
|
+
return;
|
|
3451
|
+
}
|
|
3452
|
+
this.rafId = null;
|
|
3453
|
+
this.isListening = false;
|
|
3454
|
+
if (onComplete) onComplete();
|
|
3455
|
+
};
|
|
3456
|
+
this.rafId = this.raf(tick);
|
|
3457
|
+
}
|
|
3458
|
+
onScroll() {
|
|
3459
|
+
if (this.rafId) return;
|
|
3460
|
+
this.rafId = this.raf(() => {
|
|
3461
|
+
this.rafId = null;
|
|
3462
|
+
this.calculateProgress();
|
|
3463
|
+
this.updateAll();
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
calculateProgress() {
|
|
3467
|
+
if (!this.container?.getBoundingClientRect) {
|
|
3468
|
+
this.progress = 0;
|
|
3469
|
+
return;
|
|
3470
|
+
}
|
|
3471
|
+
const winHeight = this.window?.innerHeight ?? FALLBACK_VIEWPORT_HEIGHT;
|
|
3472
|
+
const rect = this.container.getBoundingClientRect();
|
|
3473
|
+
const totalDist = winHeight + rect.height;
|
|
3474
|
+
const currentDist = winHeight - rect.top;
|
|
3475
|
+
this.progress = totalDist > 0 ? clamp(currentDist / totalDist, 0, 1) : 0;
|
|
3476
|
+
}
|
|
3477
|
+
updateAll() {
|
|
3478
|
+
for (const step of this.steps) {
|
|
3479
|
+
this.updateElement(step.element, step.config, this.progress);
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
updateElement(el, cfg, globalProgress) {
|
|
3483
|
+
const [start, end] = cfg.range;
|
|
3484
|
+
if (start >= end) return;
|
|
3485
|
+
const style = styleOf(this.ledgers, el);
|
|
3486
|
+
if (!style) return;
|
|
3487
|
+
const localT = clamp((globalProgress - start) / (end - start), 0, 1);
|
|
3488
|
+
if (cfg.opacity) {
|
|
3489
|
+
style.set("opacity", String(lerp(cfg.opacity.from, cfg.opacity.to, localT)));
|
|
3490
|
+
}
|
|
3491
|
+
const t = buildStepTransforms(cfg, localT);
|
|
3492
|
+
if (t) style.set("transform", t);
|
|
3493
|
+
}
|
|
3494
|
+
/** `try`/`finally` for the reason `camera-3d.ts`'s `destroy()` documents. */
|
|
3495
|
+
destroy() {
|
|
3496
|
+
try {
|
|
3497
|
+
this.stop();
|
|
3498
|
+
} finally {
|
|
3499
|
+
this.ledgers.restore();
|
|
3500
|
+
this.steps = [];
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
};
|
|
3504
|
+
function prepareScene(el, params, ctx) {
|
|
3505
|
+
const resolvedEnv = resolveEnv(ctx);
|
|
3506
|
+
const authoredProgress = params.text ? params.text("progress", "scroll") : "scroll";
|
|
3507
|
+
const progressMode = authoredProgress === "time" ? "time" : "scroll";
|
|
3508
|
+
const duration = clamp(params.num ? params.num("duration", 1e3) : 1e3, 100, 6e4);
|
|
3509
|
+
const htmlEl = el;
|
|
3510
|
+
const controller = new SceneController(
|
|
3511
|
+
htmlEl,
|
|
3512
|
+
{ progress: progressMode, duration },
|
|
3513
|
+
resolvedEnv
|
|
3514
|
+
);
|
|
3515
|
+
const childSteps = ownedDescendants(htmlEl, "scene", "scene-step");
|
|
3516
|
+
for (const child of childSteps) {
|
|
3517
|
+
const config = parseChildStep(child);
|
|
3518
|
+
const [start, end] = config.range;
|
|
3519
|
+
if (start >= end) {
|
|
3520
|
+
ctx?.warn(
|
|
3521
|
+
`scene-step from:${start} to:${end} is an empty window \u2014 from: must be less than to:, so this step never animates`
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
controller.addStep(child, config);
|
|
3525
|
+
}
|
|
3526
|
+
if (isReducedMotion(ctx)) {
|
|
3527
|
+
controller.progress = 1;
|
|
3528
|
+
controller.updateAll();
|
|
3529
|
+
return createInertInstance(() => controller.destroy());
|
|
3530
|
+
}
|
|
3531
|
+
const inst = createEffectInstance({
|
|
3532
|
+
continuous: progressMode === "scroll",
|
|
3533
|
+
activate() {
|
|
3534
|
+
if (progressMode === "time") {
|
|
3535
|
+
controller.start(() => inst.finish());
|
|
3536
|
+
} else {
|
|
3537
|
+
controller.start();
|
|
3538
|
+
}
|
|
3539
|
+
},
|
|
3540
|
+
cancel() {
|
|
3541
|
+
controller.stop();
|
|
3542
|
+
},
|
|
3543
|
+
finish() {
|
|
3544
|
+
controller.progress = 1;
|
|
3545
|
+
controller.updateAll();
|
|
3546
|
+
controller.stop();
|
|
3547
|
+
},
|
|
3548
|
+
destroy() {
|
|
3549
|
+
controller.destroy();
|
|
3550
|
+
}
|
|
3551
|
+
});
|
|
3552
|
+
return inst;
|
|
3553
|
+
}
|
|
3554
|
+
var SCENE_PARAMETERS = {
|
|
3555
|
+
progress: {
|
|
3556
|
+
type: "keyword",
|
|
3557
|
+
default: "scroll",
|
|
3558
|
+
keywords: ["scroll", "time"],
|
|
3559
|
+
cssProperty: "--kui-scene-progress"
|
|
3560
|
+
},
|
|
3561
|
+
duration: {
|
|
3562
|
+
type: "number",
|
|
3563
|
+
default: "1000",
|
|
3564
|
+
minimum: 100,
|
|
3565
|
+
maximum: 6e4,
|
|
3566
|
+
cssProperty: "--kui-scene-duration"
|
|
3567
|
+
}
|
|
3568
|
+
};
|
|
3569
|
+
var SCENE_STEP_PARAMETERS = {
|
|
3570
|
+
from: { type: "number", default: "0", minimum: 0, maximum: 1, cssProperty: "--kui-scene-step-from" },
|
|
3571
|
+
to: { type: "number", default: "1", minimum: 0, maximum: 1, cssProperty: "--kui-scene-step-to" },
|
|
3572
|
+
opacity: { type: "text", default: "", cssProperty: "--kui-scene-step-opacity" },
|
|
3573
|
+
x: { type: "text", default: "", cssProperty: "--kui-scene-step-x" },
|
|
3574
|
+
y: { type: "text", default: "", cssProperty: "--kui-scene-step-y" },
|
|
3575
|
+
scale: { type: "text", default: "", cssProperty: "--kui-scene-step-scale" }
|
|
3576
|
+
};
|
|
3577
|
+
function prepareSceneStep() {
|
|
3578
|
+
return createInertInstance();
|
|
3579
|
+
}
|
|
3580
|
+
var SCENE_PRIMITIVES = [
|
|
3581
|
+
{
|
|
3582
|
+
id: "scene",
|
|
3583
|
+
renderer: "javascript",
|
|
3584
|
+
channels: ["opacity", "skew"],
|
|
3585
|
+
parameters: SCENE_PARAMETERS,
|
|
3586
|
+
supportedTimelines: ["scroll", "time"],
|
|
3587
|
+
supportedActivations: ["load", "enter"],
|
|
3588
|
+
defaultActivation: "load",
|
|
3589
|
+
perfClass: "continuous",
|
|
3590
|
+
// Still `disable`: it is what stops `openGate` activating the instance, which is precisely
|
|
3591
|
+
// what makes the held last keyframe in `prepareScene` the only thing that runs.
|
|
3592
|
+
reducedMotion: "disable",
|
|
3593
|
+
prepare: prepareScene
|
|
3594
|
+
},
|
|
3595
|
+
{
|
|
3596
|
+
id: "scene-step",
|
|
3597
|
+
renderer: "javascript",
|
|
3598
|
+
// The same pair the parent declares, and for the same reason: `updateElement` writes `opacity`
|
|
3599
|
+
// and the `transform` *shorthand*, and per `core/types.ts`'s `CHANNEL` note anything writing
|
|
3600
|
+
// the shorthand is on the `skew` channel whatever the transform is for. Not `translate`/`scale`
|
|
3601
|
+
// — those are the independent transform properties, which this module never writes.
|
|
3602
|
+
channels: ["opacity", "skew"],
|
|
3603
|
+
parameters: SCENE_STEP_PARAMETERS,
|
|
3604
|
+
supportedTimelines: ["scroll", "time"],
|
|
3605
|
+
supportedActivations: ["load", "enter"],
|
|
3606
|
+
defaultActivation: "load",
|
|
3607
|
+
perfClass: "dom-transform",
|
|
3608
|
+
reducedMotion: "disable",
|
|
3609
|
+
prepare: prepareSceneStep
|
|
3610
|
+
}
|
|
3611
|
+
];
|
|
3612
|
+
var SCENE_PRESETS = [
|
|
3613
|
+
{ name: "scene", primitive: "scene" },
|
|
3614
|
+
{ name: "scene-step", primitive: "scene-step" }
|
|
3615
|
+
];
|
|
3616
|
+
function registerScenes(target) {
|
|
3617
|
+
return registerInto(target, SCENE_PRIMITIVES, SCENE_PRESETS, "Scenes");
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
// src/advanced/camera-3d.ts
|
|
3621
|
+
var AUDIO_PUSH_RATIO = 0.1;
|
|
3622
|
+
function parseDepth(str) {
|
|
3623
|
+
if (!str) return 0;
|
|
3624
|
+
const num = parseFloat(str);
|
|
3625
|
+
return Number.isNaN(num) ? 0 : num;
|
|
3626
|
+
}
|
|
3627
|
+
function computeLayerTransform(depthZ, cameraZ, tiltX = 0, tiltY = 0) {
|
|
3628
|
+
const zOffset = depthZ + cameraZ * 0.5;
|
|
3629
|
+
if (tiltX || tiltY) {
|
|
3630
|
+
return `translate3d(0, 0, ${zOffset}px) rotateX(${tiltX}deg) rotateY(${tiltY}deg)`;
|
|
3631
|
+
}
|
|
3632
|
+
return `translate3d(0, 0, ${zOffset}px)`;
|
|
3633
|
+
}
|
|
3634
|
+
var CameraController = class {
|
|
3635
|
+
container;
|
|
3636
|
+
options;
|
|
3637
|
+
env;
|
|
3638
|
+
window;
|
|
3639
|
+
raf;
|
|
3640
|
+
caf;
|
|
3641
|
+
layers = [];
|
|
3642
|
+
cameraZ = 0;
|
|
3643
|
+
mouse = { x: 0, y: 0, targetX: 0, targetY: 0 };
|
|
3644
|
+
rafId = null;
|
|
3645
|
+
scrollRafId = null;
|
|
3646
|
+
audioRafId = null;
|
|
3647
|
+
/** Last value read for the selected band, 0..1. Stays 0 for a scene with no `audio:`. */
|
|
3648
|
+
audioLevel = 0;
|
|
3649
|
+
isListening = false;
|
|
3650
|
+
/**
|
|
3651
|
+
* Container and every depth layer, each ledger opened at that element's first write and shared
|
|
3652
|
+
* with every other controller writing to the same element — see `createAdvancedLedgers`.
|
|
3653
|
+
*
|
|
3654
|
+
* This used to say "an inner scene claims the outer scene's layers", which is backwards: an
|
|
3655
|
+
* inner `querySelectorAll` only ever sees its own descendants, so it is the **outer** scene that
|
|
3656
|
+
* over-reaches into the inner one. `prepareCameraScene` now scopes the scan with
|
|
3657
|
+
* {@link ownedDescendants}, so an outer camera stops at a nested `camera-scene` and the two no
|
|
3658
|
+
* longer write one layer's `transform` on alternate frames.
|
|
3659
|
+
*
|
|
3660
|
+
* Sharing still matters after that fix, and not only for the controllers built directly by the
|
|
3661
|
+
* suites: a `camera-layer` that is *also* a `scene-step` has two owners by design, because they
|
|
3662
|
+
* are different kinds of host. The ledger is what keeps that case honest.
|
|
3663
|
+
*/
|
|
3664
|
+
ledgers;
|
|
3665
|
+
constructor(container, options = {}, env = {}) {
|
|
3666
|
+
this.container = container;
|
|
3667
|
+
this.options = options;
|
|
3668
|
+
this.env = env;
|
|
3669
|
+
const resolved = resolveEnv(null, env);
|
|
3670
|
+
this.window = resolved.window;
|
|
3671
|
+
this.raf = resolved.raf;
|
|
3672
|
+
this.caf = resolved.caf;
|
|
3673
|
+
this.ledgers = createAdvancedLedgers(container);
|
|
3674
|
+
this.onScroll = this.onScroll.bind(this);
|
|
3675
|
+
this.onMouseMove = this.onMouseMove.bind(this);
|
|
3676
|
+
}
|
|
3677
|
+
addLayer(element, depthZ) {
|
|
3678
|
+
this.layers.push({ element, depthZ });
|
|
3679
|
+
}
|
|
3680
|
+
applyLayer3D() {
|
|
3681
|
+
for (const layer of this.layers) {
|
|
3682
|
+
styleOf(this.ledgers, layer.element)?.set("transform-style", "preserve-3d");
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
bindEvents() {
|
|
3686
|
+
if (this.window?.addEventListener) {
|
|
3687
|
+
this.window.addEventListener("scroll", this.onScroll, { passive: true });
|
|
3688
|
+
if (this.options.mouseTilt) {
|
|
3689
|
+
this.window.addEventListener("pointermove", this.onMouseMove, { passive: true });
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
/**
|
|
3694
|
+
* The 3D space itself: the container's perspective and every layer's `preserve-3d`.
|
|
3695
|
+
*
|
|
3696
|
+
* Separated from {@link start} because the reduced-motion path needs exactly this and none of
|
|
3697
|
+
* the listeners or loops below it — see {@link renderStatic}.
|
|
3698
|
+
*/
|
|
3699
|
+
applyStage() {
|
|
3700
|
+
const stage = styleOf(this.ledgers, this.container);
|
|
3701
|
+
stage?.set("perspective", `${this.options.depth || 1e3}px`);
|
|
3702
|
+
stage?.set("transform-style", "preserve-3d");
|
|
3703
|
+
this.applyLayer3D();
|
|
3704
|
+
}
|
|
3705
|
+
/**
|
|
3706
|
+
* The scene composed once, with the camera parked where its travel begins.
|
|
3707
|
+
*
|
|
3708
|
+
* What a visitor who asked for reduced motion gets instead of nothing, following the decision
|
|
3709
|
+
* `shaders.ts` records: a static frame where one genuinely exists. Here it does, and it is most
|
|
3710
|
+
* of the effect — `depth` and each layer's `z:` *are* the composition, and the scroll only moves
|
|
3711
|
+
* a camera through an arrangement the author already described. One `render()` at `cameraZ = 0`
|
|
3712
|
+
* puts every layer at its authored depth through the container's perspective, so the scene keeps
|
|
3713
|
+
* its real scale hierarchy and simply does not travel. Without it the layers are written nothing
|
|
3714
|
+
* at all and the whole scene collapses flat — the same bug `start()`'s own `render()` call fixed
|
|
3715
|
+
* for the live path.
|
|
3716
|
+
*
|
|
3717
|
+
* Deliberately *not* the scene's end state, which is where the animator leaves a CSS effect under
|
|
3718
|
+
* this policy (`openGate`, `core/animator.ts`). "The end" here is `cameraZ = depth`, i.e. the
|
|
3719
|
+
* camera pushed all the way through: a layer then sits at `z = depthZ + depth / 2`, which against
|
|
3720
|
+
* a `perspective` of `depth` is a 2x blow-up at `z:0` and 10x at `z:400px` of a 1000px scene, with
|
|
3721
|
+
* nearer layers passing the viewer entirely. An end state nobody can see is not a still frame.
|
|
3722
|
+
*
|
|
3723
|
+
* Runs during `prepare`, because it has to — under reduced motion `openGate` marks the element
|
|
3724
|
+
* finished and never activates any instance. See `prepareReducedMotion` in `shaders.ts`.
|
|
3725
|
+
*/
|
|
3726
|
+
renderStatic() {
|
|
3727
|
+
if (!this.container) return;
|
|
3728
|
+
this.applyStage();
|
|
3729
|
+
this.render();
|
|
3730
|
+
}
|
|
3731
|
+
start() {
|
|
3732
|
+
if (this.isListening || !this.container) return;
|
|
3733
|
+
this.applyStage();
|
|
3734
|
+
this.bindEvents();
|
|
3735
|
+
this.isListening = true;
|
|
3736
|
+
this.updateScroll();
|
|
3737
|
+
this.readAudio();
|
|
3738
|
+
this.render();
|
|
3739
|
+
this.startAudioLoop();
|
|
3740
|
+
this.startLoop();
|
|
3741
|
+
}
|
|
3742
|
+
stop() {
|
|
3743
|
+
if (!this.isListening) return;
|
|
3744
|
+
if (this.window?.removeEventListener) {
|
|
3745
|
+
this.window.removeEventListener("scroll", this.onScroll);
|
|
3746
|
+
this.window.removeEventListener("pointermove", this.onMouseMove);
|
|
3747
|
+
}
|
|
3748
|
+
if (this.rafId) this.caf(this.rafId);
|
|
3749
|
+
this.rafId = null;
|
|
3750
|
+
if (this.scrollRafId) this.caf(this.scrollRafId);
|
|
3751
|
+
this.scrollRafId = null;
|
|
3752
|
+
if (this.audioRafId) this.caf(this.audioRafId);
|
|
3753
|
+
this.audioRafId = null;
|
|
3754
|
+
this.isListening = false;
|
|
3755
|
+
}
|
|
3756
|
+
updateScroll() {
|
|
3757
|
+
if (!this.container?.getBoundingClientRect) return;
|
|
3758
|
+
const winHeight = this.window?.innerHeight ?? 800;
|
|
3759
|
+
const rect = this.container.getBoundingClientRect();
|
|
3760
|
+
const travel = winHeight + rect.height;
|
|
3761
|
+
const dist = winHeight - rect.top;
|
|
3762
|
+
const progress = travel > 0 ? clamp(dist / travel, 0, 1) : 0;
|
|
3763
|
+
this.cameraZ = progress * (this.options.depth || 1e3);
|
|
3764
|
+
}
|
|
3765
|
+
/** The selected band's current value, or 0 for a scene that asked for none. */
|
|
3766
|
+
readAudio() {
|
|
3767
|
+
this.audioLevel = this.options.audio ? readAudioBand(this.container, this.options.audio) : 0;
|
|
3768
|
+
}
|
|
3769
|
+
/**
|
|
3770
|
+
* The one frame loop an audio-driven scene runs, doing every read before every write.
|
|
3771
|
+
*
|
|
3772
|
+
* An audio band changes on its own, without a scroll or a pointer move to hang a frame on, so
|
|
3773
|
+
* this scene needs a loop of its own — and then it needs to be the *only* loop, which is why
|
|
3774
|
+
* `onScroll` and `startLoop` both stand down while it is running. Three callbacks each reading
|
|
3775
|
+
* geometry (`getBoundingClientRect`) or style (`readAudioBand`'s computed fallback) and then
|
|
3776
|
+
* writing transforms would force a layout or style recalculation per callback per frame; one
|
|
3777
|
+
* pass that reads scroll position, reads the band, steps the pointer spring and only then
|
|
3778
|
+
* renders forces at most one.
|
|
3779
|
+
*/
|
|
3780
|
+
startAudioLoop() {
|
|
3781
|
+
if (this.audioRafId || !this.options.audio) return;
|
|
3782
|
+
const tick = () => {
|
|
3783
|
+
this.updateScroll();
|
|
3784
|
+
this.readAudio();
|
|
3785
|
+
if (this.options.mouseTilt) this.stepMouse();
|
|
3786
|
+
this.render();
|
|
3787
|
+
this.audioRafId = this.isListening ? this.raf(tick) : null;
|
|
3788
|
+
};
|
|
3789
|
+
this.audioRafId = this.raf(tick);
|
|
3790
|
+
}
|
|
3791
|
+
onScroll() {
|
|
3792
|
+
if (this.scrollRafId || this.audioRafId) return;
|
|
3793
|
+
const id = this.raf(() => {
|
|
3794
|
+
this.scrollRafId = null;
|
|
3795
|
+
this.updateScroll();
|
|
3796
|
+
this.render();
|
|
3797
|
+
});
|
|
3798
|
+
if (id == null) {
|
|
3799
|
+
this.updateScroll();
|
|
3800
|
+
this.render();
|
|
3801
|
+
} else {
|
|
3802
|
+
this.scrollRafId = id;
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
onMouseMove(e) {
|
|
3806
|
+
if (!this.options.mouseTilt) return;
|
|
3807
|
+
const winWidth = this.window?.innerWidth ?? 1e3;
|
|
3808
|
+
const winHeight = this.window?.innerHeight ?? 800;
|
|
3809
|
+
const cx = winWidth * 0.5;
|
|
3810
|
+
const cy = winHeight * 0.5;
|
|
3811
|
+
this.mouse.targetX = (e.clientX - cx) / cx;
|
|
3812
|
+
this.mouse.targetY = (e.clientY - cy) / cy;
|
|
3813
|
+
this.startLoop();
|
|
3814
|
+
}
|
|
3815
|
+
stepMouse() {
|
|
3816
|
+
const dx = this.mouse.targetX - this.mouse.x;
|
|
3817
|
+
const dy = this.mouse.targetY - this.mouse.y;
|
|
3818
|
+
this.mouse.x += dx * 0.08;
|
|
3819
|
+
this.mouse.y += dy * 0.08;
|
|
3820
|
+
if (Math.abs(dx) < 1e-3 && Math.abs(dy) < 1e-3) {
|
|
3821
|
+
this.mouse.x = this.mouse.targetX;
|
|
3822
|
+
this.mouse.y = this.mouse.targetY;
|
|
3823
|
+
return false;
|
|
3824
|
+
}
|
|
3825
|
+
return true;
|
|
3826
|
+
}
|
|
3827
|
+
startLoop() {
|
|
3828
|
+
if (this.rafId || this.audioRafId || !this.options.mouseTilt) return;
|
|
3829
|
+
const tick = () => {
|
|
3830
|
+
const moving = this.stepMouse();
|
|
3831
|
+
this.render();
|
|
3832
|
+
if (this.isListening && moving) {
|
|
3833
|
+
this.rafId = this.raf(tick);
|
|
3834
|
+
} else {
|
|
3835
|
+
this.rafId = null;
|
|
3836
|
+
}
|
|
3837
|
+
};
|
|
3838
|
+
this.rafId = this.raf(tick);
|
|
3839
|
+
}
|
|
3840
|
+
/**
|
|
3841
|
+
* Tilt the whole stage toward the pointer.
|
|
3842
|
+
*
|
|
3843
|
+
* The author's own transform is read here, at the first frame that overwrites it, rather than
|
|
3844
|
+
* when the effect activated. Between those two moments sits at least one frame in which the
|
|
3845
|
+
* page's own code can write a transform, and a capture taken at activation would both overwrite
|
|
3846
|
+
* that write and hand the older value back on destroy as if it had never happened.
|
|
3847
|
+
*
|
|
3848
|
+
* `claim()` is what makes that true without a private snapshot field: it remembers `transform`'s
|
|
3849
|
+
* current value the first time this runs and is a no-op on every call after, so `peek()` always
|
|
3850
|
+
* answers with whatever was there just before this method's first write — the ledger already
|
|
3851
|
+
* tracks that for `restore()`; `peek()` is just asking it without writing.
|
|
3852
|
+
*/
|
|
3853
|
+
renderStageTilt(tiltX, tiltY) {
|
|
3854
|
+
const stage = styleOf(this.ledgers, this.container);
|
|
3855
|
+
if (!stage) return;
|
|
3856
|
+
stage.claim("transform");
|
|
3857
|
+
if (tiltX !== 0 || tiltY !== 0) {
|
|
3858
|
+
stage.set("transform", `rotateX(${tiltX.toFixed(2)}deg) rotateY(${tiltY.toFixed(2)}deg)`);
|
|
3859
|
+
return;
|
|
3860
|
+
}
|
|
3861
|
+
this.restStageTransform(stage);
|
|
3862
|
+
}
|
|
3863
|
+
/**
|
|
3864
|
+
* Hand `transform` back when the tilt settles at exactly zero.
|
|
3865
|
+
*
|
|
3866
|
+
* `peek()` reports only what `restore()` would put back, which is the **inline** capture and
|
|
3867
|
+
* nothing else — by design (`owned-styles.ts`, and `test/owned-styles.test.ts` asserts it). So
|
|
3868
|
+
* `peek() || 'rotateX(0deg) rotateY(0deg)'` was right only for an author who had written the
|
|
3869
|
+
* transform inline. For the ordinary case — `.hero { transform: translateY(-20px) }` in a
|
|
3870
|
+
* stylesheet — `peek()` answers `''` and the fallback wrote a *fabricated* identity inline,
|
|
3871
|
+
* which outranks the class and switches the author's own transform off the moment the effect
|
|
3872
|
+
* activates. `start()` renders immediately with the pointer at the synthetic centre, so that is
|
|
3873
|
+
* activation, not some edge case.
|
|
3874
|
+
*
|
|
3875
|
+
* Removing the declaration instead is what actually lands on "the author's own transform": their
|
|
3876
|
+
* inline value if they had one, their stylesheet's if they did not, and nothing at all if
|
|
3877
|
+
* neither. The `removeProperty` is not a ledger bypass — `claim()` above has already recorded
|
|
3878
|
+
* the property, `peek()` answering `''` *is* the ledger saying it found no inline declaration
|
|
3879
|
+
* here, and removal is precisely what its own `restore()` does for that capture.
|
|
3880
|
+
*
|
|
3881
|
+
* Not fixed here: an inline `transform: scale(1.2) !important` comes back through `peek()`
|
|
3882
|
+
* without its priority, so while the effect is live at rest a competing `!important` rule the
|
|
3883
|
+
* author had deliberately beaten wins again. `restore()` puts the priority back, so it ends at
|
|
3884
|
+
* the right value. Closing that needs a per-property give-back on `StyleLedger` (core).
|
|
3885
|
+
*
|
|
3886
|
+
* @complexity O(1).
|
|
3887
|
+
*/
|
|
3888
|
+
restStageTransform(stage) {
|
|
3889
|
+
const authored = stage.peek("transform");
|
|
3890
|
+
if (authored) {
|
|
3891
|
+
stage.set("transform", authored);
|
|
3892
|
+
return;
|
|
3893
|
+
}
|
|
3894
|
+
this.container.style.removeProperty("transform");
|
|
3895
|
+
}
|
|
3896
|
+
render() {
|
|
3897
|
+
if (this.options.mouseTilt) {
|
|
3898
|
+
this.renderStageTilt(this.mouse.y * -8, this.mouse.x * 8);
|
|
3899
|
+
}
|
|
3900
|
+
const cameraZ = this.cameraZ + this.audioLevel * (this.options.depth || 1e3) * AUDIO_PUSH_RATIO;
|
|
3901
|
+
for (const layer of this.layers) {
|
|
3902
|
+
styleOf(this.ledgers, layer.element)?.set(
|
|
3903
|
+
"transform",
|
|
3904
|
+
computeLayerTransform(layer.depthZ, cameraZ)
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
/**
|
|
3909
|
+
* `try`/`finally` because the restore must not be hostage to the listener teardown.
|
|
3910
|
+
*
|
|
3911
|
+
* The animator swallows a throwing instance teardown (`runQuietly(() => instance.destroy())`),
|
|
3912
|
+
* and every element this controller reached on its own — the layers — is in no ledger core will
|
|
3913
|
+
* ever restore. A `removeEventListener` the page has replaced, or a window torn down under a
|
|
3914
|
+
* detached document, would otherwise leave `transform` on each layer permanently.
|
|
3915
|
+
*/
|
|
3916
|
+
destroy() {
|
|
3917
|
+
try {
|
|
3918
|
+
this.stop();
|
|
3919
|
+
} finally {
|
|
3920
|
+
this.ledgers.restore();
|
|
3921
|
+
this.layers = [];
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
};
|
|
3925
|
+
function parseMouseTilt(params) {
|
|
3926
|
+
if (params.is) return params.is("mouse-tilt", "on");
|
|
3927
|
+
if (params.text) return params.text("mouse-tilt", "on") === "on";
|
|
3928
|
+
return true;
|
|
3929
|
+
}
|
|
3930
|
+
function prepareCameraScene(el, params, ctx) {
|
|
3931
|
+
const resolvedEnv = resolveEnv(ctx);
|
|
3932
|
+
const depth = clamp(params.num ? params.num("depth", 1e3) : 1e3, 100, 5e3);
|
|
3933
|
+
const mouseTilt = parseMouseTilt(params);
|
|
3934
|
+
const audio = parseAudioBand(params.text ? params.text("audio", "off") : "off");
|
|
3935
|
+
const reduced = isReducedMotion(ctx);
|
|
3936
|
+
const options = reduced ? { depth, mouseTilt: false, audio: null } : { depth, mouseTilt, audio };
|
|
3937
|
+
const htmlEl = el;
|
|
3938
|
+
const controller = new CameraController(htmlEl, options, resolvedEnv);
|
|
3939
|
+
const layerEls = ownedDescendants(htmlEl, "camera-scene", "camera-layer");
|
|
3940
|
+
for (const layer of layerEls) {
|
|
3941
|
+
const kui = layer.getAttribute("data-kui") || "";
|
|
3942
|
+
const zMatch = /z:([+-]?[0-9.]+)(?:px)?/.exec(kui);
|
|
3943
|
+
const depthZ = zMatch ? parseDepth(zMatch[1]) : 0;
|
|
3944
|
+
controller.addLayer(layer, depthZ);
|
|
3945
|
+
}
|
|
3946
|
+
if (reduced) {
|
|
3947
|
+
controller.renderStatic();
|
|
3948
|
+
return createInertInstance(() => controller.destroy());
|
|
3949
|
+
}
|
|
3950
|
+
return createEffectInstance({
|
|
3951
|
+
continuous: true,
|
|
3952
|
+
activate() {
|
|
3953
|
+
controller.start();
|
|
3954
|
+
},
|
|
3955
|
+
cancel() {
|
|
3956
|
+
controller.stop();
|
|
3957
|
+
},
|
|
3958
|
+
finish() {
|
|
3959
|
+
controller.stop();
|
|
3960
|
+
},
|
|
3961
|
+
destroy() {
|
|
3962
|
+
controller.destroy();
|
|
3963
|
+
}
|
|
3964
|
+
});
|
|
3965
|
+
}
|
|
3966
|
+
var CAMERA_PARAMETERS = {
|
|
3967
|
+
depth: { type: "number", default: "1000", minimum: 100, maximum: 5e3, cssProperty: "--kui-camera-depth" },
|
|
3968
|
+
"mouse-tilt": {
|
|
3969
|
+
type: "keyword",
|
|
3970
|
+
default: "on",
|
|
3971
|
+
keywords: ["on", "off"],
|
|
3972
|
+
cssProperty: "--kui-camera-tilt"
|
|
3973
|
+
},
|
|
3974
|
+
/**
|
|
3975
|
+
* Which `audio-source` band pushes the camera forward, if any.
|
|
3976
|
+
*
|
|
3977
|
+
* Same name, same words and the same `off` default as the `shaders` primitive's: one spelling
|
|
3978
|
+
* for "follow this band" across this directory rather than a second word per consumer. Opt-in
|
|
3979
|
+
* for the reason that one is — the value is written by another primitive, and a scene that read
|
|
3980
|
+
* it unasked would start pulsing the moment an unrelated `audio-source` appeared above it.
|
|
3981
|
+
*
|
|
3982
|
+
* Turning it on also changes *how* the scene renders: see `startAudioLoop`, which becomes the
|
|
3983
|
+
* scene's only frame loop.
|
|
3984
|
+
*/
|
|
3985
|
+
audio: {
|
|
3986
|
+
type: "keyword",
|
|
3987
|
+
default: "off",
|
|
3988
|
+
keywords: ["off", ...AUDIO_BANDS],
|
|
3989
|
+
cssProperty: "--kui-camera-audio"
|
|
3990
|
+
}
|
|
3991
|
+
};
|
|
3992
|
+
var CAMERA_LAYER_PARAMETERS = {
|
|
3993
|
+
z: { type: "number", default: "0", minimum: -5e3, maximum: 5e3, cssProperty: "--kui-camera-layer-z" }
|
|
3994
|
+
};
|
|
3995
|
+
function prepareCameraLayer() {
|
|
3996
|
+
return createInertInstance();
|
|
3997
|
+
}
|
|
3998
|
+
var CAMERA_PRIMITIVES = [
|
|
3999
|
+
{
|
|
4000
|
+
id: "camera-scene",
|
|
4001
|
+
renderer: "javascript",
|
|
4002
|
+
channels: ["skew", "perspective", "transform-style"],
|
|
4003
|
+
parameters: CAMERA_PARAMETERS,
|
|
4004
|
+
supportedTimelines: ["scroll", "time"],
|
|
4005
|
+
supportedActivations: ["load", "enter"],
|
|
4006
|
+
defaultActivation: "load",
|
|
4007
|
+
perfClass: "continuous",
|
|
4008
|
+
// Still `disable`, and it has to be: it is what makes `openGate` refuse to activate this
|
|
4009
|
+
// instance, and the still frame is baked in `prepare` precisely because nothing else runs.
|
|
4010
|
+
// `shaders.ts` keeps its own for the same reason.
|
|
4011
|
+
reducedMotion: "disable",
|
|
4012
|
+
prepare: prepareCameraScene
|
|
4013
|
+
},
|
|
4014
|
+
{
|
|
4015
|
+
id: "camera-layer",
|
|
4016
|
+
renderer: "javascript",
|
|
4017
|
+
channels: ["translate", "rotate", "transform-style"],
|
|
4018
|
+
parameters: CAMERA_LAYER_PARAMETERS,
|
|
4019
|
+
supportedTimelines: ["scroll", "time"],
|
|
4020
|
+
supportedActivations: ["load", "enter"],
|
|
4021
|
+
defaultActivation: "load",
|
|
4022
|
+
perfClass: "dom-transform",
|
|
4023
|
+
// Nothing to reconsider here when `camera-scene` gained a still frame: this primitive animates
|
|
4024
|
+
// nothing in either mode. The scene above writes every layer's transform, including the static
|
|
4025
|
+
// one, so a layer is already covered by whatever its scene decided.
|
|
4026
|
+
reducedMotion: "disable",
|
|
4027
|
+
prepare: prepareCameraLayer
|
|
4028
|
+
}
|
|
4029
|
+
];
|
|
4030
|
+
var CAMERA_PRESETS = [
|
|
4031
|
+
{ name: "camera-scene", primitive: "camera-scene" },
|
|
4032
|
+
{ name: "camera-layer", primitive: "camera-layer" }
|
|
4033
|
+
];
|
|
4034
|
+
function registerCamera(target) {
|
|
4035
|
+
return registerInto(target, CAMERA_PRIMITIVES, CAMERA_PRESETS, "Camera");
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
// src/advanced/particles.ts
|
|
4039
|
+
function stepParticle(p, mouseX, mouseY, repelRadius) {
|
|
4040
|
+
const dx = mouseX - p.x;
|
|
4041
|
+
const dy = mouseY - p.y;
|
|
4042
|
+
const dist = Math.hypot(dx, dy);
|
|
4043
|
+
if (dist < repelRadius && dist > 0) {
|
|
4044
|
+
const force = (repelRadius - dist) / repelRadius;
|
|
4045
|
+
p.vx -= dx / dist * force * 5;
|
|
4046
|
+
p.vy -= dy / dist * force * 5;
|
|
4047
|
+
}
|
|
4048
|
+
const ox = p.originX - p.x;
|
|
4049
|
+
const oy = p.originY - p.y;
|
|
4050
|
+
p.vx += ox * 0.05;
|
|
4051
|
+
p.vy += oy * 0.05;
|
|
4052
|
+
p.vx *= 0.88;
|
|
4053
|
+
p.vy *= 0.88;
|
|
4054
|
+
p.x += p.vx;
|
|
4055
|
+
p.y += p.vy;
|
|
4056
|
+
}
|
|
4057
|
+
function spawnParticlesGrid(count, color, width, height) {
|
|
4058
|
+
const cols = Math.max(1, Math.round(Math.sqrt(count * (width / Math.max(height, 1)))));
|
|
4059
|
+
const rows = Math.max(1, Math.ceil(count / cols));
|
|
4060
|
+
const particles = [];
|
|
4061
|
+
for (let i = 0; i < count; i++) {
|
|
4062
|
+
const originX = i % cols * (width / cols) + width / (cols * 2);
|
|
4063
|
+
const originY = Math.floor(i / cols) * (height / rows) + height / (rows * 2);
|
|
4064
|
+
particles.push({
|
|
4065
|
+
x: originX,
|
|
4066
|
+
y: originY,
|
|
4067
|
+
originX,
|
|
4068
|
+
originY,
|
|
4069
|
+
vx: 0,
|
|
4070
|
+
vy: 0,
|
|
4071
|
+
radius: i % 3 + 1.5,
|
|
4072
|
+
color
|
|
4073
|
+
});
|
|
4074
|
+
}
|
|
4075
|
+
return particles;
|
|
4076
|
+
}
|
|
4077
|
+
function isParticleMoving(p) {
|
|
4078
|
+
if (Math.abs(p.vx) > 0.02 || Math.abs(p.vy) > 0.02) return true;
|
|
4079
|
+
return Math.abs(p.x - p.originX) > 0.5 || Math.abs(p.y - p.originY) > 0.5;
|
|
4080
|
+
}
|
|
4081
|
+
var ParticleEmitter = class {
|
|
4082
|
+
element;
|
|
4083
|
+
options;
|
|
4084
|
+
env;
|
|
4085
|
+
window;
|
|
4086
|
+
document;
|
|
4087
|
+
createCanvas;
|
|
4088
|
+
raf;
|
|
4089
|
+
caf;
|
|
4090
|
+
canvas = null;
|
|
4091
|
+
ctx = null;
|
|
4092
|
+
particles = [];
|
|
4093
|
+
mouse = { x: -999, y: -999 };
|
|
4094
|
+
rafId = null;
|
|
4095
|
+
isListening = false;
|
|
4096
|
+
dpr = 1;
|
|
4097
|
+
/**
|
|
4098
|
+
* The host's `position`, and what it was before this module needed a positioning context.
|
|
4099
|
+
*
|
|
4100
|
+
* The hand-rolled pair this replaced read back only the value, never the priority, so an
|
|
4101
|
+
* author's `position: relative !important` came back as a plain declaration on teardown. The
|
|
4102
|
+
* canvas's own `cssText` needs no ledger — that element is created and removed here.
|
|
4103
|
+
*
|
|
4104
|
+
* Shared with every other controller writing to this element — see `createAdvancedLedgers`.
|
|
4105
|
+
*/
|
|
4106
|
+
ledgers;
|
|
4107
|
+
constructor(element, options = {}, env = {}) {
|
|
4108
|
+
this.element = element;
|
|
4109
|
+
this.options = options;
|
|
4110
|
+
this.env = env;
|
|
4111
|
+
this.ledgers = createAdvancedLedgers(element);
|
|
4112
|
+
const resolved = resolveEnv(null, env);
|
|
4113
|
+
this.window = resolved.window;
|
|
4114
|
+
this.document = resolved.document;
|
|
4115
|
+
this.createCanvas = resolved.createCanvas;
|
|
4116
|
+
this.raf = resolved.raf;
|
|
4117
|
+
this.caf = resolved.caf;
|
|
4118
|
+
this.onMouseMove = this.onMouseMove.bind(this);
|
|
4119
|
+
this.onMouseLeave = this.onMouseLeave.bind(this);
|
|
4120
|
+
this.onResize = this.onResize.bind(this);
|
|
4121
|
+
}
|
|
4122
|
+
syncDimensions() {
|
|
4123
|
+
if (!this.canvas || !this.element?.getBoundingClientRect) return 1;
|
|
4124
|
+
const rect = this.element.getBoundingClientRect();
|
|
4125
|
+
this.dpr = Math.min(this.window?.devicePixelRatio ?? 1, 2);
|
|
4126
|
+
const cw = Math.round(rect.width * this.dpr);
|
|
4127
|
+
const ch = Math.round(rect.height * this.dpr);
|
|
4128
|
+
if (this.canvas.width !== cw || this.canvas.height !== ch) {
|
|
4129
|
+
this.canvas.width = cw;
|
|
4130
|
+
this.canvas.height = ch;
|
|
4131
|
+
this.canvas.style.width = `${rect.width}px`;
|
|
4132
|
+
this.canvas.style.height = `${rect.height}px`;
|
|
4133
|
+
}
|
|
4134
|
+
return this.dpr;
|
|
4135
|
+
}
|
|
4136
|
+
setupPosition() {
|
|
4137
|
+
if (!this.window?.getComputedStyle) return;
|
|
4138
|
+
if (this.window.getComputedStyle(this.element).position !== "static") return;
|
|
4139
|
+
styleOf(this.ledgers, this.element)?.set("position", "relative");
|
|
4140
|
+
}
|
|
4141
|
+
init() {
|
|
4142
|
+
if (!this.document || !this.element?.getBoundingClientRect) return false;
|
|
4143
|
+
this.canvas = this.createCanvas();
|
|
4144
|
+
if (!this.canvas) return false;
|
|
4145
|
+
this.canvas.className = "kui-particle-canvas";
|
|
4146
|
+
this.canvas.style.cssText = "position:absolute;top:0;left:0;pointer-events:none;z-index:10;";
|
|
4147
|
+
this.ctx = this.canvas.getContext ? this.canvas.getContext("2d") : null;
|
|
4148
|
+
if (!this.ctx) return false;
|
|
4149
|
+
this.setupPosition();
|
|
4150
|
+
this.element.appendChild(this.canvas);
|
|
4151
|
+
this.syncDimensions();
|
|
4152
|
+
const rect = this.element.getBoundingClientRect();
|
|
4153
|
+
this.spawnParticles(rect.width, rect.height);
|
|
4154
|
+
this.bindEvents();
|
|
4155
|
+
return true;
|
|
4156
|
+
}
|
|
4157
|
+
spawnParticles(width, height) {
|
|
4158
|
+
const count = this.options.count || 60;
|
|
4159
|
+
const color = this.options.color || "#e4f222";
|
|
4160
|
+
this.particles = spawnParticlesGrid(count, color, width, height);
|
|
4161
|
+
}
|
|
4162
|
+
resizeObserver = null;
|
|
4163
|
+
bindEvents() {
|
|
4164
|
+
if (this.isListening) return;
|
|
4165
|
+
this.element.addEventListener("pointermove", this.onMouseMove, { passive: true });
|
|
4166
|
+
this.element.addEventListener("pointerleave", this.onMouseLeave, { passive: true });
|
|
4167
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
4168
|
+
this.resizeObserver = new ResizeObserver(() => this.onResize());
|
|
4169
|
+
this.resizeObserver.observe(this.element);
|
|
4170
|
+
} else if (this.window?.addEventListener) {
|
|
4171
|
+
this.window.addEventListener("resize", this.onResize, { passive: true });
|
|
4172
|
+
}
|
|
4173
|
+
this.isListening = true;
|
|
4174
|
+
}
|
|
4175
|
+
unbindEvents() {
|
|
4176
|
+
if (!this.isListening) return;
|
|
4177
|
+
if (this.element?.removeEventListener) {
|
|
4178
|
+
this.element.removeEventListener("pointermove", this.onMouseMove);
|
|
4179
|
+
this.element.removeEventListener("pointerleave", this.onMouseLeave);
|
|
4180
|
+
}
|
|
4181
|
+
if (this.resizeObserver) {
|
|
4182
|
+
this.resizeObserver.disconnect();
|
|
4183
|
+
this.resizeObserver = null;
|
|
4184
|
+
}
|
|
4185
|
+
if (this.window?.removeEventListener) {
|
|
4186
|
+
this.window.removeEventListener("resize", this.onResize);
|
|
4187
|
+
}
|
|
4188
|
+
this.isListening = false;
|
|
4189
|
+
}
|
|
4190
|
+
onResize() {
|
|
4191
|
+
this.syncDimensions();
|
|
4192
|
+
if (this.element?.getBoundingClientRect) {
|
|
4193
|
+
const rect = this.element.getBoundingClientRect();
|
|
4194
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
4195
|
+
this.spawnParticles(rect.width, rect.height);
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
this.draw();
|
|
4199
|
+
}
|
|
4200
|
+
onMouseMove(e) {
|
|
4201
|
+
if (!this.canvas?.getBoundingClientRect) return;
|
|
4202
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
4203
|
+
this.mouse.x = e.clientX - rect.left;
|
|
4204
|
+
this.mouse.y = e.clientY - rect.top;
|
|
4205
|
+
this.startLoop();
|
|
4206
|
+
}
|
|
4207
|
+
onMouseLeave() {
|
|
4208
|
+
this.mouse.x = -999;
|
|
4209
|
+
this.mouse.y = -999;
|
|
4210
|
+
}
|
|
4211
|
+
startLoop() {
|
|
4212
|
+
if (this.rafId) return;
|
|
4213
|
+
const tick = () => {
|
|
4214
|
+
const moving = this.update();
|
|
4215
|
+
this.draw();
|
|
4216
|
+
if (this.isListening && (moving || this.mouse.x !== -999)) {
|
|
4217
|
+
this.rafId = this.raf(tick);
|
|
4218
|
+
} else {
|
|
4219
|
+
this.rafId = null;
|
|
4220
|
+
}
|
|
4221
|
+
};
|
|
4222
|
+
this.rafId = this.raf(tick);
|
|
4223
|
+
}
|
|
4224
|
+
stopLoop() {
|
|
4225
|
+
if (this.rafId) {
|
|
4226
|
+
this.caf(this.rafId);
|
|
4227
|
+
this.rafId = null;
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
update() {
|
|
4231
|
+
const repelRadius = this.options.radius || 80;
|
|
4232
|
+
let anyMoving = false;
|
|
4233
|
+
for (const p of this.particles) {
|
|
4234
|
+
stepParticle(p, this.mouse.x, this.mouse.y, repelRadius);
|
|
4235
|
+
if (isParticleMoving(p)) {
|
|
4236
|
+
anyMoving = true;
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
return anyMoving;
|
|
4240
|
+
}
|
|
4241
|
+
draw() {
|
|
4242
|
+
if (!this.ctx || !this.canvas) return;
|
|
4243
|
+
const dpr = this.dpr || 1;
|
|
4244
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
4245
|
+
for (const p of this.particles) {
|
|
4246
|
+
this.ctx.beginPath();
|
|
4247
|
+
this.ctx.arc(p.x * dpr, p.y * dpr, p.radius * dpr, 0, Math.PI * 2);
|
|
4248
|
+
this.ctx.fillStyle = p.color;
|
|
4249
|
+
this.ctx.fill();
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
/** `try`/`finally` for the reason `camera-3d.ts`'s `destroy()` documents. */
|
|
4253
|
+
destroy() {
|
|
4254
|
+
try {
|
|
4255
|
+
this.stopLoop();
|
|
4256
|
+
this.unbindEvents();
|
|
4257
|
+
} finally {
|
|
4258
|
+
this.ledgers.restore();
|
|
4259
|
+
this.removeCanvas();
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
removeCanvas() {
|
|
4263
|
+
this.canvas?.remove();
|
|
4264
|
+
this.canvas = null;
|
|
4265
|
+
this.ctx = null;
|
|
4266
|
+
this.particles = [];
|
|
4267
|
+
}
|
|
4268
|
+
};
|
|
4269
|
+
function prepareParticles(el, params, ctx) {
|
|
4270
|
+
if (isReducedMotion(ctx)) return createInertInstance();
|
|
4271
|
+
const resolvedEnv = resolveEnv(ctx);
|
|
4272
|
+
const count = clamp(params.num ? params.num("count", 60) : 60, 10, 300);
|
|
4273
|
+
const radius = clamp(params.num ? params.num("radius", 80) : 80, 20, 300);
|
|
4274
|
+
const color = params.text ? params.text("color", "#e4f222") : "#e4f222";
|
|
4275
|
+
const htmlEl = el;
|
|
4276
|
+
const emitter = new ParticleEmitter(htmlEl, { count, radius, color }, resolvedEnv);
|
|
4277
|
+
let isMounted = false;
|
|
4278
|
+
return createEffectInstance({
|
|
4279
|
+
continuous: true,
|
|
4280
|
+
activate() {
|
|
4281
|
+
if (!isMounted) {
|
|
4282
|
+
isMounted = emitter.init();
|
|
4283
|
+
} else if (!emitter.isListening) {
|
|
4284
|
+
emitter.bindEvents();
|
|
4285
|
+
}
|
|
4286
|
+
if (isMounted) {
|
|
4287
|
+
emitter.startLoop();
|
|
4288
|
+
}
|
|
4289
|
+
},
|
|
4290
|
+
cancel() {
|
|
4291
|
+
emitter.stopLoop();
|
|
4292
|
+
emitter.unbindEvents();
|
|
4293
|
+
},
|
|
4294
|
+
finish() {
|
|
4295
|
+
emitter.stopLoop();
|
|
4296
|
+
emitter.unbindEvents();
|
|
4297
|
+
},
|
|
4298
|
+
destroy() {
|
|
4299
|
+
if (isMounted) {
|
|
4300
|
+
emitter.destroy();
|
|
4301
|
+
isMounted = false;
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
});
|
|
4305
|
+
}
|
|
4306
|
+
var PARTICLE_PARAMETERS = {
|
|
4307
|
+
count: { type: "number", default: "60", minimum: 10, maximum: 300, cssProperty: "--kui-particle-count" },
|
|
4308
|
+
radius: { type: "number", default: "80", minimum: 20, maximum: 300, cssProperty: "--kui-particle-radius" },
|
|
4309
|
+
color: { type: "color", default: "#e4f222", cssProperty: "--kui-particle-color" }
|
|
4310
|
+
};
|
|
4311
|
+
var PARTICLE_PRIMITIVES = [
|
|
4312
|
+
{
|
|
4313
|
+
id: "particle-dissolve",
|
|
4314
|
+
renderer: "javascript",
|
|
4315
|
+
channels: ["position"],
|
|
4316
|
+
parameters: PARTICLE_PARAMETERS,
|
|
4317
|
+
supportedTimelines: ["time"],
|
|
4318
|
+
supportedActivations: ["hover", "load"],
|
|
4319
|
+
defaultActivation: "hover",
|
|
4320
|
+
perfClass: "continuous",
|
|
4321
|
+
reducedMotion: "disable",
|
|
4322
|
+
prepare: prepareParticles
|
|
4323
|
+
}
|
|
4324
|
+
];
|
|
4325
|
+
var PARTICLE_PRESETS = [
|
|
4326
|
+
{ name: "particle-dissolve", primitive: "particle-dissolve" }
|
|
4327
|
+
];
|
|
4328
|
+
function registerParticles(target) {
|
|
4329
|
+
return registerInto(target, PARTICLE_PRIMITIVES, PARTICLE_PRESETS, "Particles");
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
// src/advanced/fluid-cursor.ts
|
|
4333
|
+
function stepFluidDrop(drop, friction) {
|
|
4334
|
+
drop.x += drop.vx;
|
|
4335
|
+
drop.y += drop.vy;
|
|
4336
|
+
drop.vx *= friction;
|
|
4337
|
+
drop.vy *= friction;
|
|
4338
|
+
drop.alpha *= 0.94;
|
|
4339
|
+
return drop.alpha < 0.02;
|
|
4340
|
+
}
|
|
4341
|
+
function syncCanvasStyleBox(canvas, vw, vh) {
|
|
4342
|
+
const style = canvas.style;
|
|
4343
|
+
if (!style) return;
|
|
4344
|
+
const cssW = `${vw}px`;
|
|
4345
|
+
const cssH = `${vh}px`;
|
|
4346
|
+
if (style.width !== cssW) style.width = cssW;
|
|
4347
|
+
if (style.height !== cssH) style.height = cssH;
|
|
4348
|
+
}
|
|
4349
|
+
function syncCanvasSize(canvas, win) {
|
|
4350
|
+
if (!canvas) return 1;
|
|
4351
|
+
const dpr = Math.min(win?.devicePixelRatio ?? 1, 2);
|
|
4352
|
+
const vw = win?.innerWidth ?? 1e3;
|
|
4353
|
+
const vh = win?.innerHeight ?? 800;
|
|
4354
|
+
const w = Math.round(vw * dpr);
|
|
4355
|
+
const h = Math.round(vh * dpr);
|
|
4356
|
+
if (canvas.width !== w || canvas.height !== h) {
|
|
4357
|
+
canvas.width = w;
|
|
4358
|
+
canvas.height = h;
|
|
4359
|
+
}
|
|
4360
|
+
syncCanvasStyleBox(canvas, vw, vh);
|
|
4361
|
+
return dpr;
|
|
4362
|
+
}
|
|
4363
|
+
var FluidTrail = class {
|
|
4364
|
+
options;
|
|
4365
|
+
env;
|
|
4366
|
+
window;
|
|
4367
|
+
document;
|
|
4368
|
+
createCanvas;
|
|
4369
|
+
raf;
|
|
4370
|
+
caf;
|
|
4371
|
+
drops = [];
|
|
4372
|
+
canvas = null;
|
|
4373
|
+
ctx = null;
|
|
4374
|
+
rafId = null;
|
|
4375
|
+
isListening = false;
|
|
4376
|
+
lastMouse = { x: 0, y: 0 };
|
|
4377
|
+
hasLastMouse = false;
|
|
4378
|
+
wasOutside = false;
|
|
4379
|
+
refCount = 0;
|
|
4380
|
+
clients = /* @__PURE__ */ new Map();
|
|
4381
|
+
activeClients = /* @__PURE__ */ new Map();
|
|
4382
|
+
nextClientId = 1;
|
|
4383
|
+
onResize;
|
|
4384
|
+
onPointerMove;
|
|
4385
|
+
mapKey;
|
|
4386
|
+
constructor(options = {}, env = {}) {
|
|
4387
|
+
this.options = options;
|
|
4388
|
+
this.env = env;
|
|
4389
|
+
const resolved = resolveEnv(null, env);
|
|
4390
|
+
this.window = resolved.window;
|
|
4391
|
+
this.document = resolved.document;
|
|
4392
|
+
this.createCanvas = resolved.createCanvas;
|
|
4393
|
+
this.raf = resolved.raf;
|
|
4394
|
+
this.caf = resolved.caf;
|
|
4395
|
+
this.mapKey = resolved.document ?? resolved.window ?? fallbackFluidKey;
|
|
4396
|
+
}
|
|
4397
|
+
acquire() {
|
|
4398
|
+
if (this.ctx) {
|
|
4399
|
+
this.refCount++;
|
|
4400
|
+
return true;
|
|
4401
|
+
}
|
|
4402
|
+
const ok = this.init();
|
|
4403
|
+
if (ok) this.refCount++;
|
|
4404
|
+
return ok;
|
|
4405
|
+
}
|
|
4406
|
+
release() {
|
|
4407
|
+
if (this.refCount > 0) this.refCount--;
|
|
4408
|
+
if (this.refCount === 0 && this.clients.size === 0) this.destroy();
|
|
4409
|
+
}
|
|
4410
|
+
registerClient(options, el) {
|
|
4411
|
+
const id = this.nextClientId++;
|
|
4412
|
+
this.clients.set(id, { options, el });
|
|
4413
|
+
return id;
|
|
4414
|
+
}
|
|
4415
|
+
activateClient(id) {
|
|
4416
|
+
const client = this.clients.get(id);
|
|
4417
|
+
if (!client) return false;
|
|
4418
|
+
this.activeClients.set(id, client);
|
|
4419
|
+
if (!this.canvas && !this.setupCanvas()) return false;
|
|
4420
|
+
this.attachListeners();
|
|
4421
|
+
this.startLoop();
|
|
4422
|
+
return true;
|
|
4423
|
+
}
|
|
4424
|
+
deactivateClient(id) {
|
|
4425
|
+
this.activeClients.delete(id);
|
|
4426
|
+
if (this.activeClients.size > 0 || this.refCount > 0) return;
|
|
4427
|
+
this.detachListeners();
|
|
4428
|
+
this.stopLoop();
|
|
4429
|
+
this.clearOverlay();
|
|
4430
|
+
}
|
|
4431
|
+
/**
|
|
4432
|
+
* Wipe the overlay when the last client stands down.
|
|
4433
|
+
*
|
|
4434
|
+
* `draw()` clears the canvas at the *start* of a frame, so stopping the loop leaves whatever was
|
|
4435
|
+
* in flight painted. The overlay is `position: fixed` at `z-index: 9998` across the whole
|
|
4436
|
+
* viewport, so a `fluid-trail` on `on:pointerenter`/`pointerleave` — or an author's
|
|
4437
|
+
* `control.cancel()` mid-move — left a frozen blob of drops over the entire page until something
|
|
4438
|
+
* reactivated it or the element was destroyed.
|
|
4439
|
+
*
|
|
4440
|
+
* The canvas itself stays mounted on purpose: registration happens at prepare, so the last
|
|
4441
|
+
* *unregister* owns the teardown and no client can have the canvas pulled from under it.
|
|
4442
|
+
*
|
|
4443
|
+
* @complexity O(1).
|
|
4444
|
+
*/
|
|
4445
|
+
clearOverlay() {
|
|
4446
|
+
this.drops = [];
|
|
4447
|
+
const canvas = this.canvas;
|
|
4448
|
+
if (!canvas || typeof this.ctx?.clearRect !== "function") return;
|
|
4449
|
+
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
4450
|
+
}
|
|
4451
|
+
unregisterClient(id) {
|
|
4452
|
+
this.deactivateClient(id);
|
|
4453
|
+
this.clients.delete(id);
|
|
4454
|
+
if (this.clients.size === 0 && this.refCount === 0) {
|
|
4455
|
+
this.destroy();
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
setupCanvas() {
|
|
4459
|
+
if (this.canvas && this.ctx) return true;
|
|
4460
|
+
if (!this.document?.body?.appendChild) return false;
|
|
4461
|
+
this.canvas = this.createCanvas();
|
|
4462
|
+
if (!this.canvas) return false;
|
|
4463
|
+
this.canvas.className = "kui-fluid-canvas";
|
|
4464
|
+
this.canvas.style.cssText = "position:fixed;top:0;left:0;pointer-events:none;z-index:9998;";
|
|
4465
|
+
this.ctx = this.canvas.getContext ? this.canvas.getContext("2d") : null;
|
|
4466
|
+
if (!this.ctx) {
|
|
4467
|
+
this.canvas = null;
|
|
4468
|
+
return false;
|
|
4469
|
+
}
|
|
4470
|
+
this.document.body.appendChild(this.canvas);
|
|
4471
|
+
syncCanvasSize(this.canvas, this.window);
|
|
4472
|
+
return true;
|
|
4473
|
+
}
|
|
4474
|
+
attachListeners() {
|
|
4475
|
+
if (this.isListening) return;
|
|
4476
|
+
this.onResize = () => syncCanvasSize(this.canvas, this.window);
|
|
4477
|
+
this.onPointerMove = (e) => {
|
|
4478
|
+
const client = this.resolveClientOwner(e);
|
|
4479
|
+
if (!client) {
|
|
4480
|
+
this.lastMouse.x = e.clientX;
|
|
4481
|
+
this.lastMouse.y = e.clientY;
|
|
4482
|
+
this.wasOutside = true;
|
|
4483
|
+
return;
|
|
4484
|
+
}
|
|
4485
|
+
const isEntry = this.wasOutside;
|
|
4486
|
+
const dx = isEntry ? 0 : e.clientX - this.lastMouse.x;
|
|
4487
|
+
const dy = isEntry ? 0 : e.clientY - this.lastMouse.y;
|
|
4488
|
+
this.wasOutside = false;
|
|
4489
|
+
if (Math.hypot(dx, dy) > 2 || isEntry || !this.hasLastMouse) {
|
|
4490
|
+
this.hasLastMouse = true;
|
|
4491
|
+
const vx = Math.max(-20, Math.min(20, dx * 0.2));
|
|
4492
|
+
const vy = Math.max(-20, Math.min(20, dy * 0.2));
|
|
4493
|
+
this.spawnDrop(e.clientX, e.clientY, vx, vy, client.options);
|
|
4494
|
+
this.startLoop();
|
|
4495
|
+
}
|
|
4496
|
+
this.lastMouse.x = e.clientX;
|
|
4497
|
+
this.lastMouse.y = e.clientY;
|
|
4498
|
+
};
|
|
4499
|
+
if (this.window?.addEventListener) {
|
|
4500
|
+
this.window.addEventListener("resize", this.onResize, { passive: true });
|
|
4501
|
+
this.window.addEventListener("pointermove", this.onPointerMove, { passive: true });
|
|
4502
|
+
this.isListening = true;
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
detachListeners() {
|
|
4506
|
+
if (this.window?.removeEventListener && this.isListening) {
|
|
4507
|
+
if (this.onResize) this.window.removeEventListener("resize", this.onResize);
|
|
4508
|
+
if (this.onPointerMove) this.window.removeEventListener("pointermove", this.onPointerMove);
|
|
4509
|
+
this.isListening = false;
|
|
4510
|
+
this.hasLastMouse = false;
|
|
4511
|
+
this.wasOutside = false;
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
init() {
|
|
4515
|
+
if (!this.setupCanvas()) return false;
|
|
4516
|
+
this.attachListeners();
|
|
4517
|
+
return true;
|
|
4518
|
+
}
|
|
4519
|
+
findClientByTarget(target) {
|
|
4520
|
+
let curr = target && typeof target.nodeType === "number" ? target : null;
|
|
4521
|
+
while (curr) {
|
|
4522
|
+
for (const client of this.activeClients.values()) {
|
|
4523
|
+
if (client.el && client.el === curr) return client;
|
|
4524
|
+
}
|
|
4525
|
+
curr = curr.parentNode;
|
|
4526
|
+
}
|
|
4527
|
+
return null;
|
|
4528
|
+
}
|
|
4529
|
+
findClientByCoords(x, y) {
|
|
4530
|
+
for (const client of this.activeClients.values()) {
|
|
4531
|
+
const rect = client.el?.getBoundingClientRect?.();
|
|
4532
|
+
if (rect && x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) return client;
|
|
4533
|
+
}
|
|
4534
|
+
return null;
|
|
4535
|
+
}
|
|
4536
|
+
resolveClientOwner(e) {
|
|
4537
|
+
if (this.activeClients.size === 0) return { options: this.options, el: void 0 };
|
|
4538
|
+
return this.findClientByTarget(e.target) ?? this.findClientByCoords(e.clientX, e.clientY);
|
|
4539
|
+
}
|
|
4540
|
+
getOptionsAt(x, y) {
|
|
4541
|
+
return this.findClientByCoords(x, y)?.options ?? null;
|
|
4542
|
+
}
|
|
4543
|
+
// eslint-disable-next-line max-params -- (x, y, vx, vy, customOpts) is the established drop coordinate and velocity signature
|
|
4544
|
+
spawnDrop(x, y, vx, vy, customOpts) {
|
|
4545
|
+
const activeOpts = customOpts ?? (this.activeClients.size === 0 ? this.options : this.getOptionsAt(x, y));
|
|
4546
|
+
if (!activeOpts) return;
|
|
4547
|
+
this.drops.push({
|
|
4548
|
+
x,
|
|
4549
|
+
y,
|
|
4550
|
+
vx,
|
|
4551
|
+
vy,
|
|
4552
|
+
size: activeOpts.size ?? this.options.size ?? 28,
|
|
4553
|
+
alpha: 0.8,
|
|
4554
|
+
color: activeOpts.color ?? this.options.color ?? "#e4f222",
|
|
4555
|
+
viscosity: activeOpts.viscosity ?? this.options.viscosity ?? 0.88
|
|
4556
|
+
});
|
|
4557
|
+
}
|
|
4558
|
+
startLoop() {
|
|
4559
|
+
if (this.rafId) return;
|
|
4560
|
+
const tick = () => {
|
|
4561
|
+
this.update();
|
|
4562
|
+
this.draw();
|
|
4563
|
+
if (this.drops.length > 0 && this.ctx) {
|
|
4564
|
+
this.rafId = this.raf(tick);
|
|
4565
|
+
} else {
|
|
4566
|
+
this.rafId = null;
|
|
4567
|
+
}
|
|
4568
|
+
};
|
|
4569
|
+
this.rafId = this.raf(tick);
|
|
4570
|
+
}
|
|
4571
|
+
stopLoop() {
|
|
4572
|
+
if (this.rafId) {
|
|
4573
|
+
this.caf(this.rafId);
|
|
4574
|
+
this.rafId = null;
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
update() {
|
|
4578
|
+
for (let i = this.drops.length - 1; i >= 0; i--) {
|
|
4579
|
+
const drop = this.drops[i];
|
|
4580
|
+
if (drop && stepFluidDrop(drop, drop.viscosity ?? 0.88)) {
|
|
4581
|
+
this.drops.splice(i, 1);
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
draw() {
|
|
4586
|
+
const ctx = this.ctx;
|
|
4587
|
+
if (!ctx || !this.canvas) return;
|
|
4588
|
+
const dpr = syncCanvasSize(this.canvas, this.window);
|
|
4589
|
+
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
4590
|
+
for (const drop of this.drops) {
|
|
4591
|
+
if (typeof ctx.save === "function") ctx.save();
|
|
4592
|
+
ctx.globalAlpha = drop.alpha;
|
|
4593
|
+
ctx.fillStyle = drop.color;
|
|
4594
|
+
ctx.beginPath();
|
|
4595
|
+
ctx.arc(drop.x * dpr, drop.y * dpr, drop.size * dpr * 0.5, 0, Math.PI * 2);
|
|
4596
|
+
ctx.fill();
|
|
4597
|
+
if (typeof ctx.restore === "function") ctx.restore();
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
destroy() {
|
|
4601
|
+
this.stopLoop();
|
|
4602
|
+
this.detachListeners();
|
|
4603
|
+
if (this.canvas) {
|
|
4604
|
+
this.canvas.remove();
|
|
4605
|
+
this.canvas = null;
|
|
4606
|
+
}
|
|
4607
|
+
this.ctx = null;
|
|
4608
|
+
this.drops = [];
|
|
4609
|
+
this.refCount = 0;
|
|
4610
|
+
this.clients.clear();
|
|
4611
|
+
this.activeClients.clear();
|
|
4612
|
+
fluidTrails.delete(this.mapKey);
|
|
4613
|
+
}
|
|
4614
|
+
};
|
|
4615
|
+
var fluidTrails = /* @__PURE__ */ new WeakMap();
|
|
4616
|
+
var fallbackFluidKey = {};
|
|
4617
|
+
function getSharedFluidTrail(env) {
|
|
4618
|
+
const resolved = resolveEnv(null, env);
|
|
4619
|
+
const key = resolved.document ?? resolved.window ?? fallbackFluidKey;
|
|
4620
|
+
let trail = fluidTrails.get(key);
|
|
4621
|
+
if (!trail) {
|
|
4622
|
+
trail = new FluidTrail({}, env);
|
|
4623
|
+
fluidTrails.set(key, trail);
|
|
4624
|
+
}
|
|
4625
|
+
return trail;
|
|
4626
|
+
}
|
|
4627
|
+
function prepareFluidCursor(el, params, ctx) {
|
|
4628
|
+
if (isReducedMotion(ctx)) return createInertInstance();
|
|
4629
|
+
const hostDoc = el?.ownerDocument;
|
|
4630
|
+
const resolvedEnv = resolveEnv(ctx, hostDoc ? { document: hostDoc, window: hostDoc.defaultView } : void 0);
|
|
4631
|
+
if (!resolvedEnv.document?.body) {
|
|
4632
|
+
return createInertInstance();
|
|
4633
|
+
}
|
|
4634
|
+
const size = clamp(params.num ? params.num("size", 28) : 28, 4, 120);
|
|
4635
|
+
const viscosity = clamp(params.num ? params.num("viscosity", 0.88) : 0.88, 0.1, 0.99);
|
|
4636
|
+
const color = params.text ? params.text("color", "#e4f222") : "#e4f222";
|
|
4637
|
+
const trail = getSharedFluidTrail(resolvedEnv);
|
|
4638
|
+
const clientId = trail.registerClient({ size, viscosity, color }, el);
|
|
4639
|
+
return createEffectInstance({
|
|
4640
|
+
continuous: true,
|
|
4641
|
+
activate() {
|
|
4642
|
+
trail.activateClient(clientId);
|
|
4643
|
+
},
|
|
4644
|
+
cancel() {
|
|
4645
|
+
trail.deactivateClient(clientId);
|
|
4646
|
+
},
|
|
4647
|
+
finish() {
|
|
4648
|
+
trail.deactivateClient(clientId);
|
|
4649
|
+
},
|
|
4650
|
+
destroy() {
|
|
4651
|
+
trail.unregisterClient(clientId);
|
|
4652
|
+
}
|
|
4653
|
+
});
|
|
4654
|
+
}
|
|
4655
|
+
var FLUID_PARAMETERS = {
|
|
4656
|
+
size: { type: "number", default: "28", minimum: 4, maximum: 120, cssProperty: "--kui-fluid-size" },
|
|
4657
|
+
viscosity: { type: "number", default: "0.88", minimum: 0.1, maximum: 0.99, cssProperty: "--kui-fluid-viscosity" },
|
|
4658
|
+
color: { type: "color", default: "#e4f222", cssProperty: "--kui-fluid-color" }
|
|
4659
|
+
};
|
|
4660
|
+
var FLUID_PRIMITIVES = [
|
|
4661
|
+
{
|
|
4662
|
+
id: "fluid-trail",
|
|
4663
|
+
renderer: "javascript",
|
|
4664
|
+
channels: [],
|
|
4665
|
+
parameters: FLUID_PARAMETERS,
|
|
4666
|
+
supportedTimelines: ["time"],
|
|
4667
|
+
supportedActivations: ["load", "hover"],
|
|
4668
|
+
defaultActivation: "load",
|
|
4669
|
+
perfClass: "continuous",
|
|
4670
|
+
reducedMotion: "disable",
|
|
4671
|
+
prepare: prepareFluidCursor
|
|
4672
|
+
}
|
|
4673
|
+
];
|
|
4674
|
+
var FLUID_PRESETS = [
|
|
4675
|
+
{ name: "fluid-trail", primitive: "fluid-trail" },
|
|
4676
|
+
{ name: "cursor-fluid", primitive: "fluid-trail" },
|
|
4677
|
+
{ name: "fluid-cursor", primitive: "fluid-trail" }
|
|
4678
|
+
];
|
|
4679
|
+
function registerFluidCursor(target) {
|
|
4680
|
+
return registerInto(target, FLUID_PRIMITIVES[0], FLUID_PRESETS, "FluidCursor");
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
// src/advanced/index.ts
|
|
4684
|
+
function registerAdvanced(target) {
|
|
4685
|
+
registerShaders(target);
|
|
4686
|
+
registerScenes(target);
|
|
4687
|
+
registerCamera(target);
|
|
4688
|
+
registerParticles(target);
|
|
4689
|
+
registerFluidCursor(target);
|
|
4690
|
+
registerAudio(target);
|
|
4691
|
+
const t = target;
|
|
4692
|
+
return t?.registry || target;
|
|
4693
|
+
}
|
|
4694
|
+
export {
|
|
4695
|
+
AUDIO_PARAMETERS,
|
|
4696
|
+
AUDIO_PRESETS,
|
|
4697
|
+
AUDIO_PRIMITIVES,
|
|
4698
|
+
AudioSourceController,
|
|
4699
|
+
CAMERA_PARAMETERS,
|
|
4700
|
+
CAMERA_PRESETS,
|
|
4701
|
+
CAMERA_PRIMITIVES,
|
|
4702
|
+
FLUID_PARAMETERS,
|
|
4703
|
+
FLUID_PRESETS,
|
|
4704
|
+
FLUID_PRIMITIVES,
|
|
4705
|
+
PARTICLE_PARAMETERS,
|
|
4706
|
+
PARTICLE_PRESETS,
|
|
4707
|
+
PARTICLE_PRIMITIVES,
|
|
4708
|
+
SCENE_PARAMETERS,
|
|
4709
|
+
SCENE_PRESETS,
|
|
4710
|
+
SCENE_PRIMITIVES,
|
|
4711
|
+
SHADERS_PRESETS,
|
|
4712
|
+
SHADERS_PRIMITIVE,
|
|
4713
|
+
SHADER_PARAMETERS,
|
|
4714
|
+
prepareAudioSource,
|
|
4715
|
+
prepareCameraScene,
|
|
4716
|
+
prepareFluidCursor,
|
|
4717
|
+
prepareParticles,
|
|
4718
|
+
prepareScene,
|
|
4719
|
+
prepareShaders,
|
|
4720
|
+
registerAdvanced,
|
|
4721
|
+
registerAudio,
|
|
4722
|
+
registerCamera,
|
|
4723
|
+
registerFluidCursor,
|
|
4724
|
+
registerParticles,
|
|
4725
|
+
registerScenes,
|
|
4726
|
+
registerShaders
|
|
4727
|
+
};
|