@pixerate/flair 0.22.0 → 0.24.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.
@@ -14,6 +14,68 @@ import {
14
14
  rgbToHex
15
15
  } from "./chunk-VYOMXE6I.js";
16
16
 
17
+ // src/catalog/inputs.ts
18
+ var EFFECT_INPUTS = ["image", "video", "svg", "text", "mask", "path"];
19
+ var EFFECT_INTERACTIONS = ["hover", "press", "click", "trail", "pointer"];
20
+ function isTextImage(url) {
21
+ if (!url.startsWith("data:image/svg+xml;charset=utf-8,")) return false;
22
+ try {
23
+ return decodeURIComponent(url.slice(url.indexOf(",") + 1)).includes("<text");
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+ var PATH_DATA = /^\s*[Mm]\s*[-+.\d]/;
29
+ function textSlot(control) {
30
+ const value = String(control.default);
31
+ const holds2 = control.name === "font" || /\bfont\b/i.test(control.label) ? "font" : PATH_DATA.test(value) ? "path" : control.name === "glyphs" ? "glyphs" : "copy";
32
+ return { name: control.name, label: control.label, holds: holds2, default: value };
33
+ }
34
+ function shaderInteractions(source) {
35
+ const code = source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "");
36
+ const found = /* @__PURE__ */ new Set();
37
+ if (/\biClicks\b/.test(code)) found.add("click");
38
+ if (/\biTrail\b/.test(code)) found.add("trail");
39
+ if (/\biPointer\s*\.\s*z\b/.test(code)) found.add("hover");
40
+ if (/\biPointer\s*\.\s*w\b/.test(code) || /\biMouse\s*\.\s*z\b/.test(code)) found.add("press");
41
+ if (/\biPointer\s*\.\s*xy?\b/.test(code) || /\biMouse\s*\.\s*xy?\b/.test(code)) found.add("pointer");
42
+ return EFFECT_INTERACTIONS.filter((i) => found.has(i));
43
+ }
44
+ function inputsOf(effect, controls) {
45
+ const accepts = /* @__PURE__ */ new Set();
46
+ const images = [];
47
+ const texts = [];
48
+ for (const control of controls) {
49
+ if (control.type === "sampler2D") {
50
+ const value = String(control.default);
51
+ const video = effect.kind === "shader" ? effect.video : void 0;
52
+ const wants = isTextImage(value) ? "text" : video === "none" ? "mask" : "picture";
53
+ images.push({ name: control.name, label: control.label, wants });
54
+ if (wants === "picture") {
55
+ accepts.add("image").add("svg");
56
+ if (video && video !== "none") accepts.add("video");
57
+ } else {
58
+ accepts.add("mask").add("svg").add("text").add("image");
59
+ }
60
+ } else if (control.type === "text") {
61
+ const slot = textSlot(control);
62
+ texts.push(slot);
63
+ if (slot.holds === "copy") accepts.add("text");
64
+ if (slot.holds === "path") accepts.add("path");
65
+ }
66
+ }
67
+ const interactions = effect.kind === "vector" ? effect.trigger ? [effect.trigger] : [] : shaderInteractions(effect.source);
68
+ return {
69
+ accepts: EFFECT_INPUTS.filter((i) => accepts.has(i)),
70
+ images,
71
+ texts,
72
+ ...effect.kind === "shader" && effect.video ? { video: effect.video } : {},
73
+ interactions,
74
+ ...effect.kind === "vector" && effect.trigger ? { trigger: effect.trigger } : {},
75
+ transparent: effect.kind === "shader" ? Boolean(effect.transparent) : false
76
+ };
77
+ }
78
+
17
79
  // src/catalog/glsl.ts
18
80
  var HASH = `
19
81
  float hash12(vec2 p) {
@@ -3653,6 +3715,53 @@ var liftCard = {
3653
3715
  };
3654
3716
  var shadowEffects = [liftCard];
3655
3717
 
3718
+ // src/catalog/lighting.ts
3719
+ var sway = (deg) => `rotate(${deg} 120 0)`;
3720
+ var pull = (step) => [
3721
+ { id: "lampSwitchCord", step, attrs: { y2: [122, 135] }, ms: 110, ease: "out" },
3722
+ { id: "lampSwitchCord", step, attrs: { y2: [135, 122] }, delay: 110, ms: 260, ease: "in-out" },
3723
+ { id: "lampSwitchBead", step, attrs: { cy: [125, 138] }, ms: 110, ease: "out" },
3724
+ { id: "lampSwitchBead", step, attrs: { cy: [138, 125] }, delay: 110, ms: 260, ease: "in-out" },
3725
+ { id: "lampSwitchRig", step, attrs: { transform: [sway(0), sway(2.5)] }, delay: 110, ms: 260, ease: "out" },
3726
+ { id: "lampSwitchRig", step, attrs: { transform: [sway(2.5), sway(-1.4)] }, delay: 370, ms: 380, ease: "in-out" },
3727
+ { id: "lampSwitchRig", step, attrs: { transform: [sway(-1.4), sway(0)] }, delay: 750, ms: 420, ease: "in-out" }
3728
+ ];
3729
+ var fade = (step, from, to, delay, ms) => ["lampSwitchCone", "lampSwitchPool", "lampSwitchRoomLit"].map((id) => ({ id, step, attrs: { opacity: [from, to] }, delay, ms, ease: "out" }));
3730
+ var lampSwitch = {
3731
+ kind: "vector",
3732
+ id: "lamp-switch",
3733
+ name: "Lamp switch",
3734
+ description: "A pendant lamp in a dark room: a click tugs its pull cord, the bulb flickers on and a warm cone of light fades in over the scene; the next click turns it off.",
3735
+ category: "lighting",
3736
+ tags: ["lighting", "lamp", "light", "switch", "toggle", "click", "interactive", "warm", "ui", "illustration"],
3737
+ trigger: "click",
3738
+ hold: 900,
3739
+ presets: [
3740
+ { name: "Cool LED", values: { light: [0.78, 0.9, 1], shade: [0.85, 0.87, 0.9], lit: [0.16, 0.2, 0.26] } },
3741
+ { name: "Brass", values: { shade: [0.75, 0.55, 0.22], light: [1, 0.72, 0.38], background: [0.08, 0.07, 0.06], lit: [0.3, 0.22, 0.14] } }
3742
+ ],
3743
+ motion: {
3744
+ svg: `<svg viewBox="0 0 240 200"><!-- {{background: color default=#14121C label='Room'}} {{lit: color default=#3B2F2A label='Lit room'}} {{shade: color default=#2F7A72 label='Shade'}} {{bulbOff: color default=#57525E label='Bulb (off)'}} {{light: color default=#FFD27A label='Light'}} {{intensity: float min=0.2 max=1 default=0.8 label='Light strength'}} --><defs><linearGradient id="lampSwitchConeFade" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="{{light}}" stop-opacity="0.75"/><stop offset="1" stop-color="{{light}}" stop-opacity="0.05"/></linearGradient><radialGradient id="lampSwitchPoolFade"><stop offset="0" stop-color="{{light}}" stop-opacity="0.9"/><stop offset="1" stop-color="{{light}}" stop-opacity="0"/></radialGradient><filter id="lampSwitchBlur" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="6"/></filter><filter id="lampSwitchSoft" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="3"/></filter></defs><rect width="240" height="200" fill="{{background}}"/><rect id="lampSwitchRoomLit" width="240" height="200" fill="{{lit}}" opacity="0"/><rect x="0" y="186" width="240" height="14" fill="#000000" opacity="0.25"/><ellipse id="lampSwitchPool" cx="120" cy="188" rx="96" ry="16" fill="url(#lampSwitchPoolFade)" opacity="0"/><g fill="#000000" opacity="0.45"><rect x="92" y="160" width="56" height="5" rx="2"/><rect x="98" y="165" width="4" height="22"/><rect x="138" y="165" width="4" height="22"/><rect x="112" y="148" width="12" height="12" rx="2"/></g><g id="lampSwitchRig" transform="${sway(0)}"><path id="lampSwitchCone" d="M84 90 L156 90 L236 196 L4 196 Z" fill="url(#lampSwitchConeFade)" opacity="0" filter="url(#lampSwitchSoft)"/><line x1="120" y1="0" x2="120" y2="54" stroke="#8A8494" stroke-width="2"/><circle id="lampSwitchGlow" cx="120" cy="94" r="24" fill="{{light}}" opacity="0" filter="url(#lampSwitchBlur)"/><circle id="lampSwitchBulb" cx="120" cy="92" r="9" fill="{{bulbOff}}"/><path d="M110 50 L130 50 L133 56 L107 56 Z" fill="{{shade}}"/><path d="M104 56 L136 56 L162 90 L78 90 Z" fill="{{shade}}"/><path d="M104 56 L136 56 L140 61 L100 61 Z" fill="#FFFFFF" opacity="0.12"/><ellipse id="lampSwitchInner" cx="120" cy="90" rx="42" ry="4" fill="{{light}}" opacity="0"/><line id="lampSwitchCord" x1="154" y1="82" x2="154" y2="122" stroke="#B9B2C4" stroke-width="1.5"/><circle id="lampSwitchBead" cx="154" cy="125" r="3.5" fill="#D9D2E4"/></g></svg>`,
3745
+ animate: [
3746
+ // Step 1, on: a tug, the bulb flickers on and the light fades in over the room.
3747
+ ...pull(1),
3748
+ { id: "lampSwitchBulb", step: 1, attrs: { fill: ["{{bulbOff}}", "{{light}}"] }, delay: 140, ms: 80 },
3749
+ { id: "lampSwitchGlow", step: 1, attrs: { opacity: [0, 0.9] }, delay: 140, ms: 60 },
3750
+ { id: "lampSwitchGlow", step: 1, attrs: { opacity: [0.9, 0.25] }, delay: 200, ms: 70 },
3751
+ { id: "lampSwitchGlow", step: 1, attrs: { opacity: [0.25, 1] }, delay: 270, ms: 240, ease: "out" },
3752
+ { id: "lampSwitchInner", step: 1, attrs: { opacity: [0, 0.9] }, delay: 140, ms: 200, ease: "out" },
3753
+ ...fade(1, 0, "{{intensity}}", 270, 520),
3754
+ // Step 2, off: a tug and everything dims back to the start.
3755
+ ...pull(2),
3756
+ { id: "lampSwitchBulb", step: 2, attrs: { fill: ["{{light}}", "{{bulbOff}}"] }, delay: 140, ms: 200 },
3757
+ { id: "lampSwitchGlow", step: 2, attrs: { opacity: [1, 0] }, delay: 140, ms: 260, ease: "out" },
3758
+ { id: "lampSwitchInner", step: 2, attrs: { opacity: [0.9, 0] }, delay: 140, ms: 220, ease: "out" },
3759
+ ...fade(2, "{{intensity}}", 0, 140, 380)
3760
+ ]
3761
+ }
3762
+ };
3763
+ var lightingEffects = [lampSwitch];
3764
+
3656
3765
  // src/catalog/electric.ts
3657
3766
  var r18 = (v) => Math.round(v * 10) / 10;
3658
3767
  var about5 = (cx, cy, s) => `translate(${cx} ${cy}) scale(${Math.round(s * 1e3) / 1e3}) translate(${-cx} ${-cy})`;
@@ -5556,17 +5665,17 @@ var particleMorph = {
5556
5665
  { id: `p${i}`, attrs: { cx: [n9(`${from}x${i}`), n9(`${mid}x${i}`)], cy: [n9(`${from}y${i}`), n9(`${mid}y${i}`)] }, delay: at3 + i * DOT_STAGGER, ms: 450, ease: "out" },
5557
5666
  { id: `p${i}`, attrs: { cx: [n9(`${mid}x${i}`), n9(`${to}x${i}`)], cy: [n9(`${mid}y${i}`), n9(`${to}y${i}`)], fill: colours }, delay: at3 + 450 + i * DOT_STAGGER, ms: 650, ease: "in-out" }
5558
5667
  ];
5559
- const fade = (id, at3, show) => ({ id, attrs: { opacity: show ? [0, n9("ghost")] : [n9("ghost"), 0] }, delay: at3, ms: 450, ease: "in-out" });
5668
+ const fade2 = (id, at3, show) => ({ id, attrs: { opacity: show ? [0, n9("ghost")] : [n9("ghost"), 0] }, delay: at3, ms: 450, ease: "in-out" });
5560
5669
  return {
5561
5670
  svg: `<svg viewBox="0 0 200 200"><!-- {{from: text default='${PARTICLE_FROM}' label='From path'}} {{to: text default='${PARTICLE_TO}' label='To path'}} {{fromColor: color default=#F472B6 label='From colour'}} {{toColor: color default=#FBBF24 label='To colour'}} {{background: color default=#0C0A1D label='Background'}} {{size: float min=0.8 max=6 default=2.6 step=0.1 label='Dot size'}} {{scatter: float min=0 max=80 default=34 step=1 label='Scatter'}} {{ghost: float min=0 max=1 default=0.16 label='Shape fill'}} --><rect width="200" height="200" fill="{{background}}"/><path id="ghostA" d="{{a}}" fill="{{fromColor}}" fill-rule="evenodd" opacity="{{ghost}}"/><path id="ghostB" d="{{b}}" fill="{{toColor}}" fill-rule="evenodd" opacity="0"/>${Array.from(
5562
5671
  { length: DOTS4 },
5563
5672
  (_, i) => `<circle id="p${i}" cx="{{ax${i}}}" cy="{{ay${i}}}" r="{{size}}" fill="{{fromColor}}"/>`
5564
5673
  ).join("")}</svg>`,
5565
5674
  animate: [
5566
- fade("ghostA", 400, false),
5567
- fade("ghostB", 400 + spread + 900, true),
5568
- fade("ghostB", back, false),
5569
- fade("ghostA", back + spread + 900, true),
5675
+ fade2("ghostA", 400, false),
5676
+ fade2("ghostB", 400 + spread + 900, true),
5677
+ fade2("ghostB", back, false),
5678
+ fade2("ghostA", back + spread + 900, true),
5570
5679
  ...Array.from({ length: DOTS4 }, (_, i) => [
5571
5680
  ...leg(i, 400, "a", "s", "b", ["{{fromColor}}", "{{toColor}}"]),
5572
5681
  ...leg(i, back, "b", "t", "a", ["{{toColor}}", "{{fromColor}}"])
@@ -10242,7 +10351,7 @@ var fireworks = {
10242
10351
  name: "Fireworks",
10243
10352
  description: "Shells rising on faint trails and bursting into peonies, chrysanthemums, gold willows and rings that droop and twinkle out; clicks fire more.",
10244
10353
  category: "pyrotechnics",
10245
- tags: ["pyro", "overlay", "fireworks", "celebration", "festive", "night", "click"],
10354
+ tags: ["pyro", "overlay", "fireworks", "celebration", "festive", "night", "click", "interactive"],
10246
10355
  transparent: true,
10247
10356
  source: glsl(
10248
10357
  `// Fireworks: staggered shells rising and bursting into peony, chrysanthemum, willow and ring patterns.
@@ -10461,7 +10570,7 @@ var explosion = {
10461
10570
  name: "Explosion",
10462
10571
  description: "A fireball cooling from white-hot to orange to rolling dark smoke, with a shockwave ring and flying debris; it loops and also goes off at each click.",
10463
10572
  category: "pyrotechnics",
10464
- tags: ["pyro", "overlay", "explosion", "fire", "smoke", "action", "click"],
10573
+ tags: ["pyro", "overlay", "explosion", "fire", "smoke", "action", "click", "interactive"],
10465
10574
  transparent: true,
10466
10575
  loop: 3,
10467
10576
  source: glsl(
@@ -11308,7 +11417,7 @@ var bulletTime = {
11308
11417
  description: "A shockwave sweeping out from the centre or a click, freezing the picture grey and zoomed behind it while the rest moves on.",
11309
11418
  category: "timewarp",
11310
11419
  video: "stills",
11311
- tags: ["time", "image", "freeze", "shockwave", "click", "cinematic"],
11420
+ tags: ["time", "image", "freeze", "shockwave", "click", "cinematic", "interactive"],
11312
11421
  loop: 8,
11313
11422
  source: glsl(
11314
11423
  `// Bullet time: every 4 s (or on a click) a wave freezes the picture behind it, then lets go.
@@ -11659,7 +11768,7 @@ var silhouetteMorph = {
11659
11768
  name: "Silhouette morph",
11660
11769
  description: "One silhouette or logo flowing into another as a gooey blob, its colour blending from the first shape's to the second's.",
11661
11770
  category: "morph",
11662
- video: "stills",
11771
+ video: "none",
11663
11772
  tags: ["morph", "shape", "logo", "goo", "blob", "text"],
11664
11773
  loop: 4,
11665
11774
  source: glsl(
@@ -18816,12 +18925,722 @@ var distortionShaders = [
18816
18925
  textureDisplace
18817
18926
  ];
18818
18927
 
18928
+ // src/catalog/shaders/lighting.ts
18929
+ var RELIEF = `
18930
+ float heightAt(vec2 uv, float lod) {
18931
+ vec3 c = textureSize(uImage, 0).x > 1 ? textureLod(uImage, uv, lod).rgb : testCard(fract(uv));
18932
+ return dot(c, vec3(0.2126, 0.7152, 0.0722));
18933
+ }
18934
+
18935
+ // The height's slope at uv, in height per sample step.
18936
+ vec2 heightSlope(vec2 uv) {
18937
+ ivec2 size = textureSize(uImage, 0);
18938
+ vec2 texel = size.x > 1 ? 1.0 / vec2(size) : 1.0 / iResolution.xy;
18939
+ float texH = size.x > 1 ? float(size.y) : iResolution.y;
18940
+ vec2 d = texel * 1.5;
18941
+ float tl = heightAt(uv + vec2(-d.x, d.y), 0.0);
18942
+ float tc = heightAt(uv + vec2(0.0, d.y), 0.0);
18943
+ float tr = heightAt(uv + d, 0.0);
18944
+ float ml = heightAt(uv - vec2(d.x, 0.0), 0.0);
18945
+ float mr = heightAt(uv + vec2(d.x, 0.0), 0.0);
18946
+ float bl = heightAt(uv - d, 0.0);
18947
+ float bc = heightAt(uv - vec2(0.0, d.y), 0.0);
18948
+ float br = heightAt(uv + vec2(d.x, -d.y), 0.0);
18949
+ vec2 fine = vec2(tr + 2.0 * mr + br - tl - 2.0 * ml - bl, tl + 2.0 * tc + tr - bl - 2.0 * bc - br) * 0.125;
18950
+ // Broader forms: 2% and 8% of the picture's height apart, each read as blurred as its taps are apart.
18951
+ float s1 = 0.02 * texH;
18952
+ vec2 d1 = texel * s1;
18953
+ float l1 = log2(max(s1, 1.0));
18954
+ vec2 mid = vec2(heightAt(uv + vec2(d1.x, 0.0), l1) - heightAt(uv - vec2(d1.x, 0.0), l1),
18955
+ heightAt(uv + vec2(0.0, d1.y), l1) - heightAt(uv - vec2(0.0, d1.y), l1)) * 0.5;
18956
+ float s2 = 0.08 * texH;
18957
+ vec2 d2 = texel * s2;
18958
+ float l2 = log2(max(s2, 1.0));
18959
+ vec2 broad = vec2(heightAt(uv + vec2(d2.x, 0.0), l2) - heightAt(uv - vec2(d2.x, 0.0), l2),
18960
+ heightAt(uv + vec2(0.0, d2.y), l2) - heightAt(uv - vec2(0.0, d2.y), l2)) * 0.5;
18961
+ return fine * 0.5 + mid * 0.6 + broad * 0.6;
18962
+ }
18963
+
18964
+ vec3 surfaceNormal(vec2 uv, float strength) {
18965
+ return normalize(vec3(-heightSlope(uv) * strength, 1.0));
18966
+ }
18967
+
18968
+ vec2 shade(vec3 n, vec3 l, float shininess) {
18969
+ float diff = max(dot(n, l), 0.0);
18970
+ vec3 h = normalize(l + vec3(0.0, 0.0, 1.0));
18971
+ float spec = pow(max(dot(n, h), 0.0), shininess) * step(0.0, diff - 1e-3);
18972
+ return vec2(diff, spec);
18973
+ }
18974
+
18975
+ // Light above 1 rolls off instead of clipping.
18976
+ vec3 rolloff(vec3 c) {
18977
+ return c / (1.0 + max(c - 1.0, 0.0));
18978
+ }`.trim();
18979
+ var relight2 = {
18980
+ kind: "shader",
18981
+ id: "relight",
18982
+ name: "Relight",
18983
+ description: "A picture relit by a directional light you can swing round and raise, its surfaces read from brightness so they catch the light or fall into shade.",
18984
+ category: "lighting",
18985
+ video: "filter",
18986
+ tags: ["lighting", "relight", "image", "normal-map", "directional", "shading", "filter"],
18987
+ source: glsl(
18988
+ `// Relight: normals from a Sobel height map of the picture's brightness, shaded by one directional light.
18989
+ uniform sampler2D uImage; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Image"
18990
+ uniform float uAzimuth; // @control min=0 max=360 default=150 label="Light from (\xB0)"
18991
+ uniform float uElevation; // @control min=5 max=90 default=35 label="Light height (\xB0)"
18992
+ uniform vec3 uLight; // @control color default=#FFE6C4 label="Light colour"
18993
+ uniform float uIntensity; // @control min=0 max=3 default=1.4 label="Intensity"
18994
+ uniform vec3 uAmbientColor; // @control color default=#8EA2C8 label="Ambient colour"
18995
+ uniform float uAmbient; // @control min=0 max=1 default=0.4 label="Ambient"
18996
+ uniform float uStrength; // @control min=0 max=10 default=4 label="Relief"
18997
+ uniform float uSpecular; // @control min=0 max=1 default=0.25 label="Specular"
18998
+ uniform float uFalloff; // @control min=0 max=1 default=0.3 label="Falloff across the frame"
18999
+ ${BACKDROP_CONTROLS}`,
19000
+ PICTURE,
19001
+ RELIEF,
19002
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
19003
+ vec2 uv = fragCoord / iResolution.xy;
19004
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
19005
+ float az = radians(uAzimuth);
19006
+ float el = radians(uElevation);
19007
+ vec3 l = vec3(cos(el) * cos(az), cos(el) * sin(az), sin(el));
19008
+ vec3 n = surfaceNormal(uv, uStrength);
19009
+ vec2 s = shade(n, l, 36.0);
19010
+ // The side of the frame nearer the light is a little brighter, more so the lower it is.
19011
+ float wash = max(1.0 + uFalloff * dot(p, l.xy) * 1.6 * (1.0 - 0.6 * l.z), 0.0);
19012
+ vec4 pic = pictureRGBA(uImage, uv);
19013
+ vec3 lit = uAmbientColor * uAmbient + uLight * uIntensity * s.x * wash;
19014
+ vec3 col = pic.rgb * lit + uLight * s.y * uSpecular * wash;
19015
+ fragColor = vec4(rolloff(col), pic.a);
19016
+ }`
19017
+ ),
19018
+ presets: [
19019
+ { name: "Morning side light", values: { uAzimuth: 175, uElevation: 16, uLight: [1, 0.78, 0.52], uIntensity: 1.8, uAmbientColor: [0.5, 0.62, 0.9], uAmbient: 0.35, uFalloff: 0.6 } },
19020
+ { name: "Top light", values: { uAzimuth: 90, uElevation: 72, uLight: [1, 0.98, 0.94], uIntensity: 1.2, uAmbient: 0.25, uFalloff: 0.5, uSpecular: 0.35 } },
19021
+ { name: "Dramatic", values: { uAzimuth: 20, uElevation: 10, uLight: [1, 0.9, 0.8], uIntensity: 2.6, uAmbient: 0.05, uStrength: 7, uSpecular: 0.5, uFalloff: 0.8 } }
19022
+ ]
19023
+ };
19024
+ var pointLight = {
19025
+ kind: "shader",
19026
+ id: "point-light",
19027
+ name: "Point light",
19028
+ description: "A torch moving over a darkened picture, following the pointer while it hovers and drifting on its own otherwise, lighting the surfaces that face it.",
19029
+ category: "lighting",
19030
+ video: "interactive",
19031
+ tags: ["lighting", "torch", "flashlight", "image", "interactive", "hover", "normal-map", "dark"],
19032
+ source: glsl(
19033
+ `// Point light: a light just above the picture, at the pointer while hovered and drifting otherwise, shading normals from brightness.
19034
+ uniform sampler2D uImage; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.landscape} label="Image"
19035
+ uniform vec3 uColor; // @control color default=#FFDDA8 label="Light colour"
19036
+ uniform float uIntensity; // @control min=0 max=4 default=1.8 label="Intensity"
19037
+ uniform float uRadius; // @control min=0.05 max=1.5 default=0.45 label="Falloff radius"
19038
+ uniform float uHeight; // @control min=0.02 max=0.5 default=0.1 label="Light height"
19039
+ uniform float uAmbient; // @control min=0 max=0.5 default=0.04 label="Ambient"
19040
+ uniform float uStrength; // @control min=0 max=10 default=4 label="Relief"
19041
+ uniform float uSpecular; // @control min=0 max=1 default=0.4 label="Specular"
19042
+ uniform float uSpeed; // @control min=0 max=2 default=0.6 label="Drift speed"
19043
+ ${BACKDROP_CONTROLS}`,
19044
+ PICTURE,
19045
+ RELIEF,
19046
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
19047
+ vec2 uv = fragCoord / iResolution.xy;
19048
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
19049
+ // Where the light is: the pointer while it hovers, else a slow wander.
19050
+ float t = iTime * uSpeed;
19051
+ vec2 drift = iResolution.xy * (0.5 + vec2(0.32 * sin(t * 0.63), 0.26 * sin(t * 0.91 + 1.3)));
19052
+ float hover = clamp(iPointer.z, 0.0, 1.0);
19053
+ vec2 at = (mix(drift, iPointer.xy, hover) - 0.5 * iResolution.xy) / iResolution.y;
19054
+ vec3 toLight = vec3(at - p, uHeight);
19055
+ float dist = length(toLight);
19056
+ vec3 l = toLight / dist;
19057
+ // Inverse-square near the light, faded to nothing past the radius.
19058
+ float k = dist / (0.35 * uRadius);
19059
+ float att = 1.0 / (1.0 + k * k) * (1.0 - smoothstep(0.4 * uRadius, 1.5 * uRadius, dist));
19060
+ vec3 n = surfaceNormal(uv, uStrength);
19061
+ vec2 s = shade(n, l, 28.0);
19062
+ vec4 pic = pictureRGBA(uImage, uv);
19063
+ vec3 light = uColor * uIntensity * att;
19064
+ vec3 col = pic.rgb * (vec3(uAmbient) * vec3(0.7, 0.8, 1.0) + light * s.x) + light * s.y * uSpecular;
19065
+ fragColor = vec4(rolloff(col), pic.a);
19066
+ }`
19067
+ ),
19068
+ presets: [
19069
+ { name: "Torch", values: { uColor: [1, 0.93, 0.8], uRadius: 0.35, uAmbient: 0.01, uIntensity: 2.4 } },
19070
+ { name: "Lantern", values: { uColor: [1, 0.66, 0.3], uRadius: 0.8, uHeight: 0.2, uAmbient: 0.08, uSpecular: 0.2 } },
19071
+ { name: "Inspection lamp", values: { uImage: SAMPLE_IMAGES.portrait, uColor: [0.85, 0.93, 1], uHeight: 0.04, uStrength: 8, uSpecular: 0.8, uAmbient: 0.1 } }
19072
+ ]
19073
+ };
19074
+ var studioLighting = {
19075
+ kind: "shader",
19076
+ id: "studio-lighting",
19077
+ name: "Studio lighting",
19078
+ description: "Three-point lighting on a subject: a warm key from one side, a cool fill from the other and a bright rim tracing its silhouette, against a darkened studio backdrop.",
19079
+ category: "lighting",
19080
+ // Each frame is lit on its own, so it works live on a subject shot against a plain backdrop (or with alpha).
19081
+ video: "filter",
19082
+ tags: ["lighting", "studio", "portrait", "three-point", "key-light", "rim", "image", "filter"],
19083
+ source: glsl(
19084
+ `// Studio lighting: key and fill shade normals from brightness; a rim lights the subject's edge (alpha, or the corner colour keyed out) on the side away from the key.
19085
+ uniform sampler2D uImage; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Subject"
19086
+ uniform float uKeyAngle; // @control min=0 max=360 default=155 label="Key from (\xB0)"
19087
+ uniform vec3 uKeyColor; // @control color default=#FFD2A0 label="Key colour"
19088
+ uniform float uKey; // @control min=0 max=3 default=1.4 label="Key strength"
19089
+ uniform vec3 uFillColor; // @control color default=#86A8FF label="Fill colour"
19090
+ uniform float uFill; // @control min=0 max=2 default=0.5 label="Fill strength"
19091
+ uniform vec3 uRimColor; // @control color default=#EAF2FF label="Rim colour"
19092
+ uniform float uRim; // @control min=0 max=4 default=1.8 label="Rim strength"
19093
+ uniform float uRimWidth; // @control min=0.003 max=0.06 default=0.016 label="Rim width"
19094
+ uniform float uStrength; // @control min=0 max=10 default=4 label="Relief"
19095
+ uniform float uKeyOut; // @control min=0 max=0.5 default=0.12 label="Key out the corner colour (0 off)"
19096
+ uniform vec3 uStudio; // @control color default=#1B1D24 label="Studio backdrop"
19097
+ uniform float uStudioMix; // @control min=0 max=1 default=0.85 label="Replace the background"
19098
+ ${BACKDROP_CONTROLS}`,
19099
+ PICTURE,
19100
+ RELIEF,
19101
+ `vec3 gKey;
19102
+ bool gKeyOn;
19103
+
19104
+ // How much of the subject there is at uv (alpha, less the backdrop colour when keyed), read at mip level lod.
19105
+ float subjectAt(vec2 uv, float lod) {
19106
+ if (textureSize(uImage, 0).x <= 1) return 1.0;
19107
+ if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) return 0.0;
19108
+ vec4 c = textureLod(uImage, uv, lod);
19109
+ float a = c.a;
19110
+ if (gKeyOn) a *= smoothstep(uKeyOut, uKeyOut + 0.12, distance(c.rgb / max(c.a, 1e-3), gKey));
19111
+ return a;
19112
+ }
19113
+
19114
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
19115
+ vec2 uv = fragCoord / iResolution.xy;
19116
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
19117
+ vec4 corner = textureLod(uImage, vec2(0.01, 0.99), 0.0);
19118
+ gKey = corner.rgb / max(corner.a, 1e-3);
19119
+ // A picture already cut out (a clear corner) keeps its alpha as the subject.
19120
+ gKeyOn = uKeyOut > 0.0 && corner.a > 0.5;
19121
+ ivec2 size = textureSize(uImage, 0);
19122
+ float texH = size.x > 1 ? float(size.y) : iResolution.y;
19123
+ vec2 aspect = vec2(size.x > 1 ? float(size.y) / float(size.x) : iResolution.y / iResolution.x, 1.0);
19124
+
19125
+ float ka = radians(uKeyAngle);
19126
+ vec2 keyDir = vec2(cos(ka), sin(ka));
19127
+ vec3 key = normalize(vec3(keyDir * 0.8, 0.6));
19128
+ vec3 fill = normalize(vec3(-keyDir * 0.9, 0.45));
19129
+ // The rim comes from behind, on the side away from the key and a little above.
19130
+ vec2 rimDir = normalize(vec2(-keyDir.x, 0.5 + 0.5 * abs(keyDir.y)));
19131
+
19132
+ vec3 n = surfaceNormal(uv, uStrength);
19133
+ vec2 ks = shade(n, key, 30.0);
19134
+ float fillWrap = clamp((dot(n, fill) + 0.4) / 1.4, 0.0, 1.0);
19135
+
19136
+ float subject = subjectAt(uv, 0.0);
19137
+ // The rim: the subject where what lies toward the rim light is backdrop, plus a little all round its edge.
19138
+ float lod = log2(max(uRimWidth * texH * 0.6, 1.0));
19139
+ vec2 w = rimDir * uRimWidth * aspect;
19140
+ float toward = 0.5 * (subjectAt(uv + w, lod) + subjectAt(uv + w * 0.5, lod - 1.0));
19141
+ float around = 0.25 * (subjectAt(uv + vec2(uRimWidth, 0.0) * aspect, lod) + subjectAt(uv - vec2(uRimWidth, 0.0) * aspect, lod)
19142
+ + subjectAt(uv + vec2(0.0, uRimWidth), lod) + subjectAt(uv - vec2(0.0, uRimWidth), lod));
19143
+ float rim = subject * (clamp(1.0 - toward, 0.0, 1.0) + 0.3 * clamp(1.0 - around, 0.0, 1.0));
19144
+ rim = smoothstep(0.05, 0.6, rim);
19145
+
19146
+ vec4 pic = pictureRGBA(uImage, uv);
19147
+ vec3 lit = vec3(0.06) + uKeyColor * uKey * ks.x + uFillColor * uFill * fillWrap;
19148
+ vec3 col = pic.rgb * lit + uKeyColor * ks.y * uKey * 0.25 + uRimColor * rim * uRim * (0.6 + 0.4 * pic.rgb);
19149
+
19150
+ // The studio backdrop behind: its colour, a soft pool of key light on the key's side, darker toward the corners.
19151
+ float pool = exp(-2.5 * dot(p - keyDir * 0.35, p - keyDir * 0.35));
19152
+ vec3 studio = uStudio * (0.55 + 0.9 * pool) + uKeyColor * uStudio * pool * 0.6;
19153
+ float back = (1.0 - subject) * uStudioMix;
19154
+ col = mix(col, studio, back);
19155
+ // Where the picture is clear the backdrop's opacity holds, so a cut-out stays clear on a clear backdrop.
19156
+ fragColor = vec4(rolloff(col), pic.a);
19157
+ }`
19158
+ ),
19159
+ presets: [
19160
+ { name: "Classic portrait", values: { uKeyColor: [1, 0.9, 0.78], uFillColor: [0.75, 0.82, 1], uFill: 0.6, uRim: 1.2 } },
19161
+ { name: "Noir", values: { uKeyColor: [1, 1, 1], uKey: 1.8, uFill: 0.08, uRimColor: [1, 1, 1], uRim: 2.6, uStudio: [0.04, 0.04, 0.05], uStudioMix: 1 } },
19162
+ { name: "Music video", values: { uKeyColor: [1, 0.35, 0.65], uFillColor: [0.2, 0.55, 1], uFill: 1.2, uRimColor: [0.5, 1, 0.9], uRim: 2.6, uStudio: [0.08, 0.04, 0.14] } }
19163
+ ]
19164
+ };
19165
+ var timeOfDay = {
19166
+ kind: "shader",
19167
+ id: "time-of-day",
19168
+ name: "Time of day",
19169
+ description: "A picture regraded and relit through the day, from cool dawn through white noon and golden hour to blue hour and a night where its brightest spots glow like lit windows.",
19170
+ category: "lighting",
19171
+ video: "filter",
19172
+ loop: 24,
19173
+ tags: ["lighting", "time-of-day", "golden-hour", "blue-hour", "night", "sun", "grade", "image", "filter"],
19174
+ source: glsl(
19175
+ `// Time of day: a sun on a 24-hour arc shades normals from brightness, a sky colour tints and darkens the picture, and its bright spots light up at night.
19176
+ uniform sampler2D uImage; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.landscape} label="Image"
19177
+ uniform float uHour; // @control min=0 max=24 default=6 label="Hour"
19178
+ uniform float uSpeed; // @control min=0 max=4 default=1 label="Hours per second (0 holds)"
19179
+ uniform float uStrength; // @control min=0 max=10 default=3 label="Relief"
19180
+ uniform float uSkyTint; // @control min=0 max=1 default=0.6 label="Sky tint"
19181
+ uniform float uNightLights; // @control min=0 max=2 default=1 label="Night lights"
19182
+ uniform vec3 uLampColor; // @control color default=#FFC46B label="Night light colour"
19183
+ uniform float uThreshold; // @control min=0 max=1 default=0.62 label="Lights from brightness above"
19184
+ ${BACKDROP_CONTROLS}`,
19185
+ PICTURE,
19186
+ RELIEF,
19187
+ `float luma(vec3 c) {
19188
+ return dot(c, vec3(0.2126, 0.7152, 0.0722));
19189
+ }
19190
+
19191
+ // Brightness above the lights' threshold at mip level lod, so the brightest spots light up at night.
19192
+ float glowAt(vec2 uv, float lod) {
19193
+ vec3 c = textureSize(uImage, 0).x > 1 ? textureLod(uImage, uv, lod).rgb : testCard(fract(uv));
19194
+ return smoothstep(uThreshold, min(uThreshold + 0.25, 1.0), luma(c));
19195
+ }
19196
+
19197
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
19198
+ vec2 uv = fragCoord / iResolution.xy;
19199
+ float hour = mod(uHour + iTime * uSpeed, 24.0);
19200
+ // The sun: up at 6, highest at 12, down at 18; it crosses from the left (morning) to the right (evening).
19201
+ float arc = (hour - 6.0) / 12.0 * 3.14159;
19202
+ float e = sin(arc);
19203
+ float across = -cos(arc);
19204
+ float morning = 1.0 - smoothstep(10.0, 14.0, hour) * (1.0 - smoothstep(22.0, 24.0, hour));
19205
+ vec3 l = normalize(vec3(across, 0.25 + 0.6 * max(e, 0.0), 0.2 + 0.9 * max(e, 0.0)));
19206
+
19207
+ float day = smoothstep(-0.04, 0.22, e);
19208
+ float night = 1.0 - smoothstep(-0.38, -0.12, e);
19209
+ // Sun colour: warm and low (pinker at dawn, deeper gold at dusk), near white overhead.
19210
+ vec3 low = mix(vec3(1.0, 0.5, 0.22), vec3(1.0, 0.62, 0.58), morning);
19211
+ vec3 sun = mix(low, vec3(1.0, 0.97, 0.92), smoothstep(0.12, 0.65, e));
19212
+ // Sky light: deep blue night, blue hour, a warm low-sun sky, then daylight blue.
19213
+ vec3 sky = mix(vec3(0.05, 0.07, 0.17), vec3(0.24, 0.32, 0.72), smoothstep(-0.4, -0.12, e));
19214
+ sky = mix(sky, mix(vec3(0.85, 0.55, 0.5), vec3(0.8, 0.62, 0.7), morning), smoothstep(-0.12, 0.04, e));
19215
+ sky = mix(sky, vec3(0.62, 0.74, 1.0), smoothstep(0.12, 0.55, e));
19216
+
19217
+ vec4 pic = pictureRGBA(uImage, uv);
19218
+ float lum = luma(pic.rgb);
19219
+ // Night sees in blue-grey: colour drains as it darkens.
19220
+ vec3 albedo = mix(pic.rgb, vec3(lum), night * 0.55);
19221
+ vec3 n = surfaceNormal(uv, uStrength);
19222
+ vec2 s = shade(n, l, 30.0);
19223
+ vec3 col = albedo * (sky * mix(0.55, 0.45, day) + sun * day * (0.25 + 1.0 * s.x)) + sun * day * s.y * 0.15;
19224
+ // The grade: the whole picture leans toward the sky's colour.
19225
+ vec3 tint = sky / max(luma(sky), 0.05);
19226
+ col *= mix(vec3(1.0), clamp(tint, 0.0, 2.0), uSkyTint * 0.4);
19227
+
19228
+ // Lights come on at dusk: the brightest spots glow, with a soft halo.
19229
+ float on = (1.0 - smoothstep(-0.22, 0.02, e)) * uNightLights;
19230
+ float lamp = glowAt(uv, 0.0);
19231
+ float halo = glowAt(uv, 3.0) * 0.6 + glowAt(uv, 5.0) * 0.5;
19232
+ col += uLampColor * on * (lamp * (0.6 + 0.6 * lum) + halo * 0.5);
19233
+ fragColor = vec4(rolloff(col), pic.a);
19234
+ }`
19235
+ ),
19236
+ presets: [
19237
+ { name: "Golden hour", values: { uHour: 17.4, uSpeed: 0 } },
19238
+ { name: "Blue hour", values: { uHour: 19.3, uSpeed: 0 } },
19239
+ { name: "Night city", values: { uImage: SAMPLE_IMAGES.city, uHour: 22, uSpeed: 0, uThreshold: 0.5, uNightLights: 1.4 } }
19240
+ ]
19241
+ };
19242
+ var goboProjection = {
19243
+ kind: "shader",
19244
+ id: "gobo-projection",
19245
+ name: "Gobo projection",
19246
+ description: "A theatre light throwing a patterned gobo onto a picture (window panes, foliage, stars, venetian slats or your own text), softly focused and slowly drifting.",
19247
+ category: "lighting",
19248
+ video: "filter",
19249
+ tags: ["lighting", "gobo", "projection", "stage", "theatre", "pattern", "text", "image", "filter"],
19250
+ source: glsl(
19251
+ `// Gobo projection: a soft beam broken up by a pattern (or the gobo image's alpha), drifting, multiplied into the picture.
19252
+ uniform sampler2D uImage; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Image"
19253
+ uniform int uPattern; // @control min=0 max=4 default=1 label="Gobo (0 window, 1 foliage, 2 stars, 3 slats, 4 gobo image)"
19254
+ uniform sampler2D uGobo; // @control filter=mipmap wrap=clamp default=${textImage("LIGHTS")} label="Gobo image (alpha lets light through)"
19255
+ uniform vec3 uColor; // @control color default=#FFE4BC label="Light colour"
19256
+ uniform float uIntensity; // @control min=0 max=3 default=1.1 label="Intensity"
19257
+ uniform float uAmbient; // @control min=0 max=1 default=0.3 label="Ambient"
19258
+ uniform float uScale; // @control min=0.3 max=3 default=1 label="Scale"
19259
+ uniform float uAngle; // @control min=-180 max=180 default=-12 label="Angle (\xB0)"
19260
+ uniform float uSoftness; // @control min=0 max=1 default=0.3 label="Soft focus"
19261
+ uniform float uDrift; // @control min=0 max=2 default=0.6 label="Drift"
19262
+ ${BACKDROP_CONTROLS}`,
19263
+ PICTURE,
19264
+ HASH,
19265
+ NOISE,
19266
+ FBM,
19267
+ ROTATE,
19268
+ `float windowPanes(vec2 q, float s) {
19269
+ vec2 frame = abs(q) - vec2(0.42, 0.34);
19270
+ float inside = smoothstep(s, -s, max(frame.x, frame.y));
19271
+ // Three panes across, two down, between glazing bars.
19272
+ vec2 g = abs(fract((q + vec2(0.42, 0.34)) / vec2(0.28, 0.34)) - 0.5);
19273
+ float bar = 0.07 + s * 0.5;
19274
+ float pane = smoothstep(0.5 - bar - s, 0.5 - bar + s, 0.5 - max(g.x * 1.0, g.y));
19275
+ return inside * pane;
19276
+ }
19277
+
19278
+ float foliage(vec2 q, float s, float t) {
19279
+ vec2 sway = vec2(sin(t * 0.7 + q.y * 2.0), cos(t * 0.5 + q.x * 1.7)) * 0.03 * uDrift;
19280
+ float n = fbm((q + sway) * 6.0 + 2.0, 5);
19281
+ float w = 0.02 + s * 0.1;
19282
+ return smoothstep(0.5 - w, 0.5 + w, n);
19283
+ }
19284
+
19285
+ float stars(vec2 q, float s) {
19286
+ vec2 cell = floor(q / 0.2);
19287
+ vec2 f = fract(q / 0.2) - 0.5;
19288
+ vec2 h = hash22(cell + 3.1);
19289
+ f -= (h - 0.5) * 0.35;
19290
+ f = rot(h.x * 6.28) * f;
19291
+ float r = length(f);
19292
+ float a = atan(f.y, f.x);
19293
+ float edge = (0.16 + 0.12 * h.y) * (0.55 + 0.45 * pow(abs(cos(2.5 * a)), 3.0));
19294
+ float keep = step(0.25, hash12(cell + 7.7));
19295
+ return keep * smoothstep(edge + 0.02 + s * 0.12, edge - 0.02 - s * 0.05, r);
19296
+ }
19297
+
19298
+ float slats(vec2 q, float s) {
19299
+ float f = fract(q.y * 8.0);
19300
+ float w = 0.04 + s * 0.25;
19301
+ float lit = smoothstep(0.0, w, f) * smoothstep(0.62 + w, 0.62, f);
19302
+ return lit * smoothstep(0.5 + s * 0.1, 0.44, abs(q.x));
19303
+ }
19304
+
19305
+ float goboImage(vec2 q, float s) {
19306
+ ivec2 size = textureSize(uGobo, 0);
19307
+ if (size.x <= 1) return 0.0;
19308
+ float aspect = float(size.x) / float(size.y);
19309
+ float w = min(1.3, 1.3 * aspect);
19310
+ vec2 g = q / vec2(w, w / aspect) + 0.5;
19311
+ if (g.x < 0.0 || g.x > 1.0 || g.y < 0.0 || g.y > 1.0) return 0.0;
19312
+ return textureLod(uGobo, g, log2(1.0 + s * 0.03 * float(size.y))).a;
19313
+ }
19314
+
19315
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
19316
+ vec2 uv = fragCoord / iResolution.xy;
19317
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
19318
+ float t = iTime;
19319
+ vec2 shift = vec2(sin(t * 0.13), cos(t * 0.11)) * 0.05 * uDrift;
19320
+ vec2 q = rot(radians(uAngle) + sin(t * 0.09) * 0.06 * uDrift) * (p - shift) / uScale;
19321
+ float s = uSoftness * 0.06;
19322
+ float lit = uPattern == 0 ? windowPanes(q, s)
19323
+ : uPattern == 1 ? foliage(q, uSoftness, t)
19324
+ : uPattern == 2 ? stars(q, s)
19325
+ : uPattern == 3 ? slats(q, uSoftness)
19326
+ : goboImage(q, uSoftness);
19327
+ // The beam: a soft-edged spot round the gobo.
19328
+ float beam = 1.0 - smoothstep(0.45, 0.85 + 0.2 * uSoftness, length(q * vec2(0.75, 1.0)));
19329
+ vec4 pic = pictureRGBA(uImage, uv);
19330
+ vec3 light = mix(vec3(uAmbient) * vec3(0.92, 0.95, 1.0), uColor * uIntensity, lit * beam);
19331
+ vec3 col = pic.rgb * light;
19332
+ fragColor = vec4(col / (1.0 + max(col - 1.0, 0.0)), pic.a);
19333
+ }`
19334
+ ),
19335
+ presets: [
19336
+ { name: "Window", values: { uPattern: 0, uAngle: -8, uSoftness: 0.2, uColor: [1, 0.86, 0.66] } },
19337
+ { name: "Starry stage", values: { uPattern: 2, uColor: [0.7, 0.8, 1], uAmbient: 0.15, uIntensity: 1.8, uScale: 1.2 } },
19338
+ { name: "Marquee text", values: { uPattern: 4, uGobo: textImage("OPEN"), uAmbient: 0.2, uIntensity: 1.8, uSoftness: 0.15, uAngle: 0 } }
19339
+ ]
19340
+ };
19341
+ var neonSpill = {
19342
+ kind: "shader",
19343
+ id: "neon-spill",
19344
+ name: "Neon spill",
19345
+ description: "Coloured light spilling onto a picture from a flickering two-colour neon tube, lighting the surfaces that face it and falling off with distance.",
19346
+ category: "lighting",
19347
+ video: "filter",
19348
+ tags: ["lighting", "neon", "cyberpunk", "night", "colour", "flicker", "image", "normal-map", "filter"],
19349
+ source: glsl(
19350
+ `// Neon spill: five point lights along a tube, one colour each half, inverse-square falloff, shading normals from brightness; the tube drawn on top.
19351
+ uniform sampler2D uImage; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Image"
19352
+ uniform vec2 uSource; // @control min=-1 max=1 default=-0.7,0.1 label="Neon position"
19353
+ uniform float uTubeAngle; // @control min=-90 max=90 default=80 label="Tube angle (\xB0)"
19354
+ uniform float uLength; // @control min=0 max=1 default=0.5 label="Tube length"
19355
+ uniform vec3 uColorA; // @control color default=#FF2E9A label="Colour A"
19356
+ uniform vec3 uColorB; // @control color default=#3EE4FF label="Colour B"
19357
+ uniform float uIntensity; // @control min=0 max=4 default=2.2 label="Power"
19358
+ uniform float uFlicker; // @control min=0 max=1 default=0.35 label="Flicker"
19359
+ uniform float uAmbient; // @control min=0 max=1 default=0.18 label="Ambient"
19360
+ uniform float uStrength; // @control min=0 max=10 default=4 label="Relief"
19361
+ uniform bool uShowTube; // @control default=true label="Show the tube"
19362
+ ${BACKDROP_CONTROLS}`,
19363
+ PICTURE,
19364
+ HASH,
19365
+ RELIEF,
19366
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
19367
+ vec2 uv = fragCoord / iResolution.xy;
19368
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
19369
+ float aspect = iResolution.x / iResolution.y;
19370
+ vec2 c = uSource * vec2(aspect, 1.0) * 0.5;
19371
+ float ta = radians(uTubeAngle);
19372
+ vec2 dir = vec2(cos(ta), sin(ta)) * uLength * 0.5;
19373
+ // Flicker: a fast buzz, and now and then a stutter.
19374
+ float t = iTime;
19375
+ float buzz = 0.08 * hash12(vec2(floor(t * 40.0), 1.0));
19376
+ float stutter = step(0.88, hash12(vec2(floor(t * 5.0), 2.0))) * step(0.5, hash12(vec2(floor(t * 30.0), 3.0)));
19377
+ float on = 1.0 - uFlicker * (buzz + 0.75 * stutter);
19378
+
19379
+ vec3 n = surfaceNormal(uv, uStrength);
19380
+ vec3 light = vec3(0.0);
19381
+ // The tube as five point lights a little off the wall, each falling off with the square of distance.
19382
+ for (int i = 0; i < 5; i++) {
19383
+ float k = float(i) * 0.25;
19384
+ vec2 at = c + dir * (k * 2.0 - 1.0);
19385
+ vec3 toLight = vec3(at - p, 0.2);
19386
+ float d2 = dot(toLight, toLight);
19387
+ float diff = max(dot(n, toLight * inversesqrt(d2)), 0.0);
19388
+ light += mix(uColorA, uColorB, smoothstep(0.3, 0.7, k)) * diff * 0.022 / (d2 + 0.04);
19389
+ }
19390
+ light *= uIntensity * on;
19391
+ vec4 pic = pictureRGBA(uImage, uv);
19392
+ vec3 col = pic.rgb * (vec3(uAmbient) * vec3(0.55, 0.6, 0.8) + light);
19393
+
19394
+ if (uShowTube) {
19395
+ vec2 a = c - dir;
19396
+ vec2 ab = 2.0 * dir;
19397
+ float h = clamp(dot(p - a, ab) / max(dot(ab, ab), 1e-6), 0.0, 1.0);
19398
+ float d = length(p - a - ab * h);
19399
+ vec3 tint = mix(uColorA, uColorB, smoothstep(0.45, 0.55, h));
19400
+ float core = smoothstep(0.009, 0.004, d);
19401
+ float halo = exp(-d * 40.0) * 0.8 + exp(-d * 12.0) * 0.3;
19402
+ col = mix(col, mix(tint, vec3(1.0), 0.7), core * on) + tint * halo * on * uIntensity * 0.5;
19403
+ }
19404
+ fragColor = vec4(rolloff(col), pic.a);
19405
+ }`
19406
+ ),
19407
+ presets: [
19408
+ { name: "Cyberpunk", values: { uColorA: [1, 0.15, 0.6], uColorB: [0.2, 0.5, 1], uAmbient: 0.1, uIntensity: 2 } },
19409
+ { name: "Diner sign", values: { uSource: [0, 0.85], uTubeAngle: 0, uLength: 0.9, uColorA: [1, 0.3, 0.2], uColorB: [1, 0.75, 0.3], uFlicker: 0.6 } },
19410
+ { name: "Steady", values: { uFlicker: 0, uShowTube: false, uColorA: [0.6, 0.2, 1], uColorB: [0.1, 1, 0.7] } }
19411
+ ]
19412
+ };
19413
+ var candlelight = {
19414
+ kind: "shader",
19415
+ id: "candlelight",
19416
+ name: "Candlelight",
19417
+ description: "A picture lit only by a warm candle low in the frame, its flicker shifting the light and the deep shadows, with a soft warm bloom.",
19418
+ category: "lighting",
19419
+ video: "filter",
19420
+ tags: ["lighting", "candle", "fire", "warm", "flicker", "cosy", "dark", "image", "normal-map", "filter"],
19421
+ source: glsl(
19422
+ `// Candlelight: a flickering point light low in the frame shading normals from brightness, a small flame and a warm bloom.
19423
+ uniform sampler2D uImage; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Image"
19424
+ uniform vec2 uSource; // @control min=-1 max=1 default=-0.45,-0.8 label="Candle position"
19425
+ uniform vec3 uColor; // @control color default=#FF9A3D label="Flame colour"
19426
+ uniform float uIntensity; // @control min=0 max=4 default=2.2 label="Intensity"
19427
+ uniform float uFlicker; // @control min=0 max=1 default=0.6 label="Flicker"
19428
+ uniform float uReach; // @control min=0.2 max=2 default=1.2 label="Reach"
19429
+ uniform float uAmbient; // @control min=0 max=0.3 default=0.02 label="Ambient"
19430
+ uniform float uBloom; // @control min=0 max=1 default=0.5 label="Warm bloom"
19431
+ uniform float uStrength; // @control min=0 max=10 default=4 label="Relief"
19432
+ uniform bool uShowFlame; // @control default=true label="Show the flame"
19433
+ ${BACKDROP_CONTROLS}`,
19434
+ PICTURE,
19435
+ HASH,
19436
+ NOISE,
19437
+ RELIEF,
19438
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
19439
+ vec2 uv = fragCoord / iResolution.xy;
19440
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
19441
+ float aspect = iResolution.x / iResolution.y;
19442
+ float t = iTime;
19443
+ // Flicker: the flame dips and gutters, and leans a little as it does.
19444
+ float f = vnoise(vec2(t * 7.0, 0.5)) * 0.6 + vnoise(vec2(t * 17.0, 4.0)) * 0.4;
19445
+ float power = 1.0 - uFlicker * 0.45 * (1.0 - f);
19446
+ vec2 lean = vec2(vnoise(vec2(t * 3.0, 9.0)) - 0.5, vnoise(vec2(t * 5.0, 13.0)) - 0.5) * vec2(0.03, 0.015) * uFlicker;
19447
+ vec2 base = uSource * vec2(aspect, 1.0) * 0.5;
19448
+ vec2 flamePos = base + vec2(0.0, 0.035) + lean;
19449
+
19450
+ vec3 toLight = vec3(flamePos - p, 0.12);
19451
+ float dist = length(toLight);
19452
+ float k = dist / (0.3 * uReach);
19453
+ float att = 1.0 / (1.0 + k * k) * (1.0 - smoothstep(0.5 * uReach, 1.6 * uReach, dist));
19454
+ vec3 n = surfaceNormal(uv, uStrength);
19455
+ vec2 s = shade(n, toLight / dist, 20.0);
19456
+ vec4 pic = pictureRGBA(uImage, uv);
19457
+ vec3 warm = uColor * uIntensity * att * power;
19458
+ // Deep shadows: little ambient, and the lit colour pushed warm.
19459
+ vec3 col = pic.rgb * (vec3(uAmbient) * vec3(0.6, 0.6, 0.9) + warm * s.x) + warm * s.y * 0.2;
19460
+ // Bloom: the bright, lit parts bleed a little warm light, and the air glows round the flame.
19461
+ vec3 soft = textureSize(uImage, 0).x > 1 ? textureLod(uImage, uv, 4.0).rgb : pic.rgb;
19462
+ col += soft * warm * 0.25 * uBloom;
19463
+ float fd = length(p - flamePos);
19464
+ col += uColor * power * uBloom * (exp(-fd * 9.0) * 0.5 + exp(-fd * 30.0) * 0.6);
19465
+
19466
+ if (uShowFlame) {
19467
+ vec2 q = (p - flamePos) / vec2(0.012, 0.03);
19468
+ q.x += lean.x * 8.0 * max(q.y, 0.0);
19469
+ float shape = length(vec2(q.x * (1.0 + max(q.y, 0.0) * 0.9), q.y));
19470
+ float flame = smoothstep(1.0, 0.55, shape);
19471
+ float core = smoothstep(0.6, 0.1, length(vec2(q.x * 1.4, q.y + 0.45)));
19472
+ col = mix(col, mix(uColor * 1.4, vec3(1.0, 0.95, 0.8), core), flame * power);
19473
+ }
19474
+ fragColor = vec4(rolloff(col), pic.a);
19475
+ }`
19476
+ ),
19477
+ presets: [
19478
+ { name: "Single candle", values: { uReach: 0.6, uBloom: 0.7, uAmbient: 0 } },
19479
+ { name: "Fireside", values: { uSource: [-0.85, -0.9], uColor: [1, 0.55, 0.2], uReach: 1.6, uIntensity: 2.4, uFlicker: 0.9, uShowFlame: false } }
19480
+ ]
19481
+ };
19482
+ var poolCaustics = {
19483
+ kind: "shader",
19484
+ id: "pool-caustics",
19485
+ name: "Pool caustics",
19486
+ description: "Rippling caustic light reflected from a pool playing across a picture, as on a wall beside the water, in a cool blue-white.",
19487
+ category: "lighting",
19488
+ video: "filter",
19489
+ tags: ["lighting", "caustics", "water", "pool", "summer", "reflection", "image", "filter"],
19490
+ source: glsl(
19491
+ `// Pool caustics: two drifting Voronoi edge networks (F2 - F1), warped and stretched, multiplied into the picture as blue-white light.
19492
+ uniform sampler2D uImage; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Image"
19493
+ uniform vec3 uTint; // @control color default=#D4F1FF label="Light tint"
19494
+ uniform vec3 uShade; // @control color default=#2E6A96 label="Shade tint"
19495
+ uniform float uIntensity; // @control min=0 max=3 default=1.1 label="Intensity"
19496
+ uniform float uAmbient; // @control min=0 max=1 default=0.6 label="Ambient"
19497
+ uniform float uScale; // @control min=0.3 max=3 default=1 label="Scale"
19498
+ uniform float uSpeed; // @control min=0 max=3 default=0.6 label="Speed"
19499
+ uniform float uSharpness; // @control min=0 max=1 default=0.6 label="Sharpness"
19500
+ uniform float uStretch; // @control min=0 max=1 default=0.35 label="Stretch"
19501
+ ${BACKDROP_CONTROLS}`,
19502
+ PICTURE,
19503
+ HASH,
19504
+ NOISE,
19505
+ `// A web of bright lines: how close q is to the edge between its two nearest moving points.
19506
+ float causticWeb(vec2 q, float t, float width) {
19507
+ vec2 cell = floor(q);
19508
+ vec2 f = fract(q);
19509
+ float f1 = 8.0;
19510
+ float f2 = 8.0;
19511
+ for (int j = -1; j <= 1; j++) {
19512
+ for (int i = -1; i <= 1; i++) {
19513
+ vec2 o = vec2(float(i), float(j));
19514
+ vec2 h = hash22(cell + o);
19515
+ vec2 pt = o + 0.5 + 0.4 * sin(t + 6.28318 * h);
19516
+ float d = length(pt - f);
19517
+ if (d < f1) { f2 = f1; f1 = d; } else if (d < f2) { f2 = d; }
19518
+ }
19519
+ }
19520
+ return 1.0 - smoothstep(0.0, width, f2 - f1);
19521
+ }
19522
+
19523
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
19524
+ vec2 uv = fragCoord / iResolution.xy;
19525
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
19526
+ float t = iTime * uSpeed;
19527
+ // Thrown up from the water below, the pattern is stretched along the wall and wobbles as the surface moves.
19528
+ vec2 q = p * vec2(1.0, 1.0 - 0.5 * uStretch) * 5.0 / uScale;
19529
+ q += 0.35 * vec2(vnoise(q * 0.6 + t * 0.4), vnoise(q * 0.6 - t * 0.35 + 5.0));
19530
+ float width = mix(0.35, 0.08, uSharpness);
19531
+ float web = causticWeb(q, t, width) * 0.6 + causticWeb(q * 1.37 + 7.3, t * 1.3, width) * 0.6;
19532
+ web = pow(web, mix(1.0, 2.2, uSharpness));
19533
+ // Brighter and darker patches as the ripples gather and spread.
19534
+ web *= 0.6 + 0.6 * vnoise(p * 2.0 + vec2(t * 0.15, -t * 0.1));
19535
+ vec4 pic = pictureRGBA(uImage, uv);
19536
+ vec3 ambient = uAmbient * mix(vec3(1.0), uShade * 1.7, 0.6);
19537
+ vec3 col = pic.rgb * (ambient + uTint * web * uIntensity) + uTint * web * uIntensity * 0.06;
19538
+ fragColor = vec4(col / (1.0 + max(col - 1.0, 0.0)), pic.a);
19539
+ }`
19540
+ ),
19541
+ presets: [
19542
+ { name: "Midday pool", values: { uIntensity: 1.6, uAmbient: 0.75, uSharpness: 0.8 } },
19543
+ { name: "Night pool", values: { uTint: [0.45, 0.85, 1], uShade: [0.05, 0.15, 0.35], uAmbient: 0.25, uIntensity: 1.8, uSpeed: 0.4 } }
19544
+ ]
19545
+ };
19546
+ var partyLights = {
19547
+ kind: "shader",
19548
+ id: "party-lights",
19549
+ name: "Party lights",
19550
+ description: "Club lighting over a picture: coloured spot beams cutting through haze and pools of light sweeping across it, with mirror-ball sparkles and an optional strobe.",
19551
+ category: "lighting",
19552
+ video: "filter",
19553
+ tags: ["lighting", "party", "club", "disco", "stage", "concert", "strobe", "colour", "image", "filter"],
19554
+ source: glsl(
19555
+ `// Party lights: beams from above sweeping light pools across the picture, added as light, plus mirror-ball dots and a strobe.
19556
+ uniform sampler2D uImage; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.city} label="Image"
19557
+ uniform int uCount; // @control min=1 max=8 default=5 label="Lights"
19558
+ uniform vec3 uColorA; // @control color default=#FF2D95 label="Colour A"
19559
+ uniform vec3 uColorB; // @control color default=#2DD4FF label="Colour B"
19560
+ uniform vec3 uColorC; // @control color default=#B6FF3B label="Colour C"
19561
+ uniform float uIntensity; // @control min=0 max=3 default=1.3 label="Intensity"
19562
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
19563
+ uniform float uPoolSize; // @control min=0.05 max=0.5 default=0.17 label="Pool size"
19564
+ uniform float uBeams; // @control min=0 max=1 default=0.5 label="Beams in the haze"
19565
+ uniform float uDarken; // @control min=0 max=1 default=0.55 label="Darken the room"
19566
+ uniform float uSparkle; // @control min=0 max=1 default=0.35 label="Mirror ball"
19567
+ uniform float uStrobe; // @control min=0 max=1 default=0 label="Strobe"
19568
+ ${BACKDROP_CONTROLS}`,
19569
+ PICTURE,
19570
+ HASH,
19571
+ ROTATE,
19572
+ `vec3 lightColour(int i, float t) {
19573
+ int k = int(mod(float(i) + floor(t * 0.25), 3.0));
19574
+ return k == 0 ? uColorA : k == 1 ? uColorB : uColorC;
19575
+ }
19576
+
19577
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
19578
+ vec2 uv = fragCoord / iResolution.xy;
19579
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
19580
+ float aspect = iResolution.x / iResolution.y;
19581
+ float t = iTime * uSpeed;
19582
+ vec3 light = vec3(0.0);
19583
+ vec3 haze = vec3(0.0);
19584
+ for (int i = 0; i < 8; i++) {
19585
+ if (i >= uCount) break;
19586
+ float fi = float(i);
19587
+ float h = hash12(vec2(fi, 4.2));
19588
+ // Each light hangs above the frame and swings its pool back and forth over the picture.
19589
+ vec2 src = vec2(((fi + 0.5) / float(uCount) - 0.5) * aspect * 0.9, 0.62);
19590
+ vec2 pool = vec2(sin(t * (0.5 + 0.4 * h) + fi * 2.1) * 0.45 * aspect, -0.1 + 0.3 * sin(t * (0.37 + 0.3 * h) + fi * 1.3));
19591
+ vec3 c = lightColour(i, t + fi);
19592
+ float size = uPoolSize * (0.8 + 0.4 * h);
19593
+ float d = length((p - pool) * vec2(1.0, 1.3));
19594
+ light += c * (smoothstep(size, size * 0.55, d) * 0.8 + exp(-d * d / (size * size)) * 0.5);
19595
+ // The beam: a cone from the light widening to the pool.
19596
+ vec2 ab = pool - src;
19597
+ float s = clamp(dot(p - src, ab) / dot(ab, ab), 0.0, 1.0);
19598
+ float w = mix(0.008, size * 0.9, s);
19599
+ float cone = smoothstep(w, w * 0.2, length(p - src - ab * s)) * s * s;
19600
+ haze += c * cone;
19601
+ }
19602
+ // Mirror ball: a sparse grid of dots turning slowly across the picture.
19603
+ vec2 g = rot(t * 0.15) * (p + vec2(0.0, 0.6)) * 22.0;
19604
+ vec2 cell = floor(g);
19605
+ float dotLit = step(0.94, hash12(cell)) * smoothstep(0.22, 0.05, length(fract(g) - 0.5));
19606
+ float twinkle = 0.5 + 0.5 * sin(iTime * 6.0 + hash12(cell + 1.7) * 20.0);
19607
+ vec3 sparkle = mix(vec3(1.0), lightColour(int(hash12(cell + 2.3) * 3.0), t), 0.5) * dotLit * twinkle * uSparkle;
19608
+ // Strobe: now and then a burst of white flashes.
19609
+ float burst = step(1.0 - uStrobe * 0.6, hash12(vec2(floor(iTime * 1.5), 9.0)));
19610
+ float strobe = uStrobe * burst * step(0.6, fract(iTime * 9.0));
19611
+
19612
+ vec4 pic = pictureRGBA(uImage, uv);
19613
+ vec3 room = pic.rgb * (1.0 - uDarken);
19614
+ vec3 col = room + pic.rgb * (light + sparkle) * uIntensity * 1.4 + (light * 0.08 + haze * uBeams * 0.6 + sparkle * 0.4) * uIntensity;
19615
+ col += strobe * mix(vec3(0.4), vec3(1.0), pic.rgb);
19616
+ fragColor = vec4(col / (1.0 + max(col - 1.0, 0.0)), pic.a);
19617
+ }`
19618
+ ),
19619
+ presets: [
19620
+ { name: "Disco", values: { uColorA: [1, 0.25, 0.55], uColorB: [1, 0.8, 0.2], uColorC: [0.4, 0.4, 1], uSparkle: 0.8, uBeams: 0.3 } },
19621
+ { name: "Rave strobe", values: { uStrobe: 0.7, uSpeed: 2, uCount: 8, uDarken: 0.8, uBeams: 0.9, uColorC: [0.6, 0.2, 1] } },
19622
+ { name: "Concert", values: { uImage: SAMPLE_IMAGES.portrait, uCount: 3, uColorA: [0.3, 0.5, 1], uColorB: [1, 0.4, 0.2], uColorC: [1, 1, 1], uSparkle: 0, uBeams: 0.9, uDarken: 0.7 } }
19623
+ ]
19624
+ };
19625
+ var lightingShaders = [
19626
+ relight2,
19627
+ pointLight,
19628
+ studioLighting,
19629
+ timeOfDay,
19630
+ goboProjection,
19631
+ neonSpill,
19632
+ candlelight,
19633
+ poolCaustics,
19634
+ partyLights
19635
+ ];
19636
+
18819
19637
  // src/catalog/types.ts
18820
19638
  var EFFECT_CATEGORIES = [
18821
19639
  "gradient",
18822
19640
  "organic",
18823
19641
  "pattern",
18824
19642
  "light",
19643
+ "lighting",
18825
19644
  "space",
18826
19645
  "retro",
18827
19646
  "image",
@@ -20082,6 +20901,7 @@ var SHADER_EFFECTS = [
20082
20901
  sparkles,
20083
20902
  neonRipple,
20084
20903
  lightRibbons,
20904
+ ...lightingShaders,
20085
20905
  starfield,
20086
20906
  nebula,
20087
20907
  blackHole,
@@ -20121,13 +20941,19 @@ var SHADER_EFFECTS = [
20121
20941
  ...distortionShaders,
20122
20942
  ...transitions
20123
20943
  ];
20124
- var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects, ...pyroEffects, ...timewarpEffects, ...meltEffects, ...electricEffects, ...pixelEffects, ...splatterEffects, ...bubbleEffects, ...stickerEffects, ...trailEffects, ...impactEffects, ...comicEffects, ...glowEffects, ...shadowEffects].sort(
20944
+ var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects, ...pyroEffects, ...timewarpEffects, ...meltEffects, ...electricEffects, ...pixelEffects, ...splatterEffects, ...bubbleEffects, ...stickerEffects, ...trailEffects, ...impactEffects, ...comicEffects, ...glowEffects, ...shadowEffects, ...lightingEffects].sort(
20125
20945
  (a, b) => EFFECT_CATEGORIES.indexOf(a.category) - EFFECT_CATEGORIES.indexOf(b.category)
20126
20946
  );
20127
20947
  var byId = new Map(EFFECTS.map((e) => [e.id, e]));
20128
20948
  function getEffect(id) {
20129
20949
  return byId.get(id);
20130
20950
  }
20951
+ var inputsCache = /* @__PURE__ */ new WeakMap();
20952
+ function effectInputs(effect) {
20953
+ let inputs = inputsCache.get(effect);
20954
+ if (!inputs) inputsCache.set(effect, inputs = inputsOf(effect, effectControls(effect)));
20955
+ return inputs;
20956
+ }
20131
20957
  function findEffects(query = {}) {
20132
20958
  const words = (query.text ?? "").toLowerCase().split(/\s+/).filter(Boolean);
20133
20959
  return EFFECTS.filter((e) => {
@@ -20135,6 +20961,15 @@ function findEffects(query = {}) {
20135
20961
  if (query.kind && e.kind !== query.kind) return false;
20136
20962
  if (query.tags && !query.tags.every((t) => e.tags.includes(t))) return false;
20137
20963
  if (query.video && !(e.kind === "shader" && e.video && query.video.includes(e.video))) return false;
20964
+ if (query.accepts || query.interaction || query.transparent !== void 0) {
20965
+ const inputs = effectInputs(e);
20966
+ if (query.accepts && !query.accepts.every((i) => inputs.accepts.includes(i))) return false;
20967
+ if (query.interaction) {
20968
+ const answers = query.interaction.length === 0 ? inputs.interactions.length === 0 : query.interaction.some((i) => inputs.interactions.includes(i));
20969
+ if (!answers) return false;
20970
+ }
20971
+ if (query.transparent !== void 0 && inputs.transparent !== query.transparent) return false;
20972
+ }
20138
20973
  const haystack = [e.id, e.name, e.description, e.category, ...e.tags].join(" ").toLowerCase();
20139
20974
  return words.every((w) => haystack.includes(w));
20140
20975
  });
@@ -20150,6 +20985,8 @@ function effectDefaults(effect, options = {}) {
20150
20985
  }
20151
20986
 
20152
20987
  export {
20988
+ EFFECT_INPUTS,
20989
+ EFFECT_INTERACTIONS,
20153
20990
  HASH,
20154
20991
  NOISE,
20155
20992
  FBM,
@@ -20175,8 +21012,9 @@ export {
20175
21012
  SHADER_EFFECTS,
20176
21013
  EFFECTS,
20177
21014
  getEffect,
21015
+ effectInputs,
20178
21016
  findEffects,
20179
21017
  effectControls,
20180
21018
  effectDefaults
20181
21019
  };
20182
- //# sourceMappingURL=chunk-3RKDS64M.js.map
21020
+ //# sourceMappingURL=chunk-OQ4EWZ3B.js.map