{
  "license": "CC0-1.0",
  "shader": {
    "id": "344dbnaf",
    "title": "Metallurgy",
    "license": "CC0-1.0",
    "provenance_type": "witnessed_generation",
    "made_with": "Claude",
    "status": "active",
    "compile_ok": true,
    "render_hash": "1018171c126a8c12",
    "tags": [
      "fractal",
      "chaos",
      "chaos"
    ],
    "parents": [],
    "author": "u7y1evhf0znj",
    "author_name": "ksin",
    "author_url": "/u/ksin",
    "created_at": "2026-08-31T04:14:32.490Z",
    "poster_url": "/posters/344dbnaf.avif",
    "stats": {
      "slops": 0,
      "views": 133,
      "forks": 0
    },
    "self": "/api/v1/shaders/344dbnaf",
    "isf": "/*{\n  \"DESCRIPTION\": \"The Markus-Lyapunov fractal, computed rather than drawn. Take the logistic map x -> r x (1 - x), the standing summary of how order becomes chaos, and let r alternate between two values A and B following a fixed binary word - ABAB, AABAB, AAAAAABBBBBB. Put A on the horizontal axis and B on the vertical, and at every point of the plane measure how fast two neighbouring orbits pull apart: lambda = lim 1/N SUM ln |f'(x_n)|. Where lambda is negative the orbit settles and the system is ordered, drawn warm; where it is positive neighbours separate exponentially - chaos, drawn cold and dark. Nothing comes from a distance field or a noise stack: every pixel runs its own small dynamical system a few hundred steps and reports the exponent it earned, so the lace, the nested swallows strung along the diagonal and the self-similar chains are all the period-doubling cascade seen from two directions at once. Two liberties, both dialable to zero. The A/B word is generated as a rotation word - symbol i is B when fract(i*stride/period + phase) lands inside an arc of length 1-duty - which at soft = 0 is exactly the classic binary word; open the soft slider and the arc's edge blurs, so r glides between A and B instead of snapping and the phase can then be turned continuously, morphing the fractal rather than cutting between sequences. That drift is the animation, and since one full turn of the word is period steps it closes on itself. The map slider cross-fades the logistic map into the sine map r sin(pi x)/4, which shares its range and has its own rounder portrait. Relief comes free: the four anti-aliasing taps double as a finite difference, so the height of the exponent field is had from a gradient that was already paid for. The metal slider then stops treating the palette as an albedo and treats it as a conductor - a real normal is built from that gradient and it reflects a small procedural studio of a dark floor, a cool sky and two soft strip lights. Strips rather than points on purpose: a band is what makes a curved surface streak instead of sparkle. A polished conductor returns most of what strikes it, so the palette is carried most of the way to white and kept as a tint on the reflection, which is what a metal's colour physically is; there is no diffuse term at all. Two more things ride on that reflection. Anisotropy brushes the metal along lambda's OWN contours rather than along some fixed direction - a groove acts as a tiny cylindrical mirror and smears the reflection across its axis, so taking the tangent from the level sets makes the grain follow the form the way a turned or spun surface does. And a thin film sits on top: light reflects both off the film and off the metal beneath, the two paths differ by 2*n*d*cos(theta), and each wavelength lands somewhere different on that difference, so some cancel and some reinforce. That is precisely what puts straw, purple and blue onto tempered steel, it is angle-dependent so the bands follow the curvature instead of sitting on the surface, and the film's depth is driven by lambda as well, so the colour maps the dynamics too. Recipes: the default is AABAB - period 5, stride 2, duty 0.60 at centre (3.3,3.3) span 0.80, where stride 2 is what interleaves the word rather than blocking it, since stride 1 at the same period and duty would give AAABB; AAABBB is period 6, stride 1, duty 0.50 at centre (3,3) span 2; classic Markus is period 2, stride 1, duty 0.50 at the same window - the iconic AB image, though two symbols is the one word length at which a drifting phase moves both onto the arc's edge together, so it dissolves to a plain gradient twice per cycle unless soft is set to 0; Zircon Zity is period 12, stride 1, duty 0.50 at centre (3.70, 2.95) span 0.62. Twenty-seven controls: speed, the window's span and centre on both axes, the word's period, stride and duty, word softness, the logistic-to-sine blend, iterations and how many are discarded as transient, the seed, separate saturation curves for the ordered and chaotic sides, seven palettes with a continuous cross-fade, a metal dial that turns the palette from an albedo into a conductor reflecting a procedural studio, anisotropic brushing, thin-film strength, film thickness in nanometres and a travelling swell in that thickness that sets the temper colours crawling along lambda's level sets - clocked to the same phase that turns the word, so it closes on the same loop, relief and the angle the room is turned to, anti-aliasing, exposure, bloom, vignette and grain.\",\n  \"CREDIT\": \"CC0\",\n  \"CATEGORIES\": [\"generator\", \"fractal\", \"chaos\", \"lyapunov\", \"logistic-map\", \"dynamical-systems\", \"mathematics\", \"2d\"],\n  \"INPUTS\": [\n    { \"NAME\": \"uSpeed\",      \"TYPE\": \"float\", \"DEFAULT\": 1.0,   \"MIN\": 0.0,   \"MAX\": 3.0 },\n    { \"NAME\": \"uSpan\",       \"TYPE\": \"float\", \"DEFAULT\": 0.8,   \"MIN\": 0.01,  \"MAX\": 2.5 },\n    { \"NAME\": \"uCentreA\",    \"TYPE\": \"float\", \"DEFAULT\": 3.3,   \"MIN\": 0.0,   \"MAX\": 4.0 },\n    { \"NAME\": \"uCentreB\",    \"TYPE\": \"float\", \"DEFAULT\": 3.3,   \"MIN\": 0.0,   \"MAX\": 4.0 },\n    { \"NAME\": \"uPeriod\",     \"TYPE\": \"float\", \"DEFAULT\": 5.0,   \"MIN\": 1.0,   \"MAX\": 24.0 },\n    { \"NAME\": \"uSkip\",       \"TYPE\": \"float\", \"DEFAULT\": 2.0,   \"MIN\": 1.0,   \"MAX\": 12.0 },\n    { \"NAME\": \"uDuty\",       \"TYPE\": \"float\", \"DEFAULT\": 0.6,   \"MIN\": 0.02,  \"MAX\": 0.98 },\n    { \"NAME\": \"uSoft\",       \"TYPE\": \"float\", \"DEFAULT\": 0.3,   \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uMap\",        \"TYPE\": \"float\", \"DEFAULT\": 0.0,   \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uIter\",       \"TYPE\": \"float\", \"DEFAULT\": 160.0, \"MIN\": 16.0,  \"MAX\": 384.0 },\n    { \"NAME\": \"uWarm\",       \"TYPE\": \"float\", \"DEFAULT\": 40.0,  \"MIN\": 0.0,   \"MAX\": 200.0 },\n    { \"NAME\": \"uSeed\",       \"TYPE\": \"float\", \"DEFAULT\": 0.5,   \"MIN\": 0.01,  \"MAX\": 0.99 },\n    { \"NAME\": \"uOrderScale\", \"TYPE\": \"float\", \"DEFAULT\": 1.8,   \"MIN\": 0.1,   \"MAX\": 3.0 },\n    { \"NAME\": \"uChaosScale\", \"TYPE\": \"float\", \"DEFAULT\": 0.15,  \"MIN\": 0.05,  \"MAX\": 1.5 },\n    { \"NAME\": \"uPalette\",    \"TYPE\": \"float\", \"DEFAULT\": 6.0,   \"MIN\": 0.0,   \"MAX\": 6.0 },\n    { \"NAME\": \"uMetal\",      \"TYPE\": \"float\", \"DEFAULT\": 0.9,   \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uAniso\",      \"TYPE\": \"float\", \"DEFAULT\": 0.35,  \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uFilm\",       \"TYPE\": \"float\", \"DEFAULT\": 1.0,   \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uFilmThick\",  \"TYPE\": \"float\", \"DEFAULT\": 420.0, \"MIN\": 80.0,  \"MAX\": 900.0 },\n    { \"NAME\": \"uFilmFlow\",   \"TYPE\": \"float\", \"DEFAULT\": 0.5,   \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uRelief\",     \"TYPE\": \"float\", \"DEFAULT\": 0.6,   \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uLight\",      \"TYPE\": \"float\", \"DEFAULT\": 0.9,   \"MIN\": -3.15, \"MAX\": 3.15 },\n    { \"NAME\": \"uAA\",         \"TYPE\": \"float\", \"DEFAULT\": 1.0,   \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uExposure\",   \"TYPE\": \"float\", \"DEFAULT\": 1.0,   \"MIN\": 0.0,   \"MAX\": 2.5 },\n    { \"NAME\": \"uBloom\",      \"TYPE\": \"float\", \"DEFAULT\": 0.25,  \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uVignette\",   \"TYPE\": \"float\", \"DEFAULT\": 0.3,   \"MIN\": 0.0,   \"MAX\": 1.0 },\n    { \"NAME\": \"uGrain\",      \"TYPE\": \"float\", \"DEFAULT\": 0.03,  \"MIN\": 0.0,   \"MAX\": 1.0 }\n  ]\n}*/\n\n// LYAPUNOV - ISF port of the Shadertoy/WebGL2 version\n// (GLSL ES 1.00: gl_FragColor, TIME/RENDERSIZE).\n//\n// lambda = lim 1/N SUM ln |d/dx f(x_n)| for the logistic map driven by\n// a binary A/B word, with A on the x axis and B on the y axis of the\n// plane. Negative lambda is order, positive is chaos.\n\n#define PI   3.14159265359\n#define TAU  6.28318530718\n\n// Hard ceiling on the iteration loop. It is constant-bound, which is\n// the shape ANGLE likes to unroll flat — and this body gets inlined\n// four times over for the AA taps. iq's ZERO trick is the usual\n// defence, but GLSL ES 1.00 requires a constant loop initialiser and\n// ISF rejects it, so the early `break` carries the cost instead. It\n// is used in both builds so the two stay identical.\n#define MAXI 384\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\n// ------------------------------------------------------------\n// The exponent at one point of the (A,B) plane.\n//\n// `phase` rotates the word. With a hard word that is only a cyclic\n// shift, and a cyclic shift leaves lambda alone once the transient is\n// discarded — so the drift stays invisible until `soft` opens the\n// arc's edge and the intermediate phases become words of their own.\n// ------------------------------------------------------------\nfloat lyapunov(vec2 ab, float phase){\n    int   P    = int(uPeriod + 0.5);\n    float fp   = float(P);\n    float invP = floor(uSkip + 0.5) / fp;\n\n    // The symbols sample the phase circle on a grid of spacing 1/P, whatever\n    // the stride. Quantising duty onto that same grid and offsetting the\n    // samples by HALF a cell puts every symbol strictly INSIDE a cell and\n    // never on the arc's edge. Without it, period 2 at duty 0.5 drops both\n    // symbols exactly on the boundary, w comes back 0.5 for both, and the\n    // alternating map quietly collapses to a constant (a+b)/2 — a smooth\n    // gradient wearing a fractal's clothes.\n    float d  = clamp(floor(uDuty * fp + 0.5) / fp, 0.0, 1.0);\n    float hc = 0.5 / fp;               // half a cell\n\n    // The B symbols occupy an arc of the phase circle. Measuring the\n    // circular distance to that arc's CENTRE, rather than testing\n    // u > duty, makes the softening wrap properly at u = 0 — a symbol\n    // crossing the seam fades instead of popping.\n    float c = 0.5 + 0.5 * d;           // centre of the B arc\n    float L = 0.5 - 0.5 * d;           // its half-length\n    float h = max(uSoft * 0.5, 1e-4);  // softening half-width\n\n    float m = clamp(uMap, 0.0, 1.0);\n    int   N = int(uIter + 0.5);\n    // min() has no int overload in GLSL ES 1.00 — clamp in float, then cast.\n    int   W = int(min(uWarm + 0.5, uIter - 0.5));\n\n    // r outside [0,4] lets the orbit escape the unit interval and the\n    // exponent stops meaning anything, so the window is clipped to the\n    // map's own domain rather than to the frame.\n    ab = clamp(ab, 0.0, 4.0);\n\n    float x = clamp(uSeed, 0.001, 0.999);\n    float sum = 0.0, cnt = 0.0;\n    int j = 0;\n\n    for (int i = 0; i < MAXI; i++){\n        if (i >= N) break;\n\n        float u  = fract(float(j) * invP + hc + phase);\n        float dd = abs(fract(u - c + 0.5) - 0.5);\n        float w  = 1.0 - smoothstep(L - h, L + h, dd);\n        float r  = mix(ab.x, ab.y, w);\n\n        float fx = x * (1.0 - x);   // logistic\n        float dx = 1.0 - 2.0 * x;   // and its derivative\n        if (m > 0.001){             // uniform branch — coherent across the warp\n            fx = mix(fx, 0.25 * sin(PI * x),      m);\n            dx = mix(dx, 0.25 * PI * cos(PI * x), m);\n        }\n        x = r * fx;\n\n        // The derivative of the whole step is r * f'(x); the chain rule\n        // over N steps turns that product into this sum of logs.\n        if (i >= W){ sum += log(abs(r * dx) + 1e-9); cnt += 1.0; }\n\n        j++; if (j >= P) j = 0;\n    }\n    return sum / max(cnt, 1.0);\n}\n\n// ------------------------------------------------------------\n// Palettes. Three stops each way out of a SHARED colour at lambda = 0.\n//\n// The shared stop is the point. lambda is perfectly continuous across the\n// order/chaos frontier — it just passes through zero — so any jump in colour\n// there is invented by the palette, not by the dynamics. Ramping the two sides\n// away from one common dark stop makes the whole lambda -> colour map\n// continuous, and the frontier then reads as the soft dark seam it physically\n// is, rather than as a hard cut between two unrelated hues.\n// ------------------------------------------------------------\nvoid palStops(int p, out vec3 o0, out vec3 o1, out vec3 o2, out vec3 k1, out vec3 k2){\n    // 1 — violet gold.  o0 is shared: it is the colour AT lambda = 0.\n    o0 = vec3(0.10, 0.03, 0.09);\n    o1 = vec3(0.95, 0.55, 0.15); o2 = vec3(1.00, 0.94, 0.72);   // into order\n    k1 = vec3(0.34, 0.12, 0.62); k2 = vec3(0.03, 0.01, 0.09);   // into chaos\n\n    if (p <= 0){                        // 0 — Markus, as first published\n        o0 = vec3(0.08, 0.05, 0.03);\n        o1 = vec3(0.92, 0.62, 0.06); o2 = vec3(1.00, 0.97, 0.80);\n        k1 = vec3(0.10, 0.22, 0.52); k2 = vec3(0.00, 0.00, 0.03);\n    } else if (p == 2){                 // 2 — ember\n        o0 = vec3(0.09, 0.02, 0.01);\n        o1 = vec3(0.90, 0.25, 0.03); o2 = vec3(1.00, 0.88, 0.55);\n        k1 = vec3(0.25, 0.05, 0.05); k2 = vec3(0.02, 0.00, 0.00);\n    } else if (p == 3){                 // 3 — ice\n        o0 = vec3(0.02, 0.06, 0.10);\n        o1 = vec3(0.25, 0.72, 0.86); o2 = vec3(0.90, 0.99, 1.00);\n        k1 = vec3(0.04, 0.10, 0.30); k2 = vec3(0.00, 0.01, 0.04);\n    } else if (p == 4){                 // 4 — verdigris and copper\n        o0 = vec3(0.06, 0.05, 0.03);\n        o1 = vec3(0.80, 0.44, 0.20); o2 = vec3(0.98, 0.86, 0.66);\n        k1 = vec3(0.05, 0.30, 0.26); k2 = vec3(0.00, 0.04, 0.04);\n    } else if (p == 5){                 // 5 — ink\n        o0 = vec3(0.06, 0.06, 0.06);\n        o1 = vec3(0.55, 0.55, 0.55); o2 = vec3(1.00, 1.00, 1.00);\n        k1 = vec3(0.16, 0.16, 0.16); k2 = vec3(0.00, 0.00, 0.00);\n    } else if (p >= 6){                 // 6 — steel: reflectance, not colour\n        o0 = vec3(0.09, 0.10, 0.12);\n        o1 = vec3(0.52, 0.56, 0.62); o2 = vec3(0.90, 0.94, 1.00);\n        k1 = vec3(0.16, 0.18, 0.22); k2 = vec3(0.02, 0.02, 0.03);\n    }\n}\n\n// ------------------------------------------------------------\n// A studio in a function.\n//\n// What actually makes a surface read as polished metal is not a highlight, it\n// is a REFLECTION of something with structure. So this returns what the eye\n// would see looking along a reflected ray: a dark floor, a cool sky, and two\n// soft strip lights. Strips rather than points on purpose — a band is what\n// makes a curved surface streak instead of sparkle, and the streaks are most\n// of the look.\n//\n// Parameterised by the reflected ray's SCREEN-PLANE direction. Under an\n// orthographic view of a near-flat height field, rv ~ (2*slope.x, 2*slope.y, 1),\n// so rd is essentially the surface slope and the whole room is addressed by it.\n// ------------------------------------------------------------\nvec3 envMap(vec2 rd){\n    float y = clamp(rd.y * 0.7 + 0.5, 0.0, 1.0);\n    vec3 c = mix(vec3(0.045, 0.050, 0.060), vec3(0.56, 0.60, 0.68),\n                 smoothstep(0.12, 0.95, y));\n\n    // t*t, never pow(x, 2.0): pow() is undefined for a negative base.\n    float t1 = (rd.y - 0.45) / 0.11;  float k1 = exp(-t1 * t1);   // key strip\n    float t2 = (rd.y + 0.30) / 0.20;  float k2 = exp(-t2 * t2);   // fill, low and dim\n\n    // Break the strips along azimuth so they read as a room rather than a ring.\n    float az = atan(rd.y, rd.x);\n    k1 *= 0.60 + 0.40 * cos(az * 2.0 + 0.7);\n    k2 *= 0.55 + 0.45 * cos(az * 3.0 - 1.1);\n\n    c += vec3(1.00, 0.99, 0.96) * k1 * 2.2;\n    c += vec3(0.70, 0.79, 0.96) * k2 * 0.60;\n    return c;\n}\n\nvec3 shade(float lam){\n    float pf = clamp(uPalette, 0.0, 6.0);\n    int   p0 = int(floor(pf));\n    float f  = fract(pf);\n\n    vec3 a0, a1, a2, b1, b2, e0, e1, e2, g1, g2;\n    palStops(p0,     a0, a1, a2, b1, b2);\n    palStops(p0 + 1, e0, e1, e2, g1, g2);\n    // Cross-fade the STOPS, not two finished ramps: one ramp evaluation\n    // instead of two, and neighbouring palettes blend rather than\n    // dissolve through each other.\n    vec3 o0 = mix(a0, e0, f), o1 = mix(a1, e1, f), o2 = mix(a2, e2, f);\n    vec3 k1 = mix(b1, g1, f), k2 = mix(b2, g2, f);\n\n    // exp() rather than a clamp: lambda has no bound either way — a\n    // superstable orbit sends it to -inf — so nothing should saturate.\n    float t = 1.0 - exp( min(lam, 0.0) / max(uOrderScale, 0.01));\n    float s = 1.0 - exp(-max(lam, 0.0) / max(uChaosScale, 0.01));\n\n    vec3 ord = (t < 0.5) ? mix(o0, o1, t * 2.0) : mix(o1, o2, t * 2.0 - 1.0);\n    vec3 cha = (s < 0.5) ? mix(o0, k1, s * 2.0) : mix(k1, k2, s * 2.0 - 1.0);\n\n    // Both ramps leave from o0, and t = s = 0 at lambda = 0, so the two\n    // branches agree exactly on the frontier. No blend band is needed to hide\n    // a seam, because there is no longer a seam to hide.\n    return (lam < 0.0) ? ord : cha;\n}\n\nvoid main(){\n    vec2 fragCoord = gl_FragCoord.xy;\n    vec2 R = RENDERSIZE.xy;\n\n    // Fit the window to WIDTH, so `span` always means the same slice of\n    // the A axis whatever shape the frame is.\n    //\n    // The map is only defined for r in [0,4]. Past r = 4 the orbit escapes the\n    // unit interval and the exponent is +inf EVERYWHERE, so any part of the\n    // frame beyond that edge is a dead flat slab bounded by a razor-straight\n    // line — it looks exactly like a clipping bug because it effectively is\n    // one. There is no cleverer shading to apply: the region genuinely holds no\n    // structure. So the window itself is kept inside the domain. The span is\n    // capped so the frame's short axis still fits, and the centre is then\n    // clamped to the remaining travel, which makes the flat slab unreachable\n    // instead of merely unattractive.\n    float span = min(uSpan, 4.0 * min(1.0, R.x / R.y));\n    float px   = span / R.x;\n    vec2  hx   = 0.5 * px * R;                  // half-extent of the frame, in r\n    vec2  ctr  = clamp(vec2(uCentreA, uCentreB), hx, vec2(4.0) - hx);\n    vec2  ab   = ctr + (fragCoord - 0.5 * R) * px;\n\n    // Drift is measured in whole symbols: one full turn of the word is\n    // P steps, so the animation closes on itself every P/speed seconds.\n    float phase = fract(TIME * uSpeed * 0.05);\n\n    bool quad = (uAA > 0.5) || (uRelief > 0.001);\n\n    float l00 = lyapunov(ab + vec2(-0.25, -0.25) * px, phase);\n    float lam = l00;\n    vec2  g   = vec2(0.0);\n    if (quad){\n        float l10 = lyapunov(ab + vec2( 0.25, -0.25) * px, phase);\n        float l01 = lyapunov(ab + vec2(-0.25,  0.25) * px, phase);\n        float l11 = lyapunov(ab + vec2( 0.25,  0.25) * px, phase);\n        lam = (l00 + l10 + l01 + l11) * 0.25;\n        // The AA taps ARE the finite difference — the emboss is free.\n        g = vec2((l10 + l11) - (l00 + l01),\n                 (l01 + l11) - (l00 + l10)) * 0.5;\n    }\n\n    vec3 albedo = shade(lam);\n    vec3 col = albedo;\n\n    if (uRelief > 0.001){\n        // Shape the ORDERED side only. Inside a chaotic region lambda is a\n        // genuinely noisy estimate at this sample count, so its pixel-scale\n        // gradient is sampling noise rather than shape, and lighting it just\n        // renders the noise in relief. The fade reaches past lambda = 0 so\n        // the order/chaos frontier itself still catches a rim.\n        float om = smoothstep(0.12, -0.02, lam);\n        vec2  gr = g * uRelief * om;\n        float ca = cos(uLight), sa = sin(uLight);\n\n        // --- matte: the cheap directional emboss (uMetal = 0) -------------\n        float e = dot(gr, vec2(ca, sa)) * 26.0;\n        e = e / (1.0 + abs(e));                     // no tanh in ES 1.00\n        vec3 emb = col * (1.0 + 0.85 * e)\n                 + vec3(1.0, 0.97, 0.92) * pow(max(e, 0.0), 3.0) * 0.45;\n\n        // --- metal: treat lambda as a height field and actually shade it ---\n        // A real normal, not a directional derivative. normalize() also does\n        // the right thing where the gradient explodes at the order/chaos\n        // frontier: the normal lies down flat and the edge takes a hard dark\n        // crease, which is exactly how a struck metal surface behaves.\n        vec3 N = normalize(vec3(-gr * 55.0, 1.0));\n\n        // Orthographic view down +Z, so reflect((0,0,-1), N) folds to this.\n        vec3 rv = vec3(2.0 * N.z * N.x, 2.0 * N.z * N.y, 2.0 * N.z * N.z - 1.0);\n        vec2 rd = mat2(ca, -sa, sa, ca) * rv.xy;    // uLight turns the room\n\n        // --- anisotropy: brush the metal ALONG ITS OWN CONTOURS ----------\n        // A groove behaves like a tiny cylindrical mirror: it smears what it\n        // reflects along its own axis. So the operation is a directional BLUR\n        // of the room, not a compression of the reflected ray — compressing it\n        // merely drags every sample toward the middle of the room, which dims\n        // the strip lights instead of stretching them. Averaging keeps the\n        // energy and lets the highlight run.\n        //\n        // The brush direction is not global: it is the tangent to lambda's own\n        // level sets (perpendicular to the raw gradient), so the grain follows\n        // the form the way a turned or spun surface does, and it falls out of\n        // the field rather than being bolted on top of it.\n        vec3 room = envMap(rd);\n        if (uAniso > 0.001){\n            float gl = length(g);\n            vec2 tang = (gl > 1e-9) ? vec2(-g.y, g.x) / gl : vec2(1.0, 0.0);\n            tang = mat2(ca, -sa, sa, ca) * tang;      // turn with the room\n            // Radius is scaled to the STRIP WIDTH (~0.11 in rd), not to the\n            // room: blur much wider than a strip and every tap averages the\n            // whole room, which flattens the frame to mid-grey and throws\n            // away the blacks the material depends on.\n            float wd = clamp(uAniso, 0.0, 1.0) * 0.14;\n            room = (envMap(rd - tang * wd) + room + envMap(rd + tang * wd))\n                 * (1.0 / 3.0);\n        }\n\n        // Schlick. For a conductor there is no diffuse term at all — the\n        // palette colour stops being an albedo and becomes the REFLECTANCE,\n        // which is why a metal tints what it reflects instead of what it is.\n        // A polished conductor returns most of what hits it: F0 sits near 0.9,\n        // not at the albedo. Pulling the palette most of the way to white keeps\n        // it as a TINT on the reflection - which is what a metal's colour\n        // actually is - instead of letting a dark fractal swallow the light.\n        float fr = pow(clamp(1.0 - N.z, 0.0, 1.0), 5.0);\n        vec3  F0  = mix(albedo, vec3(1.0), 0.62);\n        vec3  F   = F0 + (1.0 - F0) * fr;\n\n        // --- thin film: the temper colours of oxidised steel --------------\n        // Light reflects off the top of a thin oxide film and off the metal\n        // beneath it; the two paths differ by 2*n*d*cos(theta_film), and each\n        // wavelength lands somewhere different on that difference, so some\n        // cancel and some reinforce. That is exactly what puts straw, purple\n        // and blue on tempered steel, and it is angle-dependent, which is why\n        // the bands follow the curvature rather than sitting on the surface.\n        // Thickness is driven by lambda as well, so the ordered side runs to a\n        // thicker film and the colour maps the dynamics too.\n        if (uFilm > 0.001){\n            float ordT = 1.0 - exp(min(lam, 0.0) / max(uOrderScale, 0.01));\n            // A travelling swell in the film's depth, so the temper colours\n            // crawl instead of sitting still. Its spatial phase is lambda\n            // itself, so the bands run along the exponent's own level sets\n            // rather than sweeping across the frame like a wipe.\n            //\n            // Driven by the SAME `phase` that turns the A/B word, at a whole\n            // number of cycles per turn. Anything else — a raw TIME term, or a\n            // non-integer rate — would beat against the 20 s loop and quietly\n            // destroy it, and the loop is a stated property of this shader.\n            // Few spatial cycles on purpose. Wind ordT's coefficient up and the\n            // swell stops reading as motion and starts reading as fixed rainbow\n            // banding that happens to slide — the metal disappears under it.\n            float flow = uFilmFlow * 0.42 * sin(TAU * (ordT * 1.2 - phase * 2.0));\n            float dnm  = uFilmThick * (0.80 + 0.35 * ordT + flow);  // depth, nm\n            // A LOW film index on purpose. cos(theta) inside the film is\n            // what carries the angle dependence, and a high index refracts\n            // everything toward the normal so that cosine barely moves —\n            // 1.42 bottoms out at 0.71 and the bands go flat, leaving a\n            // uniform tint. 1.20 bottoms out near 0.55, so the hue sweeps a\n            // full cycle across a ridge and the colour follows the FORM.\n            float nf   = 1.20;                                // film IOR\n            float ct   = clamp(N.z, 0.0, 1.0);                // cos in air\n            float st2  = max(1.0 - ct * ct, 0.0);\n            float ctf  = sqrt(max(1.0 - st2 / (nf * nf), 0.0));\n            float opd  = 2.0 * nf * dnm * ctf;                // path difference\n            // Centred on 1 rather than on 0.5, so turning the film up tints the\n            // reflection instead of halving it.\n            vec3 phase = TAU * opd / vec3(650.0, 550.0, 450.0) + PI;\n            F *= max(vec3(1.0) + clamp(uFilm, 0.0, 1.0) * 0.55 * cos(phase), 0.0);\n        }\n\n        vec3 met = room * F;\n\n        col = mix(emb, met, clamp(uMetal, 0.0, 1.0));\n    }\n\n    col *= uExposure;\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.30, l) * uBloom * 0.9;\n    }\n\n    // max() on the base: pow() with a negative base and a fractional exponent\n    // is undefined in GLSL ES 1.00, and glslop names it as a silent-failure\n    // cause. q is inside (0,1) so the product cannot actually go negative here,\n    // but the guard is free and the rule is worth obeying literally.\n    vec2 q = fragCoord / R;\n    float vig = pow(max(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.0), 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    gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);\n}\n",
    "generator": {
      "model": "Claude-opus-5",
      "provider": null,
      "prompt": null,
      "params": null
    }
  }
}