@dopaminefx/effect-ripple 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/ripple-shader.d.ts +50 -0
- package/dist/ripple-shader.d.ts.map +1 -0
- package/dist/ripple-shader.js +253 -0
- package/dist/ripple-shader.js.map +1 -0
- package/dist/ripple.dope.json +359 -0
- package/package.json +46 -0
- package/src/index.ts +44 -0
- package/src/ripple-shader.ts +263 -0
- package/src/ripple.dope.json +359 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ripple (the tactile "droplet in a still pool" acknowledge effect) as an
|
|
3
|
+
* `EffectFactory` on the Dopamine backbone.
|
|
4
|
+
*
|
|
5
|
+
* FULLY DATA-DRIVEN (P2): everything that isn't the GLSL lives in
|
|
6
|
+
* ripple.dope.json — the mood→params mapping + OKLCH palette (the loader), AND
|
|
7
|
+
* the per-frame logic: `tempo.frame` (the held-breath envelope amp),
|
|
8
|
+
* `render.shadowHeightFrac` (the wave field's outward reach), `render.consts`
|
|
9
|
+
* (MAX_RINGS/MIN_RINGS), `render.config` and the uniform `binding` contract.
|
|
10
|
+
* `registerDopeEffect` interprets that data through the generic
|
|
11
|
+
* `createPassInstance` fullscreen-pass runner; this module is just the water
|
|
12
|
+
* SHADER + the registration call.
|
|
13
|
+
*
|
|
14
|
+
* Anchored at `uOrigin` (usesOrigin: true): concentric wavefronts expand from
|
|
15
|
+
* the action point. Distinct from Solarbloom's soft radial CORE — Ripple's light
|
|
16
|
+
* lives only on thin, moving ring crests + the caustics they refract.
|
|
17
|
+
*/
|
|
18
|
+
import { type EffectFactory, type PassParams } from "@dopaminefx/core";
|
|
19
|
+
/** The resolved render params Ripple's shader consumes. */
|
|
20
|
+
export interface RippleParams extends PassParams {
|
|
21
|
+
exposure: number;
|
|
22
|
+
amplitude: number;
|
|
23
|
+
rings: number;
|
|
24
|
+
wavelength: number;
|
|
25
|
+
speed: number;
|
|
26
|
+
caustic: number;
|
|
27
|
+
overshoot: number;
|
|
28
|
+
rippleSeed: number;
|
|
29
|
+
}
|
|
30
|
+
export declare const ripple: EffectFactory<RippleParams>;
|
|
31
|
+
export default ripple;
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAiC,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKtG,2DAA2D;AAC3D,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,eAAO,MAAM,MAAM,EAGgB,aAAa,CAAC,YAAY,CAAC,CAAC;AAE/D,eAAe,MAAM,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ripple (the tactile "droplet in a still pool" acknowledge effect) as an
|
|
3
|
+
* `EffectFactory` on the Dopamine backbone.
|
|
4
|
+
*
|
|
5
|
+
* FULLY DATA-DRIVEN (P2): everything that isn't the GLSL lives in
|
|
6
|
+
* ripple.dope.json — the mood→params mapping + OKLCH palette (the loader), AND
|
|
7
|
+
* the per-frame logic: `tempo.frame` (the held-breath envelope amp),
|
|
8
|
+
* `render.shadowHeightFrac` (the wave field's outward reach), `render.consts`
|
|
9
|
+
* (MAX_RINGS/MIN_RINGS), `render.config` and the uniform `binding` contract.
|
|
10
|
+
* `registerDopeEffect` interprets that data through the generic
|
|
11
|
+
* `createPassInstance` fullscreen-pass runner; this module is just the water
|
|
12
|
+
* SHADER + the registration call.
|
|
13
|
+
*
|
|
14
|
+
* Anchored at `uOrigin` (usesOrigin: true): concentric wavefronts expand from
|
|
15
|
+
* the action point. Distinct from Solarbloom's soft radial CORE — Ripple's light
|
|
16
|
+
* lives only on thin, moving ring crests + the caustics they refract.
|
|
17
|
+
*/
|
|
18
|
+
import { RIPPLE_FRAGMENT_SRC, RIPPLE_VERTEX_SRC } from "./ripple-shader.js";
|
|
19
|
+
import { parseDope, registerDopeEffect } from "@dopaminefx/core";
|
|
20
|
+
import doc from "./ripple.dope.json";
|
|
21
|
+
const DOPE = parseDope(doc);
|
|
22
|
+
// The whole factory (resolve / create / reducedMotion / program registration)
|
|
23
|
+
// is data: ripple.dope.json interpreted by the core backbone.
|
|
24
|
+
export const ripple = registerDopeEffect(DOPE, {
|
|
25
|
+
vertex: RIPPLE_VERTEX_SRC,
|
|
26
|
+
fragment: RIPPLE_FRAGMENT_SRC,
|
|
27
|
+
});
|
|
28
|
+
export default ripple;
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAuC,MAAM,kBAAkB,CAAC;AACtG,OAAO,GAAG,MAAM,oBAAoB,CAAC;AAErC,MAAM,IAAI,GAAG,SAAS,CAAC,GAAa,CAAC,CAAC;AActC,8EAA8E;AAC9E,8DAA8D;AAC9D,MAAM,CAAC,MAAM,MAAM,GAAG,kBAAkB,CAAC,IAAI,EAAE;IAC7C,MAAM,EAAE,iBAAiB;IACzB,QAAQ,EAAE,mBAAmB;CAC9B,CAA6D,CAAC;AAE/D,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLSL ES 3.00 source for **Ripple** — Dopamine's tactile "droplet in a still
|
|
3
|
+
* pool" acknowledge effect.
|
|
4
|
+
*
|
|
5
|
+
* Governing metaphor: a single drop strikes a calm water surface at the action
|
|
6
|
+
* point (`uOrigin`). Concentric WAVES expand outward, and each travelling
|
|
7
|
+
* wavefront REFRACTS bright caustic light that dances across the UI as the ring
|
|
8
|
+
* passes; behind the front, the surface settles back to still. It reads as
|
|
9
|
+
* water + light: physical, tactile, anchored.
|
|
10
|
+
*
|
|
11
|
+
* This is a deliberate DIVERGENCE from Solarbloom's volumetric radial bloom.
|
|
12
|
+
* Solarbloom is a soft glowing CORE that fills outward and lingers; Ripple has
|
|
13
|
+
* NO bright core — its light lives entirely on thin, moving RING crests and the
|
|
14
|
+
* caustic sparkle they refract. The motion is a set of discrete expanding
|
|
15
|
+
* annuli, not a swelling blob.
|
|
16
|
+
*
|
|
17
|
+
* Layers, summed as light (canvas is black, `mix-blend-mode: screen`, so black
|
|
18
|
+
* == no change, bright == cast light onto the page beneath):
|
|
19
|
+
* 1. WAVEFIELD — a sum of `uRings` radially-travelling cosine waves whose
|
|
20
|
+
* phase = k*r - w*t, launched in a quick stagger from the origin. The
|
|
21
|
+
* surface height + its radial gradient (slope) drive everything else.
|
|
22
|
+
* 2. CAUSTICS — the wave SLOPE bends light: bright filaments form where the
|
|
23
|
+
* curved surface focuses light (|slope| high, near crests), animated as the
|
|
24
|
+
* rings travel. This is the dancing light the brief describes.
|
|
25
|
+
* 3. CREST GLINT — a thin specular highlight riding the leading crest of each
|
|
26
|
+
* ring (the wet "shine" of the moving wavefront).
|
|
27
|
+
* 4. SETTLE — the whole field is gated by a radial wavefront envelope so light
|
|
28
|
+
* only appears where a ring currently is, then the pool goes still.
|
|
29
|
+
*
|
|
30
|
+
* Reward timing: uAmp (held-breath envelope) gates global brightness — a quick
|
|
31
|
+
* expanding swell then a gentle settle. Pure function of uTimeS (frame-perfect,
|
|
32
|
+
* cheap under SwiftShader: analytic waves + noise, single pass).
|
|
33
|
+
*
|
|
34
|
+
* whimsy == uStyle:
|
|
35
|
+
* 0 = photoreal smooth refraction — soft caustics, continuous crests, smooth
|
|
36
|
+
* OKLCH colour drift across the rings.
|
|
37
|
+
* 1 = stylized: hard concentric CEL rings (posterized crest bands), posterized
|
|
38
|
+
* caustics (chunky light cells), and the motion snaps "on twos" (the
|
|
39
|
+
* pass-runner already steps the clock; we also quantize the wave phase so
|
|
40
|
+
* the rings advance in discrete, posed jumps).
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Max concurrent expanding rings. Single source of truth for the loop cap: it is
|
|
44
|
+
* BOTH the GLSL `#define MAX_RINGS` (interpolated below) and the integer-clamp
|
|
45
|
+
* const the `.dope` mapping references (passed to the loader as `MAX_RINGS`).
|
|
46
|
+
*/
|
|
47
|
+
export declare const MAX_RINGS = 7;
|
|
48
|
+
export declare const RIPPLE_VERTEX_SRC = "#version 300 es\nvoid main() {\n // Single full-screen triangle from gl_VertexID \u2014 no vertex buffers needed.\n vec2 pos = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);\n}";
|
|
49
|
+
export declare const RIPPLE_FRAGMENT_SRC = "#version 300 es\nprecision highp float;\nout vec4 fragColor;\n\nuniform vec2 uResolution; // device pixels\nuniform vec2 uOrigin; // drop point, gl coords (y up)\nuniform float uAmp; // envelope amplitude (peaks > 1)\nuniform float uLife; // whole-effect progress 0..1\nuniform float uTimeS; // elapsed seconds (snapped \"on twos\" by style)\nuniform float uExposure;\nuniform float uAmplitude; // wave height (intensity)\nuniform float uRings; // number of concentric wavefronts launched\nuniform float uWavelength; // crest spacing as a fraction of min viewport dim\nuniform float uSpeed; // wave propagation speed (fraction of minDim / s)\nuniform float uCaustic; // 0..1 caustic-light brightness (intensity)\nuniform float uSeed; // per-fire hash offset\nuniform float uStyle; // 0..1 photoreal smooth refraction -> cel rings (whimsy)\nuniform float uShadow; // 0 = light pass (screen), 1 = shadow pass (multiply)\nuniform vec2 uShadowOffset; // device-px offset of the cast silhouette (away from light)\nuniform float uShadowSoft; // penumbra softness in device px (blur tap radius)\nuniform float uShadowStrength;// 0..1 max darkening of the multiply layer\nuniform vec3 uC0; // crest core color\nuniform vec3 uC1; // mid\nuniform vec3 uC2; // caustic accent\n\n#define MAX_RINGS 7\n\n#define TAU 6.28318530718\n\n\nfloat hash11(float p){ p = fract(p * 0.1031); p *= p + 33.33; p *= p + p; return fract(p); }\nvec2 hash21(float p){\n vec3 p3 = fract(vec3(p) * vec3(0.1031, 0.1030, 0.0973));\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\n\nfloat vnoise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n float a = hash11(dot(i, vec2(1.0, 57.0)));\n float b = hash11(dot(i + vec2(1.0, 0.0), vec2(1.0, 57.0)));\n float c = hash11(dot(i + vec2(0.0, 1.0), vec2(1.0, 57.0)));\n float d = hash11(dot(i + vec2(1.0, 1.0), vec2(1.0, 57.0)));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\nfloat fbm(vec2 p){\n float s = 0.0, a = 0.5;\n mat2 rot = mat2(0.80, -0.60, 0.60, 0.80);\n for (int i = 0; i < 4; i++) { s += a * vnoise(p); p = rot * p * 2.03; a *= 0.5; }\n return s;\n}\n\n\nvec3 paletteMix(float t){\n t = clamp(t, 0.0, 1.0);\n return t < 0.5 ? mix(uC0, uC1, t * 2.0) : mix(uC1, uC2, (t - 0.5) * 2.0);\n}\n\n\nvec3 tonemapACES(vec3 x){\n const float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;\n return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);\n}\n\n\nvec3 ditherAdd(vec3 col, vec2 frag, float t, float fade){\n float dz = hash11(dot(frag, vec2(12.989, 78.233)) + t) - 0.5;\n return col + (dz / 255.0) * fade;\n}\n\n\n// A travelling ring's launch time as a fraction of life. The drop strikes at\n// t=0 and successive rings (the secondary swells of a real impact) follow in a\n// stagger wide enough that, at any instant, the rings sit at clearly DIFFERENT\n// radii \u2014 a family of distinct sizes rippling out, not bunched near-duplicates.\nfloat ringLaunch(int i){\n return float(i) * 0.12;\n}\n\n// The wave surface as a function of normalized radius rn (= r / minDim) and the\n// life clock. Returns height in h; the radial SLOPE (dHeight/dr) in slope;\n// and a 0..1 wavefront ENVELOPE in front (1 where a ring currently is, 0 in\n// the still water ahead/behind). Shared by the light pass and the shadow so the\n// cast occlusion tracks exactly the troughs that are drawn.\n//\n// Each ring is a radially-expanding wave packet: a cosine carrier (phase =\n// k*r - w*t) under a gaussian envelope that travels outward at uSpeed and\n// spreads/decays as 1/sqrt(r) (energy conservation on an expanding circle).\nvoid waveField(float rn, out float h, out float slope, out float front){\n h = 0.0; slope = 0.0; front = 0.0;\n float k = TAU / max(uWavelength, 0.001); // angular wavenumber (per rn)\n float w = k * uSpeed; // angular frequency\n int rings = int(clamp(uRings, 0.0, float(MAX_RINGS)) + 0.5);\n for (int i = 0; i < MAX_RINGS; i++) {\n if (i >= rings) break;\n float t0 = ringLaunch(i);\n float age = uLife - t0; // 0..(1-t0)\n if (age <= 0.0) continue;\n // Front radius travels outward; the packet starts tight and SWELLS markedly as\n // the ring expands, so each ring visibly changes size as it travels out (and an\n // older ring is both farther AND fatter than a younger one).\n float front_r = uSpeed * age; // expected crest of this ring\n float width = uWavelength * (1.0 + 2.6 * age); // packet half-extent (grows as it expands)\n float d = rn - front_r; // signed distance to the front\n float pkt = exp(-(d * d) / (2.0 * width * width));\n if (pkt < 0.002) continue;\n // Amplitude fades CONTINUOUSLY as the ring ages/expands (not just a late cutoff),\n // so each crest dims steadily as it grows \u2014 on top of the 1/sqrt(r) spreading.\n float decay = pow(max(1.0 - age, 0.0), 1.3);\n // 1/sqrt(r) spreading (clamped near the origin so the drop isn't a spike).\n float spread = 1.0 / sqrt(max(rn, uWavelength * 0.5));\n // On the cel end, quantize the carrier phase so the rings advance \"on twos\"\n // (discrete posed crests) instead of sliding smoothly.\n float phase = k * rn - w * uLife;\n float qstep = TAU * 0.5;\n float qphase = floor(phase / qstep) * qstep;\n phase = mix(phase, qphase, uStyle * 0.85);\n float amp = uAmplitude * pkt * decay * spread;\n h += amp * cos(phase);\n // d(h)/d(rn): carrier derivative dominates (the steep part that bends light).\n slope += -amp * k * sin(phase);\n front = max(front, pkt * decay);\n }\n}\n\n// ---------------------------------------------------------------------------\n// SHADOW silhouette \u2014 the wave TROUGHS cast a faint soft occlusion (a real\n// rippled surface dimples the light it sits in). We sample the wave height at\n// the offset shadow point and darken where the surface dips below rest (h < 0),\n// gated by the wavefront envelope so still water casts nothing. Kept subtle.\nfloat rippleOcclusion(vec2 frag){\n float minDim = min(uResolution.x, uResolution.y);\n float rn = length(frag - uOrigin) / minDim;\n float h, slope, front;\n waveField(rn, h, slope, front);\n float trough = max(-h, 0.0); // depth below rest\n return clamp(trough * 2.2 * front * uAmp, 0.0, 1.0);\n}\n\nvec4 rippleShadowColor(vec2 frag){\n vec2 sp = frag - uShadowOffset;\n float soft = uShadowSoft;\n float occ = rippleOcclusion(sp);\n occ += rippleOcclusion(sp + vec2( soft, 0.0));\n occ += rippleOcclusion(sp + vec2(-soft, 0.0));\n occ += rippleOcclusion(sp + vec2(0.0, soft));\n occ += rippleOcclusion(sp + vec2(0.0, -soft));\n occ /= 5.0;\n // Troughs are a faint dimple, so cap the darkening well below full strength.\n float dark = clamp(occ, 0.0, 1.0) * uShadowStrength * 0.5;\n vec3 tint = mix(vec3(1.0), 0.6 + 0.4 * normalize(uC0 + 1e-3), 0.2);\n vec3 mul = mix(vec3(1.0), tint, dark);\n return vec4(mul, 1.0);\n}\n\nvoid main(){\n vec2 frag = gl_FragCoord.xy;\n vec2 res = uResolution;\n float minDim = min(res.x, res.y);\n\n if (uShadow > 0.5) {\n fragColor = rippleShadowColor(frag);\n return;\n }\n\n vec3 col = vec3(0.0);\n vec2 rel = frag - uOrigin;\n float r = length(rel);\n float rn = r / minDim; // normalized radius\n vec2 rdir = rel / max(r, 1e-3); // outward unit (toward rim)\n\n // ---- The wave surface at this fragment. ----\n float h, slope, front;\n waveField(rn, h, slope, front);\n\n float gain = uAmp * uExposure;\n\n // Colour register: hue drifts gently OUTWARD across the rings (OKLCH palette\n // C0->C1->C2), so each expanding crest reads as a slightly different light \u2014\n // unique per fire (the palette is seeded). A touch of slow temporal drift +\n // tiny fbm break keeps it alive without going rainbow.\n float tcol = clamp(rn / (uWavelength * float(MAX_RINGS) * 0.9), 0.0, 1.0);\n tcol = fract(tcol + uTimeS * 0.04 + fbm(rel / minDim * 5.0 + uSeed) * 0.06);\n vec3 ringCol = paletteMix(tcol);\n\n // ---- 1. CRESTS: the bright wet ridge of each travelling wavefront. ----\n // Light lives on the positive crests (h > 0), masked to where a ring is.\n float crest = smoothstep(0.0, uAmplitude * 0.5, h) * front;\n col += ringCol * crest * gain * 0.9;\n\n // ---- 2. CAUSTICS: the wave SLOPE refracts/focuses light. A curved surface\n // bends parallel light into bright filaments; |slope| peaks on the steep\n // flanks between crest and trough, so the caustic web sits BETWEEN the rings\n // and dances as they travel. Sharpened to thin, bright lines. ----\n float foc = abs(slope);\n float caustic = pow(clamp(foc / (uAmplitude * 1.2 + 1e-3), 0.0, 1.0), 1.8);\n // A little noise breaks the caustic into a living, glittering web.\n float glit = 0.6 + 0.6 * fbm(rel / minDim * 22.0 - uTimeS * 0.5 + uSeed);\n caustic *= glit * front;\n // The accent hue carries the caustic light (a brighter, whiter highlight on top).\n col += mix(uC2, vec3(1.0), 0.35) * caustic * uCaustic * gain * 1.3;\n\n // ---- 3. CREST GLINT: a thin specular line riding each leading crest. ----\n float glint = smoothstep(0.85, 1.0, front) * smoothstep(uAmplitude * 0.55, uAmplitude * 0.9, h);\n col += vec3(1.0) * glint * gain * 0.5 * (0.5 + 0.5 * uCaustic);\n\n // ---- Tone + finishing ----\n col = tonemapACES(col * 0.95);\n\n // ---- Non-photoreal pass: cel rings + posterized caustics (whimsy). ----\n // Toward the cel end the smooth refraction becomes hard concentric BANDS: the\n // crest mask is thresholded into a flat ring, and the caustic web is posterized\n // into chunky light cells. The phase quantization in waveField already steps\n // the rings \"on twos\"; here we flatten their tone.\n if (uStyle > 0.001) {\n // Hard ring: a flat band where the crest is strong, with a brighter inner core.\n float band = smoothstep(0.18, 0.30, crest);\n float core = smoothstep(0.45, 0.60, crest);\n vec3 celRing = clamp(ringCol * 1.3, 0.0, 1.2) * band\n + clamp(uC0 * 1.6 + 0.1, 0.0, 1.3) * core;\n // Posterize the caustic light into 2 chunky levels (Ben-Day-ish cells),\n // and keep only the BRIGHT cells (drop the dim wash so the cel read stays\n // clean white-on-dark rings instead of a muddy mid-tone field).\n float caus = clamp(caustic * uCaustic, 0.0, 1.0);\n float causQ = step(0.5, caus) * 0.6 + step(0.8, caus) * 0.4;\n vec3 celCaustic = mix(uC2, vec3(1.0), 0.5) * causQ;\n vec3 cel = (celRing + celCaustic) * gain;\n col = mix(col, cel, uStyle);\n }\n\n // Ordered dither (~1/255) to kill banding the screen blend reveals; faded out\n // toward the cel end where hard bands are intended.\n col = ditherAdd(col, frag, uTimeS, 1.0 - uStyle);\n\n fragColor = vec4(max(col, 0.0), 1.0);\n}";
|
|
50
|
+
//# sourceMappingURL=ripple-shader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ripple-shader.d.ts","sourceRoot":"","sources":["../src/ripple-shader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAWH;;;;GAIG;AACH,eAAO,MAAM,SAAS,IAAI,CAAC;AAE3B,eAAO,MAAM,iBAAiB,uPAK5B,CAAC;AAEH,eAAO,MAAM,mBAAmB,izVAqM9B,CAAC"}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLSL ES 3.00 source for **Ripple** — Dopamine's tactile "droplet in a still
|
|
3
|
+
* pool" acknowledge effect.
|
|
4
|
+
*
|
|
5
|
+
* Governing metaphor: a single drop strikes a calm water surface at the action
|
|
6
|
+
* point (`uOrigin`). Concentric WAVES expand outward, and each travelling
|
|
7
|
+
* wavefront REFRACTS bright caustic light that dances across the UI as the ring
|
|
8
|
+
* passes; behind the front, the surface settles back to still. It reads as
|
|
9
|
+
* water + light: physical, tactile, anchored.
|
|
10
|
+
*
|
|
11
|
+
* This is a deliberate DIVERGENCE from Solarbloom's volumetric radial bloom.
|
|
12
|
+
* Solarbloom is a soft glowing CORE that fills outward and lingers; Ripple has
|
|
13
|
+
* NO bright core — its light lives entirely on thin, moving RING crests and the
|
|
14
|
+
* caustic sparkle they refract. The motion is a set of discrete expanding
|
|
15
|
+
* annuli, not a swelling blob.
|
|
16
|
+
*
|
|
17
|
+
* Layers, summed as light (canvas is black, `mix-blend-mode: screen`, so black
|
|
18
|
+
* == no change, bright == cast light onto the page beneath):
|
|
19
|
+
* 1. WAVEFIELD — a sum of `uRings` radially-travelling cosine waves whose
|
|
20
|
+
* phase = k*r - w*t, launched in a quick stagger from the origin. The
|
|
21
|
+
* surface height + its radial gradient (slope) drive everything else.
|
|
22
|
+
* 2. CAUSTICS — the wave SLOPE bends light: bright filaments form where the
|
|
23
|
+
* curved surface focuses light (|slope| high, near crests), animated as the
|
|
24
|
+
* rings travel. This is the dancing light the brief describes.
|
|
25
|
+
* 3. CREST GLINT — a thin specular highlight riding the leading crest of each
|
|
26
|
+
* ring (the wet "shine" of the moving wavefront).
|
|
27
|
+
* 4. SETTLE — the whole field is gated by a radial wavefront envelope so light
|
|
28
|
+
* only appears where a ring currently is, then the pool goes still.
|
|
29
|
+
*
|
|
30
|
+
* Reward timing: uAmp (held-breath envelope) gates global brightness — a quick
|
|
31
|
+
* expanding swell then a gentle settle. Pure function of uTimeS (frame-perfect,
|
|
32
|
+
* cheap under SwiftShader: analytic waves + noise, single pass).
|
|
33
|
+
*
|
|
34
|
+
* whimsy == uStyle:
|
|
35
|
+
* 0 = photoreal smooth refraction — soft caustics, continuous crests, smooth
|
|
36
|
+
* OKLCH colour drift across the rings.
|
|
37
|
+
* 1 = stylized: hard concentric CEL rings (posterized crest bands), posterized
|
|
38
|
+
* caustics (chunky light cells), and the motion snaps "on twos" (the
|
|
39
|
+
* pass-runner already steps the clock; we also quantize the wave phase so
|
|
40
|
+
* the rings advance in discrete, posed jumps).
|
|
41
|
+
*/
|
|
42
|
+
import { GLSL_CONSTANTS, GLSL_DITHER, GLSL_FBM, GLSL_HASH, GLSL_PALETTE_MIX, GLSL_TONEMAP_ACES, } from "@dopaminefx/core";
|
|
43
|
+
/**
|
|
44
|
+
* Max concurrent expanding rings. Single source of truth for the loop cap: it is
|
|
45
|
+
* BOTH the GLSL `#define MAX_RINGS` (interpolated below) and the integer-clamp
|
|
46
|
+
* const the `.dope` mapping references (passed to the loader as `MAX_RINGS`).
|
|
47
|
+
*/
|
|
48
|
+
export const MAX_RINGS = 7;
|
|
49
|
+
export const RIPPLE_VERTEX_SRC = /* glsl */ `#version 300 es
|
|
50
|
+
void main() {
|
|
51
|
+
// Single full-screen triangle from gl_VertexID — no vertex buffers needed.
|
|
52
|
+
vec2 pos = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
53
|
+
gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);
|
|
54
|
+
}`;
|
|
55
|
+
export const RIPPLE_FRAGMENT_SRC = /* glsl */ `#version 300 es
|
|
56
|
+
precision highp float;
|
|
57
|
+
out vec4 fragColor;
|
|
58
|
+
|
|
59
|
+
uniform vec2 uResolution; // device pixels
|
|
60
|
+
uniform vec2 uOrigin; // drop point, gl coords (y up)
|
|
61
|
+
uniform float uAmp; // envelope amplitude (peaks > 1)
|
|
62
|
+
uniform float uLife; // whole-effect progress 0..1
|
|
63
|
+
uniform float uTimeS; // elapsed seconds (snapped "on twos" by style)
|
|
64
|
+
uniform float uExposure;
|
|
65
|
+
uniform float uAmplitude; // wave height (intensity)
|
|
66
|
+
uniform float uRings; // number of concentric wavefronts launched
|
|
67
|
+
uniform float uWavelength; // crest spacing as a fraction of min viewport dim
|
|
68
|
+
uniform float uSpeed; // wave propagation speed (fraction of minDim / s)
|
|
69
|
+
uniform float uCaustic; // 0..1 caustic-light brightness (intensity)
|
|
70
|
+
uniform float uSeed; // per-fire hash offset
|
|
71
|
+
uniform float uStyle; // 0..1 photoreal smooth refraction -> cel rings (whimsy)
|
|
72
|
+
uniform float uShadow; // 0 = light pass (screen), 1 = shadow pass (multiply)
|
|
73
|
+
uniform vec2 uShadowOffset; // device-px offset of the cast silhouette (away from light)
|
|
74
|
+
uniform float uShadowSoft; // penumbra softness in device px (blur tap radius)
|
|
75
|
+
uniform float uShadowStrength;// 0..1 max darkening of the multiply layer
|
|
76
|
+
uniform vec3 uC0; // crest core color
|
|
77
|
+
uniform vec3 uC1; // mid
|
|
78
|
+
uniform vec3 uC2; // caustic accent
|
|
79
|
+
|
|
80
|
+
#define MAX_RINGS ${MAX_RINGS}
|
|
81
|
+
${GLSL_CONSTANTS}
|
|
82
|
+
${GLSL_HASH}
|
|
83
|
+
${GLSL_FBM}
|
|
84
|
+
${GLSL_PALETTE_MIX}
|
|
85
|
+
${GLSL_TONEMAP_ACES}
|
|
86
|
+
${GLSL_DITHER}
|
|
87
|
+
|
|
88
|
+
// A travelling ring's launch time as a fraction of life. The drop strikes at
|
|
89
|
+
// t=0 and successive rings (the secondary swells of a real impact) follow in a
|
|
90
|
+
// stagger wide enough that, at any instant, the rings sit at clearly DIFFERENT
|
|
91
|
+
// radii — a family of distinct sizes rippling out, not bunched near-duplicates.
|
|
92
|
+
float ringLaunch(int i){
|
|
93
|
+
return float(i) * 0.12;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The wave surface as a function of normalized radius rn (= r / minDim) and the
|
|
97
|
+
// life clock. Returns height in h; the radial SLOPE (dHeight/dr) in slope;
|
|
98
|
+
// and a 0..1 wavefront ENVELOPE in front (1 where a ring currently is, 0 in
|
|
99
|
+
// the still water ahead/behind). Shared by the light pass and the shadow so the
|
|
100
|
+
// cast occlusion tracks exactly the troughs that are drawn.
|
|
101
|
+
//
|
|
102
|
+
// Each ring is a radially-expanding wave packet: a cosine carrier (phase =
|
|
103
|
+
// k*r - w*t) under a gaussian envelope that travels outward at uSpeed and
|
|
104
|
+
// spreads/decays as 1/sqrt(r) (energy conservation on an expanding circle).
|
|
105
|
+
void waveField(float rn, out float h, out float slope, out float front){
|
|
106
|
+
h = 0.0; slope = 0.0; front = 0.0;
|
|
107
|
+
float k = TAU / max(uWavelength, 0.001); // angular wavenumber (per rn)
|
|
108
|
+
float w = k * uSpeed; // angular frequency
|
|
109
|
+
int rings = int(clamp(uRings, 0.0, float(MAX_RINGS)) + 0.5);
|
|
110
|
+
for (int i = 0; i < MAX_RINGS; i++) {
|
|
111
|
+
if (i >= rings) break;
|
|
112
|
+
float t0 = ringLaunch(i);
|
|
113
|
+
float age = uLife - t0; // 0..(1-t0)
|
|
114
|
+
if (age <= 0.0) continue;
|
|
115
|
+
// Front radius travels outward; the packet starts tight and SWELLS markedly as
|
|
116
|
+
// the ring expands, so each ring visibly changes size as it travels out (and an
|
|
117
|
+
// older ring is both farther AND fatter than a younger one).
|
|
118
|
+
float front_r = uSpeed * age; // expected crest of this ring
|
|
119
|
+
float width = uWavelength * (1.0 + 2.6 * age); // packet half-extent (grows as it expands)
|
|
120
|
+
float d = rn - front_r; // signed distance to the front
|
|
121
|
+
float pkt = exp(-(d * d) / (2.0 * width * width));
|
|
122
|
+
if (pkt < 0.002) continue;
|
|
123
|
+
// Amplitude fades CONTINUOUSLY as the ring ages/expands (not just a late cutoff),
|
|
124
|
+
// so each crest dims steadily as it grows — on top of the 1/sqrt(r) spreading.
|
|
125
|
+
float decay = pow(max(1.0 - age, 0.0), 1.3);
|
|
126
|
+
// 1/sqrt(r) spreading (clamped near the origin so the drop isn't a spike).
|
|
127
|
+
float spread = 1.0 / sqrt(max(rn, uWavelength * 0.5));
|
|
128
|
+
// On the cel end, quantize the carrier phase so the rings advance "on twos"
|
|
129
|
+
// (discrete posed crests) instead of sliding smoothly.
|
|
130
|
+
float phase = k * rn - w * uLife;
|
|
131
|
+
float qstep = TAU * 0.5;
|
|
132
|
+
float qphase = floor(phase / qstep) * qstep;
|
|
133
|
+
phase = mix(phase, qphase, uStyle * 0.85);
|
|
134
|
+
float amp = uAmplitude * pkt * decay * spread;
|
|
135
|
+
h += amp * cos(phase);
|
|
136
|
+
// d(h)/d(rn): carrier derivative dominates (the steep part that bends light).
|
|
137
|
+
slope += -amp * k * sin(phase);
|
|
138
|
+
front = max(front, pkt * decay);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// SHADOW silhouette — the wave TROUGHS cast a faint soft occlusion (a real
|
|
144
|
+
// rippled surface dimples the light it sits in). We sample the wave height at
|
|
145
|
+
// the offset shadow point and darken where the surface dips below rest (h < 0),
|
|
146
|
+
// gated by the wavefront envelope so still water casts nothing. Kept subtle.
|
|
147
|
+
float rippleOcclusion(vec2 frag){
|
|
148
|
+
float minDim = min(uResolution.x, uResolution.y);
|
|
149
|
+
float rn = length(frag - uOrigin) / minDim;
|
|
150
|
+
float h, slope, front;
|
|
151
|
+
waveField(rn, h, slope, front);
|
|
152
|
+
float trough = max(-h, 0.0); // depth below rest
|
|
153
|
+
return clamp(trough * 2.2 * front * uAmp, 0.0, 1.0);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
vec4 rippleShadowColor(vec2 frag){
|
|
157
|
+
vec2 sp = frag - uShadowOffset;
|
|
158
|
+
float soft = uShadowSoft;
|
|
159
|
+
float occ = rippleOcclusion(sp);
|
|
160
|
+
occ += rippleOcclusion(sp + vec2( soft, 0.0));
|
|
161
|
+
occ += rippleOcclusion(sp + vec2(-soft, 0.0));
|
|
162
|
+
occ += rippleOcclusion(sp + vec2(0.0, soft));
|
|
163
|
+
occ += rippleOcclusion(sp + vec2(0.0, -soft));
|
|
164
|
+
occ /= 5.0;
|
|
165
|
+
// Troughs are a faint dimple, so cap the darkening well below full strength.
|
|
166
|
+
float dark = clamp(occ, 0.0, 1.0) * uShadowStrength * 0.5;
|
|
167
|
+
vec3 tint = mix(vec3(1.0), 0.6 + 0.4 * normalize(uC0 + 1e-3), 0.2);
|
|
168
|
+
vec3 mul = mix(vec3(1.0), tint, dark);
|
|
169
|
+
return vec4(mul, 1.0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
void main(){
|
|
173
|
+
vec2 frag = gl_FragCoord.xy;
|
|
174
|
+
vec2 res = uResolution;
|
|
175
|
+
float minDim = min(res.x, res.y);
|
|
176
|
+
|
|
177
|
+
if (uShadow > 0.5) {
|
|
178
|
+
fragColor = rippleShadowColor(frag);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
vec3 col = vec3(0.0);
|
|
183
|
+
vec2 rel = frag - uOrigin;
|
|
184
|
+
float r = length(rel);
|
|
185
|
+
float rn = r / minDim; // normalized radius
|
|
186
|
+
vec2 rdir = rel / max(r, 1e-3); // outward unit (toward rim)
|
|
187
|
+
|
|
188
|
+
// ---- The wave surface at this fragment. ----
|
|
189
|
+
float h, slope, front;
|
|
190
|
+
waveField(rn, h, slope, front);
|
|
191
|
+
|
|
192
|
+
float gain = uAmp * uExposure;
|
|
193
|
+
|
|
194
|
+
// Colour register: hue drifts gently OUTWARD across the rings (OKLCH palette
|
|
195
|
+
// C0->C1->C2), so each expanding crest reads as a slightly different light —
|
|
196
|
+
// unique per fire (the palette is seeded). A touch of slow temporal drift +
|
|
197
|
+
// tiny fbm break keeps it alive without going rainbow.
|
|
198
|
+
float tcol = clamp(rn / (uWavelength * float(MAX_RINGS) * 0.9), 0.0, 1.0);
|
|
199
|
+
tcol = fract(tcol + uTimeS * 0.04 + fbm(rel / minDim * 5.0 + uSeed) * 0.06);
|
|
200
|
+
vec3 ringCol = paletteMix(tcol);
|
|
201
|
+
|
|
202
|
+
// ---- 1. CRESTS: the bright wet ridge of each travelling wavefront. ----
|
|
203
|
+
// Light lives on the positive crests (h > 0), masked to where a ring is.
|
|
204
|
+
float crest = smoothstep(0.0, uAmplitude * 0.5, h) * front;
|
|
205
|
+
col += ringCol * crest * gain * 0.9;
|
|
206
|
+
|
|
207
|
+
// ---- 2. CAUSTICS: the wave SLOPE refracts/focuses light. A curved surface
|
|
208
|
+
// bends parallel light into bright filaments; |slope| peaks on the steep
|
|
209
|
+
// flanks between crest and trough, so the caustic web sits BETWEEN the rings
|
|
210
|
+
// and dances as they travel. Sharpened to thin, bright lines. ----
|
|
211
|
+
float foc = abs(slope);
|
|
212
|
+
float caustic = pow(clamp(foc / (uAmplitude * 1.2 + 1e-3), 0.0, 1.0), 1.8);
|
|
213
|
+
// A little noise breaks the caustic into a living, glittering web.
|
|
214
|
+
float glit = 0.6 + 0.6 * fbm(rel / minDim * 22.0 - uTimeS * 0.5 + uSeed);
|
|
215
|
+
caustic *= glit * front;
|
|
216
|
+
// The accent hue carries the caustic light (a brighter, whiter highlight on top).
|
|
217
|
+
col += mix(uC2, vec3(1.0), 0.35) * caustic * uCaustic * gain * 1.3;
|
|
218
|
+
|
|
219
|
+
// ---- 3. CREST GLINT: a thin specular line riding each leading crest. ----
|
|
220
|
+
float glint = smoothstep(0.85, 1.0, front) * smoothstep(uAmplitude * 0.55, uAmplitude * 0.9, h);
|
|
221
|
+
col += vec3(1.0) * glint * gain * 0.5 * (0.5 + 0.5 * uCaustic);
|
|
222
|
+
|
|
223
|
+
// ---- Tone + finishing ----
|
|
224
|
+
col = tonemapACES(col * 0.95);
|
|
225
|
+
|
|
226
|
+
// ---- Non-photoreal pass: cel rings + posterized caustics (whimsy). ----
|
|
227
|
+
// Toward the cel end the smooth refraction becomes hard concentric BANDS: the
|
|
228
|
+
// crest mask is thresholded into a flat ring, and the caustic web is posterized
|
|
229
|
+
// into chunky light cells. The phase quantization in waveField already steps
|
|
230
|
+
// the rings "on twos"; here we flatten their tone.
|
|
231
|
+
if (uStyle > 0.001) {
|
|
232
|
+
// Hard ring: a flat band where the crest is strong, with a brighter inner core.
|
|
233
|
+
float band = smoothstep(0.18, 0.30, crest);
|
|
234
|
+
float core = smoothstep(0.45, 0.60, crest);
|
|
235
|
+
vec3 celRing = clamp(ringCol * 1.3, 0.0, 1.2) * band
|
|
236
|
+
+ clamp(uC0 * 1.6 + 0.1, 0.0, 1.3) * core;
|
|
237
|
+
// Posterize the caustic light into 2 chunky levels (Ben-Day-ish cells),
|
|
238
|
+
// and keep only the BRIGHT cells (drop the dim wash so the cel read stays
|
|
239
|
+
// clean white-on-dark rings instead of a muddy mid-tone field).
|
|
240
|
+
float caus = clamp(caustic * uCaustic, 0.0, 1.0);
|
|
241
|
+
float causQ = step(0.5, caus) * 0.6 + step(0.8, caus) * 0.4;
|
|
242
|
+
vec3 celCaustic = mix(uC2, vec3(1.0), 0.5) * causQ;
|
|
243
|
+
vec3 cel = (celRing + celCaustic) * gain;
|
|
244
|
+
col = mix(col, cel, uStyle);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Ordered dither (~1/255) to kill banding the screen blend reveals; faded out
|
|
248
|
+
// toward the cel end where hard bands are intended.
|
|
249
|
+
col = ditherAdd(col, frag, uTimeS, 1.0 - uStyle);
|
|
250
|
+
|
|
251
|
+
fragColor = vec4(max(col, 0.0), 1.0);
|
|
252
|
+
}`;
|
|
253
|
+
//# sourceMappingURL=ripple-shader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ripple-shader.js","sourceRoot":"","sources":["../src/ripple-shader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EACL,cAAc,EACd,WAAW,EACX,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAE1B;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAC;AAE3B,MAAM,CAAC,MAAM,iBAAiB,GAAG,UAAU,CAAC;;;;;EAK1C,CAAC;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;oBAyB1B,SAAS;EAC3B,cAAc;EACd,SAAS;EACT,QAAQ;EACR,gBAAgB;EAChB,iBAAiB;EACjB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsKX,CAAC"}
|