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