PageSourceSearch

https://flowcel-56320.web.app/assets/treatments-QP0flU3m.js

js flowcel-56320.web.app collected 2026-10-03 12:25:20 UTC 21,089 bytes, 588 lines download raw bytes

1const e=`/**
2 * THE TREATMENTS — five display passes over \`pictureField.ts\`'s one rig.
3 *
4 * Each entry is a fragment shader, its config, its knob schema and its
5 * uniform upload — the engine mounts whichever one the catalogue names. The
6 * split is the same as fluid.ts / films.ts next door, and for the same
7 * reason: five components for roughly the price of one, and a sixth is a
8 * shader, not an afternoon of plumbing.
9 *
10 * Shared vocabulary (uploaded by the engine, identical in every pass):
11 *   uTex     the photo, Y-flipped so its uv is GL-up like vUv
12 *   uCover   object-fit: cover scale pair — \`pic()\` below applies it
13 *   uHand    raw pointer, frame uv, GL-up
14 *   uF1..3   eased followers, fast → slow, taus chosen per treatment
15 *   uRes/uDpr  device-pixel geometry, for anything that must be DPR-stable
16 *
17 * Individual provenance notes sit on each pass. None of these is a port:
18 * the dither matrix is the standard published Bayer construction, the CMYK
19 * separation is the naive textbook one, and the rest is this file's own
20 * arithmetic.
21 */
22import type { Param, ParamValue } from "../params";
23import { hexToRgb } from "../params";
24
25export type TreatmentName = "grade" | "dither" | "tear" | "chroma" | "rosette";
26
27type UniformSetter = (
28  gl: WebGL2RenderingContext,
29  U: (name: string) => WebGLUniformLocation | null,
30  P: Record<string, ParamValue>,
31) => void;
32
33export type Treatment = {
34  frag: string;
35  defaults: Record<string, ParamValue>;
36  /** Follower time constants, seconds — fast, mid, slow. 0 pins to the hand. */
37  taus: [number, number, number];
38  /** Where reduced motion parks the clock. */
39  reduceT: number;
40  params(P: Record<string, ParamValue>): Param[];
41  upload: UniformSetter;
42};
43
44/* ── shared GLSL ──────────────────────────────────────────────────────────── */
45
46const HEAD = [
47  "#version 300 es",
48  "precision highp float;",
49  "in vec2 vUv;",
50  "out vec4 outColor;",
51  "uniform sampler2D uTex;",
52  "uniform float uTime, uDpr, uSeen;",
53  "uniform vec2 uRes, uCover, uHand, uF1, uF2, uF3;",
54  /* cover-fit lookup — the one line every pass shares */
55  "vec2 cov(vec2 uv){ return 0.5 + (uv - 0.5) * uCover; }",
56  "vec3 pic(vec2 uv){ return texture(uTex, cov(uv)).rgb; }",
57  "float luma(vec3 c){ return dot(c, vec3(0.2126, 0.7152, 0.0722)); }",
58];
59
60const uc = (gl: WebGL2RenderingContext, loc: WebGLUniformLocation | null, hex: string) => {
61  const [r, g, b] = hexToRgb(String(hex));
62  gl.uniform3f(loc, r, g, b);
63};
64
65/* ══ 1 · GRADE — a real gradient map, and a hand that returns the truth ════ */
66
67const GRADE_FRAG = [
68  ...HEAD,
69  "uniform vec3 uShadow, uMid, uHigh;",
70  "uniform float uThree, uGamma, uReveal, uSoft, uGrain;",
71  "float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }",
72  "void main(){",
73  "  vec3 src = pic(vUv);",
74  "  float l = pow(luma(src), uGamma);",
75  /* The map: two stops, or three with the mid engaged. The mixes are done in
76     the working space directly — a gradient map is a LOOKUP, and looking up
77     through a smoothstep per span is what keeps the transitions from banding
78     the way a single mix over 0..1 does. */
79  "  vec3 lo = mix(uShadow, uThree > 0.5 ? uMid : uHigh, smoothstep(0.0, uThree > 0.5 ? 0.55 : 1.0, l));",
80  "  vec3 mapped = uThree > 0.5 ? mix(lo, uHigh, smoothstep(0.45, 1.0, l)) : lo;",
81  /* Print grain, in device px, keyed to the mapped tone's midband so it reads
82     as paper, not as sensor noise. */
83  "  float gr = (hash(floor(gl_FragCoord.xy / uDpr)) - 0.5) * uGrain * (0.5 - abs(l - 0.5));",
84  "  mapped += gr;",
85  /* THE REVEAL: inside the lagged circle the true photograph returns. The
86     radius is in frame heights and the distance is aspect-corrected, so the
87     window is a circle on any stage. uSeen gates it — before the first
88     pointer event there is no hole parked in the middle of the frame. */
89  "  float aspect = uRes.x / max(uRes.y, 1.0);",
90  "  float d = length((vUv - uF2) * vec2(aspect, 1.0));",
91  "  float win = uSeen * (1.0 - smoothstep(uReveal * (1.0 - uSoft), uReveal, d));",
92  "  outColor = vec4(mix(mapped, src, uReveal <= 0.001 ? 0.0 : win), 1.0);",
93  "}",
94].join("\\n");
95
96const grade: Treatment = {
97  frag: GRADE_FRAG,
98  defaults: {
99    shadow: "#101b3c",
100    mid: "#b3472f",
101    high: "#f2ead8",
102    three: false,
103    gamma: 1.0,
104    reveal: 0.28,
105    soft: 0.45,
106    grain: 0.06,
107  },
108  taus: [0.05, 0.16, 0.4],
109  reduceT: 2,
110  params: (P) => [
111    { key: "shadow", label: "Shadow ink", kind: "color", value: String(P.shadow) },
112    { key: "high", label: "Highlight", kind: "color", value: String(P.high) },
113    {
114      key: "three",
115      label: "Third stop",
116      kind: "toggle",
117      value: Boolean(P.three),
118      hint: "Adds the mid ink — duotone becomes tritone.",
119    },
120    { key: "mid", label: "Mid ink", kind: "color", value: String(P.mid) },
121    {
122      key: "gamma",
123      label: "Tone",
124      kind: "range",
125      min: 0.4,
126      max: 2.4,
127      step: 0.05,
128      value: Number(P.gamma),
129      hint: "Where the map's pivot sits. Low lifts the shadows into the mid ink.",
130    },
131    {
132      group: "Reveal",
133      key: "reveal",
134      label: "Window",
135      kind: "range",
136      min: 0,
137      max: 0.7,
138      step: 0.01,
139      value: Number(P.reveal),
140      hint: "The lagged circle where the true photograph returns. 0 turns it off.",
141    },
142    {
143      group: "Reveal",
144      key: "soft",
145      label: "Feather",
146      kind: "range",
147      min: 0.05,
148      max: 0.9,
149      step: 0.01,
150      value: Number(P.soft),
151    },
152    {
153      group: "Reveal",
154      key: "grain",
155      label: "Grain",
156      kind: "range",
157      min: 0,
158      max: 0.25,
159      step: 0.01,
160      value: Number(P.grain),
161    },
162  ],
163  upload: (gl, U, P) => {
164    uc(gl, U("uShadow"), String(P.shadow));
165    uc(gl, U("uMid"), String(P.mid));
166    uc(gl, U("uHigh"), String(P.high));
167    gl.uniform1f(U("uThree"), P.three ? 1 : 0);
168    gl.uniform1f(U("uGamma"), Number(P.gamma));
169    gl.uniform1f(U("uReveal"), Number(P.reveal));
170    gl.uniform1f(U("uSoft"), Number(P.soft));
171    gl.uniform1f(U("uGrain"), Number(P.grain));
172  },
173};
174
175/* ══ 2 · DITHER — Bayer, done right ════════════════════════════════════════
176   The matrix is the standard recursive construction, written out at order 8;
177   thresholds are (b + 0.5)/64 so the average error is zero (the +0.5 is the
178   half everyone forgets, and without it the whole picture darkens by half a
179   level). Cells are DEVICE pixels divided by uDpr·scale, so a "2px" dot is
180   the same physical size on every screen — an ordered dither whose cell size
181   floats with DPR reads as two different treatments on two laptops. */
182
183const DITHER_FRAG = [
184  ...HEAD,
185  "uniform float uScale, uLevels, uContrast, uMono;",
186  "uniform vec3 uInk, uPaper;",
187  "const int B8[64] = int[64](",
188  "   0,32, 8,40, 2,34,10,42,",
189  "  48,16,56,24,50,18,58,26,",
190  "  12,44, 4,36,14,46, 6,38,",
191  "  60,28,52,20,62,30,54,22,",
192  "   3,35,11,43, 1,33, 9,41,",
193  "  51,19,59,27,49,17,57,25,",
194  "  15,47, 7,39,13,45, 5,37,",
195  "  63,31,55,23,61,29,53,21);",
196  "void main(){",
197  "  vec2 cell = floor(gl_FragCoord.xy / (uScale * uDpr));",
198  "  ivec2 m = ivec2(mod(cell, 8.0));",
199  "  float th = (float(B8[m.y * 8 + m.x]) + 0.5) / 64.0;",
200  "  vec3 src = pic(vUv);",
201  "  src = (src - 0.5) * uContrast + 0.5;",
202  "  float L = max(uLevels - 1.0, 1.0);",
203  "  if (uMono > 0.5) {",
204  /* Newspaper mode: one channel, two inks. */
205  "    float v = clamp(luma(src), 0.0, 1.0);",
206  "    float q = floor(v * L + th) / L;",
207  "    outColor = vec4(mix(uInk, uPaper, q), 1.0);",
208  "  } else {",
209  /* Colour mode: each channel quantised against the same threshold — the
210     classic RGB ordered dither, banding traded for the pattern. */
211  "    vec3 q = floor(clamp(src, 0.0, 1.0) * L + th) / L;",
212  "    outColor = vec4(q, 1.0);",
213  "  }",
214  "}",
215].join("\\n");
216
217const dither: Treatment = {
218  frag: DITHER_FRAG,
219  defaults: {
220    scale: 2,
221    levels: 2,
222    contrast: 1.15,
223    mono: true,
224    ink: "#12141c",
225    paper: "#f4f1e9",
226  },
227  taus: [0, 0, 0],
228  reduceT: 0,
229  params: (P) => [
230    {
231      key: "scale",
232      label: "Dot",
233      kind: "range",
234      min: 1,
235      max: 8,
236      step: 1,
237      value: Number(P.scale),
238      suffix: "px",
239      hint: "Cell size in physical pixels — DPR-stable, so it prints the same on every screen.",
240    },
241    {
242      key: "levels",
243      label: "Levels",
244      kind: "range",
245      min: 2,
246      max: 6,
247      step: 1,
248      value: Number(P.levels),
249      hint: "Tones per channel before the pattern carries the rest. 2 is the woodcut.",
250    },
251    {
252      key: "contrast",
253      label: "Contrast",
254      kind: "range",
255      min: 0.5,
256      max: 2,
257      step: 0.05,
258      value: Number(P.contrast),
259    },
260    {
261      key: "mono",
262      label: "Two inks only",
263      kind: "toggle",
264      value: Boolean(P.mono),
265      hint: "On: luminance between Ink and Paper. Off: full-colour ordered dither.",
266    },
267    { group: "Ink", key: "ink", label: "Ink", kind: "color", value: String(P.ink) },
268    { group: "Ink", key: "paper", label: "Paper", kind: "color", value: String(P.paper) },
269  ],
270  upload: (gl, U, P) => {
271    gl.uniform1f(U("uScale"), Number(P.scale));
272    gl.uniform1f(U("uLevels"), Number(P.levels));
273    gl.uniform1f(U("uContrast"), Number(P.contrast));
274    gl.uniform1f(U("uMono"), P.mono ? 1 : 0);
275    uc(gl, U("uInk"), String(P.ink));
276    uc(gl, U("uPaper"), String(P.paper));
277  },
278};
279
280/* ══ 3 · TEAR — displacement gated by the image's own edges ═══════════════
281   A Sobel pass finds the picture's contours live; displacement near the hand
282   is MULTIPLIED by that edge magnitude, so flat areas hold still and the
283   image slides apart along its own seams — a tear, not a smear. The
284   direction blends along-edge (the picture shears along a contour) with
285   across-edge (the seam opens). PAID: this is the one whose display pass IS
286   the product — no free entry displaces anything. */
287
288const TEAR_FRAG = [
289  ...HEAD,
290  "uniform float uStrength, uReach, uAlong, uAmbient, uFringe;",
291  "float lum(vec2 uv){ return luma(texture(uTex, uv).rgb); }",
292  "void main(){",
293  "  vec2 cuv = cov(vUv);",
294  "  vec2 tx = 1.0 / vec2(textureSize(uTex, 0));",
295  /* Sobel over the PICTURE's texels, not the frame's — edge width follows the
296     photograph, so the tear is the same feature at any stage size. */
297  "  float tl = lum(cuv + vec2(-tx.x,  tx.y)), t = lum(cuv + vec2(0.0,  tx.y)), tr = lum(cuv + tx);",
298  "  float l  = lum(cuv + vec2(-tx.x, 0.0)),                     r  = lum(cuv + vec2(tx.x, 0.0));",
299  "  float bl = lum(cuv - tx), b = lum(cuv - vec2(0.0, tx.y)), br = lum(cuv + vec2(tx.x, -tx.y));",
300  "  float gx = (tr + 2.0*r + br) - (tl + 2.0*l + bl);",
301  "  float gy = (tl + 2.0*t + tr) - (bl + 2.0*b + br);",
302  "  vec2 grad = vec2(gx, gy);",
303  "  float e = smoothstep(0.06, 0.5, length(grad));",
304  "  vec2 nrm = length(grad) > 1e-5 ? normalize(grad) : vec2(0.0);",
305  "  vec2 alongDir = vec2(nrm.y, -nrm.x);",
306  /* Drive: proximity to the fast follower, plus a slow ambient breath so the
307     card is alive with no pointer. The breath phases by position, so the
308     whole image never pumps in sync. */
309  "  float aspect = uRes.x / max(uRes.y, 1.0);",
310  "  float d = length((vUv - uF1) * vec2(aspect, 1.0));",
311  "  float near = uSeen * (1.0 - smoothstep(0.0, uReach, d));",
312  "  float breath = uAmbient * (0.5 + 0.5 * sin(uTime * 0.8 + (cuv.x + cuv.y) * 9.0));",
313  "  float amt = e * (near * uStrength + breath * 0.012);",
314  "  vec2 dir = mix(nrm, alongDir, uAlong);",
315  "  vec2 off = dir * amt;",
316  /* The razor fringe: each channel crosses the seam at a slightly different
317     width. Signed, so it can darken as well as lighten. */
318  "  float fr = uFringe * amt;",
319  "  vec3 col;",
320  "  col.r = texture(uTex, cuv + off * (1.0 + fr * 12.0)).r;",
321  "  col.g = texture(uTex, cuv + off).g;",
322  "  col.b = texture(uTex, cuv + off * (1.0 - fr * 12.0)).b;",
323  "  outColor = vec4(col, 1.0);",
324  "}",
325].join("\\n");
326
327const tear: Treatment = {
328  frag: TEAR_FRAG,
329  defaults: { strength: 0.05, reach: 0.34, along: 0.65, ambient: 0.55, fringe: 0.35 },
330  taus: [0.09, 0.25, 0.5],
331  reduceT: 1.2,
332  params: (P) => [
333    {
334      key: "strength",
335      label: "Tear",
336      kind: "range",
337      min: 0,
338      max: 0.14,
339      step: 0.002,
340      value: Number(P.strength),
341      hint: "How far the picture slides along its seams under the hand.",
342    },
343    {
344      key: "reach",
345      label: "Reach",
346      kind: "range",
347      min: 0.1,
348      max: 0.8,
349      step: 0.01,
350      value: Number(P.reach),
351    },
352    {
353      key: "along",
354      label: "Shear ↔ open",
355      kind: "range",
356      min: 0,
357      max: 1,
358      step: 0.02,
359      value: Number(P.along),
360      hint: "0 opens the seam apart; 1 shears the picture along it.",
361    },
362    {
363      key: "ambient",
364      label: "Breath",
365      kind: "range",
366      min: 0,
367      max: 1.5,
368      step: 0.05,
369      value: Number(P.ambient),
370      hint: "A slow edge shimmer with no pointer — what a touch screen sees.",
371    },
372    {
373      key: "fringe",
374      label: "Fringe",
375      kind: "range",
376      min: 0,
377      max: 1,
378      step: 0.02,
379      value: Number(P.fringe),
380      hint: "Channel split across the seam. Signed — it darkens as well as lightens.",
381    },
382  ],
383  upload: (gl, U, P) => {
384    gl.uniform1f(U("uStrength"), Number(P.strength));
385    gl.uniform1f(U("uReach"), Number(P.reach));
386    gl.uniform1f(U("uAlong"), Number(P.along));
387    gl.uniform1f(U("uAmbient"), Number(P.ambient));
388    gl.uniform1f(U("uFringe"), Number(P.fringe));
389  },
390};
391
392/* ══ 4 · CHROMA DRAG — a split that lags correctly ═════════════════════════
393   The genre effect offsets R and B by ±velocity, which smears symmetrically
394   and snaps back the instant the input stops — the tell that it is a filter.
395   Here each channel samples where its own FOLLOWER is: three copies of the
396   picture chasing the hand at three time constants. The smear is asymmetric
397   while moving (blue furthest behind), collapses to zero exactly as the
398   followers arrive, and cannot overshoot, because nothing here is velocity —
399   it is three positions converging. */
400
401const CHROMA_FRAG = [
402  ...HEAD,
403  "uniform float uGain, uBend;",
404  "void main(){",
405  "  float aspect = uRes.x / max(uRes.y, 1.0);",
406  /* Each channel's world is displaced by where its follower TRAILS the hand.
407     At rest all three sit on the hand and this is the untouched photo. */
408  "  vec2 oR = (uF1 - uHand) * uGain;",
409  "  vec2 oG = (uF2 - uHand) * uGain;",
410  "  vec2 oB = (uF3 - uHand) * uGain;",
411  /* uBend curves the smear: the offset is rotated slightly by its own length,
412     so a hard flick arcs rather than ruling a straight line. */
413  "  float bR = uBend * length(oR) * 6.0; mat2 mR = mat2(cos(bR), -sin(bR), sin(bR), cos(bR));",
414  "  float bB = -uBend * length(oB) * 6.0; mat2 mB = mat2(cos(bB), -sin(bB), sin(bB), cos(bB));",
415  "  float r = pic(vUv + mR * oR).r;",
416  "  float g = pic(vUv + oG).g;",
417  "  float b = pic(vUv + mB * oB).b;",
418  "  outColor = vec4(r, g, b, 1.0);",
419  "}",
420].join("\\n");
421
422const chroma: Treatment = {
423  frag: CHROMA_FRAG,
424  defaults: { gain: 0.55, bend: 0.35 },
425  /* The whole component is these three numbers: the channels' time constants.
426     Spread wide enough to read as a tail, close enough to stay one image. */
427  taus: [0.06, 0.14, 0.28],
428  reduceT: 0,
429  params: (P) => [
430    {
431      key: "gain",
432      label: "Drag",
433      kind: "range",
434      min: 0,
435      max: 1.5,
436      step: 0.05,
437      value: Number(P.gain),
438      hint: "How far the channels trail the hand. At rest the photo is untouched.",
439    },
440    {
441      key: "bend",
442      label: "Arc",
443      kind: "range",
444      min: 0,
445      max: 1,
446      step: 0.05,
447      value: Number(P.bend),
448      hint: "Rotates the smear by its own length, so a flick curves instead of ruling.",
449    },
450  ],
451  upload: (gl, U, P) => {
452    gl.uniform1f(U("uGain"), Number(P.gain));
453    gl.uniform1f(U("uBend"), Number(P.bend));
454  },
455};
456
457/* ══ 5 · ROSETTE — a four-plate CMYK halftone, screens at press angles ═════
458   The separation is the naive textbook one (K from max, UCR by division) and
459   the four screens sit at the classical 15/75/0/45 — the angles chosen by a
460   century of lithography because their moiré is the smallest and what
461   remains is the rosette itself. Dots grow by sqrt of coverage (a dot's AREA
462   is the tone), cells are physical pixels, and the inks multiply over paper
463   the way ink actually stacks. */
464
465const ROSETTE_FRAG = [
466  ...HEAD,
467  "uniform float uPitch, uGain, uJitter, uRegister, uLiftP;",
468  "uniform vec3 uPaper;",
469  "float screenDot(vec2 frag, float ang, float pitch, float v){",
470  "  float c = cos(ang), s = sin(ang);",
471  "  vec2 p = mat2(c, -s, s, c) * frag / pitch;",
472  "  vec2 cellUv = fract(p) - 0.5;",
473  /* sqrt: coverage lives in area. 0.75 lets a full tone close the cell. */
474  "  float r = sqrt(clamp(v, 0.0, 1.0)) * 0.75;",
475  "  float d = length(cellUv);",
476  "  float aa = 0.7 / pitch;",
477  "  return 1.0 - smoothstep(r - aa, r + aa, d);",
478  "}",
479  "void main(){",
480  "  vec3 src = pic(vUv);",
481  /* Press exposure: lift toward paper before separating, the tone curve a
482     printer applies so a night photograph still ships visible paper and a
483     legible rosette instead of a closed black plate. Measured on the demo
484     set: without it the portrait's navy ground drives K past 0.8 and the
485     mids vanish. */
486  "  src = 1.0 - (1.0 - src) * (1.0 - uLiftP);",
487  "  float k = 1.0 - max(src.r, max(src.g, src.b));",
488  "  float den = max(1.0 - k, 1e-4);",
489  "  float cy = (1.0 - src.r - k) / den;",
490  "  float mg = (1.0 - src.g - k) / den;",
491  "  float ye = (1.0 - src.b - k) / den;",
492  "  float pitch = uPitch * uDpr;",
493  "  vec2 frag = gl_FragCoord.xy;",
494  /* Misregistration: each plate slides a fraction of a cell off true. The
495     cheap-print charm, and 0 is a clean press. */
496  "  vec2 mis = vec2(uRegister * pitch * 0.35, -uRegister * pitch * 0.22);",
497  "  float C = screenDot(frag + mis,           radians(15.0 + uJitter), pitch, cy * uGain);",
498  "  float M = screenDot(frag - mis,           radians(75.0 - uJitter), pitch, mg * uGain);",
499  "  float Y = screenDot(frag + mis.yx,        radians( 0.0 + uJitter), pitch, ye * uGain);",
500  "  float K = screenDot(frag,                 radians(45.0),           pitch, k  * uGain);",
501  /* Overprint: paper, then each ink multiplies down. */
502  "  vec3 col = uPaper;",
503  "  col *= mix(vec3(1.0), vec3(0.0, 0.65, 0.95), C);",
504  "  col *= mix(vec3(1.0), vec3(0.95, 0.0, 0.55), M);",
505  "  col *= mix(vec3(1.0), vec3(1.0, 0.9, 0.0), Y);",
506  "  col *= mix(vec3(1.0), vec3(0.08), K);",
507  "  outColor = vec4(col, 1.0);",
508  "}",
509].join("\\n");
510
511const rosette: Treatment = {
512  frag: ROSETTE_FRAG,
513  defaults: { pitch: 7, gain: 0.95, jitter: 0, register: 0.25, lift: 0.24, paper: "#f6f3ea" },
514  taus: [0, 0, 0],
515  reduceT: 0,
516  params: (P) => [
517    {
518      key: "lift",
519      label: "Exposure",
520      kind: "range",
521      min: 0,
522      max: 0.6,
523      step: 0.02,
524      value: Number(P.lift),
525      hint: "Lifts the source toward paper before separation — the press tone curve. 0 prints the shadows shut.",
526    },
527    {
528      key: "pitch",
529      label: "Screen",
530      kind: "range",
531      min: 3,
532      max: 18,
533      step: 0.5,
534      value: Number(P.pitch),
535      suffix: "px",
536      hint: "Cell pitch in physical pixels. Small is magazine, large is billboard seen close.",
537    },
538    {
539      key: "gain",
540      label: "Ink gain",
541      kind: "range",
542      min: 0.5,
543      max: 1.6,
544      step: 0.05,
545      value: Number(P.gain),
546      hint: "Dot growth on press. Past 1.2 the shadows close up, exactly like real gain.",
547    },
548    {
549      key: "jitter",
550      label: "Angle drift",
551      kind: "range",
552      min: -10,
553      max: 10,
554      step: 0.5,
555      value: Number(P.jitter),
556      suffix: "°",
557      hint: "Nudges the C/M/Y screens off the press angles. The moiré you get is the real one.",
558    },
559    {
560      key: "register",
561      label: "Misregister",
562      kind: "range",
563      min: 0,
564      max: 1,
565      step: 0.05,
566      value: Number(P.register),
567      hint: "Plates off true by a fraction of a cell — the cheap-print charm. 0 is a clean press.",
568    },
569    { key: "paper", label: "Paper", kind: "color", value: String(P.paper) },
570  ],
571  upload: (gl, U, P) => {
572    gl.uniform1f(U("uPitch"), Number(P.pitch));
573    gl.uniform1f(U("uGain"), Number(P.gain));
574    gl.uniform1f(U("uJitter"), Number(P.jitter));
575    gl.uniform1f(U("uRegister"), Number(P.register));
576    gl.uniform1f(U("uLiftP"), Number(P.lift));
577    uc(gl, U("uPaper"), String(P.paper));
578  },
579};
580
581export const TREATMENTS: Record<TreatmentName, Treatment> = {
582  grade,
583  dither,
584  tear,
585  chroma,
586  rosette,
587};
588`;export{e as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.