{
  "license": "CC0-1.0",
  "shader": {
    "id": "418wdz5m",
    "title": "Strike",
    "license": "CC0-1.0",
    "provenance_type": "witnessed_generation",
    "made_with": "glslop-scaffold1",
    "status": "active",
    "compile_ok": true,
    "render_hash": "006accccf8c0f8fc",
    "tags": [
      "lichtenberg",
      "dielectric",
      "event",
      "feedback",
      "generative",
      "material",
      "monochrome"
    ],
    "parents": [],
    "author": "zazi5inxhmgi",
    "author_name": "ufffd",
    "author_url": "/u/ufffd",
    "created_at": "2026-08-30T18:04:48.401Z",
    "poster_url": "/posters/418wdz5m.avif",
    "stats": {
      "slops": 0,
      "views": 102,
      "forks": 0
    },
    "self": "/api/v1/shaders/418wdz5m",
    "isf": "/*{\n  \"DESCRIPTION\": \"A slab of cast dielectric under rising electrical stress. Charge accumulates until the medium fails; the discharge branches through it at the speed of a decision and burns a permanent Lichtenberg figure into the material. The white-hot channel lasts an instant. The tree it leaves is dark, fine and permanent — old burns anneal slowly while new ones cut across them, so the slab is a record of every discharge it has ever taken. It fails on its own on a slow irregular clock; FIRE triggers one now, on top of that. ERASE anneals the slab clean.\",\n  \"CREDIT\": \"\",\n  \"INPUTS\": [\n    { \"NAME\": \"fire\",    \"TYPE\": \"event\" },\n    { \"NAME\": \"erase\",   \"TYPE\": \"event\" },\n    { \"NAME\": \"stress\",  \"TYPE\": \"float\", \"DEFAULT\": 1.0,  \"MIN\": 0.25, \"MAX\": 3.0 },\n    { \"NAME\": \"reach\",   \"TYPE\": \"float\", \"DEFAULT\": 1.0,  \"MIN\": 0.35, \"MAX\": 1.7 },\n    { \"NAME\": \"branch\",  \"TYPE\": \"float\", \"DEFAULT\": 1.0,  \"MIN\": 0.3,  \"MAX\": 2.0 },\n    { \"NAME\": \"anneal\",  \"TYPE\": \"float\", \"DEFAULT\": 0.60, \"MIN\": 0.0,  \"MAX\": 1.0 },\n    { \"NAME\": \"conduct\", \"TYPE\": \"float\", \"DEFAULT\": 0.30, \"MIN\": 0.0,  \"MAX\": 1.0 },\n    { \"NAME\": \"grain\",   \"TYPE\": \"float\", \"DEFAULT\": 1.0,  \"MIN\": 0.0,  \"MAX\": 2.0 },\n    { \"NAME\": \"warmth\",  \"TYPE\": \"float\", \"DEFAULT\": 0.62, \"MIN\": 0.0,  \"MAX\": 1.0 }\n  ],\n  \"PASSES\": [\n    { \"TARGET\": \"tre\", \"FLOAT\": true },\n    { \"TARGET\": \"mem\",  \"PERSISTENT\": true, \"FLOAT\": true },\n    { \"TARGET\": \"mem2\", \"PERSISTENT\": true, \"FLOAT\": true },\n    { }\n  ]\n}*/\n\n#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#endif\n\n#define TAU 6.28318531\n#define GROWT 0.115         // seconds for the discharge to reach full extent\n#define EVLIFE 1.25         // seconds of life for a fired (event) discharge\n\n// ---------------------------------------------------------------- hashes\nfloat h11(float p){ p = fract(p*0.1031); p *= p + 33.33; p *= p + p; return fract(p); }\nfloat h21(vec2 p){ vec3 q = fract(vec3(p.xyx)*0.1031); q += dot(q, q.yzx+33.33); return fract((q.x+q.y)*q.z); }\nvec2  h22(vec2 p){ vec3 q = fract(vec3(p.xyx)*vec3(0.1031,0.1030,0.0973)); q += dot(q, q.yzx+33.33); return fract((q.xx+q.yz)*q.zy); }\n\nfloat vnoise(vec2 p){\n  vec2 i = floor(p), f = fract(p); f = f*f*(3.0-2.0*f);\n  return mix(mix(h21(i), h21(i+vec2(1.0,0.0)), f.x),\n             mix(h21(i+vec2(0.0,1.0)), h21(i+vec2(1.0,1.0)), f.x), f.y);\n}\n\nfloat sdSeg(vec2 p, vec2 a, vec2 b){\n  vec2 pa = p-a, ba = b-a;\n  float h = clamp(dot(pa,ba)/max(dot(ba,ba),1e-9), 0.0, 1.0);\n  return length(pa - ba*h);\n}\n\n// ------------------------------------------------------- breakdown potential\n// U = \"how easily can the discharge reach here\".\n//   * a fixed material grain (this slab's own striae) alternating with a per-strike term\n//   * angular lobes so a few directions win big and most stall — that is what keeps the\n//     figure spiky instead of a disc\n//   * a strong radial fall-off, so U descends away from the entry point; that guarantees\n//     a live cell's whole chain back to the seed is live too (no floating fragments).\n// The gradient falls out analytically, which is what makes this cheap enough to run per\n// pixel with no marching and no state.\nvoid ufield(vec2 x, vec2 sp, float sid, float B, float lob, float f0, float gn,\n            out float U, out vec2 G){\n  vec2 rv = x - sp;\n  float r  = max(length(rv), 1e-4);\n  vec2  rh = rv / r;\n\n  float v = 0.0; vec2 g = vec2(0.0);\n  for (int i = 0; i < 6; i++){\n    float fi  = float(i);\n    float per = mod(fi, 2.0);                 // alternate: material grain / this strike\n    float oc  = floor(fi*0.5);                // octaves 0,0,1,1,2,2\n    float sd  = fi*7.317 + per*(sid*3.113 + 1.7);\n    float an  = TAU*h11(sd);\n    float fr  = f0 * pow(2.13, oc);\n    float am  = gn / (6.0*fr);                // gn is the TOTAL gradient budget over all 6\n    vec2  k   = vec2(cos(an), sin(an))*fr;\n    float s   = dot(k, x) + TAU*h11(sd + 11.0);\n    v += am*sin(s);\n    g += am*cos(s)*k;\n  }\n\n  // low-order lobes: two or three directions take almost everything, the rest stall\n  float th = atan(rv.y, rv.x);\n  float L = 0.0, dL = 0.0;\n  for (int j = 0; j < 4; j++){\n    float fj = float(j);\n    float n  = floor(3.0 + fj*fj*1.35 + fj*0.35);     // 3, 4, 8, 16 lobes\n    float aj = 0.44*(1.0 - 0.17*fj)/pow(1.30, fj);\n    float p2 = TAU*h11(sid*5.71 + fj*13.13);\n    L  += aj*sin(n*th + p2);\n    dL += aj*n*cos(n*th + p2);\n  }\n  // Carbonised channels conduct, so a new discharge is drawn along old damage — but\n  // only damage that has HAD time to carbonise. mem2.g is burn recency, so weighting by\n  // (1-recency) excludes this strike own fresh deposit; without that the tree would\n  // steer into itself mid-strike and the scar depth would depend on the step count.\n  vec2 uvx = x*RENDERSIZE.y/RENDERSIZE.xy + 0.5;\n  vec4 old = IMG_NORM_PIXEL(mem2, uvx);\n  float cnd = conduct*3.2*old.r*(1.0 - clamp(old.g, 0.0, 1.0));\n\n  float fd = smoothstep(0.0, 0.10, r);                // lobes fade in past the entry point\n  // the tangential pull is what makes neighbouring tips merge into a winning arm\n  float tg = clamp(0.135*dL*lob*fd/r, -0.55*B, 0.55*B);\n  U = v + L*lob*fd + cnd - B*r;\n  G = g + tg*vec2(-rh.y, rh.x) - B*rh;\n}\n\n// ------------------------------------------------------------- the figure\n// Every cell of a jittered grid picks a parent: the neighbour that lies uphill in U, i.e.\n// one step back toward the entry point, deflected by the material grain. Together those\n// links are a spanning tree — many tips, converging as they run inward, so the channel\n// count falls and the surviving ones carry more. A cell exists only where U > T; T drops\n// fast during the strike, which is the tree growing. Thickness tracks U-T: cells that\n// joined early are channels, cells that only just made it are hair.\n// returns x=core  y=burn weight  z=leading front  w=corona\nvec4 lich(vec2 P, vec2 sp, float sid, float T, float B, float lob,\n          float cs, float f0, float gn, float tks, float wmax, float pw){\n  vec2 c0 = floor(P/cs);\n  float d = 1e3, wb = 0.0;\n  for (int j = 0; j < 9; j++){\n    vec2 c = c0 + vec2(mod(float(j),3.0)-1.0, floor(float(j)/3.0)-1.0);\n    vec2 fp = (c + 0.5 + 0.92*(h22(c + sid*17.31) - 0.5))*cs;\n    float U; vec2 G;\n    ufield(fp, sp, sid, B, lob, f0, gn, U, G);\n    float w = U - T;\n    // thin tips are culled at random so the hair spacing is not the grid pitch; trunk\n    // and mid cells always survive, so nothing that carries a subtree is cut\n    if (w > -0.085*wmax && (w > 0.20*wmax || h21(c*1.7 + sid*3.13) < 0.72)){\n      vec2 gg = G / max(max(abs(G.x), abs(G.y)), 1e-5);\n      vec2 c2 = c + floor(gg + 0.5);                       // one step uphill, 8-neighbourhood\n      vec2 fq = (c2 + 0.5 + 0.92*(h22(c2 + sid*17.31) - 0.5))*cs;\n      float tk = tks*pow(clamp(w/wmax, 0.0, 1.0), 2.7);\n      float ds = sdSeg(P, fp, fq) - tk;\n      if (ds < d){ d = ds; wb = w; }\n    }\n  }\n  if (d > 0.9e3) return vec4(0.0);\n  // cells just BELOW threshold still draw, very faintly: the long thin outrunners that\n  // shoot past the body of the figure and then stop\n  float lin = (1.0 - smoothstep(0.0, pw, d))*smoothstep(-0.085*wmax, 0.015*wmax, wb);\n  float thk = clamp(wb/wmax, 0.0, 1.0);\n  float dc = max(d, 0.0);\n  // the corona has to die INSIDE the 3x3 cell window it was measured over, or the\n  // neighbourhood boundary shows up as square blocks\n  float cor = exp(-dc/0.013)*smoothstep(0.9*cs, 0.32*cs, dc);\n  return vec4(lin*(0.09 + 0.91*thk*thk), lin, lin*exp(-4.2*thk), cor);\n}\n\n// ------------------------------------------------------------- the clock\n// One breakdown per slot, at a jittered phase inside it, so intervals are irregular.\n// Every term is a closed form of TIME — nothing is integrated — so a poster rendered at\n// any --time lands exactly where live playback would be at that moment.\nvoid schedule(out float sid, out float age, out float mag, out vec2 sp, out vec2 nsp,\n              out float chg){\n  float slot = 4.4 / max(stress, 0.05);\n  // run the schedule on a clock that is already part-way charged, so the slab fails\n  // within a second of TIME=0 instead of opening on four seconds of nothing\n  float tt = TIME + 2.73/max(stress, 0.05);\n  float sl = floor(tt/slot);\n  float fp = 0.16 + 0.68*h11(sl*7.13 + 0.5);\n  float ft = (sl + fp)*slot;\n  if (tt < ft){ sl -= 1.0; fp = 0.16 + 0.68*h11(sl*7.13 + 0.5); ft = (sl + fp)*slot; }\n  float nf = (sl + 1.16 + 0.68*h11((sl+1.0)*7.13 + 0.5))*slot;\n  sid = sl;\n  age = tt - ft;\n  mag = 0.26 + 0.74*pow(h11(sl*3.771 + 2.2), 1.55);   // mostly minor failures\n  float asp = RENDERSIZE.x/RENDERSIZE.y;\n  // golden-angle placement: consecutive entry points never cluster\n  float ga = 2.39996323*sl + 1.1;\n  float rr2 = 0.06 + 0.31*sqrt(h11(sl*2.393 + 0.9));\n  sp  = vec2(cos(ga)*rr2*asp, sin(ga)*rr2);\n  float nga = 2.39996323*(sl+1.0) + 1.1;\n  float nrr = 0.06 + 0.31*sqrt(h11((sl+1.0)*2.393 + 0.9));\n  nsp = vec2(cos(nga)*nrr*asp, sin(nga)*nrr);\n  chg = clamp((tt - ft)/max(nf - ft, 0.001), 0.0, 1.0);\n}\n\nvoid main(){\n  vec2 uv = isf_FragNormCoord;\n  vec2 P  = (gl_FragCoord.xy - 0.5*RENDERSIZE.xy)/RENDERSIZE.y;\n  float asp = RENDERSIZE.x/RENDERSIZE.y;\n\n  float sid, age, mag, chg; vec2 sp, nsp;\n  schedule(sid, age, mag, sp, nsp, chg);\n\n  // ============================================================ PASS 0: the discharge\n  if (PASSINDEX == 0){\n    float ev = IMG_THIS_PIXEL(mem).a;\n    bool evOn = ev > 0.002;\n    if (age > 0.9 && !evOn){ gl_FragColor = vec4(0.0); return; }\n\n    float pw = 1.30/RENDERSIZE.y;\n    vec4 acc = vec4(0.0);\n    for (int s = 0; s < 2; s++){\n      float a2, m2, i2; vec2 p2;\n      if (s == 0){ a2 = age; m2 = mag; i2 = sid; p2 = sp; }\n      else {\n        float ft = TIME - (1.0 - ev)*EVLIFE;\n        i2 = 400.0 + floor(ft*6.0);\n        a2 = evOn ? (1.0 - ev)*EVLIFE : 99.0;\n        m2 = 0.86;\n        float ga2 = 2.39996323*i2 + 1.1;\n        float rr3 = 0.06 + 0.31*sqrt(h11(i2*2.393 + 0.9));\n        p2 = vec2(cos(ga2)*rr3*asp, sin(ga2)*rr3);\n      }\n      if (a2 > 0.9) continue;\n\n      float gr = clamp(a2/GROWT, 0.0, 1.0);\n      float B  = 6.1/(max(reach, 0.2)*(0.55 + 0.45*m2));   // a minor failure stays small\n      float T  = mix(0.95, -0.30 - 0.98*m2, pow(gr, 0.5));\n      float ec = exp(-a2/0.50);\n\n      float wmx = 0.32 + 0.98*m2;\n      // Two scales through ONE call site, because compile cost is driven by call sites,\n      // not by file size. L=0 is the structure: a coarse graph, few channels, real\n      // bifurcations. L=1 is filigree: a much finer graph on a much rougher field, run\n      // only where L=0 already found a channel to hang it off.\n      vec4 C = vec4(0.0);\n      for (int L = 0; L < 2; L++){\n        float fn = float(L);\n        if (L == 1 && C.w <= 0.14) break;\n        vec4 X = lich(P, p2, i2, T + 0.10*fn, B, branch,\n                      mix(0.0240, 0.0086, fn), mix(16.0, 54.0, fn),\n                      mix(0.93, 1.75, fn)*B, mix(0.0108, 0.0017, fn)*m2, wmx, pw);\n        // filigree is a BURN feature, not a bright one — at full brightness it fills the\n        // interior and the live discharge stops reading as a tree\n        if (L == 1) X *= smoothstep(0.14, 0.62, C.w)*vec4(0.40, 0.80, 0.60, 0.45);\n        C = (L == 0) ? X : max(C, X);\n      }\n      // the BURN is not multiplied by ec. The glow fades with age, but how deep the\n      // scar goes must not depend on which sim steps happened to land in the window —\n      // deposit is idempotent, so a coarse poster step burns exactly as deep as live.\n      acc = max(acc, vec4(C.x*ec, C.y, C.z*ec, C.w*ec));\n    }\n    gl_FragColor = acc;\n    return;\n  }\n\n  // ================================================ PASS 1: the slab's memory of it\n  if (PASSINDEX == 1){\n    vec4 M  = IMG_THIS_PIXEL(mem);\n    vec4 Tr = IMG_THIS_PIXEL(tre);\n\n    // carry our own clock — TIMEDELTA disagrees with TIME on the render path\n    float nowc = mod(TIME, 1.0);\n    float dt = nowc - M.b; if (dt < 0.0) dt += 1.0;\n    if (dt <= 0.0 || dt > 0.50) dt = 0.0167;\n\n    float sc = M.r;\n    float tau = 70.0*exp(-2.7*anneal)*(0.30 + 1.7*sc);   // deep burns anneal slower\n    sc *= exp(-dt/max(tau, 0.30));\n    float dep = Tr.y;\n    sc = min(1.4, max(sc, dep*(1.0 + 0.35*sc)));         // idempotent: step count can't matter\n    if (erase) sc = 0.0;\n\n    float ht = max(M.g*exp(-dt/0.28), Tr.x);\n    float ev = fire ? 1.0 : max(0.0, M.a - dt/EVLIFE);\n    gl_FragColor = vec4(sc, ht, nowc, ev);\n    return;\n  }\n\n  // ============================ PASS 2: what the slab never gets back\n  // mem.r anneals on a scale of tens of seconds — that is the visible ageing. This pass\n  // holds the part that does NOT come back: a permanent floor under every burn, plus a\n  // recency term so a fresh char and a decade-old ghost can be coloured differently.\n  if (PASSINDEX == 2){\n    vec4 M2 = IMG_THIS_PIXEL(mem2);\n    float dep = IMG_THIS_PIXEL(tre).y;\n    float nowc = mod(TIME, 1.0);\n    float dt = nowc - M2.b; if (dt < 0.0) dt += 1.0;\n    if (dt <= 0.0 || dt > 0.50) dt = 0.0167;\n\n    float perm = max(M2.r*exp(-dt/600.0), dep*0.38);\n    float rec  = max(M2.g*exp(-dt/9.0),   dep);\n    if (erase){ perm = 0.0; rec = 0.0; }\n    gl_FragColor = vec4(perm, rec, nowc, 1.0);\n    return;\n  }\n\n  // ================================================ PASS 3: look into the material\n  vec4 M  = IMG_THIS_PIXEL(mem);\n  vec4 M2 = IMG_THIS_PIXEL(mem2);\n  vec4 Tr = IMG_THIS_PIXEL(tre);\n\n  vec2 e = min(uv, 1.0-uv);\n  float ed = min(e.x*asp, e.y);\n  float bev = smoothstep(0.0, 0.048, ed);\n\n  // the figure sits INSIDE the slab: displaced, softening copies read as depth\n  vec2 par = vec2(0.0058, -0.0042);\n  float sD = ( IMG_NORM_PIXEL(mem2, uv + par*2.4).r\n             + IMG_NORM_PIXEL(mem2, uv + par*2.4 + vec2(0.004,0.003)).r\n             + IMG_NORM_PIXEL(mem2, uv + par*2.4 - vec2(0.004,0.003)).r ) * 0.72;\n  float sM = max(IMG_NORM_PIXEL(mem, uv + par).r, IMG_NORM_PIXEL(mem2, uv + par).r);\n  float s   = clamp(max(M.r, M2.r), 0.0, 1.4);\n  float rec = clamp(M2.g, 0.0, 1.0);\n\n  vec3 warmS = vec3(0.888, 0.822, 0.708);\n  vec3 coolS = vec3(0.735, 0.775, 0.812);\n  vec3 base  = mix(coolS, warmS, warmth);\n\n  float lite = 1.06 - 0.40*length(P - vec2(-0.30, 0.32));\n  float striae = 0.5 + 0.5*sin(P.x*19.0 + 5.5*vnoise(P*vec2(1.5,5.0)));\n  float tooth  = vnoise(P*430.0);\n  float bub    = smoothstep(0.88, 1.0, vnoise(P*34.0 + 11.0));\n\n  vec3 col = base*lite;\n  col *= 1.0 - 0.035*striae*grain;\n  col *= 1.0 - 0.050*(tooth-0.5)*grain;\n  col *= 1.0 - 0.085*bub*grain;\n  col += vec3(0.05,0.03,0.00)*striae*grain*0.25;          // faint warm cast in the striae\n\n  // Photoelastic fringes: stress concentrating where the slab is ABOUT to fail. Keyed\n  // to the next entry point and to charge, so it telegraphs the strike a second early.\n  float rr = length(P - nsp);\n  float fr = sin(30.0*rr - 2.0)*exp(-2.0*rr);\n  col += vec3(0.10,0.15,0.26)*fr*pow(chg, 3.0)*0.55;\n\n  col = mix(col, base*vec3(0.62,0.55,0.47), clamp(sD,0.0,1.0)*0.26);\n  col = mix(col, base*vec3(0.42,0.33,0.26), clamp(sM,0.0,1.0)*0.32);\n  col = mix(col, vec3(0.325,0.237,0.170),   smoothstep(0.0,0.34,s)*0.54);   // scorch\n  col = mix(col, vec3(0.044,0.033,0.030),   smoothstep(0.55,1.15,s)*(0.62+0.38*rec)); // char\n\n  col += vec3(1.00,0.38,0.09)*M.g*0.62;                   // cooling ember\n\n  // while a channel is conducting it lights the material around it: a cheap two-ring\n  // bloom straight off the live pass, skipped entirely when nothing is burning\n  float blo = 0.0;\n  if (age < 0.9 || M.a > 0.002){\n    float rot = TAU*h21(gl_FragCoord.xy);          // per-pixel ring rotation: replicas -> grain\n    for (int i = 0; i < 10; i++){\n      float fi8 = float(i);\n      float a8 = rot + 2.39996323*fi8;\n      float r8 = 0.010 + 0.050*fi8/9.0;\n      blo += IMG_NORM_PIXEL(tre, uv + vec2(cos(a8), sin(a8))*r8).x*(0.115 - 0.008*fi8);\n    }\n  }\n  col += vec3(0.47,0.58,1.00)*blo*0.95;                   // the slab glowing around it\n  col += vec3(0.46,0.58,1.00)*Tr.w*0.72*mag;              // corona in the material\n  col += vec3(1.00,0.98,0.99)*Tr.x*1.55;                  // white-hot channel\n  col += vec3(0.78,0.85,1.00)*Tr.z*1.20;                  // advancing tips\n\n  float flash = exp(-age/0.070)*mag;\n  col += vec3(0.40,0.46,0.72)*flash*0.52*(0.45 + 0.55*bev);\n\n  // The polished FACE, composited over the burns rather than under them — this is the\n  // cue that sells the figures as being inside something rather than drawn on it.\n  float sheen = smoothstep(0.30, 1.35, 1.05 - (P.x*0.5 + P.y*0.95));\n  col += vec3(0.84,0.88,0.97)*sheen*0.052;\n  vec2 sq = vec2(P.x*0.70 - P.y*0.71, P.x*0.71 + P.y*0.70);\n  float scr = smoothstep(0.968, 1.0, vnoise(vec2(sq.x*2.2, sq.y*300.0)));\n  col += vec3(0.92,0.94,1.0)*scr*0.085*grain;\n  col += vec3(0.95)*smoothstep(0.93, 1.0, vnoise(P*185.0 + 41.0))*0.05*grain;\n\n  // the polished chamfer on the block: this is a solid object, not a picture plane\n  float cham = smoothstep(0.009, 0.046, ed);\n  col = mix(col*vec3(0.30,0.325,0.395), col, cham);\n  col = mix(col, col*vec3(1.10,1.06,0.98), smoothstep(0.046, 0.020, ed)*0.6);\n  float q1 = (ed - 0.0455)/0.0042;\n  float q2 = (ed - 0.0105)/0.0030;\n  col += vec3(0.95,0.96,1.00)*exp(-q1*q1)*0.20;\n  col += vec3(0.90,0.94,1.00)*exp(-q2*q2)*0.10;\n\n  vec3 hot = max(col - 1.0, 0.0);\n  col = min(col, vec3(1.0)) + hot/(1.0 + hot);\n  col += (h21(gl_FragCoord.xy + fract(TIME)*13.0) - 0.5)*0.009;\n  gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);\n}\n",
    "generator": {
      "model": "claude-opus-5",
      "provider": "anthropic",
      "prompt": null,
      "params": null
    }
  }
}