@pixerate/flair 0.20.0 → 0.21.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.
@@ -3510,8 +3510,154 @@ var lineBoil = {
3510
3510
  };
3511
3511
  var comicEffects = [soundEffect, speechBubble, fightCloud, dizzyStars, animeEmotes, cartoonPoof, lineBoil];
3512
3512
 
3513
- // src/catalog/electric.ts
3513
+ // src/catalog/glow.ts
3514
3514
  var r16 = (v) => Math.round(v * 10) / 10;
3515
+ var n7 = (name) => `{{${name}}}`;
3516
+ var SANS4 = `system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif`;
3517
+ var mixHex2 = (a, b, t) => {
3518
+ const x = Array.isArray(a) ? a : [1, 1, 1];
3519
+ const y = Array.isArray(b) ? b : [1, 1, 1];
3520
+ return rgbToHex([0, 1, 2].map((i) => x[i] + (y[i] - x[i]) * t));
3521
+ };
3522
+ function fit3(text, width, max) {
3523
+ const e = Math.max(0.6, textWidthEm(String(text)));
3524
+ const size = Math.min(max, width / e);
3525
+ return { size: r16(size), length: r16(e * size) };
3526
+ }
3527
+ var RINGS = 3;
3528
+ var RING_FROM = 18;
3529
+ var RING_TO = 112;
3530
+ function ringAnimations(k) {
3531
+ const at3 = (phase2) => ({ r: r16(RING_FROM + (RING_TO - RING_FROM) * phase2), o: Math.round((1 - phase2) * 100) / 100 });
3532
+ const id = `glowRingPulseRing${k}`;
3533
+ const phase = k / RINGS;
3534
+ const from = at3(phase);
3535
+ const end = at3(1);
3536
+ const start = at3(0);
3537
+ const out = [
3538
+ {
3539
+ id,
3540
+ attrs: { r: [from.r, end.r], opacity: [from.o, end.o], "stroke-width": [n7(`ws${k}`), n7("wEnd")] },
3541
+ ms: n7(`rest${k}`)
3542
+ }
3543
+ ];
3544
+ if (phase > 0)
3545
+ out.push({
3546
+ id,
3547
+ attrs: { r: [start.r, from.r], opacity: [start.o, from.o], "stroke-width": [n7("wStart"), n7(`ws${k}`)] },
3548
+ delay: n7(`rest${k}`),
3549
+ ms: n7(`lead${k}`)
3550
+ });
3551
+ return out;
3552
+ }
3553
+ var glowRingPulse = {
3554
+ kind: "vector",
3555
+ id: "glow-ring-pulse",
3556
+ name: "Glow ring pulse",
3557
+ description: "Glowing rings pulsing out from a softly breathing core in endless waves, like a beacon or a live indicator.",
3558
+ category: "glow",
3559
+ tags: ["glow", "ring", "pulse", "beacon", "live", "loop", "radar"],
3560
+ hold: 0,
3561
+ presets: [
3562
+ { name: "Live", values: { ring: [1, 0.25, 0.35], core: [1, 0.72, 0.75], period: 1.6 } },
3563
+ { name: "Gold", values: { ring: [1, 0.75, 0.25], core: [1, 0.95, 0.75], background: [0.08, 0.06, 0.03], glow: 8 } },
3564
+ { name: "Light", values: { background: [0.96, 0.97, 1], ring: [0.31, 0.27, 0.9], core: [0.55, 0.5, 1], glow: 4 } }
3565
+ ],
3566
+ derived: (v) => {
3567
+ const period = Math.round(Number(v.period) * 1e3);
3568
+ const width = Number(v.width);
3569
+ const out = { half: Math.round(period / 2), wStart: r16(width * 1.6), wEnd: r16(width * 0.4) };
3570
+ for (let k = 0; k < RINGS; k++) {
3571
+ const phase = k / RINGS;
3572
+ out[`rest${k}`] = Math.round(period * (1 - phase));
3573
+ out[`lead${k}`] = period - out[`rest${k}`];
3574
+ out[`ws${k}`] = r16(width * (1.6 - 1.2 * phase));
3575
+ }
3576
+ return out;
3577
+ },
3578
+ motion: {
3579
+ svg: `<svg viewBox="0 0 240 240"><!-- {{period: float min=0.8 max=6 default=2.4 step=0.1 label='Seconds per wave'}} {{width: float min=1 max=10 default=4 step=0.5 label='Ring width'}} --><defs><filter id="glowRingPulseGlow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur in="SourceGraphic" stdDeviation="{{glow: float min=0 max=16 default=6 step=0.5 label='Glow'}}" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter><radialGradient id="glowRingPulseCoreFill"><stop offset="0" stop-color="#FFFFFF"/><stop offset="0.45" stop-color="{{core: color default=#A5F3FC label='Core'}}"/><stop offset="1" stop-color="{{ring: color default=#22D3EE label='Ring'}}"/></radialGradient></defs><rect width="240" height="240" fill="{{background: color default=#060A14 label='Background'}}"/><g filter="url(#glowRingPulseGlow)">${Array.from(
3580
+ { length: RINGS },
3581
+ (_, k) => `<circle id="glowRingPulseRing${k}" cx="120" cy="120" r="${r16(RING_FROM + (RING_TO - RING_FROM) * k / RINGS)}" fill="none" stroke="{{ring}}" stroke-width="{{ws${k}}}" opacity="${Math.round((1 - k / RINGS) * 100) / 100}"/>`
3582
+ ).join("")}<circle id="glowRingPulseCore" cx="120" cy="120" r="22" fill="url(#glowRingPulseCoreFill)"/></g></svg>`,
3583
+ animate: [
3584
+ ...Array.from({ length: RINGS }, (_, k) => ringAnimations(k)).flat(),
3585
+ // The core breathes once a period, out and back, so it too loops seamlessly.
3586
+ { id: "glowRingPulseCore", attrs: { r: [22, 26] }, ms: n7("half"), ease: "in-out" },
3587
+ { id: "glowRingPulseCore", attrs: { r: [26, 22] }, delay: n7("half"), ms: n7("half"), ease: "in-out" }
3588
+ ]
3589
+ }
3590
+ };
3591
+ var haloPose = (s) => `translate(160 70) scale(${s}) translate(-160 -70)`;
3592
+ var glowButton = {
3593
+ kind: "vector",
3594
+ id: "glow-button",
3595
+ name: "Glow button",
3596
+ description: "A pill button with a soft glow that, on hover, brightens, spreads its glow wider and lifts a little.",
3597
+ category: "glow",
3598
+ tags: ["glow", "button", "hover", "interactive", "ui", "cta", "neon"],
3599
+ trigger: "hover",
3600
+ hold: 800,
3601
+ presets: [
3602
+ { name: "Violet", values: { accent: [0.55, 0.36, 0.96], background: [0.05, 0.04, 0.09] } },
3603
+ { name: "Mint", values: { accent: [0.2, 0.9, 0.6], ink: [0.02, 0.15, 0.1], label: "Start free trial" } },
3604
+ { name: "Sunset", values: { accent: [1, 0.45, 0.3], label: "Join now" } }
3605
+ ],
3606
+ derived: (v) => {
3607
+ const { size, length } = fit3(v.label, 180, 22);
3608
+ return { size, length, base: r16(70 + size * 0.35), bright: mixHex2(v.accent, [1, 1, 1], 0.3) };
3609
+ },
3610
+ motion: {
3611
+ svg: `<svg viewBox="0 0 320 140"><!-- {{label: text default='Get started' label='Label'}} {{background: color default=#0A0D18 label='Background'}} {{accent: color default=#38BDF8 label='Accent'}} {{ink: color default=#04121C label='Label colour'}} --><defs><filter id="glowButtonBlur" x="-60%" y="-120%" width="220%" height="340%"><feGaussianBlur id="glowButtonSpread" stdDeviation="10"/></filter><linearGradient id="glowButtonSheen" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#FFFFFF" stop-opacity="0.35"/><stop offset="0.5" stop-color="#FFFFFF" stop-opacity="0.05"/><stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/></linearGradient></defs><rect width="320" height="140" fill="{{background}}"/><g opacity="0.9"><g id="glowButtonHalo" transform="${haloPose(1)}" opacity="0.55"><rect x="50" y="42" width="220" height="56" rx="28" fill="{{accent}}" filter="url(#glowButtonBlur)"/></g></g><g id="glowButtonLift" transform="translate(0 0)"><rect id="glowButtonFace" x="50" y="42" width="220" height="56" rx="28" fill="{{accent}}"/><rect x="50" y="42" width="220" height="56" rx="28" fill="url(#glowButtonSheen)"/><text x="160" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS4}" font-size="{{size}}" font-weight="700" letter-spacing="0.3" fill="{{ink}}">{{label}}</text></g></svg>`,
3612
+ animate: [
3613
+ { id: "glowButtonHalo", attrs: { opacity: [0.55, 0.95], transform: [haloPose(1), haloPose(1.08)] }, ms: 320, ease: "out" },
3614
+ { id: "glowButtonSpread", attrs: { stdDeviation: [10, 14] }, ms: 320, ease: "out" },
3615
+ { id: "glowButtonLift", attrs: { transform: ["translate(0 0)", "translate(0 -3)"] }, ms: 260, ease: "out" },
3616
+ { id: "glowButtonFace", attrs: { fill: ["{{accent}}", "{{bright}}"] }, ms: 260 }
3617
+ ]
3618
+ }
3619
+ };
3620
+ var glowEffects = [glowRingPulse, glowButton];
3621
+
3622
+ // src/catalog/shadow.ts
3623
+ var r17 = (v) => Math.round(v * 10) / 10;
3624
+ var SANS5 = `system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif`;
3625
+ function fit4(text, width, max) {
3626
+ const e = Math.max(0.6, textWidthEm(String(text)));
3627
+ const size = Math.min(max, width / e);
3628
+ return { size: r17(size), length: r17(e * size) };
3629
+ }
3630
+ var raised = (up, scale) => `translate(0 ${-up}) translate(160 120) scale(${scale}) translate(-160 -120)`;
3631
+ var liftCard = {
3632
+ kind: "vector",
3633
+ id: "lift-card",
3634
+ name: "Lift card",
3635
+ description: "A content card that lifts toward you on hover like a material elevation change, rising and growing slightly as its shadow spreads and softens beneath it.",
3636
+ category: "shadow",
3637
+ tags: ["shadow", "card", "hover", "interactive", "elevation", "material", "ui", "micro-interaction"],
3638
+ trigger: "hover",
3639
+ hold: 800,
3640
+ presets: [
3641
+ { name: "Dark", values: { background: [0.07, 0.08, 0.11], card: [0.13, 0.15, 0.2], ink: [0.9, 0.92, 0.96], shadow: [0, 0, 0], accent: [0.4, 0.85, 1] } },
3642
+ { name: "Warm", values: { title: "Weekend recipes", background: [0.98, 0.94, 0.88], accent: [0.93, 0.42, 0.25], shadow: [0.35, 0.18, 0.08] } }
3643
+ ],
3644
+ derived: (v) => {
3645
+ const { size, length } = fit4(v.title, 116, 17);
3646
+ return { size, length, base: r17(82 + size * 0.35) };
3647
+ },
3648
+ motion: {
3649
+ svg: `<svg viewBox="0 0 320 240"><!-- {{title: text default='Getting started' label='Title'}} {{background: color default=#EEF1F6 label='Background'}} {{card: color default=#FFFFFF label='Card'}} {{ink: color default=#1F2937 label='Text colour'}} {{accent: color default=#6366F1 label='Accent'}} {{shadow: color default=#0F172A label='Shadow colour'}} --><defs><filter id="liftCardTight" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="2.5"/></filter><filter id="liftCardSoft" x="-40%" y="-40%" width="180%" height="180%"><feGaussianBlur stdDeviation="14"/></filter></defs><rect width="320" height="240" fill="{{background}}"/><rect id="liftCardAmbient" x="70" y="50" width="180" height="140" rx="14" fill="{{shadow}}" opacity="0" filter="url(#liftCardSoft)" transform="${raised(-4, 0.94)}"/><rect id="liftCardKey" x="70" y="50" width="180" height="140" rx="14" fill="{{shadow}}" opacity="0.22" filter="url(#liftCardTight)" transform="translate(0 2)"/><g id="liftCardFace" transform="${raised(0, 1)}"><rect x="70" y="50" width="180" height="140" rx="14" fill="{{card}}"/><circle cx="98" cy="80" r="12" fill="{{accent}}"/><text x="118" y="{{base}}" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS5}" font-size="{{size}}" font-weight="700" fill="{{ink}}">{{title}}</text><g fill="{{ink}}" opacity="0.16"><rect x="86" y="108" width="148" height="8" rx="4"/><rect x="86" y="124" width="148" height="8" rx="4"/><rect x="86" y="140" width="96" height="8" rx="4"/></g><rect x="86" y="160" width="54" height="16" rx="8" fill="{{accent}}"/></g></svg>`,
3650
+ animate: [
3651
+ { id: "liftCardFace", attrs: { transform: [raised(0, 1), raised(6, 1.02)] }, ms: 260, ease: "out" },
3652
+ { id: "liftCardAmbient", attrs: { opacity: [0, 0.3], transform: [raised(-4, 0.94), raised(-16, 1)] }, ms: 300, ease: "out" },
3653
+ { id: "liftCardKey", attrs: { opacity: [0.22, 0.12], transform: ["translate(0 2)", "translate(0 5)"] }, ms: 260, ease: "out" }
3654
+ ]
3655
+ }
3656
+ };
3657
+ var shadowEffects = [liftCard];
3658
+
3659
+ // src/catalog/electric.ts
3660
+ var r18 = (v) => Math.round(v * 10) / 10;
3515
3661
  var about5 = (cx, cy, s) => `translate(${cx} ${cy}) scale(${Math.round(s * 1e3) / 1e3}) translate(${-cx} ${-cy})`;
3516
3662
  var snap6 = (id, attr, value, delay, step) => ({
3517
3663
  id,
@@ -3520,7 +3666,7 @@ var snap6 = (id, attr, value, delay, step) => ({
3520
3666
  delay,
3521
3667
  ms: 0
3522
3668
  });
3523
- var SANS4 = `system-ui, -apple-system, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif`;
3669
+ var SANS6 = `system-ui, -apple-system, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif`;
3524
3670
  var BOLT = "M68 8 L28 66 L56 66 L46 112 L92 48 L64 48 L76 8 Z";
3525
3671
  var zapIcon = {
3526
3672
  kind: "vector",
@@ -3537,7 +3683,7 @@ var zapIcon = {
3537
3683
  motion: (() => {
3538
3684
  const sparks = [-55, 25, 160, 215].map((deg, i) => {
3539
3685
  const a = deg * Math.PI / 180;
3540
- const at3 = (r) => [r16(60 + r * Math.cos(a)), r16(60 + r * Math.sin(a))];
3686
+ const at3 = (r) => [r18(60 + r * Math.cos(a)), r18(60 + r * Math.sin(a))];
3541
3687
  return { id: `s${i}`, from: at3(30), mid: at3(46), to: at3(56) };
3542
3688
  });
3543
3689
  return {
@@ -3605,7 +3751,7 @@ var powerButton = {
3605
3751
  };
3606
3752
  })()
3607
3753
  };
3608
- var CELLS = Array.from({ length: 5 }, (_, i) => ({ id: `cell${i}`, x: r16(36 + i * 24.8) }));
3754
+ var CELLS = Array.from({ length: 5 }, (_, i) => ({ id: `cell${i}`, x: r18(36 + i * 24.8) }));
3609
3755
  var boltPose = (k) => `translate(96 58) scale(${Math.round(0.36 * k * 1e3) / 1e3}) translate(-60 -60)`;
3610
3756
  var batteryCharge = {
3611
3757
  kind: "vector",
@@ -3622,7 +3768,7 @@ var batteryCharge = {
3622
3768
  motion: {
3623
3769
  svg: `<svg viewBox="0 0 200 140"><!-- {{background: color default=#0F172A label='Background'}} {{shell: color default=#CBD5E1 label='Case'}} {{charge: color default=#22C55E label='Charge'}} {{bolt: color default=#FDE047 label='Bolt'}} {{ink: color default=#F8FAFC label='Percent'}} --><rect width="200" height="140" fill="{{background}}"/><rect id="case" x="28" y="26" width="136" height="64" rx="12" fill="none" stroke="{{shell}}" stroke-width="5"/><rect id="nub" x="167" y="46" width="9" height="24" rx="3" fill="{{shell}}"/>${CELLS.map(
3624
3770
  ({ id, x }) => `<rect id="${id}" x="${x}" y="34" width="20.8" height="48" rx="4" fill="{{charge}}" opacity="0"/>`
3625
- ).join("")}<path id="bolt" d="${BOLT}" transform="${boltPose(1)}" fill="{{bolt}}" stroke="{{background}}" stroke-width="10" stroke-linejoin="round" paint-order="stroke"/><rect id="flash" x="28" y="26" width="136" height="64" rx="12" fill="#FFFFFF" opacity="0"/><text id="pct" x="96" y="124" text-anchor="middle" font-family="${SANS4}" font-size="22" font-weight="700" fill="{{ink}}">0%</text></svg>`,
3771
+ ).join("")}<path id="bolt" d="${BOLT}" transform="${boltPose(1)}" fill="{{bolt}}" stroke="{{background}}" stroke-width="10" stroke-linejoin="round" paint-order="stroke"/><rect id="flash" x="28" y="26" width="136" height="64" rx="12" fill="#FFFFFF" opacity="0"/><text id="pct" x="96" y="124" text-anchor="middle" font-family="${SANS6}" font-size="22" font-weight="700" fill="{{ink}}">0%</text></svg>`,
3626
3772
  animate: [
3627
3773
  { id: "pct", count: [0, 100], format: "{n}%", ms: 2e3, ease: "linear" },
3628
3774
  // Each cell lights as the count passes its share.
@@ -3913,8 +4059,8 @@ function packGlyph(rows) {
3913
4059
  return `ivec2(${a}, ${b})`;
3914
4060
  }
3915
4061
  var vec3Hex = (hex) => {
3916
- const n10 = parseInt(hex.slice(1), 16);
3917
- const c = [n10 >> 16, n10 >> 8 & 255, n10 & 255].map((v) => +(v / 255).toFixed(3));
4062
+ const n11 = parseInt(hex.slice(1), 16);
4063
+ const c = [n11 >> 16, n11 >> 8 & 255, n11 & 255].map((v) => +(v / 255).toFixed(3));
3918
4064
  return `vec3(${c.map((v) => Number.isInteger(v) ? `${v}.0` : String(v)).join(", ")})`;
3919
4065
  };
3920
4066
  var paletteArray = (name, hexes) => `const vec3 ${name}[${hexes.length}] = vec3[${hexes.length}](
@@ -4699,7 +4845,7 @@ var pixelShaders = [
4699
4845
 
4700
4846
  // src/catalog/pixel.ts
4701
4847
  var r29 = (v) => Math.round(v * 100) / 100;
4702
- var n7 = (name) => `{{${name}}}`;
4848
+ var n8 = (name) => `{{${name}}}`;
4703
4849
  function bitmapPath(bitmap, char, unit = 1, x = 0, y = 0) {
4704
4850
  let d = "";
4705
4851
  bitmap.forEach((row, j) => {
@@ -4764,10 +4910,10 @@ var pixelText = {
4764
4910
  (_, i) => `<g id="g${i}" transform="${letterAt(i, -14)}" opacity="0"><path transform="translate(0.6 0.6)" fill="{{shadow}}" d="{{d${i}}}"/><path fill="{{ink}}" d="{{d${i}}}"/></g>`
4765
4911
  ).join("")}</g></svg>`,
4766
4912
  animate: Array.from({ length: TEXT_SLOTS }, (_, i) => [
4767
- { id: `g${i}`, attrs: { opacity: [0, 1] }, delay: n7(`t${i}`), ms: 60, ease: "linear" },
4768
- { id: `g${i}`, attrs: { transform: [letterAt(i, -14), letterAt(i, 0)] }, delay: n7(`t${i}`), ms: 260, ease: "in-out" },
4769
- { id: `g${i}`, attrs: { transform: [letterAt(i, 0), letterAt(i, -1.6)] }, delay: n7(`u${i}`), ms: 110, ease: "out" },
4770
- { id: `g${i}`, attrs: { transform: [letterAt(i, -1.6), letterAt(i, 0)] }, delay: n7(`w${i}`), ms: 120, ease: "in-out" }
4913
+ { id: `g${i}`, attrs: { opacity: [0, 1] }, delay: n8(`t${i}`), ms: 60, ease: "linear" },
4914
+ { id: `g${i}`, attrs: { transform: [letterAt(i, -14), letterAt(i, 0)] }, delay: n8(`t${i}`), ms: 260, ease: "in-out" },
4915
+ { id: `g${i}`, attrs: { transform: [letterAt(i, 0), letterAt(i, -1.6)] }, delay: n8(`u${i}`), ms: 110, ease: "out" },
4916
+ { id: `g${i}`, attrs: { transform: [letterAt(i, -1.6), letterAt(i, 0)] }, delay: n8(`w${i}`), ms: 120, ease: "in-out" }
4771
4917
  ]).flat()
4772
4918
  }
4773
4919
  };
@@ -4972,7 +5118,7 @@ var chompLoader = {
4972
5118
  var pixelEffects = [pixelText, coinSpin, spaceInvader, pixelHeartHealth, chompLoader];
4973
5119
 
4974
5120
  // src/catalog/pyro.ts
4975
- var round = (n10) => Math.round(n10 * 10) / 10;
5121
+ var round = (n11) => Math.round(n11 * 10) / 10;
4976
5122
  var fireworkBurst = {
4977
5123
  kind: "vector",
4978
5124
  id: "firework-burst",
@@ -5027,10 +5173,10 @@ var fireworkBurst = {
5027
5173
  var pyroEffects = [fireworkBurst];
5028
5174
 
5029
5175
  // src/catalog/timewarp.ts
5030
- var r17 = (n10) => Math.round(n10 * 10) / 10;
5176
+ var r19 = (n11) => Math.round(n11 * 10) / 10;
5031
5177
  var at2 = (degrees, radius) => {
5032
5178
  const a = degrees * Math.PI / 180;
5033
- return [r17(100 + radius * Math.sin(a)), r17(100 - radius * Math.cos(a))];
5179
+ return [r19(100 + radius * Math.sin(a)), r19(100 - radius * Math.cos(a))];
5034
5180
  };
5035
5181
  var slice = (from, to, radius) => {
5036
5182
  const [x0, y0] = at2(from, radius);
@@ -5101,7 +5247,7 @@ var r210 = (v) => {
5101
5247
  const r = Math.round(v * 100) / 100;
5102
5248
  return Object.is(r, -0) ? 0 : r;
5103
5249
  };
5104
- var n8 = (name) => `{{${name}}}`;
5250
+ var n9 = (name) => `{{${name}}}`;
5105
5251
  var pointsOf = (d) => {
5106
5252
  const nums = (d.match(NUMBER) ?? []).map(Number);
5107
5253
  return Array.from({ length: nums.length >> 1 }, (_, i) => [nums[2 * i], nums[2 * i + 1]]);
@@ -5161,16 +5307,16 @@ function around(c, r, from, to, avoid) {
5161
5307
  if (((avoid - from) % 360 + 360) % 360 < delta) delta -= 360;
5162
5308
  return arc2(c[0], c[1], r, r, from, from + delta);
5163
5309
  }
5164
- function crescent(c1, r19, c2, r2_) {
5310
+ function crescent(c1, r111, c2, r2_) {
5165
5311
  const [dx, dy] = [c2[0] - c1[0], c2[1] - c1[1]];
5166
5312
  const d = Math.hypot(dx, dy);
5167
- const a = (r19 * r19 - r2_ * r2_ + d * d) / (2 * d);
5168
- const h = Math.sqrt(Math.max(0, r19 * r19 - a * a));
5313
+ const a = (r111 * r111 - r2_ * r2_ + d * d) / (2 * d);
5314
+ const h = Math.sqrt(Math.max(0, r111 * r111 - a * a));
5169
5315
  const [px, py] = [c1[0] + a * dx / d, c1[1] + a * dy / d];
5170
5316
  const i1 = [px - h * dy / d, py + h * dx / d];
5171
5317
  const i2 = [px + h * dy / d, py - h * dx / d];
5172
5318
  const angle = (c, p) => Math.atan2(p[1] - c[1], p[0] - c[0]) / DEG2;
5173
- const outer = around(c1, r19, angle(c1, i1), angle(c1, i2), angle(c1, c2));
5319
+ const outer = around(c1, r111, angle(c1, i1), angle(c1, i2), angle(c1, c2));
5174
5320
  const inner = around(c2, r2_, angle(c2, i2), angle(c2, i1), angle(c2, c1) + 180);
5175
5321
  return polyline3([...outer, ...inner.slice(1, -1)], true);
5176
5322
  }
@@ -5225,8 +5371,8 @@ var shapeMorph = {
5225
5371
  motion: {
5226
5372
  svg: `<svg viewBox="0 0 100 100"><!-- {{from: text default='${SHAPE_FROM}' label='From path'}} {{to: text default='${SHAPE_TO}' label='To path'}} {{fillFrom: color default=#6366F1 label='Fill (from)'}} {{fillTo: color default=#F59E0B label='Fill (to)'}} {{strokeFrom: color default=#C7D2FE label='Outline (from)'}} {{strokeTo: color default=#FEF3C7 label='Outline (to)'}} {{background: color default=#0F172A label='Background'}} {{width: float min=0 max=6 default=1.5 step=0.25 label='Outline width'}} {{morph: float min=200 max=3000 default=1000 step=50 label='Morph ms'}} {{pause: float min=0 max=3000 default=700 step=50 label='Pause ms'}} --><rect width="100" height="100" fill="{{background}}"/><path id="shape" d="{{a}}" fill="{{fillFrom}}" fill-rule="evenodd" stroke="{{strokeFrom}}" stroke-width="{{width}}" stroke-linejoin="round"/></svg>`,
5227
5373
  animate: [
5228
- { id: "shape", attrs: { d: ["{{a}}", "{{b}}"], fill: ["{{fillFrom}}", "{{fillTo}}"], stroke: ["{{strokeFrom}}", "{{strokeTo}}"] }, delay: n8("pause"), ms: n8("morph"), ease: "in-out" },
5229
- { id: "shape", attrs: { d: ["{{b}}", "{{a}}"], fill: ["{{fillTo}}", "{{fillFrom}}"], stroke: ["{{strokeTo}}", "{{strokeFrom}}"] }, delay: n8("back"), ms: n8("morph"), ease: "in-out" }
5374
+ { id: "shape", attrs: { d: ["{{a}}", "{{b}}"], fill: ["{{fillFrom}}", "{{fillTo}}"], stroke: ["{{strokeFrom}}", "{{strokeTo}}"] }, delay: n9("pause"), ms: n9("morph"), ease: "in-out" },
5375
+ { id: "shape", attrs: { d: ["{{b}}", "{{a}}"], fill: ["{{fillTo}}", "{{fillFrom}}"], stroke: ["{{strokeTo}}", "{{strokeFrom}}"] }, delay: n9("back"), ms: n9("morph"), ease: "in-out" }
5230
5376
  ]
5231
5377
  }
5232
5378
  };
@@ -5317,7 +5463,7 @@ var morphSequenceEffect = {
5317
5463
  svg: `<svg viewBox="0 0 200 200"><!-- {{c0: color default=#38BDF8 label='Circle'}} {{c1: color default=#FACC15 label='Star'}} {{c2: color default=#F43F5E label='Heart'}} {{c3: color default=#A78BFA label='Hexagon'}} {{background: color default=#0F172A label='Background'}} {{morph: float min=200 max=3000 default=900 step=50 label='Morph ms'}} {{pause: float min=0 max=3000 default=500 step=50 label='Pause ms'}} --><rect width="200" height="200" fill="{{background}}"/><path id="shape" d="${SEQUENCE[0]}" fill="{{c0}}" fill-rule="evenodd"/></svg>`,
5318
5464
  animate: SEQUENCE.map((d, i) => {
5319
5465
  const next = (i + 1) % SEQUENCE.length;
5320
- return { id: "shape", attrs: { d: [d, SEQUENCE[next]], fill: [`{{c${i}}}`, `{{c${next}}}`] }, delay: n8(`at${i}`), ms: n8("morph"), ease: "in-out" };
5466
+ return { id: "shape", attrs: { d: [d, SEQUENCE[next]], fill: [`{{c${i}}}`, `{{c${next}}}`] }, delay: n9(`at${i}`), ms: n9("morph"), ease: "in-out" };
5321
5467
  })
5322
5468
  }
5323
5469
  };
@@ -5358,7 +5504,7 @@ var gooeyMorph = {
5358
5504
  motion: {
5359
5505
  svg: `<svg viewBox="0 0 200 200"><!-- {{colorA: color default=#22D3EE label='Colour A'}} {{colorB: color default=#A855F7 label='Colour B'}} {{background: color default=#0B0B1A label='Background'}} {{goo: float min=1 max=14 default=7 step=0.5 label='Gooeyness'}} {{sharp: float min=8 max=40 default=20 step=1 label='Edge sharpness'}} {{morph: float min=300 max=3000 default=1200 step=50 label='Morph ms'}} {{pause: float min=0 max=3000 default=400 step=50 label='Pause ms'}} --><defs><linearGradient id="gooFill" gradientUnits="userSpaceOnUse" x1="30" y1="30" x2="170" y2="170"><stop offset="0" stop-color="{{colorA}}"/><stop offset="1" stop-color="{{colorB}}"/></linearGradient><filter id="goo" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur in="SourceGraphic" stdDeviation="{{goo}}" result="soft"/><feColorMatrix in="soft" type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 {{sharp}} {{threshold}}"/></filter></defs><rect width="200" height="200" fill="{{background}}"/><g filter="url(#goo)" fill="url(#gooFill)">${GOO.map((stages2, i) => `<path id="goo${i}" d="${stages2[0]}" fill-rule="evenodd"/>`).join("")}</g></svg>`,
5360
5506
  animate: GOO.flatMap(
5361
- (stages2, i) => stages2.map((d, s) => ({ id: `goo${i}`, attrs: { d: [d, stages2[(s + 1) % stages2.length]] }, delay: n8(`at${s}`), ms: n8("morph"), ease: "in-out" }))
5507
+ (stages2, i) => stages2.map((d, s) => ({ id: `goo${i}`, attrs: { d: [d, stages2[(s + 1) % stages2.length]] }, delay: n9(`at${s}`), ms: n9("morph"), ease: "in-out" }))
5362
5508
  )
5363
5509
  }
5364
5510
  };
@@ -5410,10 +5556,10 @@ var particleMorph = {
5410
5556
  const back = 3e3;
5411
5557
  const spread = DOTS4 * DOT_STAGGER;
5412
5558
  const leg = (i, at3, from, mid, to, colours) => [
5413
- { id: `p${i}`, attrs: { cx: [n8(`${from}x${i}`), n8(`${mid}x${i}`)], cy: [n8(`${from}y${i}`), n8(`${mid}y${i}`)] }, delay: at3 + i * DOT_STAGGER, ms: 450, ease: "out" },
5414
- { id: `p${i}`, attrs: { cx: [n8(`${mid}x${i}`), n8(`${to}x${i}`)], cy: [n8(`${mid}y${i}`), n8(`${to}y${i}`)], fill: colours }, delay: at3 + 450 + i * DOT_STAGGER, ms: 650, ease: "in-out" }
5559
+ { 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" },
5560
+ { 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" }
5415
5561
  ];
5416
- const fade = (id, at3, show) => ({ id, attrs: { opacity: show ? [0, n8("ghost")] : [n8("ghost"), 0] }, delay: at3, ms: 450, ease: "in-out" });
5562
+ const fade = (id, at3, show) => ({ id, attrs: { opacity: show ? [0, n9("ghost")] : [n9("ghost"), 0] }, delay: at3, ms: 450, ease: "in-out" });
5417
5563
  return {
5418
5564
  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(
5419
5565
  { length: DOTS4 },
@@ -5501,10 +5647,10 @@ var digitMorph = {
5501
5647
  animate: Array.from({ length: SLOTS }, (_, j) => [
5502
5648
  ...Array.from({ length: MAX_STEPS }, (_2, i) => {
5503
5649
  const k = i + 1;
5504
- return { id: `s${j}`, attrs: { d: [`{{d${j}_${k - 1}}}`, `{{d${j}_${k}}}`], opacity: [n8(`o${j}_${k - 1}`), n8(`o${j}_${k}`)] }, delay: n8(`t${k}`), ms: n8("morph"), ease: "in-out" };
5650
+ return { id: `s${j}`, attrs: { d: [`{{d${j}_${k - 1}}}`, `{{d${j}_${k}}}`], opacity: [n9(`o${j}_${k - 1}`), n9(`o${j}_${k}`)] }, delay: n9(`t${k}`), ms: n9("morph"), ease: "in-out" };
5505
5651
  }),
5506
5652
  // Back to the first number in one morph, so it loops.
5507
- { id: `s${j}`, attrs: { d: [`{{d${j}_${MAX_STEPS}}}`, `{{d${j}_0}}`], opacity: [n8(`o${j}_${MAX_STEPS}`), n8(`o${j}_0`)] }, delay: n8("back"), ms: n8("morph"), ease: "in-out" }
5653
+ { id: `s${j}`, attrs: { d: [`{{d${j}_${MAX_STEPS}}}`, `{{d${j}_0}}`], opacity: [n9(`o${j}_${MAX_STEPS}`), n9(`o${j}_0`)] }, delay: n9("back"), ms: n9("morph"), ease: "in-out" }
5508
5654
  ]).flat()
5509
5655
  }
5510
5656
  };
@@ -16052,176 +16198,2153 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
16052
16198
  };
16053
16199
  var comicShaders = [comicBook, celShade, focusLines, actionLines, transitionComicPanels];
16054
16200
 
16055
- // src/catalog/types.ts
16056
- var EFFECT_CATEGORIES = [
16057
- "gradient",
16058
- "organic",
16059
- "pattern",
16060
- "light",
16061
- "space",
16062
- "retro",
16063
- "image",
16064
- "overlay",
16065
- "text",
16066
- "morph",
16067
- "interaction",
16068
- "pyrotechnics",
16069
- "timewarp",
16070
- "melt",
16071
- "electric",
16072
- "splatter",
16073
- "bubble",
16074
- "sticker",
16075
- "trail",
16076
- "particles",
16077
- "debris",
16078
- "impact",
16079
- "comic",
16080
- "transition",
16081
- "loader",
16082
- "data",
16083
- "icon"
16084
- ];
16085
- var VIDEO_USES = ["filter", "transition", "interactive", "stills", "none"];
16086
- var VECTOR_CONTROL_TYPES = ["float", "int", "bool", "color", "text"];
16087
- var PLACEHOLDER = /\{\{\s*([A-Za-z_]\w*)\s*(?::\s*([A-Za-z0-9]*)([^}]*))?\}\}/g;
16088
- function motionStrings(motion) {
16089
- const out = [];
16090
- const walk = (v) => {
16091
- if (typeof v === "string") out.push(v);
16092
- else if (Array.isArray(v)) v.forEach(walk);
16093
- else if (v && typeof v === "object") Object.values(v).forEach(walk);
16094
- };
16095
- walk(motion);
16096
- return out;
16097
- }
16098
- function vectorControls(motion) {
16099
- const controls = [];
16100
- const seen = /* @__PURE__ */ new Set();
16101
- for (const text of motionStrings(motion)) {
16102
- for (const [, name, type, attributes] of text.matchAll(PLACEHOLDER)) {
16103
- if (type === void 0 || seen.has(name) || !VECTOR_CONTROL_TYPES.includes(type)) continue;
16104
- seen.add(name);
16105
- const colour = type === "color";
16106
- controls.push(controlFromDeclaration(name, colour ? "vec3" : type, `${attributes}${colour ? " color" : ""}`, controls.length + 1));
16107
- }
16108
- }
16109
- return controls;
16110
- }
16111
- function vectorEffectControls(effect) {
16112
- const declared = vectorControls(effect.motion);
16113
- const names = new Set(declared.map((c) => c.name));
16114
- const listed = (effect.controls ?? []).filter((c) => !names.has(c.name));
16115
- return [...declared, ...listed.map((c, i) => vectorControlDef(c, declared.length + i + 1))];
16116
- }
16117
- function vectorControlIssues(motion) {
16118
- const issues = [];
16119
- const declarations = /* @__PURE__ */ new Map();
16120
- for (const text of motionStrings(motion)) {
16121
- for (const [whole, name, type] of text.matchAll(PLACEHOLDER)) {
16122
- if (type === void 0) continue;
16123
- if (!VECTOR_CONTROL_TYPES.includes(type))
16124
- issues.push(`${whole}: a control's type is one of ${VECTOR_CONTROL_TYPES.join(", ")}`);
16125
- else if (declarations.has(name) && declarations.get(name) !== whole)
16126
- issues.push(`"${name}" is declared twice; declare it once and write {{${name}}} everywhere else`);
16127
- else declarations.set(name, whole);
16128
- }
16129
- }
16130
- return issues;
16131
- }
16132
- function written(control, value) {
16133
- if (control.color && Array.isArray(value)) return rgbToHex(value);
16134
- if (Array.isArray(value)) return value.join(" ");
16135
- return value;
16136
- }
16137
- var escapeMarkup = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
16138
- function vectorControlDef(control, line = 0) {
16139
- const min = control.min ?? 0;
16140
- return {
16141
- name: control.name,
16142
- type: control.type,
16143
- label: control.label ?? control.name,
16144
- color: control.color ?? false,
16145
- min,
16146
- max: control.max ?? Math.max(min + 1, 1),
16147
- step: control.step ?? (control.type === "int" ? 1 : 0.01),
16148
- default: control.default,
16149
- line
16150
- };
16201
+ // src/catalog/shaders/atmosphere.ts
16202
+ var NOISE_3D = `
16203
+ float hash13(vec3 p3) {
16204
+ p3 = fract(p3 * 0.1031);
16205
+ p3 += dot(p3, p3.zyx + 31.32);
16206
+ return fract((p3.x + p3.y) * p3.z);
16151
16207
  }
16152
- var CURRENT_COLOR = "currentColor";
16153
- function isCurrentColor(value) {
16154
- return typeof value === "string" && value.trim().toLowerCase() === "currentcolor";
16208
+ float vnoise3(vec3 p) {
16209
+ vec3 i = floor(p);
16210
+ vec3 f = fract(p);
16211
+ vec3 u = f * f * (3.0 - 2.0 * f);
16212
+ float a = mix(hash13(i), hash13(i + vec3(1.0, 0.0, 0.0)), u.x);
16213
+ float b = mix(hash13(i + vec3(0.0, 1.0, 0.0)), hash13(i + vec3(1.0, 1.0, 0.0)), u.x);
16214
+ float c = mix(hash13(i + vec3(0.0, 0.0, 1.0)), hash13(i + vec3(1.0, 0.0, 1.0)), u.x);
16215
+ float d = mix(hash13(i + vec3(0.0, 1.0, 1.0)), hash13(i + vec3(1.0, 1.0, 1.0)), u.x);
16216
+ return mix(mix(a, b, u.y), mix(c, d, u.y), u.z);
16155
16217
  }
16156
- function renderVectorEffect(effect, values = {}) {
16157
- const filled = {};
16158
- const placeholders = /* @__PURE__ */ new Map();
16159
- for (const control of vectorEffectControls(effect)) {
16160
- const given = values[control.name];
16161
- if (control.color && isCurrentColor(given)) {
16162
- filled[control.name] = CURRENT_COLOR;
16163
- placeholders.set(control.name, CURRENT_COLOR);
16164
- continue;
16165
- }
16166
- const check = given === void 0 ? void 0 : checkControlValue(control, given);
16167
- filled[control.name] = check?.ok ? check.value : control.default;
16168
- placeholders.set(control.name, written(control, filled[control.name]));
16218
+ float fbm3(vec3 p, int octaves) {
16219
+ float sum = 0.0;
16220
+ float amp = 0.5;
16221
+ for (int i = 0; i < 6; i++) {
16222
+ if (i >= octaves) break;
16223
+ sum += amp * vnoise3(p);
16224
+ p = p * 2.03 + vec3(11.7, 3.1, 7.9);
16225
+ amp *= 0.5;
16169
16226
  }
16170
- for (const [name, value] of Object.entries(effect.derived?.(filled) ?? {})) placeholders.set(name, value);
16171
- const fill = (text, markup) => {
16172
- const whole = /^\{\{\s*([A-Za-z_]\w*)\s*(?::[^}]*)?\}\}$/.exec(text);
16173
- if (!markup && whole && placeholders.has(whole[1])) return placeholders.get(whole[1]);
16174
- return text.replace(PLACEHOLDER, (match, name) => {
16175
- if (!placeholders.has(name)) return match;
16176
- const value = String(placeholders.get(name));
16177
- return markup ? escapeMarkup(value) : value;
16178
- });
16179
- };
16180
- const walk = (v) => {
16181
- if (typeof v === "string") return fill(v, false);
16182
- if (Array.isArray(v)) return v.map(walk);
16183
- if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x)]));
16184
- return v;
16185
- };
16186
- const { svg, ...rest } = effect.motion;
16187
- return { svg: fill(svg.replace(/<!--[\s\S]*?-->/g, ""), true), ...walk(rest) };
16188
- }
16189
- function colorControl(name, hex, label) {
16190
- return { name, type: "vec3", color: true, default: hexToRgb(hex) ?? [1, 1, 1], label: label ?? name };
16227
+ return sum;
16228
+ }`.trim();
16229
+ var OVER10 = `
16230
+ vec4 over(vec4 acc, vec3 c, float a) {
16231
+ return vec4(acc.rgb * (1.0 - a) + c * a, acc.a * (1.0 - a) + a);
16191
16232
  }
16192
- function numberControl(name, value, min, max, label, step) {
16193
- return { name, type: "float", default: value, min, max, label: label ?? name, step };
16233
+ vec4 unpremultiply(vec4 acc) {
16234
+ return vec4(acc.rgb / max(acc.a, 1e-4), acc.a);
16235
+ }`.trim();
16236
+ var volumetricClouds = {
16237
+ kind: "shader",
16238
+ id: "volumetric-clouds",
16239
+ name: "Volumetric clouds",
16240
+ description: "A ray-marched layer of soft, self-shadowed clouds drifting overhead, rimmed with a silver lining toward the sun.",
16241
+ category: "atmosphere",
16242
+ tags: ["atmosphere", "clouds", "sky", "background", "3d", "weather"],
16243
+ source: glsl(
16244
+ `// Volumetric clouds: a cloud slab ray-marched with fbm density and a short march toward the sun.
16245
+ uniform vec3 uSkyTop; // @control color default=#3A78C9 label="Sky (top)"
16246
+ uniform vec3 uSkyHorizon; // @control color default=#B8D8F2 label="Sky (horizon)"
16247
+ uniform vec3 uCloudColor; // @control color default=#FFFFFF label="Cloud colour"
16248
+ uniform vec3 uShadowColor; // @control color default=#5E6F8A label="Shadow colour"
16249
+ uniform vec3 uSunColor; // @control color default=#FFF3DC label="Sun colour"
16250
+ uniform float uSunAzimuth; // @control min=-3.1416 max=3.1416 default=0.4 label="Sun direction"
16251
+ uniform float uSunHeight; // @control min=0 max=1.5 default=0.35 label="Sun height"
16252
+ uniform float uCoverage; // @control min=0 max=1 default=0.5 label="Coverage"
16253
+ uniform float uDensity; // @control min=0.1 max=3 default=1 label="Density"
16254
+ uniform float uSpeed; // @control min=0 max=3 default=0.6 label="Drift speed"`,
16255
+ NOISE_3D,
16256
+ `const float BOTTOM = 1.5;
16257
+ const float TOP = 3.2;
16258
+
16259
+ float cloudDensity(vec3 p, int octaves) {
16260
+ vec3 q = p * 0.42 + vec3(iTime * uSpeed * 0.12, 0.0, iTime * uSpeed * 0.05);
16261
+ float n = fbm3(q, octaves);
16262
+ float h = clamp((p.y - BOTTOM) / (TOP - BOTTOM), 0.0, 1.0);
16263
+ // Rounded tops and flat-ish bases.
16264
+ float shape = smoothstep(0.0, 0.15, h) * (1.0 - smoothstep(0.45, 1.0, h));
16265
+ return clamp((n - (1.0 - uCoverage) * 0.75) * 3.0 * shape, 0.0, 1.0) * uDensity;
16194
16266
  }
16195
16267
 
16196
- // src/catalog/text.ts
16197
- var r18 = (n10) => Math.round(n10 * 10) / 10;
16198
- function fit3(text, width, max, ems = textWidthEm(text)) {
16199
- const e = Math.max(0.6, ems);
16200
- const size = Math.min(max, width / e);
16201
- return { size: r18(size), length: r18(e * size), left: r18(200 - e * size / 2) };
16202
- }
16203
- var lettersOf = (text) => Array.from(String(text));
16204
- var mixHex2 = (a, b, t) => {
16205
- if (isCurrentColor(a) || isCurrentColor(b)) {
16206
- const css = (c) => isCurrentColor(c) ? CURRENT_COLOR : rgbToHex(Array.isArray(c) ? c : [1, 1, 1]);
16207
- return `color-mix(in srgb, ${css(a)}, ${css(b)} ${Math.round(t * 100)}%)`;
16268
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
16269
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
16270
+ vec3 rd = normalize(vec3(p.x, p.y + 0.28, 1.0));
16271
+ vec3 sun = normalize(vec3(sin(uSunAzimuth), uSunHeight, cos(uSunAzimuth)));
16272
+ float mu = dot(rd, sun);
16273
+
16274
+ vec3 sky = mix(uSkyHorizon, uSkyTop, pow(clamp(rd.y, 0.0, 1.0), 0.6));
16275
+ sky += uSunColor * (pow(max(mu, 0.0), 600.0) * 2.0 + pow(max(mu, 0.0), 12.0) * 0.25);
16276
+ if (rd.y < 0.0) sky = mix(uSkyHorizon, uShadowColor, clamp(-rd.y * 4.0, 0.0, 0.6));
16277
+
16278
+ vec3 col = vec3(0.0);
16279
+ float trans = 1.0;
16280
+ if (rd.y > 0.02) {
16281
+ float t0 = BOTTOM / rd.y;
16282
+ float t1 = min(TOP / rd.y, t0 + 14.0);
16283
+ float dt = (t1 - t0) / 40.0;
16284
+ float t = t0 + dt * fract(sin(dot(fragCoord, vec2(12.9898, 78.233))) * 43758.5453);
16285
+ // Henyey-Greenstein: a forward lobe for the silver lining plus a soft back lobe.
16286
+ float g = 0.65;
16287
+ float hg = (1.0 - g * g) / pow(1.0 + g * g - 2.0 * g * mu, 1.5) / 12.566;
16288
+ float phase = mix(0.08, hg * 4.0, 0.7);
16289
+ for (int i = 0; i < 40; i++) {
16290
+ if (trans < 0.02 || t > 60.0) break;
16291
+ vec3 pos = rd * t;
16292
+ int oct = t < 18.0 ? 5 : 3;
16293
+ float d = cloudDensity(pos, oct);
16294
+ if (d > 0.01) {
16295
+ // Light march toward the sun for self-shadowing.
16296
+ float shade = 0.0;
16297
+ for (int j = 1; j <= 4; j++) shade += cloudDensity(pos + sun * 0.22 * float(j), 3);
16298
+ float light = exp(-shade * 1.4);
16299
+ float powder = 1.0 - exp(-d * 4.0);
16300
+ vec3 lit = mix(uShadowColor, uCloudColor, light) + uSunColor * light * powder * phase * 2.5;
16301
+ float a = 1.0 - exp(-d * dt * 2.2);
16302
+ col += trans * a * lit;
16303
+ trans *= 1.0 - a;
16304
+ }
16305
+ t += dt;
16306
+ }
16307
+ // Far clouds fade into the horizon haze.
16308
+ float haze = exp(-max(t0 - 4.0, 0.0) * 0.06);
16309
+ col = mix(sky * (1.0 - trans), col, haze);
16208
16310
  }
16209
- const x = Array.isArray(a) ? a : [1, 1, 1];
16210
- const y = Array.isArray(b) ? b : [1, 1, 1];
16211
- return rgbToHex([0, 1, 2].map((i) => x[i] + (y[i] - x[i]) * t));
16311
+ fragColor = vec4(sky * trans + col, 1.0);
16312
+ }`
16313
+ ),
16314
+ presets: [
16315
+ { name: "Midday", values: { uSunHeight: 1.2, uCoverage: 0.45, uDensity: 0.9 } },
16316
+ {
16317
+ name: "Sunset",
16318
+ values: {
16319
+ uSkyTop: [0.24, 0.27, 0.52],
16320
+ uSkyHorizon: [1, 0.62, 0.38],
16321
+ uCloudColor: [1, 0.78, 0.6],
16322
+ uShadowColor: [0.36, 0.25, 0.38],
16323
+ uSunColor: [1, 0.55, 0.25],
16324
+ uSunAzimuth: 0,
16325
+ uSunHeight: 0.08,
16326
+ uCoverage: 0.5
16327
+ }
16328
+ },
16329
+ {
16330
+ name: "Storm",
16331
+ values: {
16332
+ uSkyTop: [0.22, 0.25, 0.3],
16333
+ uSkyHorizon: [0.45, 0.48, 0.52],
16334
+ uCloudColor: [0.62, 0.65, 0.7],
16335
+ uShadowColor: [0.12, 0.13, 0.17],
16336
+ uSunColor: [0.75, 0.8, 0.9],
16337
+ uCoverage: 0.85,
16338
+ uDensity: 2.2,
16339
+ uSpeed: 1.8
16340
+ }
16341
+ }
16342
+ ]
16212
16343
  };
16213
- var holds = (id, attr, keys2) => keys2.map(([delay, value]) => ({ id, attrs: { [attr]: [value, value] }, delay, ms: 0 }));
16214
- var n9 = (name) => `{{${name}}}`;
16344
+ var fogBanks = {
16345
+ kind: "shader",
16346
+ id: "fog-banks",
16347
+ name: "Fog banks",
16348
+ description: "Horizontal banks of fog drifting at different depths and speeds, thickest near the ground, to lay over any scene.",
16349
+ category: "atmosphere",
16350
+ tags: ["atmosphere", "fog", "overlay", "moody", "weather"],
16351
+ transparent: true,
16352
+ source: glsl(
16353
+ `// Fog banks: layered bands of fbm fog sliding sideways, near ones lower, faster and denser.
16354
+ uniform vec3 uColor; // @control color default=#DCE3EA label="Colour"
16355
+ uniform float uDensity; // @control min=0 max=1.5 default=0.8 label="Density"
16356
+ uniform float uHeight; // @control min=0.1 max=1 default=0.55 label="Height"
16357
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
16358
+ uniform int uLayers; // @control min=1 max=6 default=4 label="Layers"`,
16359
+ HASH,
16360
+ NOISE,
16361
+ FBM,
16362
+ OVER10,
16363
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
16364
+ vec2 p = fragCoord / iResolution.y;
16365
+ float y = fragCoord.y / iResolution.y;
16366
+ vec4 acc = vec4(0.0);
16367
+ // Far layers first (high on screen, slow, thin), near layers painted over them.
16368
+ for (int i = 0; i < 6; i++) {
16369
+ if (i >= uLayers) break;
16370
+ float fi = float(i);
16371
+ float near = (fi + 1.0) / float(uLayers);
16372
+ float centre = uHeight * mix(0.85, 0.12, near);
16373
+ float thick = uHeight * mix(0.12, 0.3, near);
16374
+ float band = exp(-pow((y - centre) / thick, 2.0));
16375
+ band = max(band, step(y, centre) * mix(0.3, 1.0, near));
16376
+ float scale = mix(1.2, 2.6, near);
16377
+ vec2 q = vec2(p.x * scale + iTime * uSpeed * mix(0.015, 0.08, near) + fi * 7.3, p.y * scale * 3.0 + fi * 2.1);
16378
+ float n = fbm(q + 0.6 * vec2(fbm(q * 0.7 - iTime * 0.02, 3), 0.0), 5);
16379
+ float a = smoothstep(0.3, 0.8, n) * band * uDensity * mix(0.35, 0.8, near);
16380
+ // Ground haze: everything thickens toward the bottom.
16381
+ a = clamp(a + (1.0 - smoothstep(0.0, uHeight * 0.4, y)) * 0.12 * uDensity * near, 0.0, 1.0);
16382
+ vec3 c = uColor * mix(0.85, 1.0, n);
16383
+ acc = over(acc, c, a);
16384
+ }
16385
+ fragColor = unpremultiply(acc);
16386
+ }`
16387
+ ),
16388
+ presets: [
16389
+ { name: "Valley morning", values: { uColor: [0.96, 0.93, 0.88], uDensity: 0.7, uHeight: 0.45, uSpeed: 0.6 } },
16390
+ { name: "Moor", values: { uColor: [0.66, 0.72, 0.75], uDensity: 1.2, uHeight: 0.8, uSpeed: 1.4, uLayers: 6 } }
16391
+ ]
16392
+ };
16393
+ var risingSteam = {
16394
+ kind: "shader",
16395
+ id: "rising-steam",
16396
+ name: "Rising steam",
16397
+ description: "Wisps of steam curling up from a cup or vent, thinning and fading as they rise.",
16398
+ category: "atmosphere",
16399
+ tags: ["atmosphere", "steam", "overlay", "coffee", "warm"],
16400
+ transparent: true,
16401
+ source: glsl(
16402
+ `// Rising steam: stretched noise wisps advected upward from a source, swaying and dissolving.
16403
+ uniform vec2 uSource; // @control min=-1 max=1 default=0,-0.6 label="Source"
16404
+ uniform vec3 uColor; // @control color default=#FFFFFF label="Colour"
16405
+ uniform float uWidth; // @control min=0.02 max=0.6 default=0.12 label="Width"
16406
+ uniform float uHeight; // @control min=0.2 max=1.5 default=0.8 label="Height"
16407
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
16408
+ uniform float uTurbulence; // @control min=0 max=2 default=0.8 label="Curl"
16409
+ uniform float uOpacity; // @control min=0 max=1 default=0.7 label="Opacity"`,
16410
+ HASH,
16411
+ NOISE,
16412
+ FBM,
16413
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
16414
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
16415
+ float aspect = iResolution.x / iResolution.y;
16416
+ vec2 src = uSource * vec2(aspect, 1.0) * 0.5;
16417
+ vec2 q = p - src;
16418
+ float h = q.y / uHeight;
16419
+ if (h < -0.05 || h > 1.2) { fragColor = vec4(uColor, 0.0); return; }
16420
+ float hc = clamp(h, 0.0, 1.0);
16421
+ float t = iTime * uSpeed;
16422
+ // The plume widens and sways more as it rises.
16423
+ float w = uWidth * (0.6 + 1.6 * hc);
16424
+ float sway = (sin(q.y * 5.0 - t * 1.1) * 0.5 + sin(q.y * 9.0 - t * 1.7 + 1.3) * 0.3) * uTurbulence * 0.06 * hc;
16425
+ vec2 flow = vec2(q.x, q.y - t * 0.25);
16426
+ vec2 warp = vec2(fbm(flow * 3.0 + vec2(0.0, -t * 0.2), 4), fbm(flow * 3.0 + vec2(5.2, 1.3), 4)) - 0.5;
16427
+ float x = (q.x - sway + warp.x * uTurbulence * 0.25 * hc) / w;
16428
+ // Wisps: noise stretched along the rise, so it reads as strands.
16429
+ vec2 sq = vec2(x * 2.2, (q.y - t * 0.3) / uHeight * 1.6) + warp * uTurbulence * 1.5;
16430
+ float strands = fbm(sq * vec2(1.0, 0.45), 5);
16431
+ float body = exp(-x * x * 1.6);
16432
+ float wisp = smoothstep(0.42, 0.75, strands + 0.25 * body);
16433
+ float a = wisp * body;
16434
+ // Born at the source, thinning and fading as it rises.
16435
+ a *= smoothstep(-0.05, 0.12, h) * (1.0 - smoothstep(0.35, 1.05, h));
16436
+ a *= mix(1.0, 0.55, hc);
16437
+ fragColor = vec4(uColor, clamp(a * uOpacity * 1.4, 0.0, 1.0));
16438
+ }`
16439
+ ),
16440
+ presets: [
16441
+ { name: "Coffee cup", values: { uSource: [0, -0.5], uWidth: 0.08, uHeight: 0.6, uSpeed: 0.7, uTurbulence: 1, uOpacity: 0.55 } },
16442
+ { name: "Vent", values: { uSource: [0, -1], uWidth: 0.3, uHeight: 1.3, uSpeed: 1.4, uTurbulence: 0.6, uOpacity: 0.8 } },
16443
+ { name: "Kettle burst", values: { uSource: [-0.4, -0.3], uWidth: 0.05, uHeight: 1.1, uSpeed: 2.6, uTurbulence: 1.8, uOpacity: 0.95 } }
16444
+ ]
16445
+ };
16446
+ var mistOverImage = {
16447
+ kind: "shader",
16448
+ id: "mist-over-image",
16449
+ name: "Mist over image",
16450
+ description: "Slow mist drifting over a picture, thickest low down and in the distance, with light softly scattering through it.",
16451
+ category: "atmosphere",
16452
+ video: "filter",
16453
+ tags: ["atmosphere", "mist", "image", "fog", "moody", "photo"],
16454
+ source: glsl(
16455
+ `// Mist over image: drifting fbm haze over a picture, weighted to the ground and the horizon, with a soft bloom.
16456
+ uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.landscape} label="Image"
16457
+ uniform vec3 uMistColor; // @control color default=#E4EAF0 label="Mist colour"
16458
+ uniform float uDensity; // @control min=0 max=1 default=0.6 label="Density"
16459
+ uniform float uHorizon; // @control min=0 max=1 default=0.4 label="Horizon"
16460
+ uniform float uGround; // @control min=0 max=1 default=0.6 label="Ground mist"
16461
+ uniform float uScatter; // @control min=0 max=1.5 default=0.6 label="Light scatter"
16462
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Drift speed"
16463
+ ${BACKDROP_CONTROLS}`,
16464
+ PICTURE,
16465
+ HASH,
16466
+ NOISE,
16467
+ FBM,
16468
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
16469
+ vec2 uv = fragCoord / iResolution.xy;
16470
+ vec4 pic = pictureRGBA(uImage, uv);
16471
+ float aspect = iResolution.x / iResolution.y;
16472
+ float t = iTime * uSpeed;
16473
+
16474
+ // Depth proxy: hazier toward the horizon (far away) and right at the bottom (ground mist).
16475
+ float far = exp(-pow((uv.y - uHorizon) / 0.18, 2.0));
16476
+ float ground = (1.0 - smoothstep(0.0, 0.45, uv.y)) * uGround;
16477
+ vec2 q = vec2(uv.x * aspect, uv.y);
16478
+ float n1 = fbm(vec2(q.x * 2.0 - t * 0.04, q.y * 6.0) + 3.0, 5);
16479
+ float n2 = fbm(vec2(q.x * 3.5 + t * 0.025, q.y * 9.0 - t * 0.01) + 11.0, 4);
16480
+ float drift = smoothstep(0.25, 0.8, 0.6 * n1 + 0.4 * n2);
16481
+ float mist = clamp((far * 0.7 + ground) * mix(0.45, 1.25, drift) + 0.12, 0.0, 1.0) * uDensity;
16482
+
16483
+ // Soft scattering: bright parts of the picture bleed into the mist around them.
16484
+ vec3 glow = vec3(0.0);
16485
+ for (int i = 0; i < 8; i++) {
16486
+ float a = float(i) * 0.785398 + 0.4;
16487
+ vec2 o = vec2(cos(a), sin(a) * aspect) * 0.035;
16488
+ vec3 s = picture(uImage, uv + o);
16489
+ vec3 s2 = picture(uImage, uv + o * 2.4);
16490
+ glow += max(s - 0.55, 0.0) + max(s2 - 0.55, 0.0) * 0.6;
16491
+ }
16492
+ glow /= 8.0;
16493
+ vec3 haze = uMistColor + glow * uScatter * 1.6;
16494
+ // Mist lifts the blacks and washes out the colour behind it.
16495
+ vec3 col = mix(pic.rgb, haze, mist);
16496
+ col += glow * uScatter * 0.5 * (1.0 - mist);
16497
+ fragColor = vec4(col, pic.a);
16498
+ }`
16499
+ ),
16500
+ presets: [
16501
+ { name: "Dawn", values: { uMistColor: [1, 0.9, 0.8], uDensity: 0.7, uScatter: 1.1, uGround: 0.7 } },
16502
+ { name: "Cold valley", values: { uMistColor: [0.78, 0.84, 0.9], uDensity: 0.85, uHorizon: 0.5, uScatter: 0.3, uSpeed: 0.6 } }
16503
+ ]
16504
+ };
16505
+ var skyTimelapse = {
16506
+ kind: "shader",
16507
+ id: "sky-timelapse",
16508
+ name: "Sky timelapse",
16509
+ description: "A timelapse sky: layers of clouds racing over at different speeds as the light turns from day to golden hour and back.",
16510
+ category: "atmosphere",
16511
+ tags: ["atmosphere", "clouds", "sky", "background", "timelapse"],
16512
+ source: glsl(
16513
+ `// Sky timelapse: parallax cloud layers on perspective planes, under a sky cycling through the day.
16514
+ uniform vec3 uDayTop; // @control color default=#2F6FD0 label="Day sky (top)"
16515
+ uniform vec3 uDayHorizon; // @control color default=#A9D3F5 label="Day sky (horizon)"
16516
+ uniform vec3 uGoldTop; // @control color default=#3E3A7A label="Golden sky (top)"
16517
+ uniform vec3 uGoldHorizon; // @control color default=#FFA45C label="Golden sky (horizon)"
16518
+ uniform float uSpeed; // @control min=0 max=4 default=1.5 label="Cloud speed"
16519
+ uniform float uCycle; // @control min=0 max=1 default=0.08 label="Day cycle speed"
16520
+ uniform float uCoverage; // @control min=0 max=1 default=0.5 label="Coverage"
16521
+ uniform int uLayers; // @control min=1 max=4 default=3 label="Layers"`,
16522
+ HASH,
16523
+ NOISE,
16524
+ FBM,
16525
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
16526
+ vec2 uv = fragCoord / iResolution.xy;
16527
+ float x = (fragCoord.x - 0.5 * iResolution.x) / iResolution.y;
16528
+ float y = uv.y;
16529
+ // 0 at midday, 1 at golden hour.
16530
+ float gold = 0.5 - 0.5 * cos(iTime * uCycle * 6.28318);
16531
+ vec3 top = mix(uDayTop, uGoldTop, gold);
16532
+ vec3 horizon = mix(uDayHorizon, uGoldHorizon, gold);
16533
+ vec3 sky = mix(horizon, top, pow(y, 0.7));
16534
+ vec2 sunPos = vec2(0.35, mix(0.85, 0.08, gold));
16535
+ float sd = length(vec2(x, y) - sunPos);
16536
+ vec3 sunCol = mix(vec3(1.0, 0.97, 0.9), vec3(1.0, 0.6, 0.3), gold);
16537
+ sky += sunCol * (smoothstep(0.045, 0.04, sd) + exp(-sd * 6.0) * 0.35);
16538
+
16539
+ vec3 litDay = vec3(1.0);
16540
+ vec3 litGold = vec3(1.0, 0.72, 0.5);
16541
+ vec3 shadowDay = vec3(0.6, 0.66, 0.76);
16542
+ vec3 shadowGold = vec3(0.42, 0.3, 0.42);
16543
+ vec3 col = sky;
16544
+ // Layer 0 is the highest: farthest, smallest-looking and slowest across the screen.
16545
+ for (int i = 0; i < 4; i++) {
16546
+ if (i >= uLayers) break;
16547
+ float fi = float(i);
16548
+ float alt = mix(2.4, 0.7, fi / 3.0);
16549
+ float z = alt / (y + 0.12);
16550
+ vec2 plane = vec2(x * z, z);
16551
+ plane += vec2(iTime * uSpeed * 0.35, iTime * uSpeed * 0.12) + fi * 13.1;
16552
+ float scale = mix(0.9, 0.5, fi / 3.0);
16553
+ vec2 cq = plane * scale;
16554
+ int oct = z > 8.0 ? 3 : 5;
16555
+ float n = fbm(cq, oct);
16556
+ float cover = mix(0.75, 0.35, uCoverage);
16557
+ float a = smoothstep(cover, cover + 0.22, n);
16558
+ // Toward the sun: a lighter side; a sample offset sunward darkens bellies.
16559
+ float n2 = fbm(cq + vec2(0.08, 0.12), oct);
16560
+ float shade = clamp(0.5 + (n - n2) * 6.0, 0.0, 1.0);
16561
+ vec3 lit = mix(litDay, litGold, gold);
16562
+ vec3 shadow = mix(shadowDay, shadowGold, gold) * mix(1.0, 0.8, fi / 3.0);
16563
+ vec3 cc = mix(shadow, lit, shade);
16564
+ cc += sunCol * exp(-sd * 3.0) * 0.4 * (1.0 - a);
16565
+ // Clouds dissolve into the horizon haze.
16566
+ a *= smoothstep(0.0, 0.25, y);
16567
+ col = mix(col, mix(cc, horizon, exp(-y * 8.0) * 0.6), a);
16568
+ }
16569
+ fragColor = vec4(col, 1.0);
16570
+ }`
16571
+ ),
16572
+ presets: [
16573
+ { name: "Fair weather", values: { uSpeed: 1.2, uCoverage: 0.35, uCycle: 0.05 } },
16574
+ { name: "Racing clouds", values: { uSpeed: 3.5, uCoverage: 0.65, uCycle: 0.15, uLayers: 4 } }
16575
+ ]
16576
+ };
16577
+ var smokeCurl = {
16578
+ kind: "shader",
16579
+ id: "smoke-curl",
16580
+ name: "Smoke curl",
16581
+ description: "A thin thread of incense or cigarette smoke rising smooth and straight, then breaking into curls and turbulence.",
16582
+ category: "atmosphere",
16583
+ tags: ["atmosphere", "smoke", "incense", "overlay", "elegant", "moody"],
16584
+ transparent: true,
16585
+ source: glsl(
16586
+ `// Smoke curl: a laminar filament whose path and strands are warped more the higher it rises.
16587
+ uniform vec2 uSource; // @control min=-1 max=1 default=0,-0.85 label="Source"
16588
+ uniform vec3 uColor; // @control color default=#D9DEE6 label="Colour"
16589
+ uniform float uHeight; // @control min=0.3 max=1.6 default=1.1 label="Height"
16590
+ uniform float uBreakup; // @control min=0 max=1 default=0.35 label="Breakup height"
16591
+ uniform float uTurbulence; // @control min=0 max=2 default=1 label="Turbulence"
16592
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
16593
+ uniform float uThickness; // @control min=0.3 max=3 default=1 label="Thickness"
16594
+ uniform float uOpacity; // @control min=0 max=1 default=0.85 label="Opacity"`,
16595
+ HASH,
16596
+ NOISE,
16597
+ FBM,
16598
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
16599
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
16600
+ float px = 1.0 / iResolution.y;
16601
+ float aspect = iResolution.x / iResolution.y;
16602
+ vec2 src = uSource * vec2(aspect, 1.0) * 0.5;
16603
+ vec2 q = p - src;
16604
+ float h = q.y / uHeight;
16605
+ if (h < -0.02 || h > 1.15) { fragColor = vec4(uColor, 0.0); return; }
16606
+ float hc = clamp(h, 0.0, 1.0);
16607
+ float t = iTime * uSpeed;
16608
+ // 0 while the stream is laminar, rising to 1 where it has broken up.
16609
+ float turb = smoothstep(uBreakup - 0.05, uBreakup + 0.45, hc) * uTurbulence;
16610
+
16611
+ // Advected warp: noise carried up with the smoke, swirling the filament into curls.
16612
+ vec2 fq = vec2(q.x * 4.0, q.y * 3.0 - t * 0.6);
16613
+ vec2 warp = vec2(fbm(fq, 4), fbm(fq + vec2(4.7, 1.9), 4)) - 0.5;
16614
+ vec2 warp2 = vec2(fbm(fq * 2.3 + warp * 2.0, 3), fbm(fq * 2.3 + vec2(2.1, 8.4) - warp * 2.0, 3)) - 0.5;
16615
+ vec2 w = q + (warp * 0.18 + warp2 * 0.08) * turb * (0.4 + hc);
16616
+
16617
+ // The base path: a gentle sway low down, more after the breakup.
16618
+ float sway = 0.012 * sin(q.y * 7.0 - t * 1.2) * hc + turb * 0.05 * sin(q.y * 4.0 - t * 0.9 + 1.7);
16619
+ float a = 0.0;
16620
+ for (int i = 0; i < 3; i++) {
16621
+ float fi = float(i);
16622
+ // The strands start as one and peel apart above the breakup.
16623
+ float split = (fi - 1.0) * turb * 0.035 * hc * (1.0 + sin(w.y * 6.0 + fi * 2.3 - t * 0.7));
16624
+ float d = abs(w.x - sway - split);
16625
+ float width = (mix(0.0015, 0.016, hc) + turb * 0.008 * hc) * uThickness + px * 0.75;
16626
+ float strand = exp(-d * d / (width * width));
16627
+ a += strand * (fi == 1.0 ? 1.0 : turb * 0.6);
16628
+ }
16629
+ // A faint diffuse veil around the curls, and dissolving gaps higher up.
16630
+ float veil = smoothstep(0.45, 0.8, fbm(w * 6.0 + vec2(0.0, -t * 0.4), 4)) * turb * 0.25 * hc;
16631
+ a = a * mix(1.0, 0.5, hc) + veil;
16632
+ float gaps = mix(1.0, smoothstep(0.25, 0.6, fbm(w * 3.0 + vec2(1.3, -t * 0.5), 3)), hc * min(turb, 1.0));
16633
+ a *= gaps;
16634
+ a *= smoothstep(-0.02, 0.01, h) * (1.0 - smoothstep(0.6, 1.1, h));
16635
+ fragColor = vec4(uColor, clamp(a * uOpacity, 0.0, 1.0));
16636
+ }`
16637
+ ),
16638
+ presets: [
16639
+ { name: "Incense", values: { uColor: [0.86, 0.88, 0.92], uBreakup: 0.45, uTurbulence: 0.8, uSpeed: 0.7, uThickness: 0.8 } },
16640
+ { name: "Draughty room", values: { uBreakup: 0.15, uTurbulence: 1.8, uSpeed: 1.4, uThickness: 1.3 } }
16641
+ ]
16642
+ };
16643
+ var dryIceFog = {
16644
+ kind: "shader",
16645
+ id: "dry-ice-fog",
16646
+ name: "Dry ice fog",
16647
+ description: "Dense, low stage fog rolling along the floor and spilling over the edge in a slow, tumbling curtain.",
16648
+ category: "atmosphere",
16649
+ tags: ["atmosphere", "fog", "overlay", "stage", "spooky", "concert"],
16650
+ transparent: true,
16651
+ source: glsl(
16652
+ `// Dry ice fog: rolling fbm fog hugging a floor, pouring over its edge and pooling below.
16653
+ uniform vec3 uColor; // @control color default=#EEF2F7 label="Colour"
16654
+ uniform float uFloor; // @control min=0 max=0.8 default=0.38 label="Floor height"
16655
+ uniform float uEdge; // @control min=0 max=1.2 default=0.68 label="Edge position"
16656
+ uniform float uDepth; // @control min=0.03 max=0.5 default=0.14 label="Fog depth"
16657
+ uniform float uDensity; // @control min=0 max=1.5 default=1 label="Density"
16658
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Flow speed"
16659
+ uniform float uTurbulence; // @control min=0 max=2 default=1 label="Turbulence"`,
16660
+ HASH,
16661
+ NOISE,
16662
+ FBM,
16663
+ `float rolling(vec2 q, float t) {
16664
+ vec2 warp = vec2(fbm(q * 0.8 + vec2(-t * 0.3, t * 0.1), 4), fbm(q * 0.8 + vec2(3.1, -t * 0.2), 4)) - 0.5;
16665
+ return fbm(q + warp * 2.2 * uTurbulence, 5);
16666
+ }
16667
+
16668
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
16669
+ vec2 uv = fragCoord / iResolution.xy;
16670
+ float aspect = iResolution.x / iResolution.y;
16671
+ float x = uv.x;
16672
+ float y = uv.y;
16673
+ float t = iTime * uSpeed;
16674
+ float a = 0.0;
16675
+
16676
+ // On the stage: a dense layer above the floor, billowing higher in places, soft at the top.
16677
+ float stage = 1.0 - smoothstep(uEdge - 0.04, uEdge + 0.01, x);
16678
+ float n = 0.5;
16679
+ if (stage > 0.0) {
16680
+ float hy = (y - uFloor) / uDepth;
16681
+ n = rolling(vec2(x * aspect * 3.0 - t * 0.35, max(hy, 0.0) * 0.9 + t * 0.05), t);
16682
+ float d = n * 1.5 + 0.15 - max(hy, 0.0) * 0.8 - max(-hy, 0.0) * 2.5;
16683
+ a = max(a, smoothstep(0.0, 0.7, d) * stage);
16684
+ }
16685
+ // Over the edge: a curtain falling in an arc, widening as it drops, pooling on the ground.
16686
+ if (x > uEdge - 0.04) {
16687
+ float dx = max(x - uEdge, 0.0) * aspect;
16688
+ float fall = uFloor - dx * dx * 6.0 - dx * 0.4;
16689
+ float spread = uDepth * (0.5 + 2.0 * max(uFloor - fall, 0.0));
16690
+ float off = (y - fall) / spread;
16691
+ float nc = rolling(vec2(dx * 4.0 + off * 0.3, y * 3.0 + t * 0.6), t);
16692
+ float curtain = smoothstep(0.0, 0.7, nc * 1.5 + 0.1 - abs(off) * 0.9) * smoothstep(uEdge - 0.04, uEdge, x);
16693
+ curtain *= 1.0 - smoothstep(uDepth * 0.5, uDepth * 1.5, y - uFloor);
16694
+ float np = rolling(vec2(x * aspect * 3.0 - t * 0.2, y / uDepth * 0.8), t);
16695
+ float pool = smoothstep(0.0, 0.7, np * 1.5 + 0.1 - y / (uDepth * 0.8) * 0.8) * smoothstep(uEdge, uEdge + 0.1, x);
16696
+ float spill = max(curtain, pool * 0.85);
16697
+ if (spill > a) n = mix(n, x > uEdge + 0.05 ? max(nc, np) : nc, 0.5);
16698
+ a = max(a, spill);
16699
+ }
16700
+ a = clamp(a * uDensity, 0.0, 1.0);
16701
+ // Denser billows catch more light; thin edges fall slightly into shadow.
16702
+ vec3 c = uColor * mix(0.8, 1.05, smoothstep(0.3, 0.75, n));
16703
+ fragColor = vec4(c, a);
16704
+ }`
16705
+ ),
16706
+ presets: [
16707
+ { name: "Concert stage", values: { uFloor: 0.25, uEdge: 1.2, uDepth: 0.2, uDensity: 1.2, uSpeed: 0.8 } },
16708
+ { name: "Haunted ledge", values: { uColor: [0.72, 0.9, 0.82], uFloor: 0.5, uEdge: 0.55, uDepth: 0.1, uTurbulence: 1.6 } }
16709
+ ]
16710
+ };
16711
+ var atmosphereShaders = [
16712
+ volumetricClouds,
16713
+ fogBanks,
16714
+ risingSteam,
16715
+ mistOverImage,
16716
+ skyTimelapse,
16717
+ smokeCurl,
16718
+ dryIceFog
16719
+ ];
16720
+
16721
+ // src/catalog/shaders/glow.ts
16722
+ var GLOW7 = `
16723
+ vec4 glow(vec3 c) {
16724
+ c = 1.0 - exp(-max(c, 0.0));
16725
+ float a = max(c.r, max(c.g, c.b));
16726
+ return vec4(c / max(a, 1e-4), a);
16727
+ }`.trim();
16728
+ var MASK = `
16729
+ vec3 gKeyColour;
16730
+ float gKeyTolerance;
16731
+
16732
+ // The colour in the picture's top-left corner: the ground to key out.
16733
+ void setKey(float tolerance) {
16734
+ gKeyTolerance = tolerance;
16735
+ vec4 c = textureLod(uMask, vec2(0.01, 0.99), 0.0);
16736
+ gKeyColour = c.rgb / max(c.a, 1e-3);
16737
+ }
16738
+
16739
+ // Where frame point p falls in the picture (xy) and texels per frame unit (z).
16740
+ vec3 maskPlace(vec2 p) {
16741
+ vec2 size = vec2(textureSize(uMask, 0));
16742
+ float aspect = size.x / size.y;
16743
+ float h = min(uSize, iResolution.x / iResolution.y * uSize / aspect);
16744
+ return vec3(p / vec2(h * aspect, h) + 0.5, size.y / h);
16745
+ }
16746
+
16747
+ float shapeAt(vec2 p, float blur) {
16748
+ if (textureSize(uMask, 0).x <= 1) {
16749
+ float d = length(p) - 0.3 * uSize;
16750
+ return clamp(0.5 - d / max(blur, 0.004), 0.0, 1.0);
16751
+ }
16752
+ vec3 m = maskPlace(p);
16753
+ if (m.x < 0.0 || m.x > 1.0 || m.y < 0.0 || m.y > 1.0) return 0.0;
16754
+ vec4 c = textureLod(uMask, m.xy, log2(max(blur * m.z, 1.0)));
16755
+ float a = c.a;
16756
+ if (gKeyTolerance > 0.0) {
16757
+ float away = distance(c.rgb / max(c.a, 1e-3), gKeyColour);
16758
+ a *= smoothstep(gKeyTolerance, gKeyTolerance + 0.12, away);
16759
+ }
16760
+ return a;
16761
+ }
16762
+
16763
+ float coverage(vec2 p, float radius) {
16764
+ float sum = 0.0;
16765
+ for (int i = 0; i < 16; i++) {
16766
+ float r = radius * sqrt((float(i) + 0.5) / 16.0);
16767
+ float a = float(i) * 2.39996;
16768
+ sum += shapeAt(p + r * vec2(cos(a), sin(a)), radius * 0.4);
16769
+ }
16770
+ return sum / 16.0;
16771
+ }`.trim();
16772
+ var aura = {
16773
+ kind: "shader",
16774
+ id: "aura",
16775
+ name: "Aura",
16776
+ description: "A silhouette wreathed in an energetic aura, flame-like wisps of light rising off its outline and pulsing.",
16777
+ category: "glow",
16778
+ video: "stills",
16779
+ tags: ["glow", "aura", "energy", "flame", "silhouette", "anime", "power-up"],
16780
+ source: glsl(
16781
+ `// Aura: a blurred silhouette, lifted and torn by rising fbm, read as tongues of light round the outline.
16782
+ uniform sampler2D uMask; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Silhouette"
16783
+ uniform float uKey; // @control min=0 max=0.5 default=0.12 label="Key out the corner colour (0 off)"
16784
+ uniform float uSize; // @control min=0.2 max=1 default=1 label="Size"
16785
+ uniform vec3 uColor; // @control color default=#FFC93C label="Aura"
16786
+ uniform vec3 uCore; // @control color default=#FFF6D5 label="Core"
16787
+ uniform vec3 uBackground; // @control color default=#0B0A12 label="Background"
16788
+ uniform float uHeight; // @control min=0 max=0.4 default=0.16 label="Flame height"
16789
+ uniform float uSpread; // @control min=0.01 max=0.15 default=0.06 label="Spread"
16790
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
16791
+ uniform float uPulse; // @control min=0 max=1 default=0.4 label="Pulse"
16792
+ uniform float uIntensity; // @control min=0 max=3 default=1.1 label="Intensity"`,
16793
+ HASH,
16794
+ NOISE,
16795
+ FBM,
16796
+ MASK,
16797
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
16798
+ setKey(uKey);
16799
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
16800
+ float t = iTime * uSpeed;
16801
+ float px = 1.0 / iResolution.y;
16802
+ // Rising noise: tongues flow up and sway.
16803
+ vec2 q = p * 6.0;
16804
+ float n = fbm(vec2(q.x, q.y - t * 2.0), 5);
16805
+ float n2 = fbm(vec2(q.x * 1.7 + 4.1, q.y * 0.9 - t * 2.9), 4);
16806
+ float pulse = 1.0 + uPulse * (0.3 * sin(t * 3.0) + 0.12 * sin(t * 7.3 + 1.0));
16807
+ // The silhouette blurred at three widths, swayed and stretched upward by the noise, so light gathers above it.
16808
+ vec2 warp = vec2((n2 - 0.5) * uSpread * 1.5, -(n - 0.3) * uHeight);
16809
+ float inner = coverage(p + warp * 0.3, uSpread * 0.5);
16810
+ float outer = coverage(p + warp, uSpread * 1.4);
16811
+ float tall = coverage(p + warp * 1.5 - vec2(0.0, uHeight), uSpread * 2.2);
16812
+ float field = (inner * 0.9 + outer * 0.8 + tall * 0.6) * pulse;
16813
+ // Torn into tongues: the noise breaks the thin outer field into licks, the thick inner field holds.
16814
+ float tongues = smoothstep(0.12, 0.95, field * (0.45 + 1.1 * n));
16815
+ float wisps = smoothstep(0.55, 0.75, n2) * smoothstep(0.02, 0.3, tall) * 0.5;
16816
+ float body = shapeAt(p, 1.5 * px);
16817
+ vec3 light = uColor * (tongues * 1.3 + wisps + field * 0.25) + uCore * smoothstep(0.5, 0.9, inner * pulse) * 0.8;
16818
+ light *= uIntensity;
16819
+ vec3 col = uBackground + (1.0 - exp(-light));
16820
+ // The figure in front, its own colours, rim-lit by the aura.
16821
+ if (textureSize(uMask, 0).x > 1 && body > 0.0) {
16822
+ vec3 m = maskPlace(p);
16823
+ vec4 c = texture(uMask, m.xy);
16824
+ vec3 own = c.rgb / max(c.a, 1e-3);
16825
+ float rim = 1.0 - coverage(p, uSpread * 0.25);
16826
+ vec3 lit = own * mix(vec3(0.75), uColor, 0.25) + uColor * rim * 1.6 * uIntensity * pulse * 0.5;
16827
+ col = mix(col, lit, body);
16828
+ } else {
16829
+ col = mix(col, uBackground * 0.6 + uColor * 0.15, body);
16830
+ }
16831
+ fragColor = vec4(col, 1.0);
16832
+ }`
16833
+ ),
16834
+ presets: [
16835
+ { name: "Super saiyan", values: { uColor: [1, 0.79, 0.24], uCore: [1, 0.97, 0.84], uHeight: 0.22, uSpeed: 1.6, uPulse: 0.6, uIntensity: 1.8 } },
16836
+ { name: "Spirit", values: { uColor: [0.25, 0.85, 1], uCore: [0.85, 1, 1], uBackground: [0.02, 0.05, 0.09], uHeight: 0.1, uSpeed: 0.6, uPulse: 0.3 } },
16837
+ { name: "Dark", values: { uColor: [0.55, 0.2, 0.95], uCore: [0.95, 0.75, 1], uBackground: [0.02, 0.01, 0.04], uHeight: 0.18, uSpread: 0.07, uIntensity: 1.6 } }
16838
+ ]
16839
+ };
16840
+ var bloom = {
16841
+ kind: "shader",
16842
+ id: "bloom",
16843
+ name: "Bloom",
16844
+ description: "A picture whose bright areas bleed soft light into their surroundings, with an optional anamorphic lens streak.",
16845
+ category: "glow",
16846
+ video: "filter",
16847
+ tags: ["glow", "bloom", "image", "lens", "light", "filter", "anamorphic"],
16848
+ source: glsl(
16849
+ `// Bloom: the brightest parts of the image, blurred at two scales (and streaked sideways), added back as light.
16850
+ uniform sampler2D uImage; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.landscape} label="Image"
16851
+ uniform float uThreshold; // @control min=0 max=1 default=0.6 label="Threshold"
16852
+ uniform float uKnee; // @control min=0.01 max=0.5 default=0.2 label="Softness"
16853
+ uniform float uRadius; // @control min=0.01 max=0.2 default=0.08 label="Radius"
16854
+ uniform float uIntensity; // @control min=0 max=4 default=2.5 label="Intensity"
16855
+ uniform vec3 uTint; // @control color default=#FFFFFF label="Tint"
16856
+ uniform float uStreak; // @control min=0 max=2 default=0 label="Anamorphic streak"
16857
+ uniform vec3 uStreakColor; // @control color default=#7FB2FF label="Streak colour"
16858
+ ${BACKDROP_CONTROLS}`,
16859
+ PICTURE,
16860
+ `// The light above the threshold at uv, read at a mip level (a soft knee, so light fades in smoothly). A coarse
16861
+ // level has averaged small lights with their dark surroundings, so its threshold is lowered a little to match.
16862
+ vec3 bright(vec2 uv, float lod) {
16863
+ vec3 c = textureSize(uImage, 0).x > 1 ? textureLod(uImage, uv, lod).rgb : testCard(fract(uv));
16864
+ float l = dot(c, vec3(0.2126, 0.7152, 0.0722));
16865
+ float thr = uThreshold * exp2(-max(lod, 0.0) * 0.1);
16866
+ float knee = uKnee * exp2(-max(lod, 0.0) * 0.1);
16867
+ float k = clamp((l - thr + knee) / (2.0 * knee), 0.0, 1.0);
16868
+ float soft = l > thr ? l - thr : k * k * knee * 0.5;
16869
+ return c * max(soft, 0.0) / max(l, 1e-4);
16870
+ }
16871
+
16872
+ // A disc blur of the bright pass, radius r (uv height units): 20 golden-angle taps at a mip level about as
16873
+ // coarse as their spacing, so they run together.
16874
+ vec3 blurBright(vec2 uv, float r, float texels) {
16875
+ float lod = log2(max(r * 0.4 * texels, 1.0));
16876
+ vec3 sum = vec3(0.0);
16877
+ vec2 aspect = vec2(iResolution.y / iResolution.x, 1.0);
16878
+ for (int i = 0; i < 20; i++) {
16879
+ float rr = r * sqrt((float(i) + 0.5) / 20.0);
16880
+ float a = float(i) * 2.39996;
16881
+ sum += bright(uv + rr * vec2(cos(a), sin(a)) * aspect, lod);
16882
+ }
16883
+ return sum / 20.0;
16884
+ }
16885
+
16886
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
16887
+ vec2 uv = fragCoord / iResolution.xy;
16888
+ float texels = textureSize(uImage, 0).y > 1 ? float(textureSize(uImage, 0).y) : iResolution.y;
16889
+ vec3 light = blurBright(uv, uRadius * 0.3, texels) * 0.3 + blurBright(uv, uRadius, texels) * 0.4 + blurBright(uv, uRadius * 3.0, texels) * 0.5;
16890
+ light *= uTint;
16891
+ if (uStreak > 0.0) {
16892
+ // A long horizontal smear of the bright pass, read a little blurred so its taps run together.
16893
+ vec3 streak = vec3(0.0);
16894
+ float w = 0.0;
16895
+ float jitter = fract(sin(dot(fragCoord, vec2(12.9898, 78.233))) * 43758.5453) - 0.5;
16896
+ float texelsX = textureSize(uImage, 0).x > 1 ? float(textureSize(uImage, 0).x) : iResolution.x;
16897
+ float lodStreak = log2(max(0.0125 * texelsX, 1.0));
16898
+ for (int i = -24; i <= 24; i++) {
16899
+ float f = (float(i) + jitter) / 24.0;
16900
+ float k = exp(-abs(f) * 3.5);
16901
+ streak += bright(uv + vec2(f * 0.3, 0.0), lodStreak) * k;
16902
+ w += k;
16903
+ }
16904
+ light += dot(streak / w, vec3(0.33)) * uStreakColor * uStreak * 4.0;
16905
+ }
16906
+ light *= uIntensity;
16907
+ vec4 pic = pictureRGBA(uImage, uv);
16908
+ // The bloom is light added over the picture, so it also shows where the picture is clear.
16909
+ float glowA = clamp(max(light.r, max(light.g, light.b)), 0.0, 1.0);
16910
+ float alpha = max(pic.a, glowA);
16911
+ vec3 col = (pic.rgb * pic.a + light) / max(alpha, 1e-4);
16912
+ fragColor = vec4(col, alpha);
16913
+ }`
16914
+ ),
16915
+ presets: [
16916
+ { name: "Dreamy", values: { uThreshold: 0.45, uRadius: 0.12, uIntensity: 1.8, uTint: [1, 0.9, 0.95] } },
16917
+ { name: "Anamorphic night", values: { uImage: SAMPLE_IMAGES.city, uStreak: 1.2, uThreshold: 0.6, uIntensity: 3 } },
16918
+ { name: "Subtle", values: { uThreshold: 0.75, uRadius: 0.04, uIntensity: 1.2 } }
16919
+ ]
16920
+ };
16921
+ var pulsingHalo = {
16922
+ kind: "shader",
16923
+ id: "pulsing-halo",
16924
+ name: "Pulsing halo",
16925
+ description: "Soft halo rings behind a centre spot, breathing slowly in and out with a gentle shimmer running round them.",
16926
+ category: "glow",
16927
+ tags: ["glow", "halo", "ring", "breathe", "overlay", "pulse", "ui"],
16928
+ transparent: true,
16929
+ loop: 4,
16930
+ source: glsl(
16931
+ `// Pulsing halo: soft rings round the centre that grow and brighten with each breath, shimmering round.
16932
+ uniform vec3 uColor; // @control color default=#7DD3FC label="Outer colour"
16933
+ uniform vec3 uInner; // @control color default=#E0F2FE label="Inner colour"
16934
+ uniform float uRadius; // @control min=0.05 max=0.45 default=0.2 label="Radius"
16935
+ uniform float uWidth; // @control min=0.004 max=0.12 default=0.025 label="Ring width"
16936
+ uniform int uRings; // @control min=1 max=4 default=2 label="Rings"
16937
+ uniform float uSpacing; // @control min=0.02 max=0.2 default=0.07 label="Ring spacing"
16938
+ uniform float uBreath; // @control min=0 max=0.5 default=0.12 label="Breath"
16939
+ uniform float uPeriod; // @control min=1 max=10 default=4 label="Seconds per breath"
16940
+ uniform float uShimmer; // @control min=0 max=1 default=0.5 label="Shimmer"
16941
+ uniform float uIntensity; // @control min=0 max=3 default=1.2 label="Intensity"`,
16942
+ HASH,
16943
+ NOISE,
16944
+ GLOW7,
16945
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
16946
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
16947
+ float d = length(p);
16948
+ vec2 dir = p / max(d, 1e-4);
16949
+ float phase = 6.28318 * iTime / uPeriod;
16950
+ float b = 0.5 - 0.5 * cos(phase);
16951
+ float scale = 1.0 + uBreath * (b - 0.3);
16952
+ // The shimmer: noise round the ring, drifting on a circle so it repeats every breath.
16953
+ vec2 drift = vec2(cos(phase), sin(phase));
16954
+ vec3 light = vec3(0.0);
16955
+ for (int i = 0; i < 4; i++) {
16956
+ if (i >= uRings) break;
16957
+ float fi = float(i);
16958
+ float r = (uRadius + fi * uSpacing * (1.0 + 0.5 * b)) * scale;
16959
+ float w = uWidth * (1.0 + 0.5 * fi);
16960
+ float ring = exp(-pow((d - r) / w, 2.0)) + 0.4 * exp(-abs(d - r) / (w * 3.0));
16961
+ float s = vnoise(dir * 3.0 + drift * 0.8 + fi * 7.3) * 0.6 + vnoise(dir * 7.0 - drift * 1.3 + fi * 3.1) * 0.4;
16962
+ float sh = mix(1.0, 0.35 + 1.3 * s, uShimmer);
16963
+ float fade = 1.0 / (1.0 + 0.9 * fi);
16964
+ vec3 tint = mix(uInner, uColor, uRings > 1 ? fi / float(uRings - 1) : 0.6);
16965
+ light += tint * ring * sh * fade;
16966
+ }
16967
+ // A faint glow filling the centre, behind whatever sits there.
16968
+ light += uInner * 0.18 * exp(-pow(d / (uRadius * scale), 2.0) * 1.5);
16969
+ light *= uIntensity * (0.7 + 0.45 * b);
16970
+ fragColor = glow(light);
16971
+ }`
16972
+ ),
16973
+ presets: [
16974
+ { name: "Calm", values: { uRings: 1, uWidth: 0.04, uPeriod: 6, uShimmer: 0.2 } },
16975
+ { name: "Golden", values: { uColor: [1, 0.66, 0.2], uInner: [1, 0.93, 0.7], uRings: 3, uShimmer: 0.8 } },
16976
+ { name: "Notify", values: { uColor: [0.98, 0.3, 0.45], uInner: [1, 0.8, 0.85], uRadius: 0.12, uBreath: 0.35, uPeriod: 1.5, uRings: 3 } }
16977
+ ]
16978
+ };
16979
+ var neonOutline = {
16980
+ kind: "shader",
16981
+ id: "neon-outline",
16982
+ name: "Neon outline",
16983
+ description: "Any shape or word traced in a crisp neon line with a wide soft glow, its colour flowing along the outline and flickering now and then.",
16984
+ category: "glow",
16985
+ video: "none",
16986
+ tags: ["glow", "neon", "outline", "text", "logo", "overlay", "sign"],
16987
+ transparent: true,
16988
+ source: glsl(
16989
+ `// Neon outline: the mask's edge as a bright tube, a soft halo either side, coloured along the outline.
16990
+ uniform sampler2D uMask; // @control filter=mipmap wrap=clamp default=${textImage("GLOW")} label="Shape"
16991
+ uniform float uSize; // @control min=0.2 max=1 default=0.85 label="Size"
16992
+ uniform vec3 uColorA; // @control color default=#FF3DD8 label="Colour"
16993
+ uniform vec3 uColorB; // @control color default=#38E1FF label="Gradient to"
16994
+ uniform float uGradient; // @control min=0 max=1 default=1 label="Gradient along outline"
16995
+ uniform float uFlow; // @control min=0 max=2 default=0.3 label="Gradient flow"
16996
+ uniform float uLine; // @control min=0.5 max=8 default=2 label="Line width (px)"
16997
+ uniform float uGlow; // @control min=0.01 max=0.2 default=0.05 label="Glow size"
16998
+ uniform float uIntensity; // @control min=0 max=3 default=1.3 label="Intensity"
16999
+ uniform float uFlicker; // @control min=0 max=1 default=0.3 label="Flicker"
17000
+ uniform float uFill; // @control min=0 max=1 default=0 label="Fill"`,
17001
+ HASH,
17002
+ MASK,
17003
+ GLOW7,
17004
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17005
+ setKey(0.0);
17006
+ float px = 1.0 / iResolution.y;
17007
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
17008
+ // The edge, softened a few pixels so its slope gives a smooth distance in pixels.
17009
+ float v = coverage(p, 3.0 * px) - 0.5;
17010
+ vec2 g = vec2(dFdx(v), dFdy(v));
17011
+ float d = abs(v) / max(length(g), 0.02);
17012
+ float tube = exp(-pow(d / uLine, 2.0));
17013
+ float core = exp(-pow(d / (uLine * 0.4), 2.0));
17014
+ // The halo: blurred coverage is half way across the edge, falling off either side.
17015
+ float c1 = coverage(p, uGlow * 0.4);
17016
+ float c2 = coverage(p, uGlow);
17017
+ float halo = (1.0 - abs(2.0 * c1 - 1.0)) * 0.55 + (1.0 - abs(2.0 * c2 - 1.0)) * 0.35;
17018
+ float inside = shapeAt(p, px);
17019
+ // Colour along the outline, flowing.
17020
+ float k = mix(0.0, 0.5 + 0.5 * sin(p.x * 5.0 + p.y * 2.5 - iTime * uFlow * 3.0), uGradient);
17021
+ vec3 tint = mix(uColorA, uColorB, k);
17022
+ // Flicker: now and then a burst of quick dips, as a tired tube does.
17023
+ float burst = step(0.72, hash12(vec2(floor(iTime * 1.7), 3.0)));
17024
+ float dip = step(0.45, hash12(vec2(floor(iTime * 24.0), 7.0)));
17025
+ float flick = 1.0 - uFlicker * burst * (1.0 - dip) * 0.85;
17026
+ vec3 light = tint * (tube * 1.6 + halo) + mix(tint, vec3(1.0), 0.7) * core * 0.9;
17027
+ light += tint * inside * uFill * 0.6;
17028
+ fragColor = glow(light * uIntensity * flick);
17029
+ }`
17030
+ ),
17031
+ presets: [
17032
+ { name: "Bar sign", values: { uColorA: [1, 0.24, 0.4], uGradient: 0, uFlicker: 0.8, uLine: 3 } },
17033
+ { name: "Heart", values: { uMask: SAMPLE_IMAGES.heart, uSize: 0.7, uColorA: [1, 0.3, 0.55], uColorB: [1, 0.65, 0.3], uFlicker: 0 } },
17034
+ { name: "Filled", values: { uFill: 0.35, uGlow: 0.09, uColorA: [0.5, 1, 0.4], uColorB: [0.2, 0.8, 1] } }
17035
+ ]
17036
+ };
17037
+ var backlightGlow = {
17038
+ kind: "shader",
17039
+ id: "backlight-glow",
17040
+ name: "Backlight glow",
17041
+ description: "A dark silhouette with a light behind it: radiant rays stream past its edges and glow spills round its rim.",
17042
+ category: "glow",
17043
+ video: "none",
17044
+ tags: ["glow", "backlight", "silhouette", "rays", "eclipse", "rim", "logo"],
17045
+ source: glsl(
17046
+ `// Backlight glow: a light behind a mask, marched toward for shafts the shape blocks, with spill and a rim.
17047
+ uniform sampler2D uMask; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.logo} label="Silhouette"
17048
+ uniform float uKey; // @control min=0 max=0.5 default=0 label="Key out the corner colour (0 off)"
17049
+ uniform float uSize; // @control min=0.2 max=1 default=0.6 label="Size"
17050
+ uniform vec3 uLight; // @control color default=#FFC46B label="Light"
17051
+ uniform vec3 uBackground; // @control color default=#07060D label="Background"
17052
+ uniform vec3 uObject; // @control color default=#040308 label="Object"
17053
+ uniform float uLightX; // @control min=-0.5 max=0.5 default=0 label="Light across"
17054
+ uniform float uLightY; // @control min=-0.5 max=0.5 default=0.04 label="Light up"
17055
+ uniform float uGlowSize; // @control min=0.05 max=0.8 default=0.3 label="Light size"
17056
+ uniform float uRays; // @control min=0 max=1 default=0.8 label="Rays"
17057
+ uniform float uRim; // @control min=0 max=1 default=0.6 label="Rim light"
17058
+ uniform float uSpeed; // @control min=0 max=2 default=0.4 label="Ray drift"
17059
+ uniform float uIntensity; // @control min=0 max=3 default=1.3 label="Intensity"`,
17060
+ HASH,
17061
+ NOISE,
17062
+ MASK,
17063
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17064
+ setKey(uKey);
17065
+ float px = 1.0 / iResolution.y;
17066
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
17067
+ vec2 L = vec2(uLightX, uLightY);
17068
+ vec2 toL = L - p;
17069
+ float dl = length(toL);
17070
+ // Light reaching here: march toward the light, the shape shadowing what is behind it.
17071
+ float shafts = 0.0;
17072
+ float decay = 1.0;
17073
+ float jitter = hash12(fragCoord);
17074
+ for (int i = 0; i < 40; i++) {
17075
+ float s = (float(i) + jitter) / 40.0;
17076
+ vec2 q = p + toL * s;
17077
+ float open = 1.0 - shapeAt(q, 2.0 * px + 0.01 * s);
17078
+ shafts += open * exp(-pow(length(q - L) / uGlowSize, 2.0)) * decay;
17079
+ decay *= 0.965;
17080
+ }
17081
+ shafts /= 20.0;
17082
+ // Streaks round the light, drifting slowly.
17083
+ vec2 dir = -toL / max(dl, 1e-4);
17084
+ float a = iTime * uSpeed * 0.2;
17085
+ float streak = vnoise(dir * 6.0 + vec2(a, -a)) * 0.6 + vnoise(dir * 15.0 - vec2(a * 2.0, a)) * 0.4;
17086
+ float rays = shafts * mix(1.0, 0.25 + 1.5 * streak * streak, uRays);
17087
+ // Spill: light wrapping just round the edges.
17088
+ float body = shapeAt(p, 1.2 * px);
17089
+ float spill = coverage(p, 0.03) * 0.8 + coverage(p, 0.08) * 0.5;
17090
+ float glowAt = exp(-pow(dl / (uGlowSize * 1.6), 2.0));
17091
+ vec3 light = uLight * (rays * 1.4 + spill * (1.0 - body) * glowAt * 1.2) * uIntensity;
17092
+ vec3 col = uBackground + (1.0 - exp(-light));
17093
+ // The object, dark, its edges facing the light lit.
17094
+ float edge = 1.0 - coverage(p, 0.012);
17095
+ vec2 grad = vec2(dFdx(edge), dFdy(edge));
17096
+ float facing = 0.5 + 0.5 * dot(normalize(grad + 1e-6), normalize(toL + 1e-6));
17097
+ vec3 obj = uObject + uLight * edge * uRim * (0.4 + 0.6 * facing) * glowAt * 1.5 * uIntensity;
17098
+ col = mix(col, obj, body);
17099
+ fragColor = vec4(col, 1.0);
17100
+ }`
17101
+ ),
17102
+ presets: [
17103
+ { name: "Eclipse", values: { uLight: [1, 0.95, 0.85], uGlowSize: 0.22, uRays: 0.9, uRim: 0.8 } },
17104
+ { name: "Hero portrait", values: { uMask: SAMPLE_IMAGES.portrait, uKey: 0.12, uSize: 1, uLightY: 0.12, uLight: [0.6, 0.8, 1] } },
17105
+ { name: "Sunset", values: { uLight: [1, 0.45, 0.2], uBackground: [0.12, 0.04, 0.1], uLightY: -0.08, uGlowSize: 0.5 } }
17106
+ ]
17107
+ };
17108
+ var rimLight = {
17109
+ kind: "shader",
17110
+ id: "rim-light",
17111
+ name: "Rim light",
17112
+ description: "A picture given a coloured studio kicker light along the edges that face a chosen direction.",
17113
+ category: "glow",
17114
+ video: "filter",
17115
+ tags: ["glow", "rim", "light", "image", "portrait", "studio", "filter"],
17116
+ source: glsl(
17117
+ `// Rim light: luminance edges, kept where they face the light, glowing in its colour over the dimmed picture.
17118
+ uniform sampler2D uImage; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Image"
17119
+ uniform vec3 uColor; // @control color default=#4FD1FF label="Light colour"
17120
+ uniform float uAngle; // @control min=0 max=360 default=35 label="Light from (\xB0)"
17121
+ uniform float uWidth; // @control min=0.5 max=8 default=3 label="Rim width (px)"
17122
+ uniform float uFocus; // @control min=0 max=1 default=1 label="Focus on the light side"
17123
+ uniform float uThreshold; // @control min=0 max=0.5 default=0.06 label="Threshold"
17124
+ uniform float uIntensity; // @control min=0 max=4 default=2.4 label="Intensity"
17125
+ uniform float uDim; // @control min=0 max=1 default=0.3 label="Darken picture"
17126
+ uniform bool uBrightSubject; // @control default=false label="Subject brighter than background"
17127
+ ${BACKDROP_CONTROLS}`,
17128
+ PICTURE,
17129
+ `float luma(vec2 uv) {
17130
+ vec4 c = pictureRGBA(uImage, uv);
17131
+ return dot(c.rgb, vec3(0.2126, 0.7152, 0.0722)) * c.a;
17132
+ }
17133
+
17134
+ // The luminance gradient (Sobel), sampling d apart in uv: it points from dark to bright.
17135
+ vec2 gradient(vec2 uv, vec2 d) {
17136
+ float tl = luma(uv + vec2(-d.x, d.y));
17137
+ float t = luma(uv + vec2(0.0, d.y));
17138
+ float tr = luma(uv + d);
17139
+ float l = luma(uv - vec2(d.x, 0.0));
17140
+ float r = luma(uv + vec2(d.x, 0.0));
17141
+ float bl = luma(uv - d);
17142
+ float b = luma(uv - vec2(0.0, d.y));
17143
+ float br = luma(uv + vec2(d.x, -d.y));
17144
+ return vec2(tr + 2.0 * r + br - tl - 2.0 * l - bl, tl + 2.0 * t + tr - bl - 2.0 * b - br) * 0.25;
17145
+ }
17146
+
17147
+ // How much rim light an edge sampled d apart gets: its strength, toward the light.
17148
+ float rimAt(vec2 uv, vec2 d, vec2 toLight) {
17149
+ vec2 g = gradient(uv, d);
17150
+ float s = length(g);
17151
+ // A dark subject on a light ground: its outward normal points to the brighter side.
17152
+ vec2 n = (uBrightSubject ? -g : g) / max(s, 1e-5);
17153
+ float facing = mix(1.0, smoothstep(0.25, 0.95, dot(n, toLight)), uFocus);
17154
+ return smoothstep(uThreshold, uThreshold + 0.12, s) * facing;
17155
+ }
17156
+
17157
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17158
+ vec2 uv = fragCoord / iResolution.xy;
17159
+ float a = radians(uAngle);
17160
+ vec2 toLight = vec2(cos(a), sin(a));
17161
+ vec2 d = uWidth / iResolution.xy;
17162
+ // The light falls on the subject's side of the edge: read it a little toward the light, and keep it where
17163
+ // this pixel is on the subject's side (darker than what lies toward the light, or brighter if so set).
17164
+ float crisp = rimAt(uv + toLight * d, d, toLight);
17165
+ float soft = rimAt(uv + toLight * d * 2.5, d * 3.0, toLight);
17166
+ float rise = luma(uv + toLight * d * 3.0) - luma(uv);
17167
+ float side = smoothstep(0.0, 0.06, uBrightSubject ? -rise : rise);
17168
+ crisp *= side;
17169
+ soft *= mix(0.5, 1.0, side);
17170
+ vec4 pic = pictureRGBA(uImage, uv);
17171
+ vec3 light = uColor * (crisp + 0.45 * soft) * uIntensity + vec3(1.0) * crisp * crisp * 0.12 * uIntensity;
17172
+ vec3 col = pic.rgb * (1.0 - uDim) + light * (1.0 - 0.5 * pic.rgb);
17173
+ fragColor = vec4(col, pic.a);
17174
+ }`
17175
+ ),
17176
+ presets: [
17177
+ { name: "Kicker", values: { uColor: [1, 0.86, 0.66], uAngle: 160, uDim: 0.2 } },
17178
+ { name: "Cyberpunk", values: { uColor: [1, 0.2, 0.7], uAngle: 20, uDim: 0.55, uIntensity: 2.5, uWidth: 3.5 } },
17179
+ { name: "Top light", values: { uColor: [0.85, 0.95, 1], uAngle: 90, uFocus: 0.7 } }
17180
+ ]
17181
+ };
17182
+ var glowShaders = [aura, bloom, pulsingHalo, neonOutline, backlightGlow, rimLight];
17183
+
17184
+ // src/catalog/shaders/shadow.ts
17185
+ var SHAPE = `
17186
+ vec4 shapeAt(vec2 p, float spacing) {
17187
+ vec2 size = vec2(textureSize(uMask, 0));
17188
+ if (size.x <= 1.0) {
17189
+ vec2 q = abs(p) - 0.18 * uSize;
17190
+ float a = step(length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - 0.08 * uSize, 0.0);
17191
+ return vec4(vec3(a), a);
17192
+ }
17193
+ float aspect = size.x / size.y;
17194
+ float h = min(uSize, iResolution.x / iResolution.y * uSize / aspect);
17195
+ vec2 uv = p / vec2(h * aspect, h) + 0.5;
17196
+ if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) return vec4(0.0);
17197
+ vec4 c = textureLod(uMask, uv, log2(max(spacing * size.y / h, 1.0)));
17198
+ // Clear texels upload black, so filtered colour is about colour \xD7 alpha already.
17199
+ return vec4(clamp(c.rgb / max(c.a, 1e-3), 0.0, 1.0) * c.a, c.a);
17200
+ }`.trim();
17201
+ var SDF = `
17202
+ float capsule(vec2 p, vec2 a, vec2 b, float r) {
17203
+ vec2 pa = p - a;
17204
+ vec2 ba = b - a;
17205
+ float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
17206
+ return length(pa - ba * h) - r;
17207
+ }
17208
+ float box(vec2 p, vec2 b, float r) {
17209
+ vec2 q = abs(p) - b + r;
17210
+ return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
17211
+ }
17212
+ float smin(float a, float b, float k) {
17213
+ float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
17214
+ return mix(b, a, h) - k * h * (1.0 - h);
17215
+ }`.trim();
17216
+ var longShadow = {
17217
+ kind: "shader",
17218
+ id: "long-shadow",
17219
+ name: "Long shadow",
17220
+ description: "The flat-design long shadow: a word or icon casting a long diagonal band across a flat colour, fading along its length and slowly swinging round.",
17221
+ category: "shadow",
17222
+ video: "none",
17223
+ tags: ["shadow", "long-shadow", "flat", "text", "logo", "icon", "mask"],
17224
+ source: glsl(
17225
+ `// Long shadow: the shape's mask marched back along a direction, so it casts a long band that fades as it goes.
17226
+ uniform sampler2D uMask; // @control filter=mipmap wrap=clamp default=${textImage("LONG")} label="Shape (its alpha)"
17227
+ uniform float uSize; // @control min=0.2 max=1 default=0.8 label="Shape size"
17228
+ uniform float uAngle; // @control min=0 max=360 default=315 label="Direction (\xB0)"
17229
+ uniform float uSwing; // @control min=0 max=60 default=15 label="Swing (\xB0)"
17230
+ uniform float uSpin; // @control min=-30 max=30 default=0 label="Turn (\xB0/s)"
17231
+ uniform float uLength; // @control min=0 max=1.5 default=0.7 label="Length"
17232
+ uniform float uFade; // @control min=0 max=1 default=0.85 label="Fade along"
17233
+ uniform vec3 uBackground; // @control color default=#EF5A5A label="Background"
17234
+ uniform vec3 uShadow; // @control color default=#B8383F label="Shadow"
17235
+ uniform vec3 uColour; // @control color default=#FFFFFF label="Shape colour"
17236
+ uniform bool uImageColours; // @control default=false label="Use the shape's own colours"`,
17237
+ SHAPE,
17238
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17239
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
17240
+ float a = radians(uAngle + uSpin * iTime + uSwing * sin(iTime * 0.4));
17241
+ vec2 dir = vec2(cos(a), sin(a));
17242
+ // Look back toward the shape: any copy of it along the way covers this point.
17243
+ const int STEPS = 96;
17244
+ float spacing = uLength / float(STEPS);
17245
+ float shade = 0.0;
17246
+ for (int i = 1; i <= STEPS; i++) {
17247
+ float m = shapeAt(p - dir * (float(i) * spacing), spacing).a;
17248
+ shade = max(shade, m);
17249
+ if (shade > 0.99) break;
17250
+ }
17251
+ // The band fades in one smooth gradient along the direction, from the back of the shape's box to the shadow's end.
17252
+ vec2 size = vec2(textureSize(uMask, 0));
17253
+ float aspect = size.x <= 1.0 ? 1.0 : size.x / size.y;
17254
+ float h = size.x <= 1.0 ? 0.52 * uSize : min(uSize, iResolution.x / iResolution.y * uSize / aspect);
17255
+ float reach = 0.5 * h * (abs(dir.x) * aspect + abs(dir.y));
17256
+ shade *= 1.0 - uFade * clamp((dot(p, dir) + reach) / (2.0 * reach + uLength), 0.0, 1.0);
17257
+ vec3 col = mix(uBackground, uShadow, shade);
17258
+ vec4 c = shapeAt(p, 1.0 / iResolution.y);
17259
+ vec3 ink = uImageColours ? c.rgb / max(c.a, 1e-4) : uColour;
17260
+ fragColor = vec4(mix(col, ink, c.a), 1.0);
17261
+ }`
17262
+ ),
17263
+ presets: [
17264
+ { name: "Flat", values: { uFade: 0, uLength: 1.5, uSwing: 0, uBackground: [0.16, 0.71, 0.65], uShadow: [0.11, 0.55, 0.5] } },
17265
+ { name: "Icon", values: { uMask: SAMPLE_IMAGES.heart, uImageColours: true, uSize: 0.5, uBackground: [0.99, 0.82, 0.32], uShadow: [0.86, 0.62, 0.14] } },
17266
+ { name: "Spinning", values: { uSwing: 0, uSpin: 20, uFade: 1, uBackground: [0.2, 0.22, 0.32], uShadow: [0.1, 0.11, 0.18] } }
17267
+ ]
17268
+ };
17269
+ var dropShadowLift = {
17270
+ kind: "shader",
17271
+ id: "drop-shadow-lift",
17272
+ name: "Drop shadow lift",
17273
+ description: "A logo or icon resting on a soft drop shadow that lifts off the page when hovered, its shadow growing larger, softer and further below as it scales up.",
17274
+ category: "shadow",
17275
+ video: "none",
17276
+ tags: ["shadow", "drop-shadow", "hover", "interactive", "elevation", "logo", "icon", "mask"],
17277
+ source: glsl(
17278
+ `// Drop shadow lift: a shape on a blurred copy of its own mask, which spreads and drops as the shape lifts (on hover).
17279
+ uniform sampler2D uMask; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.logo} label="Object (its alpha)"
17280
+ uniform float uSize; // @control min=0.2 max=1 default=0.55 label="Object size"
17281
+ uniform bool uImageColours; // @control default=true label="Use the object's own colours"
17282
+ uniform vec3 uColour; // @control color default=#6366F1 label="Object colour"
17283
+ uniform vec3 uBackground; // @control color default=#EEF1F7 label="Background"
17284
+ uniform vec3 uShadow; // @control color default=#1E2340 label="Shadow colour"
17285
+ uniform float uOpacity; // @control min=0 max=1 default=0.45 label="Shadow strength"
17286
+ uniform float uRestBlur; // @control min=0 max=0.1 default=0.012 label="Blur at rest"
17287
+ uniform float uLiftBlur; // @control min=0 max=0.2 default=0.07 label="Blur when lifted"
17288
+ uniform float uLiftDrop; // @control min=0 max=0.15 default=0.06 label="Shadow drop when lifted"
17289
+ uniform float uLiftScale; // @control min=0 max=0.2 default=0.06 label="Scale when lifted"
17290
+ uniform bool uDemo; // @control default=true label="Demo when idle"`,
17291
+ SHAPE,
17292
+ `// The mask blurred over a disc of the radius: 24 taps on a golden-angle spiral, each from a mip level as coarse as their spacing.
17293
+ float blurred(vec2 p, float radius) {
17294
+ float sum = 0.0;
17295
+ for (int i = 0; i < 24; i++) {
17296
+ float r = radius * sqrt((float(i) + 0.5) / 24.0);
17297
+ float a = float(i) * 2.39996;
17298
+ sum += shapeAt(p + r * vec2(cos(a), sin(a)), radius * 0.35).a;
17299
+ }
17300
+ return sum / 24.0;
17301
+ }
17302
+
17303
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17304
+ float px = 1.0 / iResolution.y;
17305
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
17306
+ // Hovering lifts it; idle, the demo breathes it up and down.
17307
+ float idle = uDemo ? smoothstep(0.0, 1.0, 0.5 - 0.5 * cos(iTime * 1.3)) : 0.0;
17308
+ float lift = mix(idle, 1.0, clamp(iPointer.z, 0.0, 1.0));
17309
+ float k = 1.0 + uLiftScale * lift;
17310
+ vec2 rise = vec2(0.0, 0.012 * lift);
17311
+ // The shadow spreads a little wider than the object, drops below it and softens.
17312
+ float ks = k * (1.0 + 0.05 * lift);
17313
+ vec2 drop = vec2(0.0, -(0.008 + uLiftDrop * lift));
17314
+ float radius = mix(uRestBlur, uLiftBlur, lift) + px;
17315
+ float shadow = blurred((p - drop) / ks, radius / ks) * uOpacity * mix(1.0, 0.7, lift);
17316
+ vec3 col = mix(uBackground, uShadow, shadow);
17317
+ vec4 c = shapeAt((p - rise) / k, px / k);
17318
+ vec3 ink = uImageColours ? c.rgb / max(c.a, 1e-4) : uColour;
17319
+ fragColor = vec4(mix(col, ink, c.a), 1.0);
17320
+ }`
17321
+ ),
17322
+ presets: [
17323
+ { name: "Heart", values: { uMask: SAMPLE_IMAGES.heart, uBackground: [1, 0.94, 0.95], uShadow: [0.45, 0.05, 0.15], uOpacity: 0.4 } },
17324
+ { name: "Dramatic", values: { uOpacity: 0.7, uLiftBlur: 0.12, uLiftDrop: 0.12, uLiftScale: 0.1, uBackground: [0.85, 0.87, 0.9] } }
17325
+ ]
17326
+ };
17327
+ var lightSweepShadows = {
17328
+ kind: "shader",
17329
+ id: "light-sweep-shadows",
17330
+ name: "Light sweep shadows",
17331
+ description: "A coloured light drifting across a floor of shapes (or following the pointer), each casting a real soft shadow that swings round as the light moves.",
17332
+ category: "shadow",
17333
+ tags: ["shadow", "light", "soft-shadows", "sdf", "lamp", "interactive", "hover"],
17334
+ source: glsl(
17335
+ `// Light sweep shadows: soft 2D shadows marched from each point toward a moving light through the shapes' distance field.
17336
+ uniform vec3 uLight; // @control color default=#FFC46E label="Light colour"
17337
+ uniform float uIntensity; // @control min=0 max=4 default=1.8 label="Light strength"
17338
+ uniform float uReach; // @control min=0.1 max=2 default=0.45 label="Light reach"
17339
+ uniform float uSoftness; // @control min=0.005 max=0.3 default=0.06 label="Shadow softness"
17340
+ uniform float uSpeed; // @control min=0 max=2 default=0.5 label="Speed"
17341
+ uniform vec3 uFloor; // @control color default=#3A3F52 label="Floor"
17342
+ uniform vec3 uShapes; // @control color default=#ECE6DA label="Shapes"
17343
+ uniform float uAmbient; // @control min=0 max=1 default=0.12 label="Ambient light"
17344
+ uniform bool uFollow; // @control default=true label="Follow the pointer"`,
17345
+ ROTATE,
17346
+ SDF,
17347
+ `float scene(vec2 p) {
17348
+ float d = length(p - vec2(-0.45, 0.13)) - 0.09;
17349
+ d = min(d, length(p - vec2(0.36, -0.17)) - 0.065);
17350
+ d = min(d, length(p - vec2(-0.2, -0.28)) - 0.045);
17351
+ d = min(d, box(rot(0.5) * (p - vec2(0.04, 0.0)), vec2(0.1, 0.05), 0.012));
17352
+ d = min(d, box(rot(-0.3) * (p - vec2(0.55, 0.2)), vec2(0.03, 0.12), 0.01));
17353
+ d = min(d, box(rot(0.9) * (p - vec2(-0.62, -0.22)), vec2(0.05, 0.05), 0.008));
17354
+ return d;
17355
+ }
17356
+
17357
+ vec2 sceneNormal(vec2 p) {
17358
+ vec2 e = vec2(0.002, 0.0);
17359
+ return normalize(vec2(scene(p + e.xy) - scene(p - e.xy), scene(p + e.yx) - scene(p - e.yx)) + 1e-6);
17360
+ }
17361
+
17362
+ // How much of the light reaches p: march toward it, keeping the narrowest miss relative to how far along.
17363
+ float softShadow(vec2 p, vec2 light) {
17364
+ vec2 to = light - p;
17365
+ float dist = length(to);
17366
+ vec2 dir = to / max(dist, 1e-5);
17367
+ float res = 1.0;
17368
+ float t = 0.004;
17369
+ for (int i = 0; i < 56; i++) {
17370
+ if (t >= dist) break;
17371
+ float d = scene(p + dir * t);
17372
+ res = min(res, d / (uSoftness * t));
17373
+ if (res < -1.0) break;
17374
+ t += clamp(d, 0.004, 0.08);
17375
+ }
17376
+ res = max(res, -1.0);
17377
+ return 0.25 * (1.0 + res) * (1.0 + res) * (2.0 - res);
17378
+ }
17379
+
17380
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17381
+ float px = 1.0 / iResolution.y;
17382
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
17383
+ float aspect = iResolution.x / iResolution.y;
17384
+ float t = iTime * uSpeed;
17385
+ vec2 light = vec2(0.42 * aspect * sin(t * 1.06), 0.36 * sin(t * 1.62 + 1.0));
17386
+ if (uFollow) light = mix(light, (iPointer.xy - 0.5 * iResolution.xy) * px, clamp(iPointer.z, 0.0, 1.0));
17387
+ // Keep the light out of the shapes.
17388
+ float inside = scene(light);
17389
+ if (inside < 0.03) light += sceneNormal(light) * (0.03 - inside);
17390
+
17391
+ vec2 to = light - p;
17392
+ float r = length(to);
17393
+ float falloff = 1.0 / (1.0 + r * r / (uReach * uReach));
17394
+ vec3 lamp = uLight * uIntensity * falloff;
17395
+ float d = scene(p);
17396
+ // Floor: ambient, plus the light where it gets through.
17397
+ vec3 floorCol = uFloor * (uAmbient + lamp * softShadow(p, light));
17398
+ // Shapes are lit on top, brighter on the rim that faces the light.
17399
+ vec2 n = sceneNormal(p);
17400
+ float rim = smoothstep(-0.025, 0.0, d) * max(dot(n, to / max(r, 1e-5)), 0.0);
17401
+ vec3 shapeCol = uShapes * (uAmbient + 0.25 + lamp * (0.45 + 0.8 * rim));
17402
+ vec3 col = mix(shapeCol, floorCol, smoothstep(-px, px, d));
17403
+ // The bulb itself.
17404
+ col += uLight * (exp(-r * r / 0.00025) * 2.0 + exp(-r * r / 0.004) * 0.25);
17405
+ fragColor = vec4(1.0 - exp(-col * 1.3), 1.0);
17406
+ }`
17407
+ ),
17408
+ presets: [
17409
+ { name: "Candle", values: { uLight: [1, 0.6, 0.25], uReach: 0.3, uSoftness: 0.12, uSpeed: 0.3, uFloor: [0.22, 0.17, 0.14] } },
17410
+ { name: "Neon", values: { uLight: [0.3, 0.85, 1], uIntensity: 2.5, uSoftness: 0.02, uFloor: [0.1, 0.1, 0.18], uShapes: [0.9, 0.3, 0.7] } }
17411
+ ]
17412
+ };
17413
+ var contactShadow = {
17414
+ kind: "shader",
17415
+ id: "contact-shadow",
17416
+ name: "Contact shadow",
17417
+ description: "An object floating up and down above the floor, its contact shadow tightening and darkening as it nears the ground and spreading and fading as it rises.",
17418
+ category: "shadow",
17419
+ tags: ["shadow", "contact-shadow", "float", "ground", "product", "3d"],
17420
+ source: glsl(
17421
+ `// Contact shadow: an object bobbing over a floor; a tight dark core and a wide soft shadow, both spreading and fading with height.
17422
+ uniform int uShape; // @control min=0 max=2 default=0 label="Object (0 ball, 1 cube, 2 pill)"
17423
+ uniform vec3 uObject; // @control color default=#FF7A59 label="Object"
17424
+ uniform vec3 uBackground; // @control color default=#F3EEE6 label="Wall"
17425
+ uniform vec3 uGround; // @control color default=#E2D8C9 label="Floor"
17426
+ uniform vec3 uShadow; // @control color default=#3A2C22 label="Shadow colour"
17427
+ uniform float uHeight; // @control min=0 max=0.4 default=0.2 label="Float height"
17428
+ uniform float uSpeed; // @control min=0 max=3 default=0.8 label="Speed"
17429
+ uniform float uDarkness; // @control min=0 max=1 default=0.8 label="Contact darkness"
17430
+ uniform float uSpread; // @control min=0 max=3 default=1.4 label="Spread when high"`,
17431
+ SDF,
17432
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17433
+ float px = 1.0 / iResolution.y;
17434
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
17435
+ const float horizon = -0.08;
17436
+ const float groundY = -0.26;
17437
+ const float R = 0.12;
17438
+ // Lingers at the top and bottom of each bob, and comes down to just touch.
17439
+ float s = 0.5 + 0.5 * sin(iTime * uSpeed * 1.6);
17440
+ float h = 0.003 + uHeight * s * s * (3.0 - 2.0 * s);
17441
+
17442
+ // Wall above, floor below, a soft crease between.
17443
+ vec3 wall = uBackground * (0.92 + 0.08 * smoothstep(horizon, 0.5, p.y));
17444
+ vec3 floorCol = uGround * (0.94 + 0.06 * smoothstep(horizon, -0.5, p.y));
17445
+ vec3 col = mix(floorCol, wall, smoothstep(-px, px, p.y - horizon));
17446
+ col *= 1.0 - 0.06 * exp(-abs(p.y - horizon) * 40.0);
17447
+
17448
+ vec2 ext = uShape == 0 ? vec2(R) : (uShape == 1 ? vec2(0.85 * R) : vec2(1.35 * R, 0.55 * R));
17449
+ // The shadow on the floor: a squashed core that tightens and darkens near the ground, and a broad soft one.
17450
+ vec2 g = p - vec2(0.0, groundY);
17451
+ float coreW = ext.x * 0.8 + h * uSpread * 0.35;
17452
+ float core = exp(-dot(g / vec2(coreW, coreW * 0.16), g / vec2(coreW, coreW * 0.16)) * 1.6) * exp(-h * 10.0);
17453
+ float wideW = ext.x * 1.15 + h * uSpread;
17454
+ float wide = exp(-dot(g / vec2(wideW, wideW * 0.22), g / vec2(wideW, wideW * 0.22)) * 1.2) * 0.45 / (1.0 + h * 8.0);
17455
+ float shade = uDarkness * (1.0 - (1.0 - core) * (1.0 - wide));
17456
+ col = mix(col, uShadow, shade);
17457
+
17458
+ // The object.
17459
+ vec2 q = p - vec2(0.0, groundY + ext.y + h);
17460
+ float d = uShape == 0 ? length(q) - R : (uShape == 1 ? box(q, ext, 0.035) : box(q, ext, ext.y));
17461
+ vec2 u = clamp(q / ext, -1.0, 1.0);
17462
+ vec3 obj;
17463
+ if (uShape == 0) {
17464
+ vec3 n = vec3(u, sqrt(max(1.0 - dot(u, u), 0.0)));
17465
+ vec3 l = normalize(vec3(-0.45, 0.6, 0.65));
17466
+ float diff = max(dot(n, l), 0.0);
17467
+ float spec = pow(max(dot(reflect(-l, n), vec3(0.0, 0.0, 1.0)), 0.0), 24.0);
17468
+ obj = uObject * (0.35 + 0.75 * diff) + 0.35 * spec;
17469
+ } else {
17470
+ obj = uObject * (0.88 + 0.16 * u.y - 0.06 * u.x);
17471
+ obj += 0.12 * smoothstep(0.7, 1.0, u.y) * smoothstep(1.0, 0.6, abs(u.x));
17472
+ }
17473
+ // Near the ground the floor shades its underside.
17474
+ obj *= 1.0 - 0.35 * exp(-h * 14.0) * smoothstep(-0.2, -1.0, u.y);
17475
+ col = mix(obj, col, smoothstep(-px, px, d));
17476
+ fragColor = vec4(col, 1.0);
17477
+ }`
17478
+ ),
17479
+ presets: [
17480
+ { name: "Cube", values: { uShape: 1, uObject: [0.38, 0.4, 0.95], uBackground: [0.93, 0.94, 0.97], uGround: [0.85, 0.87, 0.92], uShadow: [0.1, 0.12, 0.25] } },
17481
+ { name: "Pill on dark", values: { uShape: 2, uObject: [0.98, 0.84, 0.3], uBackground: [0.13, 0.14, 0.17], uGround: [0.18, 0.19, 0.23], uShadow: [0, 0, 0], uDarkness: 0.9 } }
17482
+ ]
17483
+ };
17484
+ var windowShadows = {
17485
+ kind: "shader",
17486
+ id: "window-shadows",
17487
+ name: "Window shadows",
17488
+ description: "Warm sunlight through a window falling across a wall or a picture, dappled by the shadows of slowly tilting blinds or rustling leaves.",
17489
+ category: "shadow",
17490
+ video: "filter",
17491
+ tags: ["shadow", "light", "dappled", "window", "blinds", "leaves", "sunlight", "warm", "image"],
17492
+ source: glsl(
17493
+ `// Window shadows: a patch of sunlight from a window, striped by blinds or broken up by leaves, laid over a wall or picture.
17494
+ uniform sampler2D uImage; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Image"
17495
+ uniform bool uPlainWall; // @control default=false label="Plain wall (no image)"
17496
+ uniform vec3 uWall; // @control color default=#E8DCCB label="Wall"
17497
+ uniform int uPattern; // @control min=0 max=1 default=1 label="Shadows of (0 blinds, 1 leaves)"
17498
+ uniform vec3 uLight; // @control color default=#FFC27A label="Light colour"
17499
+ uniform float uIntensity; // @control min=0 max=2 default=0.9 label="Light strength"
17500
+ uniform float uShade; // @control min=0 max=1 default=0.45 label="Shadow depth"
17501
+ uniform float uAngle; // @control min=-60 max=60 default=-22 label="Light angle (\xB0)"
17502
+ uniform float uScale; // @control min=0.4 max=2.5 default=1 label="Scale"
17503
+ uniform float uSoftness; // @control min=0 max=1 default=0.35 label="Softness"
17504
+ uniform float uSway; // @control min=0 max=2 default=1 label="Sway"
17505
+ ${BACKDROP_CONTROLS}`,
17506
+ PICTURE,
17507
+ HASH,
17508
+ NOISE,
17509
+ FBM,
17510
+ ROTATE,
17511
+ `// The sunlit patch the window throws, slanted, with soft edges.
17512
+ float windowPatch(vec2 q, float soft) {
17513
+ vec2 e = abs(q) - vec2(0.5, 0.4);
17514
+ return smoothstep(soft, -soft, max(e.x, e.y));
17515
+ }
17516
+
17517
+ float blinds(vec2 q, float t, float soft) {
17518
+ q.x += 0.006 * sin(t * 0.7) * uSway;
17519
+ // The slats tilt slowly open and shut.
17520
+ float open = clamp(0.5 + 0.22 * sin(t * 0.35 * uSway + 0.4), 0.15, 0.85);
17521
+ float f = fract(q.y * 14.0 + 0.04 * sin(q.x * 3.0 + t * 0.5) * uSway);
17522
+ float w = 0.03 + soft * 0.25;
17523
+ float lit = smoothstep(0.0, w, f) * smoothstep(open + w, open, f);
17524
+ // The window's frame and cross bar.
17525
+ float bars = smoothstep(0.012 + soft * 0.03, 0.006, abs(q.x)) + smoothstep(0.012 + soft * 0.03, 0.006, abs(q.y - 0.02));
17526
+ return lit * (1.0 - clamp(bars, 0.0, 1.0)) * windowPatch(q, 0.02 + soft * 0.06);
17527
+ }
17528
+
17529
+ float leaves(vec2 q, float t, float soft) {
17530
+ // Branches sway slowly and the leaves rustle quickly on top.
17531
+ vec2 sway = uSway * (vec2(0.05 * sin(t * 0.6 + q.y * 1.5), 0.03 * cos(t * 0.45 + q.x * 1.2))
17532
+ + 0.012 * vec2(vnoise(q * 6.0 + t * 1.3), vnoise(q * 6.0 - t * 1.1 + 7.0)));
17533
+ float n = fbm((q + sway) * 3.2 + 4.0, 5);
17534
+ float s = 0.025 + soft * 0.1;
17535
+ float lit = smoothstep(0.5 - s, 0.5 + s, n);
17536
+ return lit * windowPatch(q * 0.75, 0.08 + soft * 0.1);
17537
+ }
17538
+
17539
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17540
+ vec2 uv = fragCoord / iResolution.xy;
17541
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
17542
+ // Light comes in at an angle, so the window's shape is turned and sheared across the wall.
17543
+ vec2 q = rot(radians(uAngle)) * p / uScale;
17544
+ q.x += q.y * 0.35;
17545
+ float lit = uPattern == 0 ? blinds(q, iTime, uSoftness) : leaves(q, iTime, uSoftness);
17546
+
17547
+ vec4 base;
17548
+ if (uPlainWall) {
17549
+ float plaster = vnoise(fragCoord * 0.35) * 0.5 + vnoise(fragCoord * 0.05) * 0.5;
17550
+ base = vec4(uWall * (0.96 + 0.06 * plaster), 1.0);
17551
+ } else {
17552
+ base = pictureRGBA(uImage, uv);
17553
+ }
17554
+ vec3 light = (1.0 - uShade) * vec3(0.95, 0.96, 1.0) + lit * uShade * uLight * (0.6 + uIntensity);
17555
+ vec3 col = base.rgb * light;
17556
+ col = col / (1.0 + max(col - 1.0, 0.0));
17557
+ fragColor = vec4(col, base.a);
17558
+ }`
17559
+ ),
17560
+ presets: [
17561
+ { name: "Blinds", values: { uPattern: 0, uPlainWall: true, uAngle: -30, uSoftness: 0.2, uLight: [1, 0.82, 0.6] } },
17562
+ { name: "Plain wall", values: { uPlainWall: true } },
17563
+ { name: "Golden hour", values: { uPlainWall: true, uLight: [1, 0.6, 0.3], uIntensity: 1.4, uShade: 0.7, uSoftness: 0.6, uWall: [0.93, 0.85, 0.78] } }
17564
+ ]
17565
+ };
17566
+ var shadowPuppets = {
17567
+ kind: "shader",
17568
+ id: "shadow-puppets",
17569
+ name: "Shadow puppets",
17570
+ description: "Hand shadow puppets on a lamp-lit wall: a flapping bird, a barking dog or a twitching rabbit, softening as the hand moves away from the wall.",
17571
+ category: "shadow",
17572
+ tags: ["shadow", "puppet", "hands", "silhouette", "bird", "dog", "rabbit", "warm", "playful"],
17573
+ source: glsl(
17574
+ `// Shadow puppets: hand shapes built from soft-unioned capsules, cast on a warmly lit wall; the further the hand from the wall, the bigger and softer.
17575
+ uniform int uPuppet; // @control min=0 max=2 default=0 label="Puppet (0 bird, 1 dog, 2 rabbit)"
17576
+ uniform vec3 uWall; // @control color default=#F2DDB8 label="Wall"
17577
+ uniform vec3 uLight; // @control color default=#FFB75E label="Lamp colour"
17578
+ uniform vec3 uShadow; // @control color default=#2B1B12 label="Shadow colour"
17579
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
17580
+ uniform float uDepth; // @control min=0 max=1 default=0.6 label="Hand moving to and from the wall"
17581
+ uniform float uSoftness; // @control min=0 max=0.1 default=0.035 label="Softness away from the wall"
17582
+ uniform float uSize; // @control min=0.5 max=1.5 default=1 label="Size"`,
17583
+ HASH,
17584
+ NOISE,
17585
+ ROTATE,
17586
+ SDF,
17587
+ `float ellipse(vec2 p, vec2 r) {
17588
+ return (length(p / r) - 1.0) * min(r.x, r.y);
17589
+ }
17590
+
17591
+ // Two hands linked at the thumbs (the head), fingers fanned out as wings.
17592
+ float bird(vec2 p, float t) {
17593
+ float flap = 0.45 * sin(t * 4.0);
17594
+ float d = capsule(p, vec2(0.0, -0.02), vec2(0.0, 0.13), 0.028);
17595
+ d = smin(d, ellipse(p - vec2(0.0, 0.165), vec2(0.04, 0.035)), 0.02);
17596
+ d = min(d, capsule(p, vec2(0.025, 0.17), vec2(0.075, 0.158), 0.009));
17597
+ for (int s = 0; s < 2; s++) {
17598
+ vec2 q = vec2(s == 0 ? p.x : -p.x, p.y);
17599
+ d = smin(d, ellipse(q - vec2(0.08, 0.0), vec2(0.075, 0.045)), 0.03);
17600
+ d = smin(d, capsule(q, vec2(0.08, -0.01), vec2(0.3, -0.65), 0.06), 0.03);
17601
+ for (int k = 0; k < 4; k++) {
17602
+ float fk = float(k);
17603
+ float a = 0.55 - fk * 0.22 + flap * (1.0 - fk * 0.12);
17604
+ vec2 root = vec2(0.13, 0.02 - fk * 0.018);
17605
+ d = smin(d, capsule(q, root, root + (0.2 - fk * 0.02) * vec2(cos(a), sin(a)), 0.019 - fk * 0.002), 0.012);
17606
+ }
17607
+ }
17608
+ return d;
17609
+ }
17610
+
17611
+ // The back of the hand is the head, the thumb an ear, the little finger a jaw that drops as it barks.
17612
+ float dog(vec2 p, float t) {
17613
+ float bark = max(sin(t * 3.0), 0.0);
17614
+ bark *= bark;
17615
+ float d = box(p - vec2(-0.02, 0.03), vec2(0.12, 0.065), 0.05);
17616
+ d = smin(d, capsule(p, vec2(0.06, 0.04), vec2(0.24, 0.025), 0.038), 0.03);
17617
+ vec2 pivot = vec2(0.0, -0.03);
17618
+ vec2 jaw = rot(0.4 * bark) * (p - pivot) + pivot;
17619
+ d = smin(d, capsule(jaw, vec2(0.02, -0.04), vec2(0.2, -0.045), 0.024), 0.012);
17620
+ d = smin(d, capsule(p, vec2(-0.06, 0.08), vec2(-0.1 + 0.04 * sin(t * 1.3), 0.2), 0.042), 0.03);
17621
+ d = smin(d, capsule(p, vec2(-0.08, 0.0), vec2(-0.5, -0.65), 0.075), 0.04);
17622
+ // A gap between the fingers for an eye.
17623
+ return max(d, -(length(p - vec2(0.07, 0.07)) - 0.013));
17624
+ }
17625
+
17626
+ // Two fingers up as ears, the fist a head with a twitching nose.
17627
+ float rabbit(vec2 p, float t) {
17628
+ float nose = 0.006 * sin(t * 18.0) * step(0.3, sin(t * 0.9));
17629
+ float d = ellipse(p, vec2(0.12, 0.085));
17630
+ d = smin(d, length(p - vec2(0.11 + nose, -0.025)) - 0.05, 0.04);
17631
+ d = smin(d, capsule(p, vec2(-0.04, 0.05), vec2(-0.09 + 0.08 * sin(t * 1.7), 0.32), 0.04), 0.03);
17632
+ d = smin(d, capsule(p, vec2(0.0, 0.06), vec2(0.03 + 0.08 * sin(t * 1.7 + 1.2), 0.3), 0.037), 0.03);
17633
+ d = smin(d, capsule(p, vec2(-0.06, -0.03), vec2(-0.45, -0.65), 0.075), 0.04);
17634
+ return max(d, -(length(p - vec2(0.045, 0.025)) - 0.013));
17635
+ }
17636
+
17637
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17638
+ float px = 1.0 / iResolution.y;
17639
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
17640
+ float t = iTime * uSpeed;
17641
+ // The lamp's pool of light on the wall, and a little plaster.
17642
+ float spot = smoothstep(0.95, 0.05, length((p - vec2(0.04, 0.05)) * vec2(0.75, 1.0)));
17643
+ vec3 wall = uWall * mix(vec3(0.16, 0.11, 0.09), uLight * 1.3, spot);
17644
+ wall *= 0.95 + 0.07 * vnoise(fragCoord * 0.3) + 0.04 * vnoise(fragCoord * 0.03);
17645
+ // Nearer the lamp the shadow grows and its edge blurs.
17646
+ float z = uDepth * (0.5 + 0.5 * sin(iTime * 0.45));
17647
+ float scale = uSize * (1.0 + 0.3 * z);
17648
+ vec2 drift = vec2(0.03 * sin(iTime * 0.4), 0.02 * sin(iTime * 0.7) - 0.04);
17649
+ vec2 q = (p - drift) / scale;
17650
+ float d = (uPuppet == 0 ? bird(q, t) : (uPuppet == 1 ? dog(q, t) : rabbit(q, t))) * scale;
17651
+ float soft = max(0.002 + uSoftness * z, px);
17652
+ float cover = 1.0 - smoothstep(-soft, soft, d);
17653
+ vec3 col = mix(wall, uShadow * (0.6 + 0.4 * spot), cover * 0.92);
17654
+ fragColor = vec4(col, 1.0);
17655
+ }`
17656
+ ),
17657
+ presets: [
17658
+ { name: "Dog", values: { uPuppet: 1 } },
17659
+ { name: "Rabbit by candle", values: { uPuppet: 2, uLight: [1, 0.55, 0.25], uWall: [0.95, 0.8, 0.62], uSoftness: 0.06 } },
17660
+ { name: "Moonlit bird", values: { uPuppet: 0, uLight: [0.7, 0.8, 1], uWall: [0.78, 0.82, 0.9], uShadow: [0.05, 0.07, 0.13] } }
17661
+ ]
17662
+ };
17663
+ var shadowShaders = [longShadow, dropShadowLift, lightSweepShadows, contactShadow, windowShadows, shadowPuppets];
17664
+
17665
+ // src/catalog/shaders/distortion.ts
17666
+ var CENTRE = `
17667
+ vec2 centreAt(vec2 centre, bool follow) {
17668
+ vec2 drift = vec2(0.03 * sin(iTime * 0.37), 0.025 * cos(iTime * 0.29));
17669
+ vec2 idle = (centre + drift) * iResolution.xy;
17670
+ float h = follow ? clamp(iPointer.z, 0.0, 1.0) : 0.0;
17671
+ return mix(idle, iPointer.xy, h);
17672
+ }`.trim();
17673
+ var twirl = {
17674
+ kind: "shader",
17675
+ id: "twirl",
17676
+ name: "Twirl",
17677
+ description: "A picture twisted into a swirl that winds and unwinds around the pointer or a centre, strongest in the middle.",
17678
+ category: "distortion",
17679
+ video: "interactive",
17680
+ tags: ["distortion", "swirl", "image", "interactive", "hover"],
17681
+ source: glsl(
17682
+ `// Twirl: the picture swirled around a centre, the twist falling off with radius.
17683
+ uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.landscape} label="Image"
17684
+ uniform float uTwist; // @control min=-12 max=12 default=5 label="Twist (radians)"
17685
+ uniform float uRadius; // @control min=0.05 max=1.2 default=0.45 label="Radius"
17686
+ uniform float uSpeed; // @control min=0 max=3 default=0.6 label="Wind speed (0 holds it)"
17687
+ uniform vec2 uCentre; // @control min=0 max=1 default=0.5,0.5 label="Centre"
17688
+ uniform bool uFollow; // @control default=true label="Follow the pointer"
17689
+ ${BACKDROP_CONTROLS}`,
17690
+ ROTATE,
17691
+ PICTURE,
17692
+ CENTRE,
17693
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17694
+ vec2 c = centreAt(uCentre, uFollow);
17695
+ vec2 d = fragCoord - c;
17696
+ float R = uRadius * iResolution.y;
17697
+ float f = clamp(1.0 - length(d) / R, 0.0, 1.0);
17698
+ f = f * f * (3.0 - 2.0 * f);
17699
+ float swing = cos(iTime * uSpeed);
17700
+ d = rot(uTwist * swing * f * f) * d;
17701
+ fragColor = pictureRGBA(uImage, (c + d) / iResolution.xy);
17702
+ }`
17703
+ ),
17704
+ presets: [
17705
+ { name: "Whirlpool", values: { uTwist: 10, uRadius: 0.8, uSpeed: 0.3 } },
17706
+ { name: "Flick", values: { uTwist: -3, uRadius: 0.25, uSpeed: 2 } }
17707
+ ]
17708
+ };
17709
+ var pinchBulge = {
17710
+ kind: "shader",
17711
+ id: "pinch-bulge",
17712
+ name: "Pinch & bulge",
17713
+ description: "A picture swelling out like a bubble or sucked in to a point around the pointer or a centre.",
17714
+ category: "distortion",
17715
+ video: "interactive",
17716
+ tags: ["distortion", "bulge", "pinch", "image", "interactive", "hover"],
17717
+ source: glsl(
17718
+ `// Pinch & bulge: the picture bulged out (positive) or pinched in (negative) inside a radius.
17719
+ uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.portrait} label="Image"
17720
+ uniform float uStrength; // @control min=-1 max=1 default=0.7 label="Pinch \u2190 \u2192 Bulge"
17721
+ uniform float uRadius; // @control min=0.05 max=1 default=0.4 label="Radius"
17722
+ uniform bool uAnimate; // @control default=true label="Animate (swing between pinch and bulge)"
17723
+ uniform float uSpeed; // @control min=0.1 max=4 default=1 label="Swing speed"
17724
+ uniform vec2 uCentre; // @control min=0 max=1 default=0.5,0.5 label="Centre"
17725
+ uniform bool uFollow; // @control default=true label="Follow the pointer"
17726
+ ${BACKDROP_CONTROLS}`,
17727
+ PICTURE,
17728
+ CENTRE,
17729
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17730
+ vec2 c = centreAt(uCentre, uFollow);
17731
+ vec2 d = fragCoord - c;
17732
+ float R = uRadius * iResolution.y;
17733
+ float t = length(d) / R;
17734
+ float s = uAnimate ? uStrength * cos(iTime * uSpeed) : uStrength;
17735
+ float shade = 1.0;
17736
+ if (t < 1.0) {
17737
+ // A power curve that meets the untouched picture at the rim: above 1 magnifies the middle, below 1 shrinks it.
17738
+ float e = exp(s * 1.3);
17739
+ d *= pow(max(t, 1e-4), e - 1.0);
17740
+ shade = 1.0 + 0.18 * s * (1.0 - t) * (1.0 - t);
17741
+ }
17742
+ vec4 pic = pictureRGBA(uImage, (c + d) / iResolution.xy);
17743
+ fragColor = vec4(pic.rgb * shade, pic.a);
17744
+ }`
17745
+ ),
17746
+ presets: [
17747
+ { name: "Bubble", values: { uStrength: 1, uRadius: 0.3, uAnimate: false } },
17748
+ { name: "Black hole", values: { uStrength: -1, uRadius: 0.6, uAnimate: false } }
17749
+ ]
17750
+ };
17751
+ var fisheye = {
17752
+ kind: "shader",
17753
+ id: "fisheye",
17754
+ name: "Fisheye",
17755
+ description: "A picture seen through a fisheye lens, bulging in the middle and curving to the corners, over the whole frame or in a round lens.",
17756
+ category: "distortion",
17757
+ video: "filter",
17758
+ tags: ["distortion", "lens", "fisheye", "barrel", "image"],
17759
+ source: glsl(
17760
+ `// Fisheye: an equidistant fisheye projection of a flat picture, framed or in a circle.
17761
+ uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.city} label="Image"
17762
+ uniform float uStrength; // @control min=0 max=1 default=0.75 label="Strength (field of view)"
17763
+ uniform float uZoom; // @control min=0.5 max=2 default=1 label="Zoom"
17764
+ uniform bool uLens; // @control default=false label="Round lens"
17765
+ uniform float uRadius; // @control min=0.1 max=0.7 default=0.42 label="Lens radius"
17766
+ uniform float uVignette; // @control min=0 max=1 default=0.35 label="Vignette"
17767
+ ${BACKDROP_CONTROLS}`,
17768
+ PICTURE,
17769
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17770
+ vec2 mid = 0.5 * iResolution.xy;
17771
+ vec2 d = fragCoord - mid;
17772
+ // The unit radius: the half diagonal (corners stay corners) or the lens.
17773
+ float unit = uLens ? uRadius * iResolution.y : 0.5 * length(iResolution.xy);
17774
+ float r = length(d) / unit;
17775
+ vec4 col = pictureRGBA(uImage, fragCoord / iResolution.xy);
17776
+ if (r < 1.0) {
17777
+ // A ray at angle r * fov / 2 lands at tan(angle) on the flat picture.
17778
+ float fov = mix(0.02, 2.9, uStrength);
17779
+ float src = tan(r * 0.5 * fov) / tan(0.5 * fov);
17780
+ vec2 p = mid + d / max(length(d), 1e-5) * src * unit / uZoom;
17781
+ vec4 lens = pictureRGBA(uImage, p / iResolution.xy);
17782
+ lens.rgb *= 1.0 - uVignette * pow(r, 2.5);
17783
+ float edge = uLens ? 1.0 - smoothstep(1.0 - 2.0 / unit, 1.0, r) : 1.0;
17784
+ col = mix(col, lens, edge);
17785
+ }
17786
+ if (uLens) {
17787
+ float rim = exp(-pow((r - 1.0) * unit / max(unit * 0.02, 1.5), 2.0));
17788
+ col.rgb = mix(col.rgb, vec3(0.06), rim * 0.6);
17789
+ } else {
17790
+ col.rgb *= 1.0 - uVignette * smoothstep(0.98, 1.0, r);
17791
+ }
17792
+ fragColor = col;
17793
+ }`
17794
+ ),
17795
+ presets: [
17796
+ { name: "Doorway peephole", values: { uLens: true, uStrength: 1, uRadius: 0.48, uVignette: 0.6 } },
17797
+ { name: "Action cam", values: { uStrength: 0.45, uZoom: 1.1, uVignette: 0.15 } }
17798
+ ]
17799
+ };
17800
+ var mirrorTiles = {
17801
+ kind: "shader",
17802
+ id: "mirror-tiles",
17803
+ name: "Mirror tiles",
17804
+ description: "A picture folded into mirrored kaleidoscope tiles, square or hexagonal, slowly turning and drifting.",
17805
+ category: "distortion",
17806
+ video: "filter",
17807
+ tags: ["distortion", "kaleidoscope", "mirror", "tiles", "image"],
17808
+ source: glsl(
17809
+ `// Mirror tiles: each tile folds the picture about its centre into mirrored wedges.
17810
+ uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.landscape} label="Image"
17811
+ uniform int uFold; // @control min=2 max=8 step=2 default=6 label="Mirror fold"
17812
+ uniform bool uHex; // @control default=false label="Hexagonal tiles"
17813
+ uniform float uTiles; // @control min=1 max=8 default=2 label="Tiles down"
17814
+ uniform float uZoom; // @control min=0.2 max=2 default=1 label="Picture scale"
17815
+ uniform float uSpin; // @control min=-1 max=1 default=0.1 label="Spin"
17816
+ uniform float uDrift; // @control min=0 max=1 default=0.2 label="Drift"
17817
+ ${BACKDROP_CONTROLS}`,
17818
+ PICTURE,
17819
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17820
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y * uTiles;
17821
+ p += uDrift * iTime * vec2(0.13, 0.07);
17822
+ vec2 q;
17823
+ if (uHex) {
17824
+ vec2 s = vec2(1.0, 1.7320508);
17825
+ vec4 hc = floor(vec4(p, p - vec2(0.5, 0.866)) / s.xyxy) + 0.5;
17826
+ vec4 h = vec4(p - hc.xy * s, p - (hc.zw + vec2(0.5, 0.5)) * s);
17827
+ q = dot(h.xy, h.xy) < dot(h.zw, h.zw) ? h.xy : h.zw;
17828
+ } else {
17829
+ q = fract(p) - 0.5;
17830
+ }
17831
+ float sector = 6.28318 / float(uFold);
17832
+ float a = mod(atan(q.y, q.x) + uSpin * iTime, sector);
17833
+ a = min(a, sector - a);
17834
+ q = length(q) * vec2(cos(a), sin(a));
17835
+ vec2 pan = uDrift * 0.15 * vec2(sin(iTime * 0.21), cos(iTime * 0.17));
17836
+ fragColor = pictureRGBA(uImage, 0.5 + pan + q * uZoom);
17837
+ }`
17838
+ ),
17839
+ presets: [
17840
+ { name: "Hex jewel", values: { uHex: true, uFold: 6, uTiles: 2.5, uZoom: 0.5 } },
17841
+ { name: "Four-way", values: { uFold: 4, uTiles: 2, uSpin: 0, uDrift: 0.4 } }
17842
+ ]
17843
+ };
17844
+ var waveWarp = {
17845
+ kind: "shader",
17846
+ id: "wave-warp",
17847
+ name: "Wave warp",
17848
+ description: "A picture rippling in sine waves sideways, up and down or both, like a flag or a reflection on water, with lit crests.",
17849
+ category: "distortion",
17850
+ video: "filter",
17851
+ tags: ["distortion", "wave", "flag", "water", "image"],
17852
+ source: glsl(
17853
+ `// Wave warp: rows and/or columns of the picture shifted along travelling sine waves.
17854
+ uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.landscape} label="Image"
17855
+ uniform bool uRows; // @control default=true label="Shift rows sideways"
17856
+ uniform bool uColumns; // @control default=false label="Shift columns up and down"
17857
+ uniform float uAmplitude; // @control min=0 max=0.08 default=0.02 label="Amplitude"
17858
+ uniform float uFrequency; // @control min=0.5 max=40 default=6 label="Waves across"
17859
+ uniform float uSpeed; // @control min=-6 max=6 default=2 label="Speed"
17860
+ uniform float uShade; // @control min=0 max=1 default=0.3 label="Crest shading"
17861
+ ${BACKDROP_CONTROLS}`,
17862
+ PICTURE,
17863
+ `float wave(float x) {
17864
+ return sin(x) + 0.3 * sin(2.3 * x + 1.0);
17865
+ }
17866
+ float slope(float x) {
17867
+ return cos(x) + 0.69 * cos(2.3 * x + 1.0);
17868
+ }
17869
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17870
+ vec2 uv = fragCoord / iResolution.xy;
17871
+ float py = uv.y * uFrequency * 6.28318 - iTime * uSpeed;
17872
+ float px = uv.x * uFrequency * 6.28318 - iTime * uSpeed;
17873
+ vec2 offset = vec2(uRows ? wave(py) : 0.0, uColumns ? wave(px) : 0.0) * uAmplitude;
17874
+ float light = (uRows ? slope(py) : 0.0) + (uColumns ? slope(px) : 0.0);
17875
+ vec4 pic = pictureRGBA(uImage, uv + offset);
17876
+ float k = uShade * 0.18 * light * clamp(uAmplitude * 40.0, 0.0, 1.0);
17877
+ fragColor = vec4(pic.rgb * (1.0 + k) + max(k, 0.0) * 0.4, pic.a);
17878
+ }`
17879
+ ),
17880
+ presets: [
17881
+ { name: "Flag", values: { uRows: false, uColumns: true, uAmplitude: 0.03, uFrequency: 2, uSpeed: 3, uShade: 0.8 } },
17882
+ { name: "Reflection", values: { uRows: true, uColumns: false, uAmplitude: 6e-3, uFrequency: 32, uSpeed: 3, uShade: 0.2 } },
17883
+ { name: "Jelly", values: { uRows: true, uColumns: true, uAmplitude: 0.015, uFrequency: 3, uSpeed: 1.5 } }
17884
+ ]
17885
+ };
17886
+ var ribbedGlass = {
17887
+ kind: "shader",
17888
+ id: "ribbed-glass",
17889
+ name: "Ribbed glass",
17890
+ description: "A picture seen through fluted glass: strips of refracted image with bright and dark rib edges, clear or frosted.",
17891
+ category: "distortion",
17892
+ video: "filter",
17893
+ tags: ["distortion", "glass", "fluted", "reeded", "frosted", "image"],
17894
+ source: glsl(
17895
+ `// Ribbed glass: each rib is a little cylinder lens that flips and squeezes the picture behind it.
17896
+ uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.city} label="Image"
17897
+ uniform float uWidth; // @control min=4 max=120 default=28 label="Rib width (px)"
17898
+ uniform float uAngle; // @control min=-90 max=90 default=0 label="Rib angle (\xB0)"
17899
+ uniform float uRefraction; // @control min=0 max=1 default=0.5 label="Refraction"
17900
+ uniform float uFrost; // @control min=0 max=1 default=0.15 label="Frost"
17901
+ uniform float uHighlight; // @control min=0 max=1 default=0.4 label="Edge highlights"
17902
+ uniform vec3 uTint; // @control color default=#F2F7FA label="Glass tint"
17903
+ ${BACKDROP_CONTROLS}`,
17904
+ HASH,
17905
+ PICTURE,
17906
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17907
+ float a = radians(uAngle);
17908
+ vec2 across = vec2(cos(a), sin(a));
17909
+ float s = dot(fragCoord - 0.5 * iResolution.xy, across) / uWidth;
17910
+ float f = fract(s) - 0.5;
17911
+ vec2 bend = -across * f * uWidth * 3.0 * uRefraction;
17912
+ // Frost: a small disc of samples, jittered per pixel.
17913
+ float blur = uFrost * 14.0;
17914
+ float spin = hash12(fragCoord) * 6.28318;
17915
+ vec4 acc = vec4(0.0);
17916
+ for (int i = 0; i < 9; i++) {
17917
+ float fi = float(i);
17918
+ float ang = fi * 2.39996 + spin;
17919
+ vec2 o = blur * sqrt((fi + 0.5) / 9.0) * vec2(cos(ang), sin(ang));
17920
+ acc += pictureRGBA(uImage, (fragCoord + bend + o) / iResolution.xy);
17921
+ }
17922
+ vec4 pic = acc / 9.0;
17923
+ vec3 col = pic.rgb * uTint;
17924
+ col = mix(col, vec3(dot(col, vec3(0.299, 0.587, 0.114))) + 0.08, uFrost * 0.2);
17925
+ float hi = exp(-pow((f - 0.3) * 12.0, 2.0));
17926
+ float lo = smoothstep(0.38, 0.5, abs(f));
17927
+ col = col * (1.0 - 0.25 * lo * uHighlight) + hi * uHighlight * 0.35;
17928
+ fragColor = vec4(col, pic.a);
17929
+ }`
17930
+ ),
17931
+ presets: [
17932
+ { name: "Reeded", values: { uWidth: 14, uRefraction: 0.7, uFrost: 0 } },
17933
+ { name: "Wide flutes", values: { uWidth: 70, uRefraction: 0.4, uHighlight: 0.6 } },
17934
+ { name: "Frosted diagonal", values: { uAngle: 30, uFrost: 0.8, uWidth: 36 } }
17935
+ ]
17936
+ };
17937
+ var showerGlass = {
17938
+ kind: "shader",
17939
+ id: "shower-glass",
17940
+ name: "Shower glass",
17941
+ description: "A picture blurred and bent behind textured shower-door glass, pebbled or streaked with rain, catching small highlights.",
17942
+ category: "distortion",
17943
+ video: "filter",
17944
+ tags: ["distortion", "glass", "privacy", "rain", "pebble", "image"],
17945
+ source: glsl(
17946
+ `// Shower glass: a bumpy height field refracts and blurs the picture, lit from the top left.
17947
+ uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.portrait2} label="Image"
17948
+ uniform bool uRain; // @control default=false label="Rain pattern (else pebbled)"
17949
+ uniform float uScale; // @control min=4 max=60 default=16 label="Bumps down"
17950
+ uniform float uRefraction; // @control min=0 max=1 default=0.5 label="Refraction"
17951
+ uniform float uBlur; // @control min=0 max=1 default=0.35 label="Blur"
17952
+ uniform float uDim; // @control min=0 max=1 default=0.15 label="Dim"
17953
+ uniform float uHighlight; // @control min=0 max=1 default=0.5 label="Highlights"
17954
+ uniform float uFlow; // @control min=0 max=1 default=0.1 label="Flow"
17955
+ ${BACKDROP_CONTROLS}`,
17956
+ HASH,
17957
+ NOISE,
17958
+ PICTURE,
17959
+ `// Round domes on a jittered grid: 1 at a pebble's top, 0 between.
17960
+ float pebbles(vec2 p) {
17961
+ vec2 i = floor(p);
17962
+ vec2 f = fract(p);
17963
+ float d = 8.0;
17964
+ for (int y = -1; y <= 1; y++) {
17965
+ for (int x = -1; x <= 1; x++) {
17966
+ vec2 g = vec2(float(x), float(y));
17967
+ vec2 o = 0.15 + 0.7 * hash22(i + g);
17968
+ d = min(d, length(g + o - f));
17969
+ }
17970
+ }
17971
+ return 1.0 - smoothstep(0.0, 0.75, d);
17972
+ }
17973
+ float height(vec2 p) {
17974
+ float t = iTime * uFlow;
17975
+ if (uRain) {
17976
+ float streak = vnoise(vec2(p.x * 2.5, p.y * 0.3 + t * 2.0));
17977
+ return 0.55 * streak + 0.45 * pebbles(p * 1.7 + vec2(0.0, t * 3.0));
17978
+ }
17979
+ return pebbles(p + vec2(0.0, t) + 0.3 * vnoise(p * 0.7));
17980
+ }
17981
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17982
+ vec2 p = fragCoord / iResolution.y * uScale;
17983
+ float e = 0.03;
17984
+ vec2 grad = vec2(height(p + vec2(e, 0.0)) - height(p - vec2(e, 0.0)), height(p + vec2(0.0, e)) - height(p - vec2(0.0, e))) / (2.0 * e);
17985
+ vec2 uv = fragCoord / iResolution.xy - grad * uRefraction * 0.02;
17986
+ float blur = uBlur * 0.012;
17987
+ float spin = hash12(fragCoord) * 6.28318;
17988
+ vec4 acc = vec4(0.0);
17989
+ for (int i = 0; i < 8; i++) {
17990
+ float fi = float(i);
17991
+ float ang = fi * 2.39996 + spin;
17992
+ acc += pictureRGBA(uImage, uv + blur * sqrt((fi + 0.5) / 8.0) * vec2(cos(ang) * iResolution.y / iResolution.x, sin(ang)));
17993
+ }
17994
+ vec4 pic = acc / 8.0;
17995
+ vec3 n = normalize(vec3(-grad * 0.12, 1.0));
17996
+ vec3 h = normalize(normalize(vec3(-0.5, 0.6, 0.6)) + vec3(0.0, 0.0, 1.0));
17997
+ float spec = pow(max(dot(n, h), 0.0), 60.0);
17998
+ float rim = clamp(dot(n.xy, vec2(-0.6, 0.6)) * 2.0, -1.0, 1.0);
17999
+ vec3 col = pic.rgb * (1.0 - uDim) * (1.0 + 0.12 * rim * uHighlight) + spec * uHighlight * 0.8;
18000
+ fragColor = vec4(col, pic.a);
18001
+ }`
18002
+ ),
18003
+ presets: [
18004
+ { name: "Rain door", values: { uRain: true, uScale: 10, uFlow: 0.3, uBlur: 0.3 } },
18005
+ { name: "Privacy", values: { uScale: 28, uRefraction: 0.8, uBlur: 0.9, uDim: 0.25 } }
18006
+ ]
18007
+ };
18008
+ var datamosh = {
18009
+ kind: "shader",
18010
+ id: "datamosh",
18011
+ name: "Datamosh",
18012
+ description: "A picture breaking down like a corrupted video stream in bursts: macroblocks smear along a false motion field, stick in the wrong place and bleed colour.",
18013
+ category: "distortion",
18014
+ video: "filter",
18015
+ tags: ["distortion", "datamosh", "glitch", "compression", "image"],
18016
+ source: glsl(
18017
+ `// Datamosh: block motion vectors applied to a stale frame, stuck blocks and smeared chroma, in bursts.
18018
+ uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.portrait} label="Image"
18019
+ uniform float uBlock; // @control min=8 max=64 default=24 step=1 label="Macroblock (px)"
18020
+ uniform float uAmount; // @control min=0 max=1 default=0.6 label="Amount"
18021
+ uniform float uSmear; // @control min=0 max=0.4 default=0.12 label="Smear length"
18022
+ uniform float uRate; // @control min=0.2 max=4 default=0.8 label="Bursts per second"
18023
+ uniform float uBleed; // @control min=0 max=1 default=0.6 label="Colour bleed"
18024
+ ${BACKDROP_CONTROLS}`,
18025
+ HASH,
18026
+ NOISE,
18027
+ PICTURE,
18028
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
18029
+ vec2 uv = fragCoord / iResolution.xy;
18030
+ float t = iTime * uRate;
18031
+ float epoch = floor(t);
18032
+ float ph = fract(t);
18033
+ float on = step(1.0 - uAmount * 0.85, hash12(vec2(epoch, 7.0))) * smoothstep(0.0, 0.08, ph);
18034
+ vec2 block = floor(fragCoord / uBlock);
18035
+ // One motion vector per macroblock, from a smooth field that changes each burst.
18036
+ vec2 field = block * 0.12 + epoch * vec2(3.1, 1.7);
18037
+ vec2 mv = vec2(vnoise(field), vnoise(field + 9.7)) * 2.0 - 1.0;
18038
+ vec2 disp = mv * uSmear * ph * on * vec2(iResolution.y / iResolution.x, 1.0);
18039
+ // Stuck blocks: content from the wrong place, sliding slowly.
18040
+ vec2 bh = hash22(block + epoch * 13.0);
18041
+ float stuck = on * step(1.0 - 0.3 * uAmount, bh.x);
18042
+ vec2 wrong = (floor(bh * 6.0) - 2.5) * uBlock / iResolution.xy;
18043
+ vec2 st = uv + stuck * (wrong + mv * 0.02 * ph);
18044
+ // Smear: samples along the motion, weighted towards the oldest.
18045
+ vec4 acc = vec4(0.0);
18046
+ float wsum = 0.0;
18047
+ for (int i = 0; i < 6; i++) {
18048
+ float k = float(i) / 5.0;
18049
+ float w = 0.4 + k;
18050
+ acc += w * pictureRGBA(uImage, st - disp * k);
18051
+ wsum += w;
18052
+ }
18053
+ vec4 pic = acc / wsum;
18054
+ // Bleed: chroma from the block's corner, dragged further along the motion.
18055
+ vec2 chromaUV = (block * uBlock + 0.5) / iResolution.xy - disp * 1.6;
18056
+ vec3 smeared = picture(uImage, chromaUV);
18057
+ float y = dot(pic.rgb, vec3(0.299, 0.587, 0.114));
18058
+ vec3 chroma = mix(pic.rgb - y, smeared - dot(smeared, vec3(0.299, 0.587, 0.114)), uBleed * on);
18059
+ vec3 col = y + chroma * (1.0 + 0.4 * on * uBleed);
18060
+ // A faint block grid where the stream is broken.
18061
+ vec2 edge = abs(fract(fragCoord / uBlock) - 0.5);
18062
+ col *= 1.0 - 0.06 * on * step(0.47, max(edge.x, edge.y));
18063
+ fragColor = vec4(clamp(col, 0.0, 1.0), pic.a);
18064
+ }`
18065
+ ),
18066
+ presets: [
18067
+ { name: "Melted stream", values: { uAmount: 1, uSmear: 0.3, uRate: 0.4, uBleed: 1 } },
18068
+ { name: "Hiccup", values: { uAmount: 0.4, uSmear: 0.05, uRate: 2.5, uBlock: 16 } }
18069
+ ]
18070
+ };
18071
+ var polarWarp = {
18072
+ kind: "shader",
18073
+ id: "polar-warp",
18074
+ name: "Polar warp",
18075
+ description: "A picture wrapped into a circle, as a tiny planet or a tunnel, or a circle unrolled into a strip, slowly turning.",
18076
+ category: "distortion",
18077
+ video: "filter",
18078
+ tags: ["distortion", "polar", "tiny-planet", "tunnel", "image"],
18079
+ source: glsl(
18080
+ `// Polar warp: rectangular \u2192 polar (the picture's bottom edge at the centre), or polar \u2192 rectangular.
18081
+ uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.landscape} label="Image"
18082
+ uniform bool uUnwrap; // @control default=false label="Unwrap (polar \u2192 rectangular)"
18083
+ uniform bool uInsideOut; // @control default=false label="Inside out (tunnel)"
18084
+ uniform float uZoom; // @control min=0.3 max=3 default=1 label="Zoom"
18085
+ uniform float uSpin; // @control min=-0.5 max=0.5 default=0.04 label="Spin (turns/s)"
18086
+ uniform float uTwist; // @control min=-2 max=2 default=0 label="Twist"
18087
+ ${BACKDROP_CONTROLS}`,
18088
+ PICTURE,
18089
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
18090
+ vec2 uv = fragCoord / iResolution.xy;
18091
+ vec2 st;
18092
+ if (uUnwrap) {
18093
+ // Across is the angle, up is the radius of the circle inscribed in the picture.
18094
+ float r = (uInsideOut ? 1.0 - uv.y : uv.y) * 0.5 / uZoom;
18095
+ float a = (uv.x + iTime * uSpin + uTwist * r) * 6.28318;
18096
+ st = 0.5 + r * vec2(cos(a), sin(a));
18097
+ } else {
18098
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
18099
+ float r = length(p) * 2.0 / uZoom;
18100
+ // The angle runs across the picture and back, so the seam is a mirror and does not show.
18101
+ float u = fract(atan(p.y, p.x) / 6.28318 + iTime * uSpin + uTwist * r * 0.25);
18102
+ u = 1.0 - abs(1.0 - 2.0 * u);
18103
+ float v = uInsideOut ? 1.0 - min(r, 1.0) : min(r, 1.0);
18104
+ st = vec2(u, v);
18105
+ }
18106
+ fragColor = pictureRGBA(uImage, st);
18107
+ }`
18108
+ ),
18109
+ presets: [
18110
+ { name: "Tiny planet", values: { uUnwrap: false, uInsideOut: false, uZoom: 1.1, uSpin: 0.03 } },
18111
+ { name: "Tunnel", values: { uUnwrap: false, uInsideOut: true, uZoom: 0.9, uSpin: -0.05, uTwist: 0.8 } },
18112
+ { name: "Unrolled", values: { uUnwrap: true, uZoom: 1, uSpin: 0.02 } }
18113
+ ]
18114
+ };
18115
+ var textureDisplace = {
18116
+ kind: "shader",
18117
+ id: "texture-displace",
18118
+ name: "Texture displace",
18119
+ description: "A picture pushed around by a second, scrolling texture, by its red and green or by its light and shade.",
18120
+ category: "distortion",
18121
+ video: "filter",
18122
+ tags: ["distortion", "displacement", "map", "image"],
18123
+ source: glsl(
18124
+ `// Texture displace: each pixel moved by a displacement map (or its brightness slope) that scrolls.
18125
+ uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.city} label="Image"
18126
+ uniform sampler2D uMap; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.landscape} label="Displacement map"
18127
+ uniform float uStrength; // @control min=0 max=0.2 default=0.06 label="Strength"
18128
+ uniform float uScale; // @control min=0.2 max=4 default=1.5 label="Map scale"
18129
+ uniform vec2 uScroll; // @control min=-0.3 max=0.3 default=0.03,0.02 label="Scroll"
18130
+ uniform bool uSlope; // @control default=false label="Push by brightness slope (else by red/green)"
18131
+ ${BACKDROP_CONTROLS}`,
18132
+ HASH,
18133
+ NOISE,
18134
+ PICTURE,
18135
+ `// The map, or soft noise while there is none (an empty image control is a 1 \xD7 1 placeholder).
18136
+ vec3 mapAt(vec2 p) {
18137
+ if (textureSize(uMap, 0).x > 1) return texture(uMap, p).rgb;
18138
+ return vec3(vnoise(p * 6.0), vnoise(p * 6.0 + 11.3), vnoise(p * 6.0 + 23.1));
18139
+ }
18140
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
18141
+ vec2 uv = fragCoord / iResolution.xy;
18142
+ vec2 m = uv * uScale + uScroll * iTime;
18143
+ vec2 offset;
18144
+ if (uSlope) {
18145
+ float e = 0.03;
18146
+ vec3 w = vec3(0.299, 0.587, 0.114);
18147
+ float dx = dot(mapAt(m + vec2(e, 0.0)) - mapAt(m - vec2(e, 0.0)), w);
18148
+ float dy = dot(mapAt(m + vec2(0.0, e)) - mapAt(m - vec2(0.0, e)), w);
18149
+ offset = vec2(dx, dy) / (2.0 * e) * uStrength * 0.1;
18150
+ } else {
18151
+ offset = (mapAt(m).rg - 0.5) * 2.0 * uStrength;
18152
+ }
18153
+ fragColor = pictureRGBA(uImage, uv + offset);
18154
+ }`
18155
+ ),
18156
+ presets: [
18157
+ { name: "Embossed", values: { uSlope: true, uStrength: 0.08, uScale: 1, uScroll: [0, 0] } },
18158
+ { name: "Drifting channels", values: { uSlope: false, uStrength: 0.03, uScale: 2, uScroll: [0.05, -0.04] } }
18159
+ ]
18160
+ };
18161
+ var distortionShaders = [
18162
+ twirl,
18163
+ pinchBulge,
18164
+ fisheye,
18165
+ mirrorTiles,
18166
+ waveWarp,
18167
+ ribbedGlass,
18168
+ showerGlass,
18169
+ datamosh,
18170
+ polarWarp,
18171
+ textureDisplace
18172
+ ];
18173
+
18174
+ // src/catalog/types.ts
18175
+ var EFFECT_CATEGORIES = [
18176
+ "gradient",
18177
+ "organic",
18178
+ "pattern",
18179
+ "light",
18180
+ "space",
18181
+ "retro",
18182
+ "image",
18183
+ "overlay",
18184
+ "text",
18185
+ "morph",
18186
+ "interaction",
18187
+ "pyrotechnics",
18188
+ "timewarp",
18189
+ "melt",
18190
+ "electric",
18191
+ "splatter",
18192
+ "bubble",
18193
+ "sticker",
18194
+ "trail",
18195
+ "particles",
18196
+ "debris",
18197
+ "impact",
18198
+ "comic",
18199
+ "atmosphere",
18200
+ "glow",
18201
+ "shadow",
18202
+ "distortion",
18203
+ "transition",
18204
+ "loader",
18205
+ "data",
18206
+ "icon"
18207
+ ];
18208
+ var VIDEO_USES = ["filter", "transition", "interactive", "stills", "none"];
18209
+ var VECTOR_CONTROL_TYPES = ["float", "int", "bool", "color", "text"];
18210
+ var PLACEHOLDER = /\{\{\s*([A-Za-z_]\w*)\s*(?::\s*([A-Za-z0-9]*)([^}]*))?\}\}/g;
18211
+ function motionStrings(motion) {
18212
+ const out = [];
18213
+ const walk = (v) => {
18214
+ if (typeof v === "string") out.push(v);
18215
+ else if (Array.isArray(v)) v.forEach(walk);
18216
+ else if (v && typeof v === "object") Object.values(v).forEach(walk);
18217
+ };
18218
+ walk(motion);
18219
+ return out;
18220
+ }
18221
+ function vectorControls(motion) {
18222
+ const controls = [];
18223
+ const seen = /* @__PURE__ */ new Set();
18224
+ for (const text of motionStrings(motion)) {
18225
+ for (const [, name, type, attributes] of text.matchAll(PLACEHOLDER)) {
18226
+ if (type === void 0 || seen.has(name) || !VECTOR_CONTROL_TYPES.includes(type)) continue;
18227
+ seen.add(name);
18228
+ const colour = type === "color";
18229
+ controls.push(controlFromDeclaration(name, colour ? "vec3" : type, `${attributes}${colour ? " color" : ""}`, controls.length + 1));
18230
+ }
18231
+ }
18232
+ return controls;
18233
+ }
18234
+ function vectorEffectControls(effect) {
18235
+ const declared = vectorControls(effect.motion);
18236
+ const names = new Set(declared.map((c) => c.name));
18237
+ const listed = (effect.controls ?? []).filter((c) => !names.has(c.name));
18238
+ return [...declared, ...listed.map((c, i) => vectorControlDef(c, declared.length + i + 1))];
18239
+ }
18240
+ function vectorControlIssues(motion) {
18241
+ const issues = [];
18242
+ const declarations = /* @__PURE__ */ new Map();
18243
+ for (const text of motionStrings(motion)) {
18244
+ for (const [whole, name, type] of text.matchAll(PLACEHOLDER)) {
18245
+ if (type === void 0) continue;
18246
+ if (!VECTOR_CONTROL_TYPES.includes(type))
18247
+ issues.push(`${whole}: a control's type is one of ${VECTOR_CONTROL_TYPES.join(", ")}`);
18248
+ else if (declarations.has(name) && declarations.get(name) !== whole)
18249
+ issues.push(`"${name}" is declared twice; declare it once and write {{${name}}} everywhere else`);
18250
+ else declarations.set(name, whole);
18251
+ }
18252
+ }
18253
+ return issues;
18254
+ }
18255
+ function written(control, value) {
18256
+ if (control.color && Array.isArray(value)) return rgbToHex(value);
18257
+ if (Array.isArray(value)) return value.join(" ");
18258
+ return value;
18259
+ }
18260
+ var escapeMarkup = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
18261
+ function vectorControlDef(control, line = 0) {
18262
+ const min = control.min ?? 0;
18263
+ return {
18264
+ name: control.name,
18265
+ type: control.type,
18266
+ label: control.label ?? control.name,
18267
+ color: control.color ?? false,
18268
+ min,
18269
+ max: control.max ?? Math.max(min + 1, 1),
18270
+ step: control.step ?? (control.type === "int" ? 1 : 0.01),
18271
+ default: control.default,
18272
+ line
18273
+ };
18274
+ }
18275
+ var CURRENT_COLOR = "currentColor";
18276
+ function isCurrentColor(value) {
18277
+ return typeof value === "string" && value.trim().toLowerCase() === "currentcolor";
18278
+ }
18279
+ function renderVectorEffect(effect, values = {}) {
18280
+ const filled = {};
18281
+ const placeholders = /* @__PURE__ */ new Map();
18282
+ for (const control of vectorEffectControls(effect)) {
18283
+ const given = values[control.name];
18284
+ if (control.color && isCurrentColor(given)) {
18285
+ filled[control.name] = CURRENT_COLOR;
18286
+ placeholders.set(control.name, CURRENT_COLOR);
18287
+ continue;
18288
+ }
18289
+ const check = given === void 0 ? void 0 : checkControlValue(control, given);
18290
+ filled[control.name] = check?.ok ? check.value : control.default;
18291
+ placeholders.set(control.name, written(control, filled[control.name]));
18292
+ }
18293
+ for (const [name, value] of Object.entries(effect.derived?.(filled) ?? {})) placeholders.set(name, value);
18294
+ const fill = (text, markup) => {
18295
+ const whole = /^\{\{\s*([A-Za-z_]\w*)\s*(?::[^}]*)?\}\}$/.exec(text);
18296
+ if (!markup && whole && placeholders.has(whole[1])) return placeholders.get(whole[1]);
18297
+ return text.replace(PLACEHOLDER, (match, name) => {
18298
+ if (!placeholders.has(name)) return match;
18299
+ const value = String(placeholders.get(name));
18300
+ return markup ? escapeMarkup(value) : value;
18301
+ });
18302
+ };
18303
+ const walk = (v) => {
18304
+ if (typeof v === "string") return fill(v, false);
18305
+ if (Array.isArray(v)) return v.map(walk);
18306
+ if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x)]));
18307
+ return v;
18308
+ };
18309
+ const { svg, ...rest } = effect.motion;
18310
+ return { svg: fill(svg.replace(/<!--[\s\S]*?-->/g, ""), true), ...walk(rest) };
18311
+ }
18312
+ function colorControl(name, hex, label) {
18313
+ return { name, type: "vec3", color: true, default: hexToRgb(hex) ?? [1, 1, 1], label: label ?? name };
18314
+ }
18315
+ function numberControl(name, value, min, max, label, step) {
18316
+ return { name, type: "float", default: value, min, max, label: label ?? name, step };
18317
+ }
18318
+
18319
+ // src/catalog/text.ts
18320
+ var r110 = (n11) => Math.round(n11 * 10) / 10;
18321
+ function fit5(text, width, max, ems = textWidthEm(text)) {
18322
+ const e = Math.max(0.6, ems);
18323
+ const size = Math.min(max, width / e);
18324
+ return { size: r110(size), length: r110(e * size), left: r110(200 - e * size / 2) };
18325
+ }
18326
+ var lettersOf = (text) => Array.from(String(text));
18327
+ var mixHex3 = (a, b, t) => {
18328
+ if (isCurrentColor(a) || isCurrentColor(b)) {
18329
+ const css = (c) => isCurrentColor(c) ? CURRENT_COLOR : rgbToHex(Array.isArray(c) ? c : [1, 1, 1]);
18330
+ return `color-mix(in srgb, ${css(a)}, ${css(b)} ${Math.round(t * 100)}%)`;
18331
+ }
18332
+ const x = Array.isArray(a) ? a : [1, 1, 1];
18333
+ const y = Array.isArray(b) ? b : [1, 1, 1];
18334
+ return rgbToHex([0, 1, 2].map((i) => x[i] + (y[i] - x[i]) * t));
18335
+ };
18336
+ var holds = (id, attr, keys2) => keys2.map(([delay, value]) => ({ id, attrs: { [attr]: [value, value] }, delay, ms: 0 }));
18337
+ var n10 = (name) => `{{${name}}}`;
16215
18338
  var MONO = `ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace`;
16216
18339
  var MONO_EM = 0.6;
16217
18340
  var HEAVY2 = `'Arial Black', 'Helvetica Neue', Impact, system-ui, sans-serif`;
16218
- var SANS5 = `system-ui, 'Helvetica Neue', Arial, sans-serif`;
18341
+ var SANS7 = `system-ui, 'Helvetica Neue', Arial, sans-serif`;
16219
18342
  var SCRIPT = `'Pacifico', 'Brush Script MT', 'Segoe Script', 'Snell Roundhand', cursive`;
16220
18343
  var CAP = 0.72;
16221
18344
  var typeface = (font, weight) => `{{font: text default="${font}" label='Font'}} {{weight: int min=100 max=900 default=${weight} step=100 label='Weight'}} {{inline: bool default=false label='Inline'}}`;
16222
18345
  function frame(v, left, width, base, size, overhang = [0.05, 0, DESCENT, 0]) {
16223
18346
  const spill = overhang.map((e) => Math.round(Math.max(0, e) * 100) / 100).join(" ");
16224
- return v.inline ? { viewBox: `${r18(left)} ${r18(base - size * CAP)} ${r18(width)} ${r18(size * CAP)}`, overflow: "visible", backdrop: 0, overhang: spill } : { viewBox: "0 0 400 200", overflow: "hidden", backdrop: 1, overhang: spill };
18347
+ return v.inline ? { viewBox: `${r110(left)} ${r110(base - size * CAP)} ${r110(width)} ${r110(size * CAP)}`, overflow: "visible", backdrop: 0, overhang: spill } : { viewBox: "0 0 400 200", overflow: "hidden", backdrop: 1, overhang: spill };
16225
18348
  }
16226
18349
  var DESCENT = 0.25;
16227
18350
  var glowing = (overhang, by) => overhang.map((e) => e + by);
@@ -16242,16 +18365,16 @@ var typewriter = {
16242
18365
  // Monospace, so the caret lands after the last letter however long the text is.
16243
18366
  derived: (v) => {
16244
18367
  const count = Math.max(1, lettersOf(v.text).length);
16245
- const size = r18(Math.min(44, room(v, 340) / ((count + 1) * MONO_EM)));
16246
- const left = r18(200 - (count + 1) * MONO_EM * size / 2);
18368
+ const size = r110(Math.min(44, room(v, 340) / ((count + 1) * MONO_EM)));
18369
+ const left = r110(200 - (count + 1) * MONO_EM * size / 2);
16247
18370
  const typing = Math.round(count * Number(v.pace));
16248
- const base = r18(100 + size * 0.35);
18371
+ const base = r110(100 + size * 0.35);
16249
18372
  return {
16250
18373
  ...frame(v, left, (count + 1) * MONO_EM * size, base, size, [0.08, 0, DESCENT, 0]),
16251
18374
  size,
16252
18375
  left,
16253
18376
  base,
16254
- end: r18(left + count * MONO_EM * size),
18377
+ end: r110(left + count * MONO_EM * size),
16255
18378
  typing,
16256
18379
  ...Object.fromEntries(Array.from({ length: BLINKS }, (_, k) => [`blink${k}`, typing + k * 500]))
16257
18380
  };
@@ -16259,9 +18382,9 @@ var typewriter = {
16259
18382
  motion: {
16260
18383
  svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='Hello, world.' label='Text'}} {{paper: color default=#111827 label='Background'}} {{ink: color default=#F9FAFB label='Text colour'}} {{pace: float min=30 max=300 default=90 step=5 label='Ms per letter'}} ${typeface(MONO, 400)} --><rect width="400" height="200" fill="{{paper}}" opacity="{{backdrop}}"/><text id="typed" x="{{left}}" y="{{base}}" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}" xml:space="preserve">{{text}}</text><text id="caret" x="{{end}}" y="{{base}}" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}" opacity="0">\u258C</text></svg>`,
16261
18384
  animate: [
16262
- { id: "typed", write: "{{text}}", caret: "\u258C", ms: n9("typing"), ease: "linear" },
18385
+ { id: "typed", write: "{{text}}", caret: "\u258C", ms: n10("typing"), ease: "linear" },
16263
18386
  // Hidden while the writer shows its own caret, then blinking where it stopped.
16264
- ...holds("caret", "opacity", [[0, 0], ...Array.from({ length: BLINKS }, (_, k) => [n9(`blink${k}`), k % 2 ? 0 : 1])])
18387
+ ...holds("caret", "opacity", [[0, 0], ...Array.from({ length: BLINKS }, (_, k) => [n10(`blink${k}`), k % 2 ? 0 : 1])])
16265
18388
  ]
16266
18389
  }
16267
18390
  };
@@ -16279,17 +18402,17 @@ var scrambleText = {
16279
18402
  ],
16280
18403
  derived: (v) => {
16281
18404
  const count = Math.max(1, lettersOf(v.text).length);
16282
- const size = r18(Math.min(60, room(v, 360) / (count * MONO_EM)));
16283
- const width = r18(count * MONO_EM * size);
16284
- const base = r18(100 + size * 0.35);
16285
- return { size, base, barX: r18(200 - width / 2), barY: r18(base + size * 0.3), barW: width, decode: Math.round(count * Number(v.pace)), glowWide: Number(v.glow) * 2, ...frame(v, 200 - width / 2, width, base, size, glowing([0.08, 0, Math.max(DESCENT, 0.3 + 3 / size), 0], Number(v.glow) * 4 / size)) };
18405
+ const size = r110(Math.min(60, room(v, 360) / (count * MONO_EM)));
18406
+ const width = r110(count * MONO_EM * size);
18407
+ const base = r110(100 + size * 0.35);
18408
+ return { size, base, barX: r110(200 - width / 2), barY: r110(base + size * 0.3), barW: width, decode: Math.round(count * Number(v.pace)), glowWide: Number(v.glow) * 2, ...frame(v, 200 - width / 2, width, base, size, glowing([0.08, 0, Math.max(DESCENT, 0.3 + 3 / size), 0], Number(v.glow) * 4 / size)) };
16286
18409
  },
16287
18410
  motion: {
16288
18411
  svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='ACCESS GRANTED' label='Text'}} {{glyphs: text default='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&@$*+' label='Scramble glyphs'}} {{background: color default=#05070D label='Background'}} {{ink: color default=#E2E8F0 label='Text colour'}} {{accent: color default=#22D3EE label='Accent'}} {{pace: float min=20 max=250 default=70 step=5 label='Ms per letter'}} {{glow: float min=0 max=6 default=2 step=0.5 label='Glow'}} ${typeface(MONO, 700)} --><defs><filter id="glow" x="-20%" y="-50%" width="140%" height="200%"><feGaussianBlur stdDeviation="{{glowWide}}" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><text id="word" x="200" y="{{base}}" text-anchor="middle" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}" filter="url(#glow)" xml:space="preserve">{{text}}</text><rect id="bar" x="{{barX}}" y="{{barY}}" width="0" height="3" fill="{{accent}}"/></svg>`,
16289
18412
  animate: [
16290
18413
  // Pure noise for a moment (the scramble runs before the change starts), then it decodes.
16291
- { id: "word", write: "{{text}}", scramble: "{{glyphs}}", delay: 500, ms: n9("decode"), ease: "linear" },
16292
- { id: "bar", attrs: { width: [0, n9("barW")] }, delay: 500, ms: n9("decode"), ease: "linear" }
18414
+ { id: "word", write: "{{text}}", scramble: "{{glyphs}}", delay: 500, ms: n10("decode"), ease: "linear" },
18415
+ { id: "bar", attrs: { width: [0, n10("barW")] }, delay: 500, ms: n10("decode"), ease: "linear" }
16293
18416
  ]
16294
18417
  }
16295
18418
  };
@@ -16316,20 +18439,20 @@ var kineticStagger = {
16316
18439
  const stagger = Number(v.stagger);
16317
18440
  const out = {
16318
18441
  ...frame(v, 200 - ems * size / 2, ems * size, base, size, [0.15, 0, Math.max(DESCENT, 0.2 + 6 / size), 0]),
16319
- size: r18(size),
16320
- base: r18(base),
16321
- rise: r18(base - size * 0.1),
16322
- drop: r18(base + size * 0.6),
16323
- lineX: r18(200 - ems * size / 2),
16324
- lineY: r18(base + size * 0.2),
16325
- lineW: r18(ems * size),
18442
+ size: r110(size),
18443
+ base: r110(base),
18444
+ rise: r110(base - size * 0.1),
18445
+ drop: r110(base + size * 0.6),
18446
+ lineX: r110(200 - ems * size / 2),
18447
+ lineY: r110(base + size * 0.2),
18448
+ lineW: r110(ems * size),
16326
18449
  lineAt: Math.round(letters.length * stagger + 400)
16327
18450
  };
16328
18451
  let x = 200 - ems * size / 2;
16329
18452
  for (let i = 0; i < SLOTS2; i++) {
16330
18453
  const w = (widths[i] ?? 0) * size;
16331
18454
  out[`c${i}`] = letters[i]?.trim() ?? "";
16332
- out[`x${i}`] = r18(x + w / 2);
18455
+ out[`x${i}`] = r110(x + w / 2);
16333
18456
  out[`d${i}`] = Math.round(i * stagger);
16334
18457
  out[`e${i}`] = Math.round(i * stagger + 380);
16335
18458
  x += w;
@@ -16346,13 +18469,13 @@ var kineticStagger = {
16346
18469
  {
16347
18470
  id: `l${i}`,
16348
18471
  attrs: { transform: [`translate({{x${i}}} {{drop}}) rotate(-28) scale(0.3)`, `translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`], opacity: [0, 1] },
16349
- delay: n9(`d${i}`),
18472
+ delay: n10(`d${i}`),
16350
18473
  ms: 380,
16351
18474
  ease: "out"
16352
18475
  },
16353
- { id: `l${i}`, attrs: { transform: [`translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`, `translate({{x${i}}} {{base}}) rotate(0) scale(1)`] }, delay: n9(`e${i}`), ms: 260, ease: "in-out" }
18476
+ { id: `l${i}`, attrs: { transform: [`translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`, `translate({{x${i}}} {{base}}) rotate(0) scale(1)`] }, delay: n10(`e${i}`), ms: 260, ease: "in-out" }
16354
18477
  ]).flat(),
16355
- { id: "line", attrs: { width: [0, n9("lineW")] }, delay: n9("lineAt"), ms: 500, ease: "in-out" }
18478
+ { id: "line", attrs: { width: [0, n10("lineW")] }, delay: n10("lineAt"), ms: 500, ease: "in-out" }
16356
18479
  ]
16357
18480
  }
16358
18481
  };
@@ -16373,18 +18496,18 @@ var neonSign = {
16373
18496
  derived: (v) => {
16374
18497
  const letters = lettersOf(v.text);
16375
18498
  const k = Math.min(Math.max(0, Math.round(Number(v.faulty)) - 1), Math.max(0, letters.length - 1));
16376
- const { size, length, left } = fit3(String(v.text), room(v, 340), 84);
18499
+ const { size, length, left } = fit5(String(v.text), room(v, 340), 84);
16377
18500
  return {
16378
18501
  ...frame(v, left, length, 100 + size * 0.3, size, glowing([0.1, 0, DESCENT, 0], Number(v.glow) * 6 / size)),
16379
- hazeX: r18(Math.max(170, length / 2 + 30)),
18502
+ hazeX: r110(Math.max(170, length / 2 + 30)),
16380
18503
  head: letters.slice(0, k).join(""),
16381
18504
  mid: letters[k] ?? "",
16382
18505
  tail: letters.slice(k + 1).join(""),
16383
18506
  size,
16384
18507
  length,
16385
- base: r18(100 + size * 0.3),
18508
+ base: r110(100 + size * 0.3),
16386
18509
  far: Number(v.glow) * 3,
16387
- core: mixHex2(v.tube, [1, 1, 1], 0.6)
18510
+ core: mixHex3(v.tube, [1, 1, 1], 0.6)
16388
18511
  };
16389
18512
  },
16390
18513
  motion: (() => {
@@ -16416,15 +18539,15 @@ var strokeDrawText = {
16416
18539
  ],
16417
18540
  // <text> takes no pathLength, so `draw` cannot measure it: the dash grows instead, longer than any letter's outline.
16418
18541
  derived: (v) => {
16419
- const { size, length, left } = fit3(String(v.text), room(v, 360), 100);
18542
+ const { size, length, left } = fit5(String(v.text), room(v, 360), 100);
16420
18543
  const edge = Number(v.width) / 2 / size;
16421
- return { size, length, base: r18(100 + size * 0.35), dash: Math.round(size * 6), ...frame(v, left, length, 100 + size * 0.35, size, glowing([0.05, 0, DESCENT, 0], edge)) };
18544
+ return { size, length, base: r110(100 + size * 0.35), dash: Math.round(size * 6), ...frame(v, left, length, 100 + size * 0.35, size, glowing([0.05, 0, DESCENT, 0], edge)) };
16422
18545
  },
16423
18546
  motion: {
16424
18547
  svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='Draw' label='Text'}} {{background: color default=#FFFBF5 label='Background'}} {{outline: color default=#1E293B label='Outline'}} {{fill: color default=#F97316 label='Fill'}} {{width: float min=0.5 max=6 default=2 step=0.5 label='Outline width'}} {{fillOpacity: float min=0 max=1 default=1 label='Fill opacity'}} ${typeface(HEAVY2, 900)} --><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><text id="word" x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{fill}}" fill-opacity="0" stroke="{{outline}}" stroke-width="{{width}}" stroke-linejoin="round" stroke-dasharray="0 {{dash}}">{{text}}</text></svg>`,
16425
18548
  animate: [
16426
18549
  { id: "word", attrs: { "stroke-dasharray": ["0 {{dash}}", "{{dash}} 0"] }, ms: 2e3, ease: "in-out" },
16427
- { id: "word", attrs: { "fill-opacity": [0, n9("fillOpacity")] }, delay: 1500, ms: 700, ease: "out" }
18550
+ { id: "word", attrs: { "fill-opacity": [0, n10("fillOpacity")] }, delay: 1500, ms: 700, ease: "out" }
16428
18551
  ]
16429
18552
  }
16430
18553
  };
@@ -16441,22 +18564,22 @@ var splitReveal = {
16441
18564
  { name: "Gold on black", values: { background: [0.02, 0.02, 0.02], ink: [0.96, 0.84, 0.55], line: [0.96, 0.84, 0.55] } }
16442
18565
  ],
16443
18566
  derived: (v) => {
16444
- const { size, length, left } = fit3(String(v.text), room(v, 360), 88);
18567
+ const { size, length, left } = fit5(String(v.text), room(v, 360), 88);
16445
18568
  return {
16446
18569
  ...frame(v, left, length, 100 + size * 0.36, size, [0.05, 10 / size, Math.max(DESCENT, 34 / size), 10 / size]),
16447
18570
  size,
16448
18571
  length,
16449
- base: r18(100 + size * 0.36),
16450
- shift: r18(size * 0.8),
16451
- lineX1: r18(left - 10),
16452
- lineX2: r18(400 - left + 10),
16453
- captionY: r18(100 + size * 0.36 + 30)
18572
+ base: r110(100 + size * 0.36),
18573
+ shift: r110(size * 0.8),
18574
+ lineX1: r110(left - 10),
18575
+ lineX2: r110(400 - left + 10),
18576
+ captionY: r110(100 + size * 0.36 + 30)
16454
18577
  };
16455
18578
  },
16456
18579
  motion: (() => {
16457
18580
  const word = `<text x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}">{{text}}</text>`;
16458
18581
  return {
16459
- svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='Chapter One' label='Text'}} {{caption: text default='The beginning' label='Caption'}} {{background: color default=#0F172A label='Background'}} {{ink: color default=#F8FAFC label='Text colour'}} {{line: color default=#38BDF8 label='Line'}} ${typeface(SANS5, 800)} --><defs><clipPath id="above"><rect x="-5000" y="-5000" width="10400" height="5100"/></clipPath><clipPath id="below"><rect x="-5000" y="100" width="10400" height="5100"/></clipPath></defs><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><g clip-path="url(#above)"><g id="top" transform="translate(0 {{shift}})">${word}</g></g><g clip-path="url(#below)"><g id="bottom" transform="translate(0 -{{shift}})">${word}</g></g><path id="line" d="M{{lineX1}} 100 H{{lineX2}}" stroke="{{line}}" stroke-width="2.5" stroke-linecap="round"/><text id="caption" x="200" y="{{captionY}}" text-anchor="middle" font-family="{{font}}" font-size="13" letter-spacing="3" fill="{{ink}}" opacity="0">{{caption}}</text></svg>`,
18582
+ svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='Chapter One' label='Text'}} {{caption: text default='The beginning' label='Caption'}} {{background: color default=#0F172A label='Background'}} {{ink: color default=#F8FAFC label='Text colour'}} {{line: color default=#38BDF8 label='Line'}} ${typeface(SANS7, 800)} --><defs><clipPath id="above"><rect x="-5000" y="-5000" width="10400" height="5100"/></clipPath><clipPath id="below"><rect x="-5000" y="100" width="10400" height="5100"/></clipPath></defs><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><g clip-path="url(#above)"><g id="top" transform="translate(0 {{shift}})">${word}</g></g><g clip-path="url(#below)"><g id="bottom" transform="translate(0 -{{shift}})">${word}</g></g><path id="line" d="M{{lineX1}} 100 H{{lineX2}}" stroke="{{line}}" stroke-width="2.5" stroke-linecap="round"/><text id="caption" x="200" y="{{captionY}}" text-anchor="middle" font-family="{{font}}" font-size="13" letter-spacing="3" fill="{{ink}}" opacity="0">{{caption}}</text></svg>`,
16460
18583
  animate: [
16461
18584
  { id: "line", draw: true, ms: 600, ease: "in-out" },
16462
18585
  { id: "top", attrs: { transform: ["translate(0 {{shift}})", "translate(0 0)"] }, delay: 450, ms: 800, ease: "out" },
@@ -16482,7 +18605,7 @@ var highlighter = {
16482
18605
  ],
16483
18606
  // The swipe runs a little past the phrase at both ends, across its lower half, with a ragged second pass.
16484
18607
  derived: (v) => {
16485
- const { size, length, left } = fit3(String(v.text), room(v, 330), 56);
18608
+ const { size, length, left } = fit5(String(v.text), room(v, 330), 56);
16486
18609
  const base = 100 + size * 0.35;
16487
18610
  const x0 = left - size * 0.25;
16488
18611
  const x1 = 400 - left + size * 0.2;
@@ -16491,15 +18614,15 @@ var highlighter = {
16491
18614
  ...frame(v, left, length, base, size, [0.05, 0.2, DESCENT, 0.25]),
16492
18615
  size,
16493
18616
  length,
16494
- base: r18(base),
16495
- swipe: `M${r18(x0)} ${r18(y + size * 0.05)} Q${r18((x0 + x1) / 2)} ${r18(y - size * 0.06)} ${r18(x1)} ${r18(y - size * 0.02)}`,
16496
- ragged: `M${r18(x0 + size * 0.15)} ${r18(y + size * 0.3)} L${r18(x1 - size * 0.1)} ${r18(y + size * 0.26)}`,
16497
- thick: r18(size * 0.72),
16498
- thin: r18(size * 0.16)
18617
+ base: r110(base),
18618
+ swipe: `M${r110(x0)} ${r110(y + size * 0.05)} Q${r110((x0 + x1) / 2)} ${r110(y - size * 0.06)} ${r110(x1)} ${r110(y - size * 0.02)}`,
18619
+ ragged: `M${r110(x0 + size * 0.15)} ${r110(y + size * 0.3)} L${r110(x1 - size * 0.1)} ${r110(y + size * 0.26)}`,
18620
+ thick: r110(size * 0.72),
18621
+ thin: r110(size * 0.16)
16499
18622
  };
16500
18623
  },
16501
18624
  motion: {
16502
- svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='Ship it faster' label='Phrase'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#111827 label='Text colour'}} {{marker: color default=#FDE047 label='Marker'}} ${typeface(SANS5, 700)} --><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><g transform="rotate(-1.2 200 100)" opacity="0.9"><path id="swipe" d="{{swipe}}" fill="none" stroke="{{marker}}" stroke-width="{{thick}}"/><path id="ragged" d="{{ragged}}" fill="none" stroke="{{marker}}" stroke-width="{{thin}}" stroke-linecap="round"/></g><text id="phrase" x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}" opacity="0">{{text}}</text></svg>`,
18625
+ svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='Ship it faster' label='Phrase'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#111827 label='Text colour'}} {{marker: color default=#FDE047 label='Marker'}} ${typeface(SANS7, 700)} --><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><g transform="rotate(-1.2 200 100)" opacity="0.9"><path id="swipe" d="{{swipe}}" fill="none" stroke="{{marker}}" stroke-width="{{thick}}"/><path id="ragged" d="{{ragged}}" fill="none" stroke="{{marker}}" stroke-width="{{thin}}" stroke-linecap="round"/></g><text id="phrase" x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}" opacity="0">{{text}}</text></svg>`,
16503
18626
  animate: [
16504
18627
  { id: "phrase", attrs: { opacity: [0, 1] }, ms: 300, ease: "out" },
16505
18628
  { id: "swipe", draw: true, delay: 350, ms: 650, ease: "in-out" },
@@ -16536,28 +18659,28 @@ var wordCycle = {
16536
18659
  return {
16537
18660
  ...frame(v, left, total * size, base, size, [0.08, 0, Math.max(DESCENT, 0.18 + 4 / size), 0]),
16538
18661
  bold: Math.min(900, Number(v.weight) + 100),
16539
- size: r18(size),
16540
- base: r18(base),
16541
- px: r18(left),
16542
- plen: r18(Math.max(0.6, lead) * size),
16543
- wx: r18(wx),
16544
- clipX: r18(wx - 4),
16545
- clipY: r18(base - size * 1),
16546
- clipW: r18(Math.max(...ems) * size + 8),
16547
- clipH: r18(size * 1.32),
16548
- underY: r18(base + size * 0.18),
18662
+ size: r110(size),
18663
+ base: r110(base),
18664
+ px: r110(left),
18665
+ plen: r110(Math.max(0.6, lead) * size),
18666
+ wx: r110(wx),
18667
+ clipX: r110(wx - 4),
18668
+ clipY: r110(base - size * 1),
18669
+ clipW: r110(Math.max(...ems) * size + 8),
18670
+ clipH: r110(size * 1.32),
18671
+ underY: r110(base + size * 0.18),
16549
18672
  ...Object.fromEntries(
16550
18673
  [...words, words[0]].flatMap((_, k) => [
16551
- [`y${k}`, r18(base + k * step)],
16552
- [`o${k}`, r18(k * step)],
16553
- [`len${k}`, r18(ems[k % CYCLE_WORDS] * size)]
18674
+ [`y${k}`, r110(base + k * step)],
18675
+ [`o${k}`, r110(k * step)],
18676
+ [`len${k}`, r110(ems[k % CYCLE_WORDS] * size)]
16554
18677
  ])
16555
18678
  ),
16556
18679
  ...Object.fromEntries(words.map((w, k) => [`w${k}`, w]))
16557
18680
  };
16558
18681
  },
16559
18682
  motion: {
16560
- svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{prefix: text default='We build' label='Lead-in'}} {{word1: text default=apps label='Word 1'}} {{word2: text default=brands label='Word 2'}} {{word3: text default=websites label='Word 3'}} {{word4: text default=games label='Word 4'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#0F172A label='Text colour'}} {{accent: color default=#6366F1 label='Accent'}} ${typeface(SANS5, 700)} --><defs><clipPath id="window"><rect x="{{clipX}}" y="{{clipY}}" width="{{clipW}}" height="{{clipH}}"/></clipPath></defs><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><text x="{{px}}" y="{{base}}" textLength="{{plen}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}">{{prefix}}</text><g clip-path="url(#window)"><g id="reel" transform="translate(0 -0)">${Array.from(
18683
+ svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{prefix: text default='We build' label='Lead-in'}} {{word1: text default=apps label='Word 1'}} {{word2: text default=brands label='Word 2'}} {{word3: text default=websites label='Word 3'}} {{word4: text default=games label='Word 4'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#0F172A label='Text colour'}} {{accent: color default=#6366F1 label='Accent'}} ${typeface(SANS7, 700)} --><defs><clipPath id="window"><rect x="{{clipX}}" y="{{clipY}}" width="{{clipW}}" height="{{clipH}}"/></clipPath></defs><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><text x="{{px}}" y="{{base}}" textLength="{{plen}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}">{{prefix}}</text><g clip-path="url(#window)"><g id="reel" transform="translate(0 -0)">${Array.from(
16561
18684
  { length: CYCLE_WORDS + 1 },
16562
18685
  (_, k) => `<text x="{{wx}}" y="{{y${k}}}" textLength="{{len${k}}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{bold}}" fill="{{accent}}">{{w${k % CYCLE_WORDS}}}</text>`
16563
18686
  ).join("")}</g></g><rect id="under" x="{{wx}}" y="{{underY}}" width="{{len0}}" height="4" rx="2" fill="{{accent}}"/></svg>`,
@@ -16565,7 +18688,7 @@ var wordCycle = {
16565
18688
  const at3 = { delay: (i + 1) * SHOW_MS + i * ROLL_MS, ms: ROLL_MS, ease: "in-out" };
16566
18689
  return [
16567
18690
  { id: "reel", attrs: { transform: [`translate(0 -{{o${i}}})`, `translate(0 -{{o${i + 1}}})`] }, ...at3 },
16568
- { id: "under", attrs: { width: [n9(`len${i}`), n9(`len${i + 1}`)] }, ...at3 }
18691
+ { id: "under", attrs: { width: [n10(`len${i}`), n10(`len${i + 1}`)] }, ...at3 }
16569
18692
  ];
16570
18693
  }).flat()
16571
18694
  }
@@ -16586,9 +18709,9 @@ var liquidFill = {
16586
18709
  ],
16587
18710
  // The surface starts under the letters and ends above their caps.
16588
18711
  derived: (v) => {
16589
- const { size, length, left } = fit3(String(v.text), room(v, 360), 110);
18712
+ const { size, length, left } = fit5(String(v.text), room(v, 360), 110);
16590
18713
  const base = 100 + size * 0.36;
16591
- return { ...frame(v, left, length, base, size), size, length, base: r18(base), low: r18(base + 12), high: r18(base - size * 0.82 - 12) };
18714
+ return { ...frame(v, left, length, base, size), size, length, base: r110(base), low: r110(base + 12), high: r110(base - size * 0.82 - 12) };
16592
18715
  },
16593
18716
  motion: (() => {
16594
18717
  const word = `x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}"`;
@@ -16635,19 +18758,19 @@ var glitchText = {
16635
18758
  { name: "Vapor", values: { left: [1, 0.3, 0.85], right: [0.4, 0.6, 1], background: [0.08, 0.02, 0.12], amount: 1.4 } }
16636
18759
  ],
16637
18760
  derived: (v) => {
16638
- const { size, length, left } = fit3(String(v.text), room(v, 340), 100);
18761
+ const { size, length, left } = fit5(String(v.text), room(v, 340), 100);
16639
18762
  const a = Number(v.amount);
16640
18763
  const base = 100 + size * 0.36;
16641
18764
  const top = base - size * 0.78;
16642
- const cuts = [-1e3, ...BANDS.map((f) => r18(top + f * (base - top))), 1200];
18765
+ const cuts = [-1e3, ...BANDS.map((f) => r110(top + f * (base - top))), 1200];
16643
18766
  const unit = size * Number(v.amount);
16644
- const shift = ([x, y]) => `translate(${r18(x * unit)} ${r18(y * unit)})`;
18767
+ const shift = ([x, y]) => `translate(${r110(x * unit)} ${r110(y * unit)})`;
16645
18768
  return {
16646
18769
  ...frame(v, left, length, base, size, [0.05 + 0.04 * a, 0.25 * a, DESCENT + 0.04 * a, 0.25 * a]),
16647
18770
  size,
16648
18771
  length,
16649
- base: r18(base),
16650
- ...Object.fromEntries([0, 1, 2, 3].flatMap((b) => [[`cut${b}`, cuts[b]], [`band${b}`, r18(cuts[b + 1] - cuts[b])]])),
18772
+ base: r110(base),
18773
+ ...Object.fromEntries([0, 1, 2, 3].flatMap((b) => [[`cut${b}`, cuts[b]], [`band${b}`, r110(cuts[b + 1] - cuts[b])]])),
16651
18774
  ...Object.fromEntries(
16652
18775
  GLITCH.flatMap((key, k) => [
16653
18776
  [`red${k}`, shift(key.red)],
@@ -16687,23 +18810,23 @@ var extrudedText = {
16687
18810
  ],
16688
18811
  // Each layer sits a step further back along the extrude direction, shaded from the side colour to the shadow.
16689
18812
  derived: (v) => {
16690
- const { size, length, left } = fit3(String(v.text), room(v, 320), 96);
18813
+ const { size, length, left } = fit5(String(v.text), room(v, 320), 96);
16691
18814
  const d = 0.36 * Number(v.depth);
16692
18815
  const step = size * 0.03 * Number(v.depth);
16693
18816
  const at3 = (deg, i) => {
16694
18817
  const a = deg * Math.PI / 180;
16695
- return `translate(${r18(Math.cos(a) * step * i)} ${r18(Math.sin(a) * step * i)})`;
18818
+ return `translate(${r110(Math.cos(a) * step * i)} ${r110(Math.sin(a) * step * i)})`;
16696
18819
  };
16697
18820
  return {
16698
18821
  ...frame(v, left, length, 92 + size * 0.35, size, [0.05, 0.77 * d, DESCENT + d, 0.77 * d]),
16699
18822
  size,
16700
18823
  length,
16701
- base: r18(92 + size * 0.35),
18824
+ base: r110(92 + size * 0.35),
16702
18825
  ...Object.fromEntries(
16703
18826
  Array.from({ length: LAYERS }, (_, k) => {
16704
18827
  const i = LAYERS - k;
16705
18828
  return [
16706
- [`fill${i}`, mixHex2(v.side, v.shade, i / LAYERS)],
18829
+ [`fill${i}`, mixHex3(v.side, v.shade, i / LAYERS)],
16707
18830
  [`a${i}`, at3(EXTRUDE_FROM, i)],
16708
18831
  [`b${i}`, at3(EXTRUDE_TO, i)]
16709
18832
  ];
@@ -17183,7 +19306,7 @@ var dripText = {
17183
19306
  const left = 200 - width / 2;
17184
19307
  const runs = Math.min(DRIP_RUNS, Math.max(1, Math.round(width / DRIP_RUN)));
17185
19308
  const run = width / runs;
17186
- const r19 = (n10) => Math.round(n10 * 10) / 10;
19309
+ const r111 = (n11) => Math.round(n11 * 10) / 10;
17187
19310
  const edge = Number(v.stroke) / 2 / DRIP_SIZE;
17188
19311
  const tip = (DRIP_TOP + Math.max(...DRIPS2.map((d) => d.length)) * Number(v.drip) + 13 * 0.72 - DRIP_BASELINE) / DRIP_SIZE;
17189
19312
  const overhang = [edge, edge, Math.max(0.05, tip) + edge, edge].map((e) => Math.round(e * 100) / 100).join(" ");
@@ -17197,15 +19320,15 @@ var dripText = {
17197
19320
  overhang,
17198
19321
  ...Object.fromEntries(
17199
19322
  DRIP_SLOTS.flatMap((d) => {
17200
- const x = r19(left + (d.run + d.at) * run);
17201
- const length = r19(d.length * Number(v.drip));
19323
+ const x = r111(left + (d.run + d.at) * run);
19324
+ const length = r111(d.length * Number(v.drip));
17202
19325
  return [
17203
19326
  [`show${d.i}`, d.run < runs ? "inline" : "none"],
17204
19327
  [`x${d.i}`, x],
17205
- [`left${d.i}`, r19(x - d.width / 2)],
17206
- [`shine${d.i}`, r19(x - d.width * 0.25)],
19328
+ [`left${d.i}`, r111(x - d.width / 2)],
19329
+ [`shine${d.i}`, r111(x - d.width * 0.25)],
17207
19330
  [`len${d.i}`, length],
17208
- [`tip${d.i}`, r19(DRIP_TOP + length)]
19331
+ [`tip${d.i}`, r111(DRIP_TOP + length)]
17209
19332
  ];
17210
19333
  })
17211
19334
  )
@@ -17347,9 +19470,13 @@ var SHADER_EFFECTS = [
17347
19470
  ...debrisShaders,
17348
19471
  ...impactShaders,
17349
19472
  ...comicShaders,
19473
+ ...atmosphereShaders,
19474
+ ...glowShaders,
19475
+ ...shadowShaders,
19476
+ ...distortionShaders,
17350
19477
  ...transitions
17351
19478
  ];
17352
- var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects, ...pyroEffects, ...timewarpEffects, ...meltEffects, ...electricEffects, ...pixelEffects, ...splatterEffects, ...bubbleEffects, ...stickerEffects, ...trailEffects, ...impactEffects, ...comicEffects].sort(
19479
+ var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects, ...pyroEffects, ...timewarpEffects, ...meltEffects, ...electricEffects, ...pixelEffects, ...splatterEffects, ...bubbleEffects, ...stickerEffects, ...trailEffects, ...impactEffects, ...comicEffects, ...glowEffects, ...shadowEffects].sort(
17353
19480
  (a, b) => EFFECT_CATEGORIES.indexOf(a.category) - EFFECT_CATEGORIES.indexOf(b.category)
17354
19481
  );
17355
19482
  var byId = new Map(EFFECTS.map((e) => [e.id, e]));
@@ -17407,4 +19534,4 @@ export {
17407
19534
  effectControls,
17408
19535
  effectDefaults
17409
19536
  };
17410
- //# sourceMappingURL=chunk-TCNONB22.js.map
19537
+ //# sourceMappingURL=chunk-KD7E3RWF.js.map