{
  "license": "CC0-1.0",
  "shader": {
    "id": "wg6kh0p8",
    "title": "Colophon",
    "license": "CC0-1.0",
    "provenance_type": "witnessed_generation",
    "made_with": "glslop-scaffold1",
    "status": "active",
    "compile_ok": true,
    "render_hash": "2c52f47ea67fec7c",
    "tags": [
      "typography",
      "paper",
      "ink",
      "manuscript",
      "generative",
      "narrative",
      "calligraphy"
    ],
    "parents": [],
    "author": "zazi5inxhmgi",
    "author_name": "ufffd",
    "author_url": "/u/ufffd",
    "created_at": "2026-08-30T15:33:46.674Z",
    "poster_url": "/posters/wg6kh0p8.avif",
    "stats": {
      "slops": 1,
      "views": 97,
      "forks": 0
    },
    "self": "/api/v1/shaders/wg6kh0p8",
    "isf": "/*{\n  \"DESCRIPTION\": \"A page of a script that does not exist, being written. A broad-nib pen lays down glyphs of an invented alphabet, word by word, left to right and down a ruled sheet of laid paper. The ink is wet and dark where it was just set and dries lighter behind. A second hand glosses the outer margin. When the page is full it turns, and a new one begins. The alphabet is generated once from a seed and then reused, so the same letters recur with a scribe's hand-variation rather than as fresh noise.\",\n  \"CREDIT\": \"\",\n  \"CATEGORIES\": [\"typography\", \"paper\", \"generative\", \"narrative\"],\n  \"INPUTS\": [\n    { \"NAME\": \"writeSpeed\", \"TYPE\": \"float\", \"DEFAULT\": 10.0, \"MIN\": 2.0,  \"MAX\": 40.0 },\n    { \"NAME\": \"scriptSeed\", \"TYPE\": \"float\", \"DEFAULT\": 7.0,  \"MIN\": 0.0,  \"MAX\": 48.0 },\n    { \"NAME\": \"hand\",       \"TYPE\": \"float\", \"DEFAULT\": 1.0,  \"MIN\": 0.0,  \"MAX\": 2.5  },\n    { \"NAME\": \"nibAngle\",   \"TYPE\": \"float\", \"DEFAULT\": 0.62, \"MIN\": 0.0,  \"MAX\": 1.5  },\n    { \"NAME\": \"nibWidth\",   \"TYPE\": \"float\", \"DEFAULT\": 1.0,  \"MIN\": 0.45, \"MAX\": 1.8  },\n    { \"NAME\": \"bleed\",      \"TYPE\": \"float\", \"DEFAULT\": 1.0,  \"MIN\": 0.0,  \"MAX\": 2.5  },\n    { \"NAME\": \"dryTime\",    \"TYPE\": \"float\", \"DEFAULT\": 1.0,  \"MIN\": 0.15, \"MAX\": 4.0  },\n    { \"NAME\": \"inkColor\",   \"TYPE\": \"color\", \"DEFAULT\": [0.115, 0.088, 0.070, 1.0] },\n    { \"NAME\": \"paperTone\",  \"TYPE\": \"color\", \"DEFAULT\": [0.895, 0.845, 0.735, 1.0] },\n    { \"NAME\": \"showQuill\",  \"TYPE\": \"bool\",  \"DEFAULT\": true }\n  ]\n}*/\n\n#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\n\n#define PI 3.14159265359\n\n// ------------------------------------------------------------------ hashes\n// sin-free (Dave Hoskins style) so they stay stable in mediump hosts too\nfloat h11(float p){ p = fract(p*0.1031); p *= p + 33.33; p *= p + p; return fract(p); }\nfloat h12(vec2 p){ vec3 p3 = fract(p.xyx*0.1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y)*p3.z); }\nvec2  h21(float p){ vec3 p3 = fract(vec3(p)*vec3(0.1031,0.1030,0.0973)); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.xx + p3.yz)*p3.zy); }\nvec4  h41(float p){ vec4 p4 = fract(vec4(p)*vec4(0.1031,0.1030,0.0973,0.1099)); p4 += dot(p4, p4.wzxy + 33.33); return fract((p4.xxyz + p4.yzzw)*p4.zywx); }\n\nfloat vnz(vec2 p){\n  vec2 i = floor(p), f = fract(p);\n  vec2 u = f*f*(3.0 - 2.0*f);\n  float a = h12(i);\n  float b = h12(i + vec2(1.0, 0.0));\n  float c = h12(i + vec2(0.0, 1.0));\n  float d = h12(i + vec2(1.0, 1.0));\n  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\nfloat fbm3(vec2 p){\n  float s = 0.0, a = 0.5;\n  for (int i = 0; i < 3; i++){ s += a*vnz(p); p = p*2.07 + 19.3; a *= 0.5; }\n  return s;\n}\n\n// Straight-segment distance. Only the quill uses this now; the glyph strokes\n// use the closed-form curve below.\nfloat sdSeg(vec2 p, vec2 a, vec2 b, out float hh, out vec2 dv){\n  vec2 pa = p - a, ba = b - a;\n  hh = clamp(dot(pa, ba)/max(dot(ba, ba), 1e-7), 0.0, 1.0);\n  dv = pa - ba*hh;\n  return length(dv);\n}\n\n// A bowed pen stroke, in ONE evaluation.\n// A quadratic Bezier whose control point is offset perpendicular to its own\n// chord is EXACTLY the parabola v = 4h*u(1-u) in chord coordinates (the x\n// component telescopes to t*L), so there is no need to chop it into segments\n// at all: rotate into the chord frame and read the curve off directly. This\n// replaced a 3-segment polyline -- it is both smoother (a real curve, no\n// joints) and a third of the compiled code, which is what got this shader\n// under the 600ms compile budget. `bow` is the mid-stroke offset as a\n// fraction of chord length. Distance is the linearised perpendicular, exact\n// to first order and only ever used within a stroke width of the curve.\nfloat sdBow(vec2 p, vec2 pa, vec2 pb, float bow, out float uu, out vec2 tg, out vec2 nvec){\n  vec2 ab = pb - pa;\n  float L = max(length(ab), 1e-5);\n  vec2 ex = ab/L, ey = vec2(-ex.y, ex.x);\n  vec2 rel = p - pa;\n  float u = dot(rel, ex)/L;\n  uu = clamp(u, 0.0, 1.0);\n  float sl = 4.0*bow*(1.0 - 2.0*uu);              // d(v)/d(x) along the arc\n  tg = normalize(ex + ey*sl);                     // continuous tangent, no joints\n  float dp = (dot(rel, ey) - 4.0*bow*L*uu*(1.0 - uu))/sqrt(1.0 + sl*sl);\n  nvec = ey*dp;\n  if (u < 0.0){ nvec = rel; return length(rel); }\n  if (u > 1.0){ vec2 e = p - pb; nvec = e; return length(e); }\n  return abs(dp);\n}\n\n// -------------------------------------------------------- layout (globals)\n// page-isotropic coords: x in [0,gPA], y DOWN from the top edge in [0,1]\nfloat gPA, gPX;\nfloat gALPH;            // alphabet seed  (constant across pages: same script)\nfloat gTXT;             // text seed      (per page: different words)\nfloat gNG;              // alphabet size\nfloat gML, gMTOP, gLH, gXH, gADV, gCOLS, gNR;\nfloat gGL, gGTOP, gGLH, gGXH, gGADV, gGCOLS, gGROWS, gGSTART;\nfloat gProgM;           // main-block progress, in glyph cells\nvec2  gNIB;             // nib edge direction\n\nvoid setLayout(float PA){\n  gPA    = PA;\n  gMTOP  = 0.095;\n  gNR    = 9.0;\n  gLH    = (1.0 - 0.095 - 0.150)/gNR;\n  gXH    = 0.33*gLH;\n  gADV   = 1.15*gXH;\n  gML    = 0.115*PA;\n  gCOLS  = max(4.0, floor((0.705*PA - gML)/gADV));\n\n  gGL     = 0.735*PA;\n  gGXH    = 0.62*gXH;\n  gGADV   = 1.15*gGXH;\n  gGCOLS  = max(3.0, floor((0.955*PA - gGL)/gGADV));\n  gGLH    = 0.55*gLH;\n  gGROWS  = 6.0;\n  gGTOP   = gMTOP + 2.4*gLH;\n  gGSTART = 0.45*gNR*gCOLS;\n\n  gNG  = 17.0 + floor(h11(gALPH*1.37 + 3.0)*13.0);  // 17..29 letters\n  gNIB = vec2(cos(nibAngle), sin(nibAngle));\n}\n\n// ------------------------------------------------------------------- text\n// TWO glyph placements per pixel: this cell's glyph and the rubricated initial,\n// in whichever block (main text or margin gloss) the pixel sits in. Both run\n// the SAME stroke loop. No hand-written per-glyph draw calls anywhere.\nfloat textLayer(vec2 ip, out float wetOut, out vec3 tintOut, out vec2 dvOut){\n  float ink = 0.0;\n  wetOut = 0.0; tintOut = inkColor.rgb; dvOut = vec2(0.0, 1.0);\n\n  // fibre field: the ink feathers along it\n  float fibre = vnz(ip*300.0);\n\n  float isG = step(gGL, ip.x);\n\n  float bLeft = mix(gML,   gGL,    isG);\n  float bTop  = mix(gMTOP, gGTOP,  isG);\n  float bLH   = mix(gLH,   gGLH,   isG);\n  float bXH   = mix(gXH,   gGXH,   isG);\n  float bADV  = mix(gADV,  gGADV,  isG);\n  float bCOL  = mix(gCOLS, gGCOLS, isG);\n  float bROW  = mix(gNR,   gGROWS, isG);\n  float bSlant = mix(0.13, 0.26, isG);\n  float bHand  = hand*mix(1.0, 1.75, isG);\n  float bSalt  = gTXT + isG*137.31;\n  vec3  bTint  = mix(inkColor.rgb, mix(inkColor.rgb, vec3(0.40, 0.25, 0.13), 0.62), isG);\n  float bProg  = mix(gProgM, (gProgM - gGSTART)*1.7, isG);\n\n  float rr = floor((ip.y - bTop - 0.3*bLH)/bLH);\n  float baseY = bTop + (rr + 1.0)*bLH;\n  // the hand does not rule perfectly: each line drifts and tilts a little\n  baseY += bXH*bHand*0.10*(h11(rr*7.13 + bSalt) - 0.5);\n  float tilt = (h11(rr*3.71 + bSalt + 61.0) - 0.5)*bXH*bHand*0.16;\n\n  float yRel = ip.y - baseY;\n  float xg   = ip.x + bSlant*yRel;\n  float c0   = floor((xg - bLeft)/bADV);\n\n  float aaW = gPX/bXH*0.7;\n  float soft = 0.010 + 0.022*bleed;\n\n  // TWO placements per pixel: this cell's glyph, and the rubricated initial.\n  // The lattice is inset and the advance widened just enough that a glyph --\n  // jitter, nib width and all -- never crosses its own cell edge, so no\n  // neighbouring cell can ever contribute here. Every extra placement unrolls\n  // a whole stroke loop, and the text layer is ~70% of this shader's compile\n  // time, so this is the difference between hover-to-play and a still poster.\n  for (int k = 0; k < 2; k++){\n    float gid, rev, wetH, cellSeed, jScale;\n    vec2 q;\n    vec3 tint;\n\n    if (k < 1){\n      float cc = c0;\n      if (cc < 0.0 || cc >= bCOL || rr < 0.0 || rr >= bROW) continue;\n      // ragged right: each line stops at a slightly different column\n      if (cc >= bCOL - floor(h11(rr*13.77 + bSalt + 21.0)*3.4)) continue;\n      // the raised initial owns the first two cells of the first line\n      if (isG < 0.5 && rr < 0.5 && cc < 3.9) continue;\n\n      float idx = rr*bCOL + cc;\n      // word statistics: a space needs the two cells before it to be letters,\n      // so word length is geometric with a minimum of two. Lines never start\n      // with a space.\n      float s0 = step(0.71, h11( idx        *1.913 + bSalt + 5.0));\n      float s1 = step(0.71, h11((idx - 1.0)*1.913 + bSalt + 5.0));\n      float s2 = step(0.71, h11((idx - 2.0)*1.913 + bSalt + 5.0));\n      if (s0*(1.0 - s1)*(1.0 - s2)*step(0.5, cc) > 0.5) continue;\n\n      rev = clamp(bProg - idx, 0.0, 1.0);\n      if (rev <= 0.0) continue;\n      wetH = exp(-max(bProg - idx - 1.0, 0.0)/(12.0*dryTime));\n\n      // letter frequency: pow() biases hard toward the low glyph ids, so a\n      // handful of letters carry most of the page, as in a real language.\n      gid = floor(pow(h11(idx*2.7183 + bSalt + 11.0), 1.9)*gNG);\n\n      float bx = bLeft + cc*bADV;\n      q = vec2(xg - bx, baseY + tilt*(cc/bCOL - 0.5)*2.0 - ip.y)/bXH;\n      cellSeed = idx*2.917 + bSalt;\n      jScale = bHand;\n      tint = bTint;\n    } else {\n      if (isG > 0.5) continue;\n      float iXH = gXH*3.15;\n      float iBase = gMTOP + gLH;\n      q = vec2(ip.x - gML - 0.004, iBase - ip.y)/iXH;\n      // cheap reject first, so the candidate search below costs nothing for\n      // the 98% of the page that is nowhere near the initial\n      if (q.x < -0.5 || q.x > 1.5 || q.y < -1.1 || q.y > 2.2) continue;\n      // a versal wants a letter with some body to it: walk the alphabet until\n      // one with three or more strokes turns up\n      gid = floor(h11(gALPH*5.11 + 2.0)*gNG);\n      for (int i = 0; i < 5; i++){\n        if (h41(gid*17.13 + gALPH*3.1 + 0.5).x > 0.62) break;\n        gid = mod(gid + 7.0, gNG);\n      }\n      rev = clamp(gProgM*0.333 + 1.0, 0.0, 1.0);\n      if (rev <= 0.0) continue;\n      wetH = exp(-max(gProgM, 0.0)/(9.0*dryTime));\n      cellSeed = gTXT + 909.0;\n      jScale = hand*0.50;\n      tint = vec3(0.615, 0.148, 0.082);\n      bXH = iXH;\n      aaW = gPX/iXH*0.7;\n    }\n\n    // per-instance hand: this occurrence of the letter is not the last one\n    vec4 cj = h41(cellSeed + 0.5);\n    q.x -= (cj.x - 0.5)*0.07*jScale;\n    q.y -= (cj.y - 0.5)*0.05*jScale;\n    float ang = (cj.z - 0.5)*0.11*jScale;\n    float cs = cos(ang), sn = sin(ang);\n    q = mat2(cs, sn, -sn, cs)*(q - vec2(0.4, 0.45)) + vec2(0.4, 0.45);\n    q /= 1.0 + (cj.w - 0.5)*0.10*jScale;\n\n    if (q.x < -0.06 || q.x > 1.32 || q.y < -0.85 || q.y > 1.95) continue;\n\n    // ---- the alphabet. Derived from the seed, identical for every\n    // occurrence of this glyph id anywhere on any page.\n    vec4 gh = h41(gid*17.13 + gALPH*3.1 + 0.5);\n    float ns     = 2.0 + floor(gh.x*1.99);                 // 2..3 strokes (3 -> 9 unrolled bodies)\n    float stemX  = mix(0.22, 0.58, step(0.5, gh.y));\n    float asc    = step(0.70, gh.z);\n    float desc   = step(0.74, gh.w)*(1.0 - asc);\n    float roundG = step(0.66, fract(gh.x*7.31));           // bowl letter\n\n    for (int s = 0; s < 3; s++){\n      float fs = float(s);\n      if (fs >= ns) break;\n\n      // ONE hash per stroke. The two extra values the glyph needs are scrambled\n      // out of the same vector -- a fract of a large multiple is what a hash\n      // does anyway, and a second and third real hash here were costing more\n      // inlined code than the whole curve evaluation.\n      vec4 r  = h41(gid*17.13 + fs*5.77 + gALPH*3.1 + 40.0);\n      vec2 r2 = fract(vec2(r.y*43.758 + r.z*19.317, r.w*27.191 + r.x*11.733));\n      vec4 jt = fract(cj*vec4(37.13, 53.79, 29.31, 41.97) + fs*vec4(0.317, 0.713, 0.521, 0.911));\n\n      vec2 pa, pb;\n      float bow, dot1 = 0.0;\n\n      if (s == 0){\n        // stroke 0 is always the spine: a stem from the descender line to the\n        // ascender line, or, for a bowl letter, a deep single curve. This is\n        // what gives the page one rhythm instead of nine.\n        pa = vec2(stemX, -0.42*desc);\n        pb = vec2(stemX, 1.0 + 0.45*asc);\n        bow = mix((r.x - 0.5)*0.20, (step(0.5, r.x)*2.0 - 1.0)*0.55, roundG);\n      } else {\n        float att = step(r.x, 0.60);                       // hang off the stem\n        float ay = floor(r.y*3.999)*0.5;                   // baseline/waist/xheight/ascender\n        float ax = mix(0.20 + 0.76*floor(r.z*2.999)*0.5, stemX, att);\n        float bx = 0.20 + 0.76*floor(r.w*2.999)*0.5;\n        float by = floor(r2.x*3.999)*0.5;\n        dot1 = step(abs(bx - ax) + abs(by - ay), 0.04);    // degenerate -> diacritic\n        ax = mix(ax, stemX + 0.20*(step(0.5, r2.y)*2.0 - 1.0), dot1);\n        ay = mix(ay, 1.30, dot1);\n        bx = mix(bx, ax, dot1);\n        by = mix(by, ay, dot1);\n        pa = vec2(ax, ay);\n        pb = vec2(bx, by);\n        bow = (floor(r2.y*2.999) - 1.0)*0.24*(1.0 - dot1);\n      }\n\n      pa += (jt.xy - 0.5)*0.10*jScale;\n      pb += (jt.zw - 0.5)*0.10*jScale;\n\n      float t; vec2 tg, dv;\n      float d = sdBow(q, pa, pb, bow, t, tg, dv);\n\n      // broad-nib weight: width is |stroke x nib-edge|, so downstrokes are fat\n      // and strokes parallel to the nib are hairlines\n      float cr = abs(tg.x*gNIB.y - tg.y*gNIB.x);\n      float hw = mix(0.018, 0.086, cr)*nibWidth;\n      hw = mix(hw, 0.050*nibWidth, dot1);\n      // the nib pools a little where it lands and where it lifts\n      hw *= 1.0 + 0.14*smoothstep(0.22, 0.0, t) + 0.20*smoothstep(0.78, 1.0, t);\n\n      float gp = (fs + t)/ns;                              // 0..1 through glyph\n      float m = 1.0 - smoothstep(rev - 0.015, rev + 0.015, gp);\n      if (m <= 0.001) continue;\n\n      float dq = d - bleed*0.26*hw*(fibre - 0.5);          // feather into fibre\n      float k1 = smoothstep(hw + soft, hw - aaW, dq);\n      float k2 = smoothstep(hw*2.1, hw*0.90, dq)*0.16;     // halo in the paper\n      float kk = max(k1, k2)*m;\n      if (kk > ink){ ink = kk; wetOut = wetH; tintOut = tint; dvOut = dv; }\n    }\n  }\n  return ink;\n}\n\n// ------------------------------------------------------------------- sheet\nvec3 sheet(vec2 ip, float inkScale){\n  // ---- laid paper\n  float blot  = vnz(ip*6.0);\n  float fine  = vnz(ip*210.0);\n  float laid  = 0.5 + 0.5*sin(ip.y*PI*2.0*76.0);\n  // chain lines are NARROW and widely spaced; a plain sine gave broad bands\n  float chain = smoothstep(0.040, 0.004, abs(fract(ip.x/max(gPA, 0.2)*7.0) - 0.5));\n\n  vec3 paper = paperTone.rgb;\n  paper *= 1.0 + 0.045*(blot - 0.5)*2.0;\n  paper *= 1.0 + 0.030*(laid - 0.5)*2.0 + 0.030*chain;\n  paper *= 1.0 + 0.017*(fine - 0.5)*2.0;\n\n  float edgeD = min(min(ip.x, gPA - ip.x), min(ip.y, 1.0 - ip.y));\n  paper *= 1.0 - 0.055*smoothstep(0.10, 0.0, edgeD);\n  paper = mix(paper, paper*vec3(1.02, 0.975, 0.90), smoothstep(0.20, 0.0, edgeD));\n\n  // ---- ruling: hard-point lines, bounding verticals, prick marks\n  float rn  = floor((ip.y - gMTOP)/gLH + 0.5);\n  float rby = gMTOP + rn*gLH;\n  float rok = step(1.0, rn)*step(rn, gNR);\n  float txR = gML + gCOLS*gADV;\n  float inTB = step(gML - 0.014, ip.x)*step(ip.x, txR + 0.014);\n  paper *= 1.0 - 0.092*smoothstep(gPX*1.7, gPX*0.3, abs(ip.y - rby))*rok*inTB;\n\n  float inV = step(gMTOP - 0.05, ip.y)*step(ip.y, gMTOP + gNR*gLH + 0.035);\n  float vv = smoothstep(gPX*1.5, gPX*0.2, abs(ip.x - (gML - 0.013)))\n           + smoothstep(gPX*1.5, gPX*0.2, abs(ip.x - (txR + 0.013)));\n  paper *= 1.0 - 0.105*vv*inV;\n\n  float pd = min(distance(ip, vec2(0.046*gPA, rby)), distance(ip, vec2(gPA - 0.046*gPA, rby)));\n  paper *= 1.0 - 0.46*smoothstep(0.0060, 0.0020, pd)*rok;\n\n  // ---- ink\n  float wet; vec3 tint; vec2 dv;\n  float ink = textLayer(ip, wet, tint, dv)*inkScale;\n\n  // Wet ink is nearly black and glossy; as it dries it sinks into the sheet\n  // and lightens to a warm brown. Making DRY lighter (rather than WET darker)\n  // is what makes the gradient down the page actually visible -- two shades of\n  // near-black read as one colour.\n  vec3 dryC = mix(tint, vec3(0.46, 0.33, 0.21), 0.34);\n  vec3 inkC = mix(dryC, tint*0.45, wet);\n  vec3 col = paper*mix(vec3(1.0), inkC, ink);\n\n  // ---- raking daylight from the upper left\n  vec2 L = normalize(vec2(-0.62, 0.78));               // ip.y is down, so this is up-left\n  float relief = vnz((ip + L*0.0030)*210.0) - fine;\n  col *= 1.0 + 0.42*relief;\n  col *= 1.0 - 0.155*smoothstep(0.0, 1.5, (ip.x/max(gPA, 0.2))*0.75 + ip.y*0.85);\n  col *= 1.0 - 0.085*smoothstep(0.075, 0.0, ip.x/max(gPA, 0.2));  // gutter\n\n  // wet ink still has a meniscus: a specular sliver on the light side\n  float sheen = wet*smoothstep(0.30, 0.95, ink)*max(0.0, dot(normalize(dv + vec2(1e-5)), L));\n  col += vec3(1.0, 0.96, 0.88)*sheen*0.30*inkScale;\n\n  return col;\n}\n\n// -------------------------------------------------------------------- main\nvoid main(){\n  vec2 fc = gl_FragCoord.xy/RENDERSIZE;\n  float AR = RENDERSIZE.x/RENDERSIZE.y;\n  float iy = 0.040, ix = 0.040/AR;\n  vec2 pg = vec2((fc.x - ix)/(1.0 - 2.0*ix), (fc.y - iy)/(1.0 - 2.0*iy));\n\n  gALPH = floor(scriptSeed)*13.77 + 5.0;\n  setLayout((AR - 2.0*iy)/(1.0 - 2.0*iy));\n  gPX = 1.0/max(RENDERSIZE.y*(1.0 - 2.0*iy), 8.0);\n\n  // ---- the arc: fill, hold, turn\n  float cells = 3.0 + gNR*gCOLS;\n  float rate  = max(writeSpeed, 0.5);\n  float Tref  = cells/rate;\n  float warm  = 0.6;                                    // the hand speeds up\n  float fillT = Tref*(sqrt(1.0 + 2.0*warm) - 1.0)/warm;\n  float holdT = 2.6, turnT = 1.7;\n  float cyc   = fillT + holdT + turnT;\n  float pIdx  = floor(TIME/cyc);\n  float tl    = TIME - pIdx*cyc;\n  float tw    = min(tl, fillT);\n  float prog  = rate*tw*(1.0 + warm*tw/(2.0*Tref));\n\n  // The leaf is bound at the left. Turning it rotates the whole sheet about\n  // the spine, so on screen the written page FORESHORTENS toward the gutter\n  // and swings out of frame, uncovering the fresh page. (A mirror-fold verso\n  // was tried first and read as three flat vertical panels.)\n  float tt = clamp((tl - fillT - holdT)/turnT, 0.0, 1.0);\n  float turning = step(0.0001, tt)*step(tt, 0.9999);\n  float cw = cos(tt*tt*(3.0 - 2.0*tt)*PI*0.5);          // projected half-width, 1 -> 0\n  float onLeaf = step(pg.x, cw);\n  float u = clamp(pg.x/max(cw, 1e-3), 0.0, 1.0);\n\n  float isNew = (1.0 - onLeaf)*turning;\n  float sx = mix(pg.x, u*(0.82 + 0.18*u), turning*onLeaf);\n\n  gTXT   = (pIdx + isNew)*77.71 + 3.0;\n  gProgM = mix(prog, 0.0, isNew) - 3.0;\n\n  vec2 ip = vec2(sx*gPA, 1.0 - pg.y);\n\n  vec3 col;\n  bool onPage = (sx > -0.001 && sx < 1.001 && pg.y > -0.001 && pg.y < 1.001);\n  if (onPage){\n    col = sheet(ip, 1.0);\n    if (turning > 0.5 && onLeaf > 0.5){\n      col *= 1.0 + 0.30*sin(u*PI)*tt;                    // the sheet bows as it lifts\n      col *= 1.0 - 0.34*smoothstep(0.11, 0.0, u)*tt;     // deepening gutter shadow\n      col += 0.13*smoothstep(0.94, 1.0, u)*tt;           // light along the free edge\n    }\n    if (isNew > 0.5) col *= 1.0 - 0.55*exp(-(pg.x - cw)*20.0);\n  } else {\n    // desk\n    vec3 dk = vec3(0.175, 0.128, 0.090);\n    dk *= 0.82 + 0.36*vnz(fc*vec2(5.0, 70.0));\n    vec2 od = max(vec2(0.0), max(-(pg - vec2(0.007, -0.010)), (pg - vec2(0.007, -0.010)) - vec2(1.0)));\n    dk *= mix(1.0, 0.28, exp(-length(od)*40.0));\n    col = dk;\n  }\n\n  // ---- the quill\n  if (showQuill && turning < 0.5 && onPage && isNew < 0.5){\n    float pc = clamp(gProgM, 0.0, gNR*gCOLS - 0.001);\n    float qr = floor(pc/gCOLS);\n    float qc = pc - qr*gCOLS;\n    // Only the nib and the barrel: the feather is out of shot above the frame.\n    // A whole drawn quill sat on the text like a graphic and read as a blade.\n    vec2 tip = vec2(gML + (qc + 0.30)*gADV, gMTOP + (qr + 1.0)*gLH + gXH*0.06);\n    vec2 qe = tip + normalize(vec2(0.78, -1.0))*1.6;   // runs off the top edge\n\n    for (int q2 = 0; q2 < 2; q2++){\n      vec2 o = (q2 == 0) ? vec2(0.016, 0.013) : vec2(0.0);\n      float hh; vec2 dvq;\n      float d = sdSeg(ip - o, tip, qe, hh, dvq);\n      float sw = 0.0023 + 0.0051*smoothstep(0.0, 0.055, hh);   // nib tapers into barrel\n      float bl2 = (q2 == 0) ? 0.013 : gPX*1.0;\n      float body = smoothstep(sw + bl2, sw - bl2*0.25, d);\n      if (q2 == 0){\n        col *= 1.0 - 0.30*body;\n      } else {\n        float lit = smoothstep(-sw*0.9, sw*0.9, dot(dvq, vec2(-0.70, -0.71)));\n        col = mix(col, mix(vec3(0.085, 0.062, 0.045), vec3(0.40, 0.33, 0.24), lit), body);\n      }\n    }\n  }\n\n  col *= 1.0 + 0.020*(h12(gl_FragCoord.xy) - 0.5)*2.0;   // paper grain, loop-safe\n  col *= 1.0 - 0.11*pow(length(fc - 0.5)*1.35, 2.4);     // lens falloff\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
    }
  }
}