{
  "license": "CC0-1.0",
  "shader": {
    "id": "jx54ed2e",
    "title": "Interference",
    "license": "CC0-1.0",
    "provenance_type": "witnessed_generation",
    "made_with": "Claude",
    "status": "active",
    "compile_ok": true,
    "render_hash": "52ef994c6cd9db1c",
    "tags": [
      "interference",
      "diffraction",
      "optics",
      "wave",
      "ripple-tank",
      "double-slit",
      "spectral",
      "physics"
    ],
    "parents": [],
    "author": "u7y1evhf0znj",
    "author_name": "ksin",
    "author_url": "/u/ksin",
    "created_at": "2026-08-18T01:13:14.030Z",
    "poster_url": "/posters/jx54ed2e.avif",
    "stats": {
      "slops": 0,
      "views": 181,
      "forks": 0
    },
    "self": "/api/v1/shaders/jx54ed2e",
    "isf": "/*{\n  \"DESCRIPTION\": \"A ripple tank, solved rather than drawn. A row of point sources sits on a plane, each emitting circular waves in step, and every pixel sums the field they all arrive with: psi = SUM a_i cos(k |p - s_i| - k c t + phi_i), with a_i falling off as 1/sqrt(r) the way a cylindrical wave must. None of the fringes are painted. The hyperbolae fall out of path difference - crests coincide where the two path lengths differ by a whole number of wavelengths and cancel where they differ by a half - so two sources give the classic double slit, and winding the count up sharpens the same sum into a grating's principal maxima. The surface is lit from the field's exact analytic gradient, the sin() that sits beside the cos() in the same loop, so the ridges catch light like real water instead of being a colour ramp. In white light every wavelength runs at its own k and the fringes separate into spectra away from the centre line; colour comes from Wyman, Sloan and Shirley's analytic fit to the CIE 1931 colour matching functions, so a wavelength gets the colour it really has. Twenty-one sliders: speed, zoom, how many sources (up to 24) and how far apart, a bend that curls the row of sources into an arc and on into a closed ring without changing their spacing, fringe density, the row's angle and spin, a phase ramp along the row that steers the beam, a rake from looking straight down to a raked horizon with the sources at the vanishing point, relief and gloss for the surface shading, wavelength in nanometres, a dial from one colour to full white light, a scientific jet colour map, fringe hardness, amplitude falloff, exposure, bloom, grain and vignette.\",\n  \"CREDIT\": \"CC0\",\n  \"CATEGORIES\": [\"generator\", \"optics\", \"interference\", \"diffraction\", \"wave\", \"spectral\", \"physics\", \"2d\"],\n  \"INPUTS\": [\n    { \"NAME\": \"uSpeed\",    \"TYPE\": \"float\", \"DEFAULT\": 1.0,    \"MIN\": 0.0,   \"MAX\": 3.0 },\n    { \"NAME\": \"uZoom\",     \"TYPE\": \"float\", \"DEFAULT\": 1.0,    \"MIN\": 0.2,   \"MAX\": 4.0 },\n    { \"NAME\": \"uSources\",  \"TYPE\": \"float\", \"DEFAULT\": 2.0,    \"MIN\": 1.0,   \"MAX\": 24.0 },\n    { \"NAME\": \"uSpacing\",  \"TYPE\": \"float\", \"DEFAULT\": 0.95,   \"MIN\": 0.0,   \"MAX\": 3.0 },\n    { \"NAME\": \"uRing\",     \"TYPE\": \"float\", \"DEFAULT\": 0.0,    \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uFreq\",     \"TYPE\": \"float\", \"DEFAULT\": 9.0,    \"MIN\": 1.0,   \"MAX\": 40.0 },\n    { \"NAME\": \"uAngle\",    \"TYPE\": \"float\", \"DEFAULT\": 0.0,    \"MIN\": -3.15, \"MAX\": 3.15 },\n    { \"NAME\": \"uSpin\",     \"TYPE\": \"float\", \"DEFAULT\": 0.0,    \"MIN\": -1.0,  \"MAX\": 1.0 },\n    { \"NAME\": \"uSteer\",    \"TYPE\": \"float\", \"DEFAULT\": 0.0,    \"MIN\": -1.0,  \"MAX\": 1.0 },\n    { \"NAME\": \"uPersp\",    \"TYPE\": \"float\", \"DEFAULT\": 0.0,    \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uRelief\",   \"TYPE\": \"float\", \"DEFAULT\": 0.8,    \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uGloss\",    \"TYPE\": \"float\", \"DEFAULT\": 0.7,    \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uWave\",     \"TYPE\": \"float\", \"DEFAULT\": 492.0,  \"MIN\": 400.0, \"MAX\": 700.0 },\n    { \"NAME\": \"uSpectral\", \"TYPE\": \"float\", \"DEFAULT\": 0.0,    \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uJet\",      \"TYPE\": \"float\", \"DEFAULT\": 0.0,    \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uBands\",    \"TYPE\": \"float\", \"DEFAULT\": 0.35,   \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uDecay\",    \"TYPE\": \"float\", \"DEFAULT\": 0.85,   \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uExposure\", \"TYPE\": \"float\", \"DEFAULT\": 1.4,    \"MIN\": 0.0,   \"MAX\": 3.0 },\n    { \"NAME\": \"uBloom\",    \"TYPE\": \"float\", \"DEFAULT\": 0.35,   \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uGrain\",    \"TYPE\": \"float\", \"DEFAULT\": 0.04,   \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uVignette\", \"TYPE\": \"float\", \"DEFAULT\": 0.35,   \"MIN\": 0.0,   \"MAX\": 1.0 }\n  ]\n}*/\n\n// DIFFRACTION - ISF port of the Shadertoy/WebGL2 version\n// (GLSL ES 1.00: gl_FragColor, TIME/RENDERSIZE).\n\n#define PI   3.14159265359\n#define TAU  6.28318530718\n#define NW   24          // wavelength samples in white light\n// Hard ceiling on sources. Both loops below are nested and constant-bound,\n// which is the shape ANGLE likes to fully unroll — 24x24 would be 576 inlined\n// copies. iq's ZERO trick is the usual defence, but GLSL ES 1.00 Appendix A\n// requires a constant loop initialiser and ISF validation rejects it, so the\n// bounds stay literal and the early `break` in each loop carries the cost\n// instead. Measured link time is ~200ms, so this is fine as it stands.\n#define MAXS 24\n\nfloat hash12(vec2 p){\n    p = fract(p * vec2(123.34, 456.21));\n    p += dot(p, p + 45.32);\n    return fract(p.x * p.y);\n}\n\nmat2 rot(float a){ float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }\n\n// ---- CIE 1931 colour matching, analytic ---------------------\n// Wyman, Sloan & Shirley — multi-lobe piecewise Gaussians. Cheap,\n// and close enough that a wavelength comes out the right colour.\nfloat pg(float x, float mu, float s1, float s2){\n    float t = (x - mu) * ((x < mu) ? 1.0 / s1 : 1.0 / s2);\n    return exp(-0.5 * t * t);\n}\n\nvec3 xyzBar(float w){\n    return vec3(\n        1.056 * pg(w, 599.8, 37.9, 31.0) + 0.362 * pg(w, 442.0, 16.0, 26.7)\n              - 0.065 * pg(w, 501.1, 20.4, 26.2),\n        0.821 * pg(w, 568.8, 46.9, 40.5) + 0.286 * pg(w, 530.9, 16.3, 31.1),\n        1.217 * pg(w, 437.0, 11.8, 36.0) + 0.681 * pg(w, 459.0, 26.0, 13.8));\n}\n\nvec3 xyz2rgb(vec3 c){\n    return vec3( 3.2406 * c.x - 1.5372 * c.y - 0.4986 * c.z,\n                -0.9689 * c.x + 1.8758 * c.y + 0.0415 * c.z,\n                 0.0557 * c.x - 0.2040 * c.y + 1.0570 * c.z);\n}\n\n// the plotting-package colour map, for when it should look like data\nvec3 jetMap(float t){\n    t = clamp(t, 0.0, 1.0);\n    return clamp(vec3(1.5 - abs(4.0 * t - 3.0),\n                      1.5 - abs(4.0 * t - 2.0),\n                      1.5 - abs(4.0 * t - 1.0)), 0.0, 1.0);\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord){\n    vec2  R  = RENDERSIZE;\n    vec2  uv = (fragCoord - 0.5 * R) / R.y;\n    float t  = TIME * uSpeed;\n\n    // ---- where on the tank is this pixel ---------------------\n    // Straight down, the plane is just the screen. Raked over, the\n    // screen is a camera looking along a ground plane: depth goes as\n    // 1/(horizon - y), which is what puts the sources up near the\n    // vanishing point and rushes the fringes toward the viewer.\n    float hz = 0.30;\n    float dy = max(hz - uv.y, 0.045);\n    vec2  gp = vec2(uv.x / dy, 1.0 / dy) * 0.30;\n    vec2  p  = mix(uv * 2.2, gp, uPersp) * uZoom;\n\n    // Pixel footprint on the plane. Under perspective it grows as\n    // 1/dy^2, and near the horizon a fringe lands inside one pixel —\n    // which aliases into moire mush unless the contrast is faded out\n    // before it gets there.\n    float foot = mix(2.2, 0.30 / (dy * dy), uPersp) * uZoom / R.y;\n\n    float n    = clamp(floor(uSources + 0.5), 1.0, float(MAXS));\n    float mid  = (n - 1.0) * 0.5;\n    mat2  rotm = rot(uAngle + t * uSpin);\n    // with the camera raked over, the sources belong out in the distance\n    vec2  ctr  = vec2(0.0, mix(0.0, 2.2, uPersp));\n\n    // The row is bent rather than re-laid-out: arc length along it is\n    // preserved, so the sources keep their spacing the whole way from a\n    // straight line to a closed circle. Radius = length / bend angle, which\n    // runs to infinity as the bend goes to zero — hence the clamps, not a\n    // branch, so there is no seam at the straight end.\n    float rowL = max((n - 1.0) * uSpacing, 1e-4);\n    float bend = uRing * TAU;\n    float bendR = rowL / max(bend, 1e-3);\n    // The arc bends off to one side, so recentre it on its own span or the\n    // whole pattern drifts as it curls. At a full turn this works out to\n    // exactly the bend radius, which is the circle's centre; at zero bend it\n    // is exactly zero, so a straight row is untouched.\n    float bendC = bendR * sin(bend * 0.25) * sin(bend * 0.25);\n\n    float k0   = TAU * uFreq;\n    float nyq  = smoothstep(2.4, 0.65, k0 * foot);   // fade past Nyquist\n\n    // Stratified sampling with a per-pixel offset. Sampling white light\n    // at fixed wavelengths lays discrete red/green/blue copies of the\n    // pattern on top of each other; jittering the strata turns that\n    // banding into fine noise, which the eye reads as a smooth spectrum.\n    float jit = hash12(fragCoord * 1.7 + fract(TIME) * 53.0);\n\n    vec3  XYZ  = vec3(0.0);\n    float norm = 0.0;\n    float hgt  = 0.0;        // luminance-weighted field, for shading\n    vec2  grd  = vec2(0.0);\n\n    for (int iw = 0; iw < NW; iw++){\n        // monochromatic is the common case and one sample is exact for it\n        if (iw > 0 && uSpectral < 0.02) break;\n\n        float f = (float(iw) + jit) / float(NW);\n        float w = mix(uWave, mix(402.0, 698.0, f), uSpectral);\n        vec3  bar = xyzBar(w);\n        // shorter wavelengths pack more fringes into the same distance\n        float k = k0 * (560.0 / w);\n\n        float psi = 0.0;\n        vec2  g   = vec2(0.0);\n        for (int is = 0; is < MAXS; is++){\n            if (float(is) >= n) break;\n            float o  = float(is) - mid;\n            float sl = o * uSpacing;                  // arc length from centre\n            float ba = sl / bendR;\n            // 2sin^2(a/2) rather than 1-cos(a): at the straight end the bend\n            // radius is enormous and the angle tiny, where 1-cos cancels away\n            // most of its float32 significand and the row picks up a wobble\n            float hs = sin(ba * 0.5);\n            vec2  lp = vec2(bendR * sin(ba), bendR * 2.0 * hs * hs - bendC);\n            vec2  dv = p - (ctr + rotm * lp);\n            float r  = length(dv);\n            float a  = mix(1.0, inversesqrt(r + 0.35), uDecay);\n            float ph = k * (r - 0.30 * t) + o * uSteer * 2.4;\n            psi += a * cos(ph);\n            // exact gradient: the derivative of the same term, so the\n            // relief is the field's own slope and not a filtered guess\n            g   += (-a * k * sin(ph)) * dv / max(r, 1e-4);\n        }\n        psi /= n;\n        g   /= n * max(k, 1e-3);      // slope, normalised out of frequency\n\n        float Y = max(bar.y, 0.0);\n        XYZ  += bar * clamp(psi * 0.5 + 0.5, 0.0, 1.0);\n        hgt  += psi * Y;\n        grd  += g   * Y;\n        norm += Y;\n    }\n    norm = max(norm, 1e-4);\n    XYZ /= norm;\n    hgt  = hgt * nyq / norm;\n    grd  = grd * nyq / norm;\n\n    // ---- height -> fringe ------------------------------------\n    float v = clamp(hgt * 0.5 + 0.5, 0.0, 1.0);\n    v = mix(v, smoothstep(0.40, 0.60, v), uBands);\n\n    // Hue at unit luminance, kept apart from brightness so the fringe\n    // shaping above stays a pure intensity operation.\n    vec3 hue = max(xyz2rgb(XYZ / max(XYZ.y, 1e-4)), 0.0);\n    hue /= max(max(hue.r, max(hue.g, hue.b)), 1e-3);\n    vec3 base = mix(hue, jetMap(v), uJet);\n\n    // ---- shade it as a surface -------------------------------\n    vec3 nrm = normalize(vec3(-grd * uRelief * 3.2, 1.0));\n    vec3 L   = normalize(vec3(-0.38, 0.52, 0.76));\n    float dif = max(dot(nrm, L), 0.0);\n    float spe = pow(max(dot(reflect(-L, nrm), vec3(0.0, 0.0, 1.0)), 0.0),\n                    mix(10.0, 96.0, uGloss));\n\n    vec3 col = base * v * (0.34 + 0.92 * dif) * uExposure;\n    col += vec3(1.0, 0.98, 0.94) * spe * uGloss * (0.25 + 0.75 * v) * 0.85;\n\n    if (uBloom > 0.001){\n        float l = dot(col, vec3(0.299, 0.587, 0.114));\n        col += col * smoothstep(0.55, 1.35, l) * uBloom * 0.9;\n    }\n\n    // sky above the horizon, once the camera is raked over\n    col *= mix(1.0, smoothstep(hz + 0.005, hz - 0.09, uv.y), uPersp);\n\n    vec2 q = fragCoord / R;\n    float vig = pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.25);\n    col *= mix(1.0, vig, uVignette);\n\n    col += (hash12(fragCoord + fract(TIME) * 311.0) - 0.5) * uGrain * 0.09;\n\n    fragColor = vec4(clamp(col, 0.0, 1.0), 1.0);\n}\n\nvoid main(){\n    vec4 c;\n    mainImage(c, gl_FragCoord.xy);\n    gl_FragColor = c;\n}\n",
    "generator": {
      "model": "Claude-Opus-5",
      "provider": null,
      "prompt": null,
      "params": null
    }
  }
}