@vibecook/ice 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -5
- package/dist/{chunk-ZTY2VRIS.js → chunk-6W2FPXR7.js} +13 -7
- package/dist/chunk-6W2FPXR7.js.map +1 -0
- package/dist/{chunk-Y7K6CHSF.js → chunk-LAZIY3J7.js} +286 -189
- package/dist/chunk-LAZIY3J7.js.map +1 -0
- package/dist/{chunk-REJTEK3V.js → chunk-M2HUDENL.js} +288 -318
- package/dist/chunk-M2HUDENL.js.map +1 -0
- package/dist/{chunk-BH5AFWJX.js → chunk-VZEZ7CYW.js} +9 -13
- package/dist/chunk-VZEZ7CYW.js.map +1 -0
- package/dist/desk-kit.js +2 -2
- package/dist/desk-objects.js +45 -14
- package/dist/desk-objects.js.map +1 -1
- package/dist/desk.js +3 -3
- package/dist/devtools.js +1 -1
- package/dist/dom.js +2 -2
- package/dist/index.js +7 -1
- package/dist/react.js +2 -2
- package/dist/types/packages/core/src/canvas/engine-catalog.d.ts +6 -0
- package/dist/types/packages/core/src/catalog/desk.d.ts +6 -6
- package/dist/types/packages/core/src/catalog/gesture.d.ts +2 -0
- package/dist/types/packages/core/src/catalog/selection-presence.d.ts +2 -0
- package/dist/types/packages/core/src/catalog/settings-resources.d.ts +8 -4
- package/dist/types/packages/core/src/facade/create-canvas-engine.d.ts +11 -5
- package/dist/types/packages/core/src/index.d.ts +1 -1
- package/dist/types/packages/core/src/ops/selection-drag.d.ts +8 -0
- package/dist/types/packages/core/src/settings/defaults.d.ts +2 -0
- package/dist/types/packages/core/src/systems/camera-sim.d.ts +13 -0
- package/dist/types/packages/core/src/systems/tray.d.ts +5 -4
- package/dist/types/packages/core/src/tools/define-tool.d.ts +2 -2
- package/dist/types/packages/core/src/widget/define-widget.d.ts +2 -1
- package/dist/types/packages/desk/src/ground.d.ts +4 -3
- package/dist/types/packages/desk/src/host/layer.d.ts +1 -3
- package/dist/types/packages/desk/src/mat/layout.d.ts +45 -5
- package/dist/types/packages/desk/src/mat/mat-pass.d.ts +7 -0
- package/dist/types/packages/desk/src/mat/ruler-regions.d.ts +17 -0
- package/dist/types/packages/desk/src/shaders.gen.d.ts +4 -4
- package/dist/types/packages/desk/src/theme.d.ts +0 -15
- package/dist/types/packages/desk/src/tray/drawer.d.ts +10 -8
- package/dist/types/packages/desk/src/tray/flux.d.ts +2 -3
- package/dist/types/packages/desk/src/tray/layout.d.ts +1 -6
- package/dist/types/packages/desk/src/tray/look.d.ts +0 -9
- package/dist/types/packages/desk/src/tray/pass.d.ts +7 -18
- package/dist/types/packages/desk/src/tray/specimens.d.ts +2 -19
- package/dist/types/packages/dom/src/pointer-adapter.d.ts +1 -1
- package/dist/types/packages/objects/src/minimat/layout.d.ts +1 -1
- package/dist/types/packages/objects/src/photo/photo.d.ts +2 -2
- package/dist/types/packages/objects/src/preset.d.ts +5 -3
- package/package.json +15 -16
- package/dist/chunk-BH5AFWJX.js.map +0 -1
- package/dist/chunk-REJTEK3V.js.map +0 -1
- package/dist/chunk-Y7K6CHSF.js.map +0 -1
- package/dist/chunk-ZTY2VRIS.js.map +0 -1
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
defineQuery,
|
|
13
13
|
field,
|
|
14
14
|
init
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-LAZIY3J7.js";
|
|
16
16
|
|
|
17
17
|
// ../desk/src/lattice/lod.ts
|
|
18
18
|
var BASE_SPACING = 20;
|
|
@@ -312,11 +312,6 @@ var TRAY = {
|
|
|
312
312
|
cavity: { edge: 0.9, heart: 1, width: 0.1 },
|
|
313
313
|
lamp: { azimuth: -36, elevation: 48 },
|
|
314
314
|
lampSize: 0.06,
|
|
315
|
-
/**
|
|
316
|
-
* K5a — the ACCESSORIES the specimens hang on (hooks, shelves, clips, rails): SKÅDIS's white powder coat warmed to sit on the
|
|
317
|
-
* hardboard; how far each stands off the board (pitches — its shadow's reach on the board under the lamp); the shadow's strength.
|
|
318
|
-
*/
|
|
319
|
-
accessory: { token: "SK\xC5DIS white, warmed", css: "#e8e3d9", height: { hook: 0.45, shelf: 0.6, clip: 0.3, rail: 0.4 }, shadow: 0.38 },
|
|
320
315
|
/** K5a — a specimen under the mouse lifts this share of the hold's lift (its own kind's `lift`): its shadow grows. */
|
|
321
316
|
hoverLift: 0.3
|
|
322
317
|
};
|
|
@@ -649,7 +644,7 @@ function matUniformValues(view, fadeIn, cfg, f, plateStrength, present, light =
|
|
|
649
644
|
gobo: [cfg.gobo.opacity, cfg.gobo.blurTexels, cfg.gobo.shadeMix, cfg.gobo.darkFloor],
|
|
650
645
|
grade: [cfg.gobo.saturate, 0, 0, 0],
|
|
651
646
|
noise: [f.noise[0], f.noise[1], 1 / NOISE_SIZE, 0],
|
|
652
|
-
wind: [f.goboTime, plateStrength, 0, 0],
|
|
647
|
+
wind: [f.goboTime, plateStrength, cfg.gobo.space === "viewport" ? 1 : 0, 0],
|
|
653
648
|
...lightValues(light),
|
|
654
649
|
...portalValues(present),
|
|
655
650
|
ruler: [r.on ? 1 : 0, r.margin, r.band, r.line],
|
|
@@ -661,7 +656,7 @@ function matUniformValues(view, fadeIn, cfg, f, plateStrength, present, light =
|
|
|
661
656
|
rulerSites: levels.flatMap((L) => [L.spacing, L.tickLen, L.tickAlpha, L.labelAlpha]),
|
|
662
657
|
rulerFormat: levels.flatMap((L) => [L.mult, L.exp, L.perWrap, 0]),
|
|
663
658
|
box: boxValues(view),
|
|
664
|
-
...slotLightValues(view, lit),
|
|
659
|
+
...cfg.gobo.space === "viewport" ? VIEWPORT_LIGHT : slotLightValues(view, lit),
|
|
665
660
|
// the slot's own opacity, which the mat is drawn at; `view.w` above is the objects' (what the ground's `objectsOf` hands the kinds'
|
|
666
661
|
// `SlotContext.present`). One view block per slot carries both (K4a, design-016 K-L3), so every kind reads it as it always read its copy
|
|
667
662
|
presence: [present?.opacity ?? 1, 0, 0, 0]
|
|
@@ -676,6 +671,7 @@ function litByOwn(view, lit) {
|
|
|
676
671
|
const s = settleLight(lit);
|
|
677
672
|
return !s || !s.b && s.a.x === view.camX && s.a.y === view.camY && s.a.zoom === view.zoom;
|
|
678
673
|
}
|
|
674
|
+
var VIEWPORT_LIGHT = { light: [0, 0, 1, 0], light2: [0, 0, 1, 0] };
|
|
679
675
|
function slotLightValues(view, lit) {
|
|
680
676
|
const own = { x: view.camX, y: view.camY, zoom: view.zoom };
|
|
681
677
|
const s = settleLight(lit);
|
|
@@ -731,15 +727,15 @@ var WGSL = {
|
|
|
731
727
|
"kit/composite.wgsl": "// (The kit's since K4a \u2014 moved from notebook/notebook-composite.wgsl: every layered kind lays its layer with it, kit/layer.ts `layerComposite`.)\n// A layered kind's layer onto the canvas: the resolved, premultiplied layer, over what the ground drew \u2014 through the slot's PORTAL CHAIN\n// (design-018 \xA74 \u2014 the kit's contract, which the kind's own passes cannot keep: they draw into the layer, not onto the canvas). The\n// whole layer is laid at the chain's cover, so its object fades as ONE (a book's cover never shows its pages through it \u2014 group\n// opacity, the tray's feather); at the root the chain is empty, the cover exactly 1, and the layer is laid as it always was.\n// K7a: the layer is its screen BOX's (kit/layer.ts `BoxTargets`) \u2014 the texel of a pixel is the pixel less the box's origin.\n//\n// THE KIT'S COMPOSITE ENTRY (`LAYER_COMPOSITE_FILE`; composed by `layerComposite` after the `view` and `portal` pieces; petition\n// I31 \u2014 every declaration's `///` block says what it takes and gives). Drawn inside the slot's scissor; its output is\n// premultiplied, laid \"one, one \u2212 src alpha\".\n\n/// The layer: the kind's resolved target, premultiplied RGBA, one texel a device px of its screen box.\n@group(0) @binding(0) var layer: texture_2d<f32>;\n/// The layer box's origin on the attachment, device px (xy; zw unused) \u2014 `BoxTargets.origin`.\n@group(0) @binding(1) var<uniform> origin: vec4f;\n/// The slot's view block (`MatPass.view`) \u2014 its portal chain and its ratio.\n@group(0) @binding(2) var<uniform> u: MatUniforms;\n\n/// One triangle over the whole attachment.\n/// `vid` \u2014 the vertex index, 0 \u2026 2.\n/// \u2192 its clip-space corner: (\u22121, \u22121), (3, \u22121), (\u22121, 3).\n@vertex\nfn vs(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4f {\n var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n return vec4f(p[vid], 0.0, 1.0);\n}\n\n/// The layer's texel at a pixel, through the slot's chain.\n/// `pos` \u2014 the fragment's position, device px of the attachment.\n/// \u2192 premultiplied RGBA: the layer's texel at `pos \u2212 origin`, times `portal_cover` at `pos` (exactly the texel at the root).\n@fragment\nfn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {\n let dpr = u.cam.w;\n return textureLoad(layer, vec2i(pos.xy) - vec2i(origin.xy), 0) * portal_cover(pos.xy / dpr, u.portals, u.clips, dpr);\n}\n",
|
|
732
728
|
"marks/marks-pass.wgsl": "// The marks pass \u2014 the desk's chrome over every stratum (design-015 \xA74.2 stratum 5, \xA77; D4a): one\n// instanced draw, one quad per mark over the screen rect the CPU gave it (`quad`, CSS px \u2014 the marks do\n// not zoom), one `marks_shade` per covered fragment, premultiplied out; a mark of light returns alpha 0,\n// so the blend (one, one \u2212 src alpha) adds it. The glyphs are the rulers' mono atlas (RULER.md), the\n// mat's own texture \u2014 the pills' and the rulers' numerals are one face.\n\n@group(0) @binding(0) var<uniform> u: MarksUniforms;\n@group(0) @binding(1) var<storage, read> marks: array<Mark>;\n@group(0) @binding(2) var glyph_tex: texture_2d<f32>;\n@group(0) @binding(3) var glyph_samp: sampler;\n\nstruct VSOut {\n @builtin(position) clip: vec4f,\n @location(0) @interpolate(flat) idx: u32,\n}\n\nconst CORNERS = array<vec2f, 6>(\n vec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0),\n vec2f(0.0, 0.0), vec2f(1.0, 1.0), vec2f(0.0, 1.0),\n);\n\n@vertex\nfn vs(@builtin(vertex_index) vid: u32, @builtin(instance_index) iid: u32) -> VSOut {\n var out: VSOut;\n out.idx = iid;\n let q = marks[iid].quad;\n let pos = mix(q.xy, q.zw, CORNERS[vid]); // CSS px\n out.clip = vec4f(pos.x / u.view.x * 2.0 - 1.0, 1.0 - pos.y / u.view.y * 2.0, 0.0, 1.0);\n return out;\n}\n\n@fragment\nfn fs(in: VSOut) -> @location(0) vec4f {\n let dpr = max(u.view.z, 1e-6);\n var c = marks_shade(marks[in.idx], in.clip.xy / dpr, dpr, u.atlas, glyph_tex, glyph_samp);\n // the tray's name tags fade in under the drawer's header as its specimens do (design-018 \xA74, R4) \u2014 from the first tag's record on\n if (u.fade.y > 0.0 && f32(in.idx) >= u.fade.z) { c *= smoothstep(u.fade.x, u.fade.x + u.fade.y, in.clip.y / dpr); }\n if (c.a <= 0.0 && max(c.r, max(c.g, c.b)) <= 0.0) { discard; }\n return c;\n}\n",
|
|
733
729
|
"marks/marks.wgsl": "// The MARKS \u2014 the desk's chrome as SDF terms (design-015 \xA77, stratum 5; D4a): *Marks on the Mat*'s\n// reference drawing code (vibe-field/draft/desk-chrome/chrome.js) as distances. Every mark is ONE\n// record (a `Mark`, laid out on the CPU by src/marks/layout.ts in SCREEN px \u2014 the chrome keeps its size\n// at every zoom), shaded here into a premultiplied colour: an outline or a fill of a turned rounded\n// box (a frame, a ring, a knob, the vellum, a pill), the four brackets of a frame, a segment (solid or\n// dotted, a laser's line), a gap's bar with its end ticks, a flare, a glyph of the rulers' mono atlas\n// (with its halo), a strip of masking tape. Light is ADDED: a mark flagged so returns alpha 0 over its\n// colour, which the pass's premultiplied blend turns into dst + src \u2014 Canvas2D's \"lighter\".\n// Pure: every resource is a parameter (engine/shader.ts's rule).\n\nconst MARK_STROKE: f32 = 0.0;\nconst MARK_FILL: f32 = 1.0;\nconst MARK_BRACKETS: f32 = 2.0;\nconst MARK_SEGMENT: f32 = 3.0;\nconst MARK_BAR: f32 = 4.0;\nconst MARK_FLARE: f32 = 5.0;\nconst MARK_GLYPH: f32 = 6.0;\nconst MARK_TAPE: f32 = 7.0;\n\n/** Coverage of a distance (CSS px) over one DEVICE px of ramp. */\nfn marks_cov(sd: f32, dpr: f32) -> f32 { return clamp(0.5 - sd * dpr, 0.0, 1.0); }\n\n/** A screen point in a mark's own frame: about its centre, turned by \u2212angle (positive turns clockwise on screen, y down). */\nfn marks_local(p: vec2f, c: vec2f, angle: f32) -> vec2f {\n let d = p - c;\n let cs = cos(angle);\n let sn = sin(angle);\n return vec2f(cs * d.x + sn * d.y, -sn * d.x + cs * d.y);\n}\n\n/**\n * The distance to the four BRACKETS of a rounded frame (half extents X, Y, corner r), each reaching L from its\n * corner along both sides (chrome.js `bracketsPath`): folded into one quadrant, the run along the top edge from\n * its end to the arc, the arc, and the run down the side \u2014 round caps come from the distance itself.\n */\nfn marks_brackets(p: vec2f, X: f32, Y: f32, r: f32, L: f32) -> f32 {\n let q = abs(p);\n let top = length(q - vec2f(clamp(q.x, X - L, X - r), Y));\n let side = length(q - vec2f(X, clamp(q.y, Y - L, Y - r)));\n var d = min(top, side);\n let v = q - vec2f(X - r, Y - r);\n if (r > 0.0 && v.x >= 0.0 && v.y >= 0.0) { d = min(d, abs(length(v) - r)); }\n return d;\n}\n\n/** The distance to a dotted line A\u2192B: a dot every `period` from A (Canvas2D's `[0.01, period \u2212 0.01]` dash with round caps). */\nfn marks_dotted(p: vec2f, a: vec2f, b: vec2f, period: f32) -> f32 {\n let ab = b - a;\n let len = length(ab);\n let dir = ab / max(len, 1e-6);\n let k = clamp(round(dot(p - a, dir) / period), 0.0, floor(len / period));\n return length(p - (a + dir * (k * period)));\n}\n\n/** A gap's bar: the segment A\u2192B and a tick `tick` either side of it at each end, one stroke. */\nfn marks_bar(p: vec2f, a: vec2f, b: vec2f, tick: f32) -> f32 {\n let ab = b - a;\n let n = vec2f(-ab.y, ab.x) / max(length(ab), 1e-6) * tick;\n return min(sdf_segment(p, a, b), min(sdf_segment(p, a - n, a + n), sdf_segment(p, b - n, b + n)));\n}\n\n/** Masking tape's torn outline (desk.css `.obj-tape i` clip-path), unit square, clockwise from the top-left. */\nconst TAPE_EDGE = array<vec2f, 12>(\n vec2f(0.03, 0.0), vec2f(0.97, 0.04), vec2f(1.0, 0.22), vec2f(0.96, 0.40), vec2f(1.0, 0.60), vec2f(0.97, 0.80),\n vec2f(1.0, 1.0), vec2f(0.02, 0.96), vec2f(0.0, 0.78), vec2f(0.04, 0.58), vec2f(0.0, 0.40), vec2f(0.03, 0.20),\n);\n\n/** The signed distance to the tape's outline, the strip `half` in its own frame (Quilez's polygon). */\nfn marks_tape_sd(p: vec2f, half: vec2f) -> f32 {\n let v0 = (TAPE_EDGE[0] - 0.5) * 2.0 * half;\n var d = dot(p - v0, p - v0);\n var s = 1.0;\n for (var i = 0u; i < 12u; i++) {\n let j = (i + 11u) % 12u;\n let vi = (TAPE_EDGE[i] - 0.5) * 2.0 * half;\n let vj = (TAPE_EDGE[j] - 0.5) * 2.0 * half;\n let e = vj - vi;\n let w = p - vi;\n let b = w - e * clamp(dot(w, e) / dot(e, e), 0.0, 1.0);\n d = min(d, dot(b, b));\n let c0 = p.y >= vi.y;\n let c1 = p.y < vj.y;\n let c2 = e.x * w.y > e.y * w.x;\n if ((c0 && c1 && c2) || (!c0 && !c1 && !c2)) { s = -s; }\n }\n return s * sqrt(d);\n}\n\n/** CSS `saturate(s) brightness(b)` on an sRGB colour (the Filter Effects matrix). */\nfn marks_moonlit(c: vec3f, s: f32, b: f32) -> vec3f {\n let r = vec3f(0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s);\n let g = vec3f(0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s);\n let bl = vec3f(0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s);\n return clamp(vec3f(dot(r, c), dot(g, c), dot(bl, c)) * b, vec3f(0.0), vec3f(1.0));\n}\n\n/** One texel sample of a cell (texels from its top-left, kept inside the cell: a neighbour's glyph never bleeds in). */\nfn marks_glyph_at(t: vec2f, cell: f32, size: vec2f, atlas: vec4f, tex: texture_2d<f32>, samp: sampler) -> f32 {\n let at = vec2f(cell * atlas.w, 0.0) + clamp(t, vec2f(0.5), size - 0.5);\n return textureSampleLevel(tex, samp, at / atlas.xy, 0.0).r;\n}\n\n/** The glyph atlas's coverage at a point of a cell's own frame (CSS px from its top-left; the cell `size` CSS px), `halo` > 0 = its coverage dilated that far. */\nfn marks_glyph_cov(q: vec2f, cell: f32, size: vec2f, atlas: vec4f, halo: f32, tex: texture_2d<f32>, samp: sampler) -> f32 {\n let scale = atlas.z;\n let t = q * scale;\n let s = size * scale;\n var c = marks_glyph_at(t, cell, s, atlas, tex, samp);\n if (halo > 0.0) {\n for (var i = 0; i < 8; i++) {\n let a = f32(i) * 0.785398163;\n let o = vec2f(cos(a), sin(a)) * halo * scale;\n c = max(c, marks_glyph_at(t + o, cell, s, atlas, tex, samp));\n c = max(c, marks_glyph_at(t + o * 0.5, cell, s, atlas, tex, samp));\n }\n }\n return c;\n}\n\n/**\n * One mark at screen point `p` (CSS px), premultiplied \u2014 alpha 0 when it is light (added). `atlas` = the glyph atlas's\n * width, height (texels), texels per CSS px and cell width (texels).\n */\nfn marks_shade(M: Mark, p: vec2f, dpr: f32, atlas: vec4f, tex: texture_2d<f32>, samp: sampler) -> vec4f {\n let kind = M.shape.x;\n let width = M.shape.z;\n var cov = 0.0;\n var col = M.colour;\n if (kind == MARK_STROKE || kind == MARK_FILL || kind == MARK_BRACKETS) {\n let q = marks_local(p, M.centre.xy, M.centre.z);\n let sd = sdf_round_box(q, M.half.xy, M.centre.w);\n if (kind == MARK_STROKE) {\n cov = marks_cov(abs(sd) - 0.5 * width, dpr);\n } else if (kind == MARK_FILL) {\n cov = marks_cov(sd, dpr);\n } else {\n cov = marks_cov(marks_brackets(q, M.half.x, M.half.y, M.centre.w, M.shape.w) - 0.5 * width, dpr);\n }\n } else if (kind == MARK_SEGMENT) {\n let d = select(sdf_segment(p, M.centre.xy, M.half.xy), marks_dotted(p, M.centre.xy, M.half.xy, M.aux.x), M.aux.x > 0.0);\n cov = marks_cov(d - 0.5 * width, dpr);\n } else if (kind == MARK_BAR) {\n cov = marks_cov(marks_bar(p, M.centre.xy, M.half.xy, M.aux.x) - 0.5 * width, dpr);\n } else if (kind == MARK_FLARE) {\n // a radial gradient: the core at the centre, the laser at `stop`, nothing at the rim \u2014 interpolated premultiplied\n let t = length(p - M.centre.xy) / M.centre.w;\n if (t >= 1.0) { return vec4f(0.0); }\n let core = vec4f(M.aux.rgb * M.aux.a, M.aux.a);\n let mid = vec4f(M.colour.rgb * M.colour.a, M.colour.a);\n let stop = M.half.x;\n let c = select(mix(mid, vec4f(0.0), (t - stop) / (1.0 - stop)), mix(core, mid, t / stop), t < stop);\n return vec4f(c.rgb, select(c.a, 0.0, (u32(M.shape.y) & 1u) != 0u));\n } else if (kind == MARK_GLYPH) {\n // the cell turned about its centre (`centre.xy`), its own frame from its top-left; a halo reaches `aux.y` past it\n let q = marks_local(p, M.centre.xy, M.centre.z) + 0.5 * M.half.xy;\n let h = M.aux.y;\n if (q.x < -h || q.y < -h || q.x > M.half.x + h || q.y > M.half.y + h) { return vec4f(0.0); }\n cov = marks_glyph_cov(q, M.aux.x, M.half.xy, atlas, h, tex, samp);\n } else if (kind == MARK_TAPE) {\n // the strip in its own frame: its torn outline, its fibres (1 in `every` object units), a light from above, moonlit by night\n let q = marks_local(p, M.centre.xy, M.centre.z);\n let half = M.half.xy;\n cov = marks_cov(marks_tape_sd(q, half), dpr);\n let uv = (q + half) / (2.0 * half);\n let units = M.aux.x; // CSS px per object unit\n let sheen = mix(vec4f(1.0, 1.0, 1.0, M.half.z), vec4f(0.0, 0.0, 0.0, M.half.w), clamp(uv.y, 0.0, 1.0));\n var c = vec4f(M.colour.rgb * M.colour.a, M.colour.a);\n c = vec4f(sheen.rgb * sheen.a, sheen.a) + c * (1.0 - sheen.a);\n let every = M.shape.w;\n let x = (q.x + half.x) / max(units, 1e-6);\n let seen = clamp(every * units * dpr / 3.0 - 0.5, 0.0, 1.0); // fibres under ~1.5 device px each fade to the mean\n let fibre = select(0.0, 1.0, fract(x / every) < 1.0 / every) * seen + (1.0 - seen) / every;\n let fa = M.aux.w * fibre;\n c = vec4f(vec3f(fa), fa) + c * (1.0 - fa);\n let rgb = marks_moonlit(c.rgb / max(c.a, 1e-6), M.aux.y, M.aux.z);\n let a = c.a * cov * M.centre.w;\n return vec4f(rgb * a, a);\n }\n let a = col.a * cov;\n return vec4f(col.rgb * a, select(a, 0.0, (u32(M.shape.y) & 1u) != 0u));\n}\n",
|
|
734
|
-
"mat/mat-pass.wgsl": "// PASS 2, the cutting mat \u2014 one fullscreen triangle: the lattice's lines as\n// analytic coverage, the grain, the gobo projected onto the desk plane, the\n// reference's grade by day or the night's eye by night (MAT.md). Alpha 1 at\n// rest (a plain write); a flight fades it and clips it to the portal.\n\n@group(0) @binding(0) var<uniform> u: MatUniforms;\n@group(0) @binding(1) var gobo_tex: texture_2d<f32>;\n@group(0) @binding(2) var gobo_samp: sampler;\n@group(0) @binding(3) var noise_tex: texture_2d<f32>;\n@group(0) @binding(4) var noise_samp: sampler;\n@group(0) @binding(5) var glyph_tex: texture_2d<f32>; // the rulers' glyph atlas (RULER.md); a 1\xD71 empty cell until a host uploads one\n\nstruct VSOut { @builtin(position) clip: vec4f }\n\n@vertex\nfn vs(@builtin(vertex_index) vid: u32) -> VSOut {\n var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var out: VSOut;\n out.clip = vec4f(p[vid], 0.0, 1.0);\n return out;\n}\n\n@fragment\nfn fs(in: VSOut) -> @location(0) vec4f {\n let zoom = mat_zoom(u);\n let dpr = mat_dpr(u);\n let frag = in.clip.xy; // device px\n let screen = frag / dpr; // CSS px\n let px = 1.0 / (zoom * dpr); // world per device px\n let bn = textureSampleLevel(noise_tex, noise_samp, frag * u.noise.z + u.noise.xy, 0.0).rgb;\n // the rulers' print \u2014 screen-space ink mixed into the albedo before the light, so it is lit and shadowed like the lines\n let ruler = ruler_ink(u, screen, frag, glyph_tex, gobo_samp);\n let albedo = mat_albedo(u, u.phase.xy + screen / zoom, px, frag, bn, ruler);\n // the lamp's gobo where the LIGHT sees this pixel (MINIMAT.md \xA74): the slot's own camera at rest\n // (`light` = `cam`, so this is the desk under the pixel), the host desk's through a mini mat's face
|
|
735
|
-
"mat/mat.wgsl": "// The cutting mat's maths \u2014 a PURE module: no bindings. The uniform block and\n// the textures arrive as parameters. Every function body is the reference's\n// (research/tree-shadow/prototype/src/shaders.js), transcribed to the engine's\n// units: world = CSS px at zoom 1, the lattice's decade rungs, `MatUniforms`\n// from src/mat/layout.ts prepended by the engine.\n//\n// THE KIT'S `light` PIECE (`kitWgsl([..., \"light\"])` \u2014 `lamp`, `gobo`, `night` and `noise` name it too; petition I31: every\n// declaration's `///` block says what it takes and gives). THE CONVENTIONS every kit piece is written in:\n// - WORLD units: one is one CSS px at zoom 1. World x runs right and world y runs DOWN, as the screen's do. A slot's camera is\n// unwrapped world (`u.cam.xy`, the world point at the attachment's top-left); the lattice's PHASE is the camera wrapped to\n// the coarse rung's period (`u.phase.xy`), what the lines are drawn from so they stay exact at any distance.\n// - CSS px: the attachment's layout px, origin its top-left, y down \u2014 `(world \u2212 u.cam.xy) \xB7 zoom`.\n// - DEVICE px: the attachment's texels \u2014 a fragment's `@builtin(position).xy`, pixel centres at .5 \u2014 CSS px \xB7 dpr. So a\n// fragment's world point is `frag / (zoom \xB7 dpr) + u.cam.xy`, and one device px is `1 / (zoom \xB7 dpr)` world units.\n// - zoom = `mat_zoom(u)`: CSS px per world unit. dpr = `mat_dpr(u)`: device px per CSS px \u2014 the ATTACHMENT's own, below 1 for\n// a capture's thumbnail or the held desk copy (petition I29). Both > 0.\n// - the DESK: the plane the lamp lights, in the projector's frame \u2014 metres, y UP (three's): world x runs along its x, world y\n// along its z, and a height above the desk along its y (`desk_of`, `lit_desk`; `u.plane`).\n// - COLOUR: 0..1 per channel. sRGB-encoded unless a parameter says LINEAR; the swap chain is a plain 8-bit format, so what a\n// pass writes is what CSS would show. The day's chain (`shade_mat`) keeps its input's encoding; the night's (`night_mat`)\n// takes linear reflectance and returns encoded colour.\n\n/// The slot's zoom: CSS px per world unit.\n/// `u` \u2014 the slot's view block (`MatUniforms`, bound as `u`).\n/// \u2192 `u.cam.z`, floored at 1e-12 so it divides safely: > 0 (the desk's band is 1e-8 \u2026 1e8).\nfn mat_zoom(u: MatUniforms) -> f32 { return max(u.cam.z, 1e-12); }\n// The ATTACHMENT's own device ratio \u2014 below 1 for a capture's thumbnail (dpr \xD7 scale), the held desk copy (dpr / 2) and a page\n// zoomed out \u2014 guarded only against an unset block. Every fragment's CSS px is its device px over it: a floor at 1 put each one\n// at the wrong point of the desk there, and the flat kinds shaded nothing (petition I29).\n/// The attachment's device px per CSS px.\n/// `u` \u2014 the slot's view block.\n/// \u2192 `u.cam.w`, floored at 1e-6 against an unset block only: > 0, and below 1 for a thumbnail capture or the held desk copy.\nfn mat_dpr(u: MatUniforms) -> f32 { return max(u.cam.w, 1e-6); }\n\n// ---- the reference's noise (hash12 \xB7 valueNoise), on framebuffer pixels\n/// The reference's hash: one pseudo-random number per point (`sin`-based \u2014 cheap, not a quality RNG; best for |p| within a\n/// few thousand, where f32 `sin` keeps its digits).\n/// `p` \u2014 any point (the mat hands framebuffer device px, scaled).\n/// \u2192 [0, 1): a different value at every distinct `p`, with no continuity between neighbours.\nfn hash12(p: vec2f) -> f32 {\n let n = sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453;\n return n - floor(n);\n}\n\n/// Value noise: `hash12` at the four corners of `p`'s lattice cell, blended by a smoothstep weight.\n/// `p` \u2014 the point in the noise's own cells (one cell per unit of `p`; the mat's grain hands device px \xD7 0.45, a cell \u2248 2.2\n/// device px).\n/// \u2192 [0, 1): continuous, with a continuous gradient across cells, mean \u2248 0.5 \u2014 centre it with `value_noise(p) \u2212 0.5`\n/// ([\u22120.5, 0.5)).\nfn value_noise(p: vec2f) -> f32 {\n let i = floor(p);\n var f = fract(p);\n f = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(hash12(i), hash12(i + vec2f(1.0, 0.0)), f.x),\n mix(hash12(i + vec2f(0.0, 1.0)), hash12(i + vec2f(1.0, 1.0)), f.x),\n f.y);\n}\n\n/// The sRGB transfer curve, decoded (IEC 61966-2-1).\n/// `c` \u2014 sRGB-encoded, 0..1 per channel. Clamp first: below \u22120.055 the curve's unused branch is NaN, and NaN survives the mix.\n/// \u2192 LINEAR, 0..1 per channel.\nfn srgb_to_linear(c: vec3f) -> vec3f {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3f(2.4)), step(vec3f(0.04045), c));\n}\n\n/// RGB \u2192 HSB (hue, saturation, brightness).\n/// `c` \u2014 RGB, 0..1 per channel (either encoding \u2014 the result is in the same one).\n/// \u2192 x the hue in TURNS, [0, 1) (0 red, 1/3 green, 2/3 blue); y the saturation, [0, 1]; z the brightness `max(r, g, b)`, [0, 1].\nfn rgb2hsb(c: vec3f) -> vec3f {\n let K = vec4f(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);\n let p = mix(vec4f(c.bg, K.wz), vec4f(c.gb, K.xy), step(c.b, c.g));\n let q = mix(vec4f(p.xyw, c.r), vec4f(c.r, p.yzx), step(p.x, c.r));\n let d = q.x - min(q.w, q.y);\n let e = 1.0e-10;\n return vec3f(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);\n}\n\n/// HSB \u2192 RGB, `rgb2hsb`'s inverse.\n/// `c` \u2014 x the hue in turns (any value: only its fraction counts), y the saturation 0..1, z the brightness 0..1.\n/// \u2192 RGB, 0..1 per channel.\nfn hsb2rgb(c: vec3f) -> vec3f {\n let K = vec4f(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n let p = abs(fract(K.xyz + c.x) * 6.0 - K.w);\n return c.z * mix(K.xxx, clamp(p - K.x, vec3f(0.0), vec3f(1.0)), c.y);\n}\n\n// ---- the lines. `px` is world units per DEVICE pixel; half-widths are in device px.\n/// One family of parallel lines \u2014 one every `spacing` along an axis, a line at every multiple of it.\n/// `x` \u2014 the coordinate across the lines, world units (the lattice's phase); `spacing` \u2014 their pitch, world units, > 0;\n/// `half_width` \u2014 a line's half width, WORLD units (`line_weight`'s device px \xD7 `px`); `px` \u2014 world units per device px, > 0.\n/// \u2192 [0, 1]: 1 within `half_width \u2212 px` of a line, 0 beyond `half_width + px`, a smoothstep between (the AA).\nfn line_coverage(x: f32, spacing: f32, half_width: f32, px: f32) -> f32 {\n let d = abs(fract(x / spacing - 0.5) - 0.5) * spacing;\n return 1.0 - smoothstep(max(half_width - px, 0.0), half_width + px, d);\n}\n\n/// A square grid: `line_coverage` along both axes, the larger.\n/// `p` \u2014 the point, world units (the lattice's phase); `spacing`, `half_width`, `px` \u2014 as `line_coverage`'s, world units.\n/// \u2192 [0, 1]: the grid's coverage at `p`.\nfn grid2(p: vec2f, spacing: f32, half_width: f32, px: f32) -> f32 {\n return max(line_coverage(p.x, spacing, half_width, px), line_coverage(p.y, spacing, half_width, px));\n}\n\n// One law for every rung (src/lattice/line.ts `lineWeight`): fade in over the\n// engine's window by the rung's OWN cell, then thicken and darken over the\n// next decade. x = half-width (device px), y = alpha.\n/// The lattice's line law for one rung \u2014 src/lattice/line.ts `lineWeight`, number for number.\n/// `u` \u2014 the slot's view block (`u.lod`: the fade-in window, CSS px; `u.line`: the half-widths and alphas); `cell_px` \u2014 the\n/// rung's cell on screen, CSS px (its spacing \xD7 zoom).\n/// \u2192 x the line's half-width, DEVICE px, from `u.line.x` (at the window's top) to `u.line.y` (a decade above it); y its\n/// alpha: 0 at or below `u.lod.x`, `u.line.z` at `u.lod.y`, rising to `u.line.w` a decade above.\nfn line_weight(u: MatUniforms, cell_px: f32) -> vec2f {\n let s = smoothstep(u.lod.x, u.lod.y, cell_px);\n let t = saturate(log(max(cell_px, 1e-9) / u.lod.y) / 2.302585093);\n return vec2f(mix(u.line.x, u.line.y, t), u.line.z * s + (u.line.w - u.line.z) * t);\n}\n\n// The mat's albedo at a point: the reference's `cuttingMatAlbedo` with the\n// three lattice rungs in place of its own decade, and the line law above in\n// place of its fade ladder. `world` is lattice-phase world (wrapped); `px` is\n// world per device pixel; `frag` is the framebuffer pixel; `ruler` the rulers'\n// print at it (ruler.wgsl) \u2014 ink over the lines, and at 0 exactly the mix below\n// returns the lined mat untouched, bit for bit.\n/// The cutting mat's albedo at a point: the sage with its grain, the three rungs' cream lines, the rulers' ink over them.\n/// `u` \u2014 the slot's view block; `world` \u2014 the point in LATTICE-PHASE world units (`u.phase.xy + css / zoom` \u2014 wrapped, not\n/// the camera's world); `px` \u2014 world units per device px, > 0; `frag` \u2014 the fragment's device px (the grain's hash runs on\n/// it); `bn` \u2014 the blue-noise texel at the fragment, 0..1 per channel; `ruler` \u2014 the rulers' ink coverage there, 0..1\n/// (`ruler_ink`; 0 = the lined mat bit for bit).\n/// \u2192 LINEAR RGB, 0..1 per channel \u2014 the reference's quirk: the chain after it (`mat_colour`) takes it as is.\nfn mat_albedo(u: MatUniforms, world: vec2f, px: f32, frag: vec2f, bn: vec3f, ruler: f32) -> vec3f {\n var sage = u.ground.xyz;\n let amp = u.grain.x;\n var g = (hash12(frag * vec2f(0.73, 0.91)) - 0.5) * amp;\n g += (value_noise(frag * 0.45 + vec2f(u.view.z * 0.9, -u.view.z * 0.6)) - 0.5) * amp;\n g += (bn.x - 0.5) * amp * 0.5;\n sage += vec3f(g * 0.4, g, g * 0.35);\n\n // A rung under the window has alpha 0 and no coverage to compute \u2014 the\n // fine rung at most zooms. The branches are uniform across the frame.\n let zoom = mat_zoom(u);\n var cover = 0.0;\n let wf = line_weight(u, u.rungs.x * zoom);\n if (wf.y > 0.0) { cover = grid2(world, u.rungs.x, wf.x * px, px) * wf.y; }\n let wm = line_weight(u, u.rungs.y * zoom);\n if (wm.y > 0.0) { cover = max(cover, grid2(world, u.rungs.y, wm.x * px, px) * wm.y); }\n let wc = line_weight(u, u.rungs.z * zoom);\n if (wc.y > 0.0) { cover = max(cover, grid2(world, u.rungs.z, wc.x * px, px) * wc.y); }\n return srgb_to_linear(clamp(mix(mix(sage, u.ink.xyz, cover), u.ink.xyz, ruler), vec3f(0.0), vec3f(1.0)));\n}\n\n// ---- the gobo (the reference's sampleBlur \xB7 getGoboBlurRatio \xB7 sampleGobo)\n/// A linear ramp.\n/// `edge0`, `edge1` \u2014 where it starts and ends (`edge0` \u2260 `edge1`); `x` \u2014 the input, in their units.\n/// \u2192 [0, 1]: 0 at or before `edge0`, 1 at or past `edge1`, linear between.\nfn linearstep(edge0: f32, edge1: f32, x: f32) -> f32 {\n return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);\n}\n\n// Linearise the WebGL-range NDC z with the projector's near/far, then ramp\n// between the two depths: sharp near the projector, soft far from it.\n/// How soft the gobo's shadow is at a depth.\n/// `ndc_z` \u2014 the point's depth in the projector's clip space, WebGL range [\u22121, 1]; `p` \u2014 `u.goboParams`: x the sharp depth\n/// and y the soft one (linear, metres from the projector), z and w the projector's near and far planes (metres).\n/// \u2192 [0, 1]: 0 (sharp) at or nearer than `p.x`, 1 (soft) at or beyond `p.y`.\nfn blur_ratio(ndc_z: f32, p: vec4f) -> f32 {\n return linearstep(p.x, p.y, (2.0 * p.z * p.w) / (p.w + p.z - ndc_z * (p.w - p.z)));\n}\n\n// Six taps on a golden-angle spiral, the start angle from blue noise so the\n// pattern differs per pixel (and per frame while the offset moves).\n/// The gobo's silhouette blurred by six taps on a golden-angle spiral.\n/// `tex` \u2014 the silhouette (r8: 1 = open, 0 = leaf); `samp` \u2014 a linear sampler; `uv` \u2014 the plate point, 0..1 (v runs down the\n/// plate); `texel` \u2014 one plate texel in uv; `blur` \u2014 the spiral's reach in texels (\u2264 0: one tap, sharp); `n` \u2014 a per-pixel\n/// random 0..1 (blue noise) that turns the spiral.\n/// \u2192 [0, 1]: the silhouette's mean over the taps.\nfn sample_blur(tex: texture_2d<f32>, samp: sampler, uv: vec2f, texel: vec2f, blur: f32, n: f32) -> f32 {\n // No blur: six taps at one point are one tap. The sharp end of the desk.\n if (blur <= 0.0) { return textureSampleLevel(tex, samp, uv, 0.0).r; }\n let G = 2.39996323;\n let rot = mat2x2f(cos(G), sin(G), -sin(G), cos(G));\n let a0 = n * 6.28318530718;\n var d = vec2f(cos(a0), sin(a0));\n let s = texel * blur * (1.0 / sqrt(6.0));\n var c = 0.0;\n for (var i = 0; i < 6; i++) {\n c += textureSampleLevel(tex, samp, uv + d * sqrt(f32(i + 1)) * s, 0.0).r;\n d = rot * d;\n }\n return c / 6.0;\n}\n\n// The gobo term at a desk point (metres): 1 = lit. Outside the projector's\n// frame the plate is not consulted at all \u2014 the cone is finite, as the\n// reference's is.\n/// The lamp's dapple at a point of the desk.\n/// `u` \u2014 the slot's view block (`u.goboMatrix` the projector, `u.gobo` its opacity and blur); `tex` \u2014 the slot's animated\n/// silhouette (`MatPass.silhouette`, r8, 1 = open); `samp` \u2014 a linear sampler; `desk` \u2014 the point in the projector's frame,\n/// metres, y up (`desk_of`, `lit_desk`, a kind's own desk point); `n` \u2014 blue noise 0..1 (the blur spiral's turn).\n/// \u2192 [0, 1]: 1 = fully lit. Exactly 1 outside the projector's frame and when the gobo is off (`u.gobo.x` \u2264 0); else\n/// `mix(1, the silhouette blurred by depth, u.gobo.x)`.\nfn sample_gobo(u: MatUniforms, tex: texture_2d<f32>, samp: sampler, desk: vec3f, n: f32) -> f32 {\n if (u.gobo.x <= 0.0) { return 1.0; }\n var clip = u.goboMatrix * vec4f(desk, 1.0);\n let ndc = clip.xyz / clip.w;\n // The wind target's row 0 is the plate's top row, so v runs downward.\n let uv = vec2f(0.5 + 0.5 * ndc.x, 0.5 - 0.5 * ndc.y);\n if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { return 1.0; }\n let g = sample_blur(tex, samp, uv, vec2f(1.0 / 512.0), blur_ratio(ndc.z, u.goboParams) * u.gobo.y, n);\n return mix(1.0, g, u.gobo.x);\n}\n\n// World (CSS px at zoom 1, unwrapped) \u2192 the desk plane in the projector's frame.\n/// A world point on the desk, in the projector's frame.\n/// `u` \u2014 the slot's view block (`u.plane`: the origin's x and z, metres per world unit, the desk's height); `world` \u2014 the\n/// point, UNWRAPPED world units (y down).\n/// \u2192 metres, y up: `(plane.x + world.x \xB7 m, plane.w, plane.y + world.y \xB7 m)`, m = `u.plane.z` \u2014 world y runs along the\n/// desk's z.\nfn desk_of(u: MatUniforms, world: vec2f) -> vec3f {\n return vec3f(u.plane.x + world.x * u.plane.z, u.plane.w, u.plane.y + world.y * u.plane.z);\n}\n\n// ---- the LIGHT a slot is lit by (MINIMAT.md \xA74; src/mat/layout.ts `SlotLight`). The lamp's gobo is\n// sampled where a light CAMERA sees a point, not where the slot's camera does: the same camera for the\n// root at rest; the host desk's for a mini mat's inside \u2014 the dapple runs across the mini mat as across\n// anything lying on the desk \u2014 and two of them cross-faded (`light.w`) while a flight hands one lamp to\n// the other. An object's world point `p` raised `h` world units, as light camera `L` (x, y, zoom) sees it:\n// the point on screen, back into L's world; a height scales as a length does.\n/// A point of the slot's world, raised, as a LIGHT camera sees it \u2014 in the projector's frame.\n/// `u` \u2014 the slot's view block; `L` \u2014 the light camera: x, y the world point at its top-left, z its zoom (`u.light` or\n/// `u.light2`; w unread); `p` \u2014 the point, the slot's world units; `h` \u2014 its height above the desk, world units, \u2265 0.\n/// \u2192 metres, y up: `p` carried through the screen into `L`'s world, its height scaled as a length is.\nfn lit_desk(u: MatUniforms, L: vec4f, p: vec2f, h: f32) -> vec3f {\n let s = max(u.cam.z, 1e-12) / max(L.z, 1e-12);\n let w = L.xy + (p - u.cam.xy) * s;\n return vec3f(u.plane.x + w.x * u.plane.z, u.plane.w + h * s * u.plane.z, u.plane.y + w.y * u.plane.z);\n}\n\n// The gobo on an object's world point `p` raised `h`: under the slot's own lamp the desk point itself,\n// `own` (the object's own `desk_at`, so a root's object reads exactly what it always read); else under\n// the light camera \u2014 then, while a flight hands it over, cross-faded to the second.\n/// The lamp's dapple on an object's point, under whichever lamp the slot is lit by.\n/// `u` \u2014 the slot's view block; `tex`, `samp` \u2014 the silhouette and its sampler, as `sample_gobo`'s; `p` \u2014 the object's point,\n/// the slot's world units; `h` \u2014 its height above the desk, world units, \u2265 0; `own` \u2014 the same point in the projector's frame\n/// as the object computes it (metres, y up), read when the slot is lit by its own camera; `n` \u2014 blue noise 0..1.\n/// \u2192 [0, 1]: 1 = fully lit (`sample_gobo` at the point the light sees; cross-faded by `u.light.w` toward `u.light2`'s).\nfn lit_gobo(u: MatUniforms, tex: texture_2d<f32>, samp: sampler, p: vec2f, h: f32, own: vec3f, n: f32) -> f32 {\n var g = 0.0;\n if (all(u.light.xyz == u.cam.xyz)) { g = sample_gobo(u, tex, samp, own, n); }\n else { g = sample_gobo(u, tex, samp, lit_desk(u, u.light, p, h), n); }\n if (u.light.w > 0.0) { g = mix(g, sample_gobo(u, tex, samp, lit_desk(u, u.light2, p, h), n), u.light.w); }\n return g;\n}\n\n// The reference's grade: `shade = mix(1, floor + (1 \u2212 floor)\xB7gobo, mix)`, then\n// HSB: saturation up by (1 \u2212 shade)\xB7k, brightness \xD7 shade. The reference did\n// this on pow(albedo, 2.2) and wrote pow(1/2.2) \u2014 kept verbatim: that chain is\n// the mat's colour on screen.\n/// The DAY's chain \u2014 the lamp's dapple graded onto a colour.\n/// `u` \u2014 the slot's view block (`u.gobo.w` the dark floor, `u.gobo.z` the shade's mix, `u.grade.x` the saturation bump);\n/// `albedo` \u2014 the colour as it is written where lit, 0..1 per channel: an object's sRGB colour (the mat hands its own linear\n/// albedo \u2014 the reference's look); `gobo` \u2014 the dapple, 0..1 (1 = lit).\n/// \u2192 RGB 0..1 in `albedo`'s encoding: exactly `albedo` where `gobo` \u2265 1; in shade, brightness \xD7 shade and saturation up by\n/// (1 \u2212 shade) \xB7 `u.grade.x`, where shade = `mix(1, floor + (1 \u2212 floor) \xB7 gobo, u.gobo.z)` (\u2248 0.51 \u2026 1 by the theme's numbers).\nfn shade_mat(u: MatUniforms, albedo: vec3f, gobo: f32) -> vec3f {\n // Lit: shade is exactly 1, the saturation bump exactly 0, and the chain is\n // pow(pow(albedo, 2.2), 1/2.2) \u2014 the identity. Every pixel outside the\n // cone, and every lit one inside it, skips six pows and the HSB round trip.\n if (gobo >= 1.0) { return albedo; }\n let shade = mix(1.0, u.gobo.w + (1.0 - u.gobo.w) * gobo, u.gobo.z);\n var hsb = rgb2hsb(pow(albedo, vec3f(2.2)));\n hsb.y = min(hsb.y + (1.0 - shade) * u.grade.x, 1.0);\n hsb.z = hsb.z * shade;\n return pow(hsb2rgb(hsb), vec3f(1.0 / 2.2));\n}\n\n// ---- the NIGHT (MAT.md; src/mat/night.ts is the same arithmetic on the CPU):\n// the Moon in place of the Sun, and the eye that sees by it. The day's chain\n// above is untouched \u2014 `mat_colour` returns it, alone, when `night.x` is 0.\n/// Photopic luminance Y (the sRGB / Rec. 709 weights).\n/// `c` \u2014 LINEAR RGB.\n/// \u2192 Y, in `c`'s scale (0..1 for 0..1 channels; white is 1).\nfn night_luminance(c: vec3f) -> f32 { return dot(c, vec3f(0.2126729, 0.7151522, 0.0721750)); }\n\n// Scotopic luminance V' from XYZ \u2014 Larson, Rushmeier & Piatko 1997's regression:\n// the rods peak at 507 nm, so greens and blues keep their brightness by night and reds die.\n/// Scotopic luminance V\u2032 \u2014 what the rods see (Larson, Rushmeier & Piatko 1997).\n/// `c` \u2014 LINEAR RGB.\n/// \u2192 V\u2032 in `c`'s scale: \u2265 0 over the sRGB gamut, white \u2248 2.57 (`night_mat` normalises by it).\nfn night_scotopic(c: vec3f) -> f32 {\n let X = dot(c, vec3f(0.4124564, 0.3575761, 0.1804375));\n let Y = dot(c, vec3f(0.2126729, 0.7151522, 0.0721750));\n let Z = dot(c, vec3f(0.0193339, 0.1191920, 0.9503041));\n return Y * (1.33 * (1.0 + (Y + Z) / max(X, 1e-6)) - 1.68);\n}\n\n// CIE 191:2010's mesopic weight \u2014 the cones' share of the seeing at L cd/m\xB2:\n// 0 at 0.005 (the rods alone), 1 at 5 (the cones alone).\n/// The cones' share of the seeing at a luminance (CIE 191:2010's mesopic weight).\n/// `L` \u2014 luminance, cd/m\xB2, > 0.\n/// \u2192 [0, 1]: 0 at or below 0.005 cd/m\xB2 (the rods alone), 1 at or above 5 (the cones alone), linear in log10(L) between.\nfn night_mesopic(L: f32) -> f32 { return saturate(0.767 + 0.3334 * log(max(L, 1e-9)) * 0.4342944819); }\n\n/// The sRGB transfer curve, encoded \u2014 `srgb_to_linear`'s inverse.\n/// `c` \u2014 LINEAR RGB, \u2265 0 (0..1 for an on-screen colour).\n/// \u2192 sRGB-encoded RGB (0..1 for 0..1).\nfn night_encode(c: vec3f) -> vec3f {\n return mix(c * 12.92, 1.055 * pow(max(c, vec3f(0.0)), vec3f(1.0 / 2.4)) - 0.055, step(vec3f(0.0031308), c));\n}\n\n/// The NIGHT's chain \u2014 a reflectance under the Moon, as the dark-adapted eye reports it.\n/// `u` \u2014 the slot's view block (`u.moon`, `u.night`, `u.rod`, `u.eigengrau`: the theme's night, mat/night.ts); `albedo` \u2014\n/// LINEAR reflectance, 0..1 per channel (unlike `shade_mat`'s: `srgb_to_linear` an sRGB colour first); `gobo` \u2014 the dapple,\n/// 0..1 (1 = lit); `noise` \u2014 blue noise 0..1 (the rods' own snow).\n/// \u2192 sRGB-ENCODED RGB, \u2248 0..1 \u2014 the Eigengrau's grey under everything, plus the snow (\xB1 `u.night.w` / 2).\nfn night_mat(u: MatUniforms, albedo: vec3f, gobo: f32, noise: f32) -> vec3f {\n let shade = u.moon.w + (1.0 - u.moon.w) * gobo; // the Moon, and the sky's share where it is blocked\n let cone = albedo * u.moon.xyz * shade; // the mat under the Moon, as the partly adapted cones take it\n let rod = max(night_scotopic(cone), 0.0) / night_scotopic(vec3f(1.0)); // the rods' signal, white = 1\n let L = u.night.y * night_luminance(cone) * 0.3183098862; // cd/m\xB2 on the desk: E \xB7 \u03C1 / \u03C0\n let see = mix(rod * u.rod.xyz, cone, night_mesopic(L)); // what the eye reports\n let disp = u.eigengrau.xyz + see * u.night.z; // the dark's own grey under it, at the appearance's scale\n return night_encode(disp) + (noise - 0.5) * u.night.w; // encoded; then the rods' own noise\n}\n\n// The mat's colour on screen: the day's chain, the night, or a cross-fade of the two.\n/// The MAT's colour on screen: the day's chain, the night's, or between them by `u.night.x`.\n/// `u` \u2014 the slot's view block; `albedo` \u2014 the colour handed to BOTH chains, as the mat's is (`mat_albedo`'s, linear \u2014 the\n/// reference's look); `gobo` \u2014 the dapple, 0..1; `noise` \u2014 blue noise 0..1.\n/// \u2192 the day's answer (`shade_mat`) while `u.night.x` \u2264 0, the night's (`night_mat`, encoded) from 1, a mix between. A kind\n/// with an sRGB colour composes its own: the colour into `shade_mat`, `srgb_to_linear` of it into `night_mat` (the book's\n/// `nb_colour`).\nfn mat_colour(u: MatUniforms, albedo: vec3f, gobo: f32, noise: f32) -> vec3f {\n if (u.night.x <= 0.0) { return shade_mat(u, albedo, gobo); }\n let night = night_mat(u, albedo, gobo, noise);\n if (u.night.x >= 1.0) { return night; }\n return mix(shade_mat(u, albedo, gobo), night, u.night.x);\n}\n",
|
|
730
|
+
"mat/mat-pass.wgsl": "// PASS 2, the cutting mat \u2014 one fullscreen triangle: the lattice's lines as\n// analytic coverage, the grain, the gobo projected onto the desk plane, the\n// reference's grade by day or the night's eye by night (MAT.md). Alpha 1 at\n// rest (a plain write); a flight fades it and clips it to the portal.\n\n@group(0) @binding(0) var<uniform> u: MatUniforms;\n@group(0) @binding(1) var gobo_tex: texture_2d<f32>;\n@group(0) @binding(2) var gobo_samp: sampler;\n@group(0) @binding(3) var noise_tex: texture_2d<f32>;\n@group(0) @binding(4) var noise_samp: sampler;\n@group(0) @binding(5) var glyph_tex: texture_2d<f32>; // the rulers' glyph atlas (RULER.md); a 1\xD71 empty cell until a host uploads one\n\nstruct VSOut { @builtin(position) clip: vec4f }\n\n@vertex\nfn vs(@builtin(vertex_index) vid: u32) -> VSOut {\n var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var out: VSOut;\n out.clip = vec4f(p[vid], 0.0, 1.0);\n return out;\n}\n\n@fragment\nfn fs(in: VSOut) -> @location(0) vec4f {\n let zoom = mat_zoom(u);\n let dpr = mat_dpr(u);\n let frag = in.clip.xy; // device px\n let screen = frag / dpr; // CSS px\n let px = 1.0 / (zoom * dpr); // world per device px\n let bn = textureSampleLevel(noise_tex, noise_samp, frag * u.noise.z + u.noise.xy, 0.0).rgb;\n // the rulers' print \u2014 screen-space ink mixed into the albedo before the light, so it is lit and shadowed like the lines\n let ruler = ruler_ink(u, screen, frag, glyph_tex, gobo_samp);\n let albedo = mat_albedo(u, u.phase.xy + screen / zoom, px, frag, bn, ruler);\n // the lamp's gobo where the LIGHT sees this pixel (MINIMAT.md \xA74): the slot's own camera at rest\n // (`light` = `cam`, so this is the desk under the pixel), the host desk's through a mini mat's face.\n // A viewport lamp uses (0, 0, 1): its projection stays in CSS pixels without an extra fragment branch.\n var gobo = project_gobo(u, gobo_tex, gobo_samp, desk_of(u, u.light.xy + screen / max(u.light.z, 1e-12)), bn.z);\n if (u.light.w > 0.0) { gobo = mix(gobo, project_gobo(u, gobo_tex, gobo_samp, desk_of(u, u.light2.xy + screen / max(u.light2.z, 1e-12)), bn.z), u.light.w); }\n // Straight alpha: the presentation's opacity through the portal clip \u2014 both 1 at rest,\n // when \"source over\" reduces to a plain write.\n return vec4f(mat_colour(u, albedo, gobo, bn.y), u.presence.x * portal_cover(screen, u.portals, u.clips, dpr));\n}\n",
|
|
731
|
+
"mat/mat.wgsl": "// The cutting mat's maths \u2014 a PURE module: no bindings. The uniform block and\n// the textures arrive as parameters. Every function body is the reference's\n// (research/tree-shadow/prototype/src/shaders.js), transcribed to the engine's\n// units: world = CSS px at zoom 1, the lattice's decade rungs, `MatUniforms`\n// from src/mat/layout.ts prepended by the engine.\n//\n// THE KIT'S `light` PIECE (`kitWgsl([..., \"light\"])` \u2014 `lamp`, `gobo`, `night` and `noise` name it too; petition I31: every\n// declaration's `///` block says what it takes and gives). THE CONVENTIONS every kit piece is written in:\n// - WORLD units: one is one CSS px at zoom 1. World x runs right and world y runs DOWN, as the screen's do. A slot's camera is\n// unwrapped world (`u.cam.xy`, the world point at the attachment's top-left); the lattice's PHASE is the camera wrapped to\n// the coarse rung's period (`u.phase.xy`), what the lines are drawn from so they stay exact at any distance.\n// - CSS px: the attachment's layout px, origin its top-left, y down \u2014 `(world \u2212 u.cam.xy) \xB7 zoom`.\n// - DEVICE px: the attachment's texels \u2014 a fragment's `@builtin(position).xy`, pixel centres at .5 \u2014 CSS px \xB7 dpr. So a\n// fragment's world point is `frag / (zoom \xB7 dpr) + u.cam.xy`, and one device px is `1 / (zoom \xB7 dpr)` world units.\n// - zoom = `mat_zoom(u)`: CSS px per world unit. dpr = `mat_dpr(u)`: device px per CSS px \u2014 the ATTACHMENT's own, below 1 for\n// a capture's thumbnail or the held desk copy (petition I29). Both > 0.\n// - the DESK: the plane the lamp lights, in the projector's frame \u2014 metres, y UP (three's): world x runs along its x, world y\n// along its z, and a height above the desk along its y (`desk_of`, `lit_desk`; `u.plane`).\n// - COLOUR: 0..1 per channel. sRGB-encoded unless a parameter says LINEAR; the swap chain is a plain 8-bit format, so what a\n// pass writes is what CSS would show. The day's chain (`shade_mat`) keeps its input's encoding; the night's (`night_mat`)\n// takes linear reflectance and returns encoded colour.\n\n/// The slot's zoom: CSS px per world unit.\n/// `u` \u2014 the slot's view block (`MatUniforms`, bound as `u`).\n/// \u2192 `u.cam.z`, floored at 1e-12 so it divides safely: > 0 (the desk's band is 1e-8 \u2026 1e8).\nfn mat_zoom(u: MatUniforms) -> f32 { return max(u.cam.z, 1e-12); }\n// The ATTACHMENT's own device ratio \u2014 below 1 for a capture's thumbnail (dpr \xD7 scale), the held desk copy (dpr / 2) and a page\n// zoomed out \u2014 guarded only against an unset block. Every fragment's CSS px is its device px over it: a floor at 1 put each one\n// at the wrong point of the desk there, and the flat kinds shaded nothing (petition I29).\n/// The attachment's device px per CSS px.\n/// `u` \u2014 the slot's view block.\n/// \u2192 `u.cam.w`, floored at 1e-6 against an unset block only: > 0, and below 1 for a thumbnail capture or the held desk copy.\nfn mat_dpr(u: MatUniforms) -> f32 { return max(u.cam.w, 1e-6); }\n\n// ---- the reference's noise (hash12 \xB7 valueNoise), on framebuffer pixels\n/// The reference's hash: one pseudo-random number per point (`sin`-based \u2014 cheap, not a quality RNG; best for |p| within a\n/// few thousand, where f32 `sin` keeps its digits).\n/// `p` \u2014 any point (the mat hands framebuffer device px, scaled).\n/// \u2192 [0, 1): a different value at every distinct `p`, with no continuity between neighbours.\nfn hash12(p: vec2f) -> f32 {\n let n = sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453;\n return n - floor(n);\n}\n\n/// Value noise: `hash12` at the four corners of `p`'s lattice cell, blended by a smoothstep weight.\n/// `p` \u2014 the point in the noise's own cells (one cell per unit of `p`; the mat's grain hands device px \xD7 0.45, a cell \u2248 2.2\n/// device px).\n/// \u2192 [0, 1): continuous, with a continuous gradient across cells, mean \u2248 0.5 \u2014 centre it with `value_noise(p) \u2212 0.5`\n/// ([\u22120.5, 0.5)).\nfn value_noise(p: vec2f) -> f32 {\n let i = floor(p);\n var f = fract(p);\n f = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(hash12(i), hash12(i + vec2f(1.0, 0.0)), f.x),\n mix(hash12(i + vec2f(0.0, 1.0)), hash12(i + vec2f(1.0, 1.0)), f.x),\n f.y);\n}\n\n/// The sRGB transfer curve, decoded (IEC 61966-2-1).\n/// `c` \u2014 sRGB-encoded, 0..1 per channel. Clamp first: below \u22120.055 the curve's unused branch is NaN, and NaN survives the mix.\n/// \u2192 LINEAR, 0..1 per channel.\nfn srgb_to_linear(c: vec3f) -> vec3f {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3f(2.4)), step(vec3f(0.04045), c));\n}\n\n/// RGB \u2192 HSB (hue, saturation, brightness).\n/// `c` \u2014 RGB, 0..1 per channel (either encoding \u2014 the result is in the same one).\n/// \u2192 x the hue in TURNS, [0, 1) (0 red, 1/3 green, 2/3 blue); y the saturation, [0, 1]; z the brightness `max(r, g, b)`, [0, 1].\nfn rgb2hsb(c: vec3f) -> vec3f {\n let K = vec4f(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);\n let p = mix(vec4f(c.bg, K.wz), vec4f(c.gb, K.xy), step(c.b, c.g));\n let q = mix(vec4f(p.xyw, c.r), vec4f(c.r, p.yzx), step(p.x, c.r));\n let d = q.x - min(q.w, q.y);\n let e = 1.0e-10;\n return vec3f(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);\n}\n\n/// HSB \u2192 RGB, `rgb2hsb`'s inverse.\n/// `c` \u2014 x the hue in turns (any value: only its fraction counts), y the saturation 0..1, z the brightness 0..1.\n/// \u2192 RGB, 0..1 per channel.\nfn hsb2rgb(c: vec3f) -> vec3f {\n let K = vec4f(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n let p = abs(fract(K.xyz + c.x) * 6.0 - K.w);\n return c.z * mix(K.xxx, clamp(p - K.x, vec3f(0.0), vec3f(1.0)), c.y);\n}\n\n// ---- the lines. `px` is world units per DEVICE pixel; half-widths are in device px.\n/// One family of parallel lines \u2014 one every `spacing` along an axis, a line at every multiple of it.\n/// `x` \u2014 the coordinate across the lines, world units (the lattice's phase); `spacing` \u2014 their pitch, world units, > 0;\n/// `half_width` \u2014 a line's half width, WORLD units (`line_weight`'s device px \xD7 `px`); `px` \u2014 world units per device px, > 0.\n/// \u2192 [0, 1]: 1 within `half_width \u2212 px` of a line, 0 beyond `half_width + px`, a smoothstep between (the AA).\nfn line_coverage(x: f32, spacing: f32, half_width: f32, px: f32) -> f32 {\n let d = abs(fract(x / spacing - 0.5) - 0.5) * spacing;\n return 1.0 - smoothstep(max(half_width - px, 0.0), half_width + px, d);\n}\n\n/// A square grid: `line_coverage` along both axes, the larger.\n/// `p` \u2014 the point, world units (the lattice's phase); `spacing`, `half_width`, `px` \u2014 as `line_coverage`'s, world units.\n/// \u2192 [0, 1]: the grid's coverage at `p`.\nfn grid2(p: vec2f, spacing: f32, half_width: f32, px: f32) -> f32 {\n return max(line_coverage(p.x, spacing, half_width, px), line_coverage(p.y, spacing, half_width, px));\n}\n\n// One law for every rung (src/lattice/line.ts `lineWeight`): fade in over the\n// engine's window by the rung's OWN cell, then thicken and darken over the\n// next decade. x = half-width (device px), y = alpha.\n/// The lattice's line law for one rung \u2014 src/lattice/line.ts `lineWeight`, number for number.\n/// `u` \u2014 the slot's view block (`u.lod`: the fade-in window, CSS px; `u.line`: the half-widths and alphas); `cell_px` \u2014 the\n/// rung's cell on screen, CSS px (its spacing \xD7 zoom).\n/// \u2192 x the line's half-width, DEVICE px, from `u.line.x` (at the window's top) to `u.line.y` (a decade above it); y its\n/// alpha: 0 at or below `u.lod.x`, `u.line.z` at `u.lod.y`, rising to `u.line.w` a decade above.\nfn line_weight(u: MatUniforms, cell_px: f32) -> vec2f {\n let s = smoothstep(u.lod.x, u.lod.y, cell_px);\n let t = saturate(log(max(cell_px, 1e-9) / u.lod.y) / 2.302585093);\n return vec2f(mix(u.line.x, u.line.y, t), u.line.z * s + (u.line.w - u.line.z) * t);\n}\n\n// The mat's albedo at a point: the reference's `cuttingMatAlbedo` with the\n// three lattice rungs in place of its own decade, and the line law above in\n// place of its fade ladder. `world` is lattice-phase world (wrapped); `px` is\n// world per device pixel; `frag` is the framebuffer pixel; `ruler` the rulers'\n// print at it (ruler.wgsl) \u2014 ink over the lines, and at 0 exactly the mix below\n// returns the lined mat untouched, bit for bit.\n/// The cutting mat's albedo at a point: the sage with its grain, the three rungs' cream lines, the rulers' ink over them.\n/// `u` \u2014 the slot's view block; `world` \u2014 the point in LATTICE-PHASE world units (`u.phase.xy + css / zoom` \u2014 wrapped, not\n/// the camera's world); `px` \u2014 world units per device px, > 0; `frag` \u2014 the fragment's device px (the grain's hash runs on\n/// it); `bn` \u2014 the blue-noise texel at the fragment, 0..1 per channel; `ruler` \u2014 the rulers' ink coverage there, 0..1\n/// (`ruler_ink`; 0 = the lined mat bit for bit).\n/// \u2192 LINEAR RGB, 0..1 per channel \u2014 the reference's quirk: the chain after it (`mat_colour`) takes it as is.\nfn mat_albedo(u: MatUniforms, world: vec2f, px: f32, frag: vec2f, bn: vec3f, ruler: f32) -> vec3f {\n var sage = u.ground.xyz;\n let amp = u.grain.x;\n var g = (hash12(frag * vec2f(0.73, 0.91)) - 0.5) * amp;\n g += (value_noise(frag * 0.45 + vec2f(u.view.z * 0.9, -u.view.z * 0.6)) - 0.5) * amp;\n g += (bn.x - 0.5) * amp * 0.5;\n sage += vec3f(g * 0.4, g, g * 0.35);\n\n // A rung under the window has alpha 0 and no coverage to compute \u2014 the\n // fine rung at most zooms. The branches are uniform across the frame.\n let zoom = mat_zoom(u);\n var cover = 0.0;\n let wf = line_weight(u, u.rungs.x * zoom);\n if (wf.y > 0.0) { cover = grid2(world, u.rungs.x, wf.x * px, px) * wf.y; }\n let wm = line_weight(u, u.rungs.y * zoom);\n if (wm.y > 0.0) { cover = max(cover, grid2(world, u.rungs.y, wm.x * px, px) * wm.y); }\n let wc = line_weight(u, u.rungs.z * zoom);\n if (wc.y > 0.0) { cover = max(cover, grid2(world, u.rungs.z, wc.x * px, px) * wc.y); }\n return srgb_to_linear(clamp(mix(mix(sage, u.ink.xyz, cover), u.ink.xyz, ruler), vec3f(0.0), vec3f(1.0)));\n}\n\n// ---- the gobo (the reference's sampleBlur \xB7 getGoboBlurRatio \xB7 sampleGobo)\n/// A linear ramp.\n/// `edge0`, `edge1` \u2014 where it starts and ends (`edge0` \u2260 `edge1`); `x` \u2014 the input, in their units.\n/// \u2192 [0, 1]: 0 at or before `edge0`, 1 at or past `edge1`, linear between.\nfn linearstep(edge0: f32, edge1: f32, x: f32) -> f32 {\n return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);\n}\n\n// Linearise the WebGL-range NDC z with the projector's near/far, then ramp\n// between the two depths: sharp near the projector, soft far from it.\n/// How soft the gobo's shadow is at a depth.\n/// `ndc_z` \u2014 the point's depth in the projector's clip space, WebGL range [\u22121, 1]; `p` \u2014 `u.goboParams`: x the sharp depth\n/// and y the soft one (linear, metres from the projector), z and w the projector's near and far planes (metres).\n/// \u2192 [0, 1]: 0 (sharp) at or nearer than `p.x`, 1 (soft) at or beyond `p.y`.\nfn blur_ratio(ndc_z: f32, p: vec4f) -> f32 {\n return linearstep(p.x, p.y, (2.0 * p.z * p.w) / (p.w + p.z - ndc_z * (p.w - p.z)));\n}\n\n// Six taps on a golden-angle spiral, the start angle from blue noise so the\n// pattern differs per pixel (and per frame while the offset moves).\n/// The gobo's silhouette blurred by six taps on a golden-angle spiral.\n/// `tex` \u2014 the silhouette (r8: 1 = open, 0 = leaf); `samp` \u2014 a linear sampler; `uv` \u2014 the plate point, 0..1 (v runs down the\n/// plate); `texel` \u2014 one plate texel in uv; `blur` \u2014 the spiral's reach in texels (\u2264 0: one tap, sharp); `n` \u2014 a per-pixel\n/// random 0..1 (blue noise) that turns the spiral.\n/// \u2192 [0, 1]: the silhouette's mean over the taps.\nfn sample_blur(tex: texture_2d<f32>, samp: sampler, uv: vec2f, texel: vec2f, blur: f32, n: f32) -> f32 {\n // No blur: six taps at one point are one tap. The sharp end of the desk.\n if (blur <= 0.0) { return textureSampleLevel(tex, samp, uv, 0.0).r; }\n let G = 2.39996323;\n let rot = mat2x2f(cos(G), sin(G), -sin(G), cos(G));\n let a0 = n * 6.28318530718;\n var d = vec2f(cos(a0), sin(a0));\n let s = texel * blur * (1.0 / sqrt(6.0));\n var c = 0.0;\n for (var i = 0; i < 6; i++) {\n c += textureSampleLevel(tex, samp, uv + d * sqrt(f32(i + 1)) * s, 0.0).r;\n d = rot * d;\n }\n return c / 6.0;\n}\n\n// The gobo term at a desk point (metres): 1 = lit. Outside the projector's\n// frame the plate is not consulted at all \u2014 the cone is finite, as the\n// reference's is.\n/// The lamp's dapple at a point already in the chosen projection space.\n/// `u` \u2014 the slot's view block (`u.goboMatrix` the projector, `u.gobo` its opacity and blur); `tex` \u2014 the slot's animated\n/// silhouette (`MatPass.silhouette`, r8, 1 = open); `samp` \u2014 a linear sampler; `desk` \u2014 the point in the projector's frame,\n/// metres, y up (`desk_of`, `lit_desk`, a kind's own desk point); `n` \u2014 blue noise 0..1 (the blur spiral's turn).\n/// \u2192 [0, 1]: 1 = fully lit. Exactly 1 outside the projector's frame and when the gobo is off (`u.gobo.x` \u2264 0); else\n/// `mix(1, the silhouette blurred by depth, u.gobo.x)`.\nfn project_gobo(u: MatUniforms, tex: texture_2d<f32>, samp: sampler, desk: vec3f, n: f32) -> f32 {\n if (u.gobo.x <= 0.0) { return 1.0; }\n var clip = u.goboMatrix * vec4f(desk, 1.0);\n let ndc = clip.xyz / clip.w;\n // The wind target's row 0 is the plate's top row, so v runs downward.\n let uv = vec2f(0.5 + 0.5 * ndc.x, 0.5 - 0.5 * ndc.y);\n if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { return 1.0; }\n let g = sample_blur(tex, samp, uv, vec2f(1.0 / 512.0), blur_ratio(ndc.z, u.goboParams) * u.gobo.y, n);\n return mix(1.0, g, u.gobo.x);\n}\n\n// A viewport gobo is a light over the visible desk, independent of canvas navigation. All kinds\n// enter here with their own desk point, so their surfaces pass through the same light as the mat.\n/// The lamp's dapple at a point of the slot's desk, respecting `u.wind.z` (0 = world, 1 = viewport).\n/// `u`, `tex`, `samp`, `n` \u2014 as `project_gobo`'s; `desk` \u2014 metres in the slot's world frame, y up.\n/// \u2192 [0, 1]: the projected light, with camera pan/zoom removed in viewport mode. Height scales with\n/// the visible object; the shadow's ground footprint stays at its original CSS-pixel position and size.\nfn sample_gobo(u: MatUniforms, tex: texture_2d<f32>, samp: sampler, desk: vec3f, n: f32) -> f32 {\n if (u.gobo.x <= 0.0) { return 1.0; }\n var receiver = desk;\n if (u.wind.z > 0.0) {\n let origin = vec3f(u.plane.x, u.plane.w, u.plane.y);\n let camera = vec3f(u.cam.x, 0.0, u.cam.y) * u.plane.z;\n receiver = origin + (desk - origin - camera) * mat_zoom(u);\n }\n return project_gobo(u, tex, samp, receiver, n);\n}\n\n// World (CSS px at zoom 1, unwrapped) \u2192 the desk plane in the projector's frame.\n/// A world point on the desk, in the projector's frame.\n/// `u` \u2014 the slot's view block (`u.plane`: the origin's x and z, metres per world unit, the desk's height); `world` \u2014 the\n/// point, UNWRAPPED world units (y down).\n/// \u2192 metres, y up: `(plane.x + world.x \xB7 m, plane.w, plane.y + world.y \xB7 m)`, m = `u.plane.z` \u2014 world y runs along the\n/// desk's z.\nfn desk_of(u: MatUniforms, world: vec2f) -> vec3f {\n return vec3f(u.plane.x + world.x * u.plane.z, u.plane.w, u.plane.y + world.y * u.plane.z);\n}\n\n// ---- the LIGHT a slot is lit by (MINIMAT.md \xA74; src/mat/layout.ts `SlotLight`). The lamp's gobo is\n// sampled where a light CAMERA sees a point, not where the slot's camera does: the same camera for the\n// root at rest; the host desk's for a mini mat's inside \u2014 the dapple runs across the mini mat as across\n// anything lying on the desk \u2014 and two of them cross-faded (`light.w`) while a flight hands one lamp to\n// the other. An object's world point `p` raised `h` world units, as light camera `L` (x, y, zoom) sees it:\n// the point on screen, back into L's world; a height scales as a length does.\n/// A point of the slot's world, raised, as a LIGHT camera sees it \u2014 in the projector's frame.\n/// `u` \u2014 the slot's view block; `L` \u2014 the light camera: x, y the world point at its top-left, z its zoom (`u.light` or\n/// `u.light2`; w unread); `p` \u2014 the point, the slot's world units; `h` \u2014 its height above the desk, world units, \u2265 0.\n/// \u2192 metres, y up: `p` carried through the screen into `L`'s world, its height scaled as a length is.\nfn lit_desk(u: MatUniforms, L: vec4f, p: vec2f, h: f32) -> vec3f {\n let s = max(u.cam.z, 1e-12) / max(L.z, 1e-12);\n let w = L.xy + (p - u.cam.xy) * s;\n return vec3f(u.plane.x + w.x * u.plane.z, u.plane.w + h * s * u.plane.z, u.plane.y + w.y * u.plane.z);\n}\n\n// The gobo on an object's world point `p` raised `h`: under the slot's own lamp the desk point itself,\n// `own` (the object's own `desk_at`, so a root's object reads exactly what it always read); else under\n// the light camera \u2014 then, while a flight hands it over, cross-faded to the second.\n/// The lamp's dapple on an object's point, under whichever lamp the slot is lit by.\n/// `u` \u2014 the slot's view block; `tex`, `samp` \u2014 the silhouette and its sampler, as `sample_gobo`'s; `p` \u2014 the object's point,\n/// the slot's world units; `h` \u2014 its height above the desk, world units, \u2265 0; `own` \u2014 the same point in the projector's frame\n/// as the object computes it (metres, y up), read when the slot is lit by its own camera; `n` \u2014 blue noise 0..1.\n/// \u2192 [0, 1]: 1 = fully lit (`sample_gobo` at the point the light sees; cross-faded by `u.light.w` toward `u.light2`'s).\nfn lit_gobo(u: MatUniforms, tex: texture_2d<f32>, samp: sampler, p: vec2f, h: f32, own: vec3f, n: f32) -> f32 {\n // The CPU selects the light camera, including a fixed (0, 0, 1) for viewport mode.\n // lit_desk already maps into that projection space; no second camera transform is needed.\n var g = 0.0;\n if (all(u.light.xyz == u.cam.xyz)) { g = project_gobo(u, tex, samp, own, n); }\n else { g = project_gobo(u, tex, samp, lit_desk(u, u.light, p, h), n); }\n if (u.light.w > 0.0) { g = mix(g, project_gobo(u, tex, samp, lit_desk(u, u.light2, p, h), n), u.light.w); }\n return g;\n}\n\n// The reference's grade: `shade = mix(1, floor + (1 \u2212 floor)\xB7gobo, mix)`, then\n// HSB: saturation up by (1 \u2212 shade)\xB7k, brightness \xD7 shade. The reference did\n// this on pow(albedo, 2.2) and wrote pow(1/2.2) \u2014 kept verbatim: that chain is\n// the mat's colour on screen.\n/// The DAY's chain \u2014 the lamp's dapple graded onto a colour.\n/// `u` \u2014 the slot's view block (`u.gobo.w` the dark floor, `u.gobo.z` the shade's mix, `u.grade.x` the saturation bump);\n/// `albedo` \u2014 the colour as it is written where lit, 0..1 per channel: an object's sRGB colour (the mat hands its own linear\n/// albedo \u2014 the reference's look); `gobo` \u2014 the dapple, 0..1 (1 = lit).\n/// \u2192 RGB 0..1 in `albedo`'s encoding: exactly `albedo` where `gobo` \u2265 1; in shade, brightness \xD7 shade and saturation up by\n/// (1 \u2212 shade) \xB7 `u.grade.x`, where shade = `mix(1, floor + (1 \u2212 floor) \xB7 gobo, u.gobo.z)` (\u2248 0.51 \u2026 1 by the theme's numbers).\nfn shade_mat(u: MatUniforms, albedo: vec3f, gobo: f32) -> vec3f {\n // Lit: shade is exactly 1, the saturation bump exactly 0, and the chain is\n // pow(pow(albedo, 2.2), 1/2.2) \u2014 the identity. Every pixel outside the\n // cone, and every lit one inside it, skips six pows and the HSB round trip.\n if (gobo >= 1.0) { return albedo; }\n let shade = mix(1.0, u.gobo.w + (1.0 - u.gobo.w) * gobo, u.gobo.z);\n var hsb = rgb2hsb(pow(albedo, vec3f(2.2)));\n hsb.y = min(hsb.y + (1.0 - shade) * u.grade.x, 1.0);\n hsb.z = hsb.z * shade;\n return pow(hsb2rgb(hsb), vec3f(1.0 / 2.2));\n}\n\n// ---- the NIGHT (MAT.md; src/mat/night.ts is the same arithmetic on the CPU):\n// the Moon in place of the Sun, and the eye that sees by it. The day's chain\n// above is untouched \u2014 `mat_colour` returns it, alone, when `night.x` is 0.\n/// Photopic luminance Y (the sRGB / Rec. 709 weights).\n/// `c` \u2014 LINEAR RGB.\n/// \u2192 Y, in `c`'s scale (0..1 for 0..1 channels; white is 1).\nfn night_luminance(c: vec3f) -> f32 { return dot(c, vec3f(0.2126729, 0.7151522, 0.0721750)); }\n\n// Scotopic luminance V' from XYZ \u2014 Larson, Rushmeier & Piatko 1997's regression:\n// the rods peak at 507 nm, so greens and blues keep their brightness by night and reds die.\n/// Scotopic luminance V\u2032 \u2014 what the rods see (Larson, Rushmeier & Piatko 1997).\n/// `c` \u2014 LINEAR RGB.\n/// \u2192 V\u2032 in `c`'s scale: \u2265 0 over the sRGB gamut, white \u2248 2.57 (`night_mat` normalises by it).\nfn night_scotopic(c: vec3f) -> f32 {\n let X = dot(c, vec3f(0.4124564, 0.3575761, 0.1804375));\n let Y = dot(c, vec3f(0.2126729, 0.7151522, 0.0721750));\n let Z = dot(c, vec3f(0.0193339, 0.1191920, 0.9503041));\n return Y * (1.33 * (1.0 + (Y + Z) / max(X, 1e-6)) - 1.68);\n}\n\n// CIE 191:2010's mesopic weight \u2014 the cones' share of the seeing at L cd/m\xB2:\n// 0 at 0.005 (the rods alone), 1 at 5 (the cones alone).\n/// The cones' share of the seeing at a luminance (CIE 191:2010's mesopic weight).\n/// `L` \u2014 luminance, cd/m\xB2, > 0.\n/// \u2192 [0, 1]: 0 at or below 0.005 cd/m\xB2 (the rods alone), 1 at or above 5 (the cones alone), linear in log10(L) between.\nfn night_mesopic(L: f32) -> f32 { return saturate(0.767 + 0.3334 * log(max(L, 1e-9)) * 0.4342944819); }\n\n/// The sRGB transfer curve, encoded \u2014 `srgb_to_linear`'s inverse.\n/// `c` \u2014 LINEAR RGB, \u2265 0 (0..1 for an on-screen colour).\n/// \u2192 sRGB-encoded RGB (0..1 for 0..1).\nfn night_encode(c: vec3f) -> vec3f {\n return mix(c * 12.92, 1.055 * pow(max(c, vec3f(0.0)), vec3f(1.0 / 2.4)) - 0.055, step(vec3f(0.0031308), c));\n}\n\n/// The NIGHT's chain \u2014 a reflectance under the Moon, as the dark-adapted eye reports it.\n/// `u` \u2014 the slot's view block (`u.moon`, `u.night`, `u.rod`, `u.eigengrau`: the theme's night, mat/night.ts); `albedo` \u2014\n/// LINEAR reflectance, 0..1 per channel (unlike `shade_mat`'s: `srgb_to_linear` an sRGB colour first); `gobo` \u2014 the dapple,\n/// 0..1 (1 = lit); `noise` \u2014 blue noise 0..1 (the rods' own snow).\n/// \u2192 sRGB-ENCODED RGB, \u2248 0..1 \u2014 the Eigengrau's grey under everything, plus the snow (\xB1 `u.night.w` / 2).\nfn night_mat(u: MatUniforms, albedo: vec3f, gobo: f32, noise: f32) -> vec3f {\n let shade = u.moon.w + (1.0 - u.moon.w) * gobo; // the Moon, and the sky's share where it is blocked\n let cone = albedo * u.moon.xyz * shade; // the mat under the Moon, as the partly adapted cones take it\n let rod = max(night_scotopic(cone), 0.0) / night_scotopic(vec3f(1.0)); // the rods' signal, white = 1\n let L = u.night.y * night_luminance(cone) * 0.3183098862; // cd/m\xB2 on the desk: E \xB7 \u03C1 / \u03C0\n let see = mix(rod * u.rod.xyz, cone, night_mesopic(L)); // what the eye reports\n let disp = u.eigengrau.xyz + see * u.night.z; // the dark's own grey under it, at the appearance's scale\n return night_encode(disp) + (noise - 0.5) * u.night.w; // encoded; then the rods' own noise\n}\n\n// The mat's colour on screen: the day's chain, the night, or a cross-fade of the two.\n/// The MAT's colour on screen: the day's chain, the night's, or between them by `u.night.x`.\n/// `u` \u2014 the slot's view block; `albedo` \u2014 the colour handed to BOTH chains, as the mat's is (`mat_albedo`'s, linear \u2014 the\n/// reference's look); `gobo` \u2014 the dapple, 0..1; `noise` \u2014 blue noise 0..1.\n/// \u2192 the day's answer (`shade_mat`) while `u.night.x` \u2264 0, the night's (`night_mat`, encoded) from 1, a mix between. A kind\n/// with an sRGB colour composes its own: the colour into `shade_mat`, `srgb_to_linear` of it into `night_mat` (the book's\n/// `nb_colour`).\nfn mat_colour(u: MatUniforms, albedo: vec3f, gobo: f32, noise: f32) -> vec3f {\n if (u.night.x <= 0.0) { return shade_mat(u, albedo, gobo); }\n let night = night_mat(u, albedo, gobo, noise);\n if (u.night.x >= 1.0) { return night; }\n return mix(shade_mat(u, albedo, gobo), night, u.night.x);\n}\n",
|
|
736
732
|
"mat/ruler.wgsl": "// The RULERS printed on the mat (RULER.md) \u2014 a PURE module: no bindings. The\n// ink coverage at a fragment from the frame's lines, the ticks and the labels,\n// in screen space; the mat pass mixes it into the albedo before the light, so\n// the print is lit and shadowed like the lines. Every number is a uniform\n// `matUniformValues` fills from lattice/ruler.ts \u2014 the levels' spacings,\n// presences and tick lengths per frame; the shader only lays them out.\n//\n// Layout: the frame's outer rect one `margin` in from the viewport; the top\n// and left BANDS one `band` wide inside it, each closed by its inner line\n// (the ticks' baseline). A tick stands on the baseline at every site of every\n// level, its length the level's, its presence the level's. A label sits in\n// the band's outer half after its tick, laid out on the finest labelled\n// level's sites; the left ruler's labels are the top's rotated a quarter turn\n// clockwise (reading down the band, their tops toward the field) \u2014 James's\n// mockup. All distances are DEVICE px inside; positions arrive as CSS px.\n//\n// THE KIT'S `ruler` PIECE (`kitWgsl([..., \"ruler\"])`, after `light`, whose `mat_zoom` and `mat_dpr` it reads; petition I31 \u2014\n// every declaration's `///` block says what it takes and gives). A face that prints the mat's numerals composes it; the ink it\n// answers is coverage 0..1, mixed into an albedo before the light (the mat's `mat_albedo` takes it as `ruler`).\n\n/// The ruler's levels: `u.rulerSites` and `u.rulerFormat` hold one record each (lattice/ruler.ts lays them out).\nconst RULER_LEVELS = 5;\n/// Texels between a glyph-atlas cell's left edge and its glyph's origin \u2014 mat/layout.ts `GLYPH_PAD`.\nconst GLYPH_PAD = 2.0;\n\n/// A printed line's coverage.\n/// `d` \u2014 the signed distance across the line, DEVICE px; `hw` \u2014 its half-width, device px, \u2265 0.\n/// \u2192 [0, 1]: 1 within `hw \u2212 0.5` of the line, 0 beyond `hw + 0.5`, a smoothstep between (a one-device-px AA ramp).\nfn ruler_line(d: f32, hw: f32) -> f32 { return 1.0 - smoothstep(hw - 0.5, hw + 0.5, abs(d)); }\n/// Inside [a, b] \u2014 the ends of a segment, the extents of a band.\n/// `x` \u2014 the value; `a`, `b` \u2014 the closed interval's ends, in `x`'s units (`a` \u2264 `b`).\n/// \u2192 1 when `a` \u2264 `x` \u2264 `b`, else 0.\nfn ruler_in(x: f32, a: f32, b: f32) -> f32 { return step(a, x) * step(x, b); }\n\n/// The decimal digits a number is written with.\n/// `a` \u2014 the number, \u2265 0.\n/// \u2192 its digit count, \u2265 1 (0 is written with one).\nfn ruler_digits(a: i32) -> i32 { var n = 1; var v = a; loop { if (v < 10) { break; } v = v / 10; n += 1; } return n; }\n/// A power of ten, as an integer.\n/// `p` \u2014 the exponent, 0 \u2026 9 (past 9 the i32 overflows).\n/// \u2192 10^`p`.\nfn ruler_pow10(p: i32) -> i32 { var r = 1; for (var i = 0; i < p; i++) { r = r * 10; } return r; }\n/// One digit of a number written to a width.\n/// `a` \u2014 the number, \u2265 0; `i` \u2014 the digit's place, 0 = the leftmost; `nd` \u2014 the width in digits (leading zeros where `a` is\n/// shorter), \u2265 `ruler_digits(a)`.\n/// \u2192 the digit, 0 \u2026 9.\nfn ruler_digit_at(a: i32, i: i32, nd: i32) -> i32 { return (a / ruler_pow10(nd - 1 - i)) % 10; }\n\n/// A label's text, one character at a time \u2014 lattice/ruler.ts `formatLabel`: plain digits, no suffix; a fraction with its\n/// trailing zeros stripped, so \"1.0\" is \"1\" at every level.\n/// `index` \u2014 the site's index along the ruler (any sign); `mult`, `exp` \u2014 the level's format: the label is `index \xB7 mult \xB7\n/// 10^exp`; `k` \u2014 the character slot, \u2265 0.\n/// \u2192 the glyph at slot `k`: 0 \u2026 9 a digit, 10 '-', 11 '.' (the atlas's first twelve cells, `GLYPHS`), \u22121 past the end.\nfn ruler_label(index: i32, mult: i32, exp: i32, k: i32) -> i32 {\n let M = index * mult;\n if (M == 0) { return select(-1, 0, k == 0); }\n let neg = M < 0;\n let a = abs(M);\n var i = k;\n if (neg) { if (i == 0) { return 10; } i -= 1; }\n if (exp >= 0) {\n let nd = ruler_digits(a);\n if (i < nd) { return ruler_digit_at(a, i, nd); }\n if (i < nd + exp) { return 0; }\n return -1;\n }\n let p = -exp;\n let nd = max(ruler_digits(a), p + 1);\n let intDigits = nd - p;\n var fracLen = p;\n loop { if (fracLen == 0) { break; } if ((a / ruler_pow10(p - fracLen)) % 10 != 0) { break; } fracLen -= 1; }\n if (i < intDigits) { return ruler_digit_at(a, i, nd); }\n if (i == intDigits) { return select(-1, 11, fracLen > 0); }\n let j = i - intDigits - 1;\n if (j < fracLen) { return ruler_digit_at(a, intDigits + j, nd); }\n return -1;\n}\n/// A label's length.\n/// `index`, `mult`, `exp` \u2014 as `ruler_label`'s.\n/// \u2192 the number of characters `ruler_label` answers before \u22121, 0 \u2026 16.\nfn ruler_label_len(index: i32, mult: i32, exp: i32) -> i32 {\n var n = 0;\n loop { if (n >= 16) { break; } if (ruler_label(index, mult, exp, n) < 0) { break; } n += 1; }\n return n;\n}\n\n/// What one band prints at a fragment: `ink` \u2014 the ticks' coverage, 0..1 (times their alpha); `glyph` \u2014 where the fragment lies\n/// in a label, the atlas point to sample (x, y \u2014 texels) and the label's presence (z, 0..1); z = 0 where no label is.\nstruct RulerHit { ink: f32, glyph: vec3f }\n\n/// The ticks and the label along ONE band, at one fragment.\n/// `u` \u2014 the slot's view block (the `ruler*` records); `along` \u2014 the fragment's CSS px along the band (x for the top band, y for\n/// the left); `across` \u2014 its DEVICE px in from the band's outer line; `phase` \u2014 the lattice's wrapped camera coordinate along\n/// the band (`u.phase.x` or `.y`, world units); `wraps` \u2014 that coordinate's wrap count (`u.rulerOrigin.x` or `.y`); `lim` \u2014\n/// the band's extent along, CSS px (the inner corner, the far frame line); `turned` \u2014 the left band (its glyphs turned a\n/// quarter clockwise).\n/// \u2192 a `RulerHit`: nothing (ink 0, glyph.z 0) outside the band's own pixels.\nfn ruler_band(u: MatUniforms, along: f32, across: f32, phase: f32, wraps: f32, lim: vec2f, turned: bool) -> RulerHit {\n var hit: RulerHit;\n hit.ink = 0.0; hit.glyph = vec3f(0.0);\n let dpr = mat_dpr(u); let zoom = mat_zoom(u);\n let band_dev = u.ruler.z * dpr;\n let hw = 0.5 * u.ruler.w * dpr;\n // only the band's own pixels pay for the levels and the text \u2014 the rest of the mat returns here\n if (across < -1.0 || across > band_dev + 1.0 || ruler_in(along, lim.x, lim.y) <= 0.0) { return hit; }\n let w = phase + along / zoom; // lattice-phase world along the band\n // the ticks: on the baseline (the band's inner edge), up the level's length; the coarsest level owning a site wins the max\n var tick = 0.0;\n for (var i = 0; i < RULER_LEVELS; i++) {\n let s = u.rulerSites[i].x;\n let len = u.rulerSites[i].y * dpr;\n let a = u.rulerSites[i].z;\n if (a <= 0.0 || len <= 0.0) { continue; }\n let d = (abs(fract(w / s - 0.5) - 0.5) * s) * zoom * dpr;\n tick = max(tick, ruler_line(d, hw) * ruler_in(across, band_dev - len - 0.5, band_dev + 0.5) * a);\n }\n hit.ink = tick * u.rulerAlpha.y;\n // the label: laid out on the finest labelled level's sites, the site at or before the fragment\n let lf = i32(u.rulerOrigin.z);\n if (lf < 0 || u.rulerAtlas.w <= 0.0) { return hit; }\n let s = u.rulerSites[lf].x;\n let nLocal = floor(w / s);\n let site = (nLocal * s - phase) * zoom; // CSS px along\n if (site < lim.x) { return hit; } // a site inside the corner square prints nothing\n let index = i32(wraps) * i32(u.rulerFormat[lf].z) + i32(nLocal);\n // the site's presence: the coarsest level it lies on\n var presence = u.rulerSites[lf].w;\n for (var j = RULER_LEVELS - 1; j > lf; j--) {\n let ratio = i32(u.rulerFormat[lf].z) / i32(u.rulerFormat[j].z);\n if (index % ratio == 0) { presence = u.rulerSites[j].w; break; }\n }\n if (presence <= 0.0) { return hit; }\n let mult = i32(u.rulerFormat[lf].x); let exp = i32(u.rulerFormat[lf].y);\n let texel = u.rulerText.w / dpr; // atlas texels per device px\n let adv = u.rulerGlyph.z / texel; // a glyph's advance, device px\n let origin = floor((site + u.rulerText.y) * dpr + 0.5); // the text's start, on a whole device px\n let t = along * dpr - origin;\n if (t < 0.0) { return hit; }\n let slot = i32(floor(t / adv));\n let g = ruler_label(index, mult, exp, slot);\n if (g < 0) { return hit; }\n // the glyph cell. The top band: the cap's top `top` in from the outer line, the glyph read down the band. The left band: the\n // glyph turned a quarter CLOCKWISE \u2014 read down the band still, but its baseline `top` in from the outer line and its top toward\n // the field (the mockup's \"1\" above \"0\", the foot of the \"1\" at the outer side). Reading along +y with the cell's rows along \u2212x\n // keeps the hand: a rotation, not the mirror a transpose would be.\n var v = 0.0;\n if (turned) { v = u.rulerGlyph.w - (across - u.rulerText.x * dpr) * texel; }\n else { v = (across - (u.rulerText.x * dpr - (u.rulerGlyph.w - u.rulerAtlas.z) / texel)) * texel; }\n if (v < 0.0 || v >= u.rulerGlyph.y) { return hit; }\n let uu = f32(g) * u.rulerGlyph.x + GLYPH_PAD + (t - f32(slot) * adv) * texel;\n hit.glyph = vec3f(uu, v, presence);\n return hit;\n}\n\n// The whole print: the frame's lines, the two bands' ticks, the labels'\n// texels sampled from the atlas. Returns ink coverage 0..1 at this fragment.\n/// The rulers' whole print at a fragment: the frame's lines, both bands' ticks, the labels' texels.\n/// `u` \u2014 the slot's view block (`u.ruler.x` on/off, the `ruler*` records, the view's size `u.view.xy`); `screen` \u2014 the\n/// fragment's CSS px; `frag` \u2014 its device px (`@builtin(position).xy`); `glyph_tex` \u2014 the glyph atlas (r8; `MatPass\n/// .glyphTexture`); `samp` \u2014 a sampler for it.\n/// \u2192 ink coverage, 0..1 \u2014 exactly 0 when the rulers are off (`u.ruler.x` \u2264 0).\nfn ruler_ink(u: MatUniforms, screen: vec2f, frag: vec2f, glyph_tex: texture_2d<f32>, samp: sampler) -> f32 {\n if (u.ruler.x <= 0.0) { return 0.0; }\n let dpr = mat_dpr(u);\n let margin = u.ruler.y; let band = u.ruler.z;\n let W = u.view.x; let H = u.view.y;\n let hw = 0.5 * u.ruler.w * dpr;\n let m = margin * dpr; let inner = (margin + band) * dpr;\n let right = (W - margin) * dpr; let bottom = (H - margin) * dpr;\n // the frame: the outer rect, and the two baselines closing the bands\n var frame = 0.0;\n frame = max(frame, ruler_line(frag.x - m, hw) * ruler_in(frag.y, m - hw, bottom + hw));\n frame = max(frame, ruler_line(frag.x - right, hw) * ruler_in(frag.y, m - hw, bottom + hw));\n frame = max(frame, ruler_line(frag.y - m, hw) * ruler_in(frag.x, m - hw, right + hw));\n frame = max(frame, ruler_line(frag.y - bottom, hw) * ruler_in(frag.x, m - hw, right + hw));\n frame = max(frame, ruler_line(frag.x - inner, hw) * ruler_in(frag.y, m, bottom));\n frame = max(frame, ruler_line(frag.y - inner, hw) * ruler_in(frag.x, m, right));\n var ink = frame * u.rulerAlpha.x;\n // the top band (across = down from the outer line) and the left band (across = right from it); a pixel in neither pays nothing more.\n // The labels: the top band's cell read along x, the left band's along y and turned \u2014 the same atlas texels either way.\n if (ruler_in(frag.y, m - 1.0, inner + 1.0) * ruler_in(frag.x, inner - 1.0, right + 1.0) > 0.0) {\n let top = ruler_band(u, screen.x, frag.y - m, u.phase.x, u.rulerOrigin.x, vec2f(margin + band, W - margin), false);\n ink = max(ink, top.ink);\n if (top.glyph.z > 0.0) { ink = max(ink, textureSampleLevel(glyph_tex, samp, top.glyph.xy / u.rulerAtlas.xy, 0.0).r * top.glyph.z * u.rulerAlpha.z); }\n }\n if (ruler_in(frag.x, m - 1.0, inner + 1.0) * ruler_in(frag.y, inner - 1.0, bottom + 1.0) > 0.0) {\n let left = ruler_band(u, screen.y, frag.x - m, u.phase.y, u.rulerOrigin.y, vec2f(margin + band, H - margin), true);\n ink = max(ink, left.ink);\n if (left.glyph.z > 0.0) { ink = max(ink, textureSampleLevel(glyph_tex, samp, left.glyph.xy / u.rulerAtlas.xy, 0.0).r * left.glyph.z * u.rulerAlpha.z); }\n }\n return ink;\n}\n",
|
|
737
733
|
"mat/wind.wgsl": "// The wind pass \u2014 the reference's `goboWind`: one fragment per plate texel,\n// R silhouette + three sines whose amplitudes and phase the plate's other\n// channels carry, written to an r8 target the mat pass samples. Runs only when\n// the gobo time moved; the target is 512\xB2 like the plate.\n\n@group(0) @binding(0) var<uniform> u: MatUniforms;\n@group(0) @binding(1) var plate: texture_2d<f32>;\n\nstruct VSOut { @builtin(position) clip: vec4f }\n\n@vertex\nfn vs(@builtin(vertex_index) vid: u32) -> VSOut {\n var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var out: VSOut;\n out.clip = vec4f(p[vid], 0.0, 1.0);\n return out;\n}\n\n@fragment\nfn fs(in: VSOut) -> @location(0) vec4f {\n let params = textureLoad(plate, vec2i(in.clip.xy), 0);\n let time = u.wind.x;\n let strength = u.wind.y;\n let s = params.r;\n let amp1 = params.g * strength;\n let phase1 = (params.b - 0.5) * 6.283185;\n let amp23 = params.a * strength;\n let waves = sin(time * vec3f(1.0, 2.0, 3.0) + phase1 * vec3f(1.0, 2.0, 0.5));\n let motion = (amp1 * waves.x) + (amp23 * 0.5 * waves.y) + (amp23 * 0.3 * waves.z);\n let v = clamp(s + motion, 0.0, 1.0);\n return vec4f(v, 0.0, 0.0, 1.0);\n}\n",
|
|
738
734
|
"missing/missing-pass.wgsl": "// THE MISSING FACE (petition I24; desk src/missing/) \u2014 what the desk draws where NOTHING can draw an object: its kind refused at\n// create or quarantined at three strikes (src/faults.ts), or its type without a kind on this desk. A faint card the size of the\n// object's box, square to the mat: a wash of the ink, the ink's hatching at 45\xB0 (a pitch and a weight in CSS px, anchored to the\n// box's corner, so it reads at every zoom and moves with the object) and a hairline edge inside the box \u2014 no text: the kind's name\n// is said in the notice, never on the desk. Premultiplied, through the slot's objects' presence (`u.view.w`) and its portal chain,\n// as every kind's face. The record (`MissingFace`, src/missing/layout.ts): `rect` \u2014 the centre and half extents, world units;\n// `ink` \u2014 the colour, and the object's fade in w; `mix` \u2014 the wash's, the hatching's and the edge's alpha; `shape` \u2014 the corner\n// (world units), the hatching's pitch and weight and the edge's weight (CSS px).\n\n@group(0) @binding(0) var<uniform> u: MatUniforms;\n@group(0) @binding(1) var<storage, read> faces: array<MissingFace>;\n@group(0) @binding(2) var<storage, read> order: array<u32>; // the draw list: paint index \u2192 the record's slot\n\nstruct VSOut {\n @builtin(position) clip: vec4f,\n @location(0) @interpolate(flat) idx: u32,\n}\n\nconst CORNERS = array<vec2f, 6>(\n vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0),\n vec2f(-1.0, -1.0), vec2f(1.0, 1.0), vec2f(-1.0, 1.0),\n);\n\n@vertex\nfn vs(@builtin(vertex_index) vid: u32, @builtin(instance_index) iid: u32) -> VSOut {\n var out: VSOut;\n out.clip = vec4f(2.0, 2.0, 2.0, 1.0); // degenerate: a culled face (or a gone one, fade 0) collapses\n let slot = order[iid];\n out.idx = slot;\n let F = faces[slot];\n if (F.ink.w <= 0.0) { return out; }\n let zoom = max(u.cam.z, 1e-12);\n let dpr = max(u.cam.w, 1e-6); // the attachment's own ratio, below 1 too (mat.wgsl `mat_dpr`, petition I29)\n let pad = 2.0 / (zoom * dpr); // two device px of the edge's ramp\n let lo = (F.rect.xy - F.rect.zw - vec2f(pad) - u.cam.xy) * zoom; // CSS px\n let hi = (F.rect.xy + F.rect.zw + vec2f(pad) - u.cam.xy) * zoom;\n if (hi.x < u.box.x || hi.y < u.box.y || lo.x > u.box.x + u.box.z || lo.y > u.box.y + u.box.w) { return out; }\n let pos = mix(lo, hi, CORNERS[vid] * 0.5 + 0.5);\n out.clip = vec4f(pos.x / u.view.x * 2.0 - 1.0, 1.0 - pos.y / u.view.y * 2.0, 0.0, 1.0);\n return out;\n}\n\n@fragment\nfn fs(in: VSOut) -> @location(0) vec4f {\n let F = faces[in.idx];\n let zoom = max(u.cam.z, 1e-12);\n let dpr = max(u.cam.w, 1e-6);\n let px = 1.0 / (zoom * dpr); // one device px, world units\n let p = in.clip.xy * px + u.cam.xy; // device px \u2192 world\n let r = min(F.shape.x, min(F.rect.z, F.rect.w));\n let d = sdf_round_box(p - F.rect.xy, F.rect.zw, r) / px; // device px; < 0 inside the box\n let inside = clamp(0.5 - d, 0.0, 1.0);\n if (inside <= 0.0) { discard; }\n // the hatching: lines at 45\xB0, `pitch` CSS px apart and `weight` wide, from the box's top-left corner\n let corner = (F.rect.xy - F.rect.zw - u.cam.xy) * zoom * dpr; // device px\n let s = in.clip.xy - corner;\n let pitch = F.shape.y * dpr;\n let t = (s.x + s.y) * 0.70710678;\n let k = abs(fract(t / pitch + 0.5) - 0.5) * pitch; // device px to the nearest line\n let hatch = clamp(0.5 * F.shape.z * dpr - k + 0.5, 0.0, 1.0);\n // the edge: a hairline just inside the box\n let ew = F.shape.w * dpr;\n let edge = clamp(0.5 * ew - abs(d + 0.5 * ew) + 0.5, 0.0, 1.0);\n var a = F.mix.x;\n a = a + (1.0 - a) * F.mix.y * hatch;\n a = a + (1.0 - a) * F.mix.z * edge;\n let cov = a * inside * F.ink.w;\n return vec4f(F.ink.xyz * cov, cov) * (u.view.w * portal_cover(in.clip.xy / dpr, u.portals, u.clips, dpr));\n}\n",
|
|
739
735
|
"portal.wgsl": "// The portal clip \u2014 a slot's ground and frames are visible only through a\n// rounded rect on screen (CSS px): its FACE and, for a nested slot, every face\n// above it \u2014 the CHAIN (src/nav/portal.ts `Presentation`, `within`). A PURE\n// module every pass includes: the records arrive as parameters. `clips[i].y`\n// < 0.5 ends the chain (an empty chain is no clip at all, and the loop is one\n// compare); `clips[i].x` is a face's corner radius; `clips[i].z` its top\n// FEATHER (CSS px; 0 = a hard edge \u2014 design-018 \xA74, the tray's face: what it\n// shows fades in over that band below its top edge, with no code of its own).\n//\n// THE KIT'S `portal` PIECE (`kitWgsl([..., \"portal\"])`; petition I31). Its points are CSS px of the ATTACHMENT \u2014 origin the\n// top-left, y down: a fragment's `@builtin(position).xy` (device px) over `mat_dpr(u)` \u2014 and the chain is the slot's view\n// block's (`u.portals`, `u.clips`). A kind multiplies what it writes by `portal_cover` (premultiplied: rgb and alpha; straight\n// alpha: alpha), so it shows only through its slot's faces; at the root the chain is empty and the cover is exactly 1.\n\n/// The portal chain's length: the faces `u.portals` and `u.clips` hold, nearest first (6 \u2014 a host's depth belt of four and a\n/// flight's face). = nav/portal.ts `PORTAL_CHAIN`, the uniform arrays' length; a mismatch fails to compile.\nconst PORTAL_CHAIN = 6u;\n\n/// A face's outline: a rounded rect centred on the origin.\n/// `p` \u2014 the point relative to the face's centre, CSS px; `half` \u2014 the face's half extents, CSS px, \u2265 0; `r` \u2014 its corner\n/// radius, CSS px, \u2265 0 (clamped to the shorter half extent).\n/// \u2192 the signed distance to the outline, CSS px: negative inside, 0 on the edge, positive outside.\nfn portal_sd(p: vec2f, half: vec2f, r: f32) -> f32 {\n let rr = min(r, min(half.x, half.y));\n let q = abs(p) - half + vec2f(rr, rr);\n return length(max(q, vec2f(0.0))) + min(max(q.x, q.y), 0.0) - rr;\n}\n\n/// The cover through ONE face.\n/// `p_css` \u2014 the fragment's point, CSS px of the attachment; `portal` \u2014 the face: its centre (xy) and half extents (zw), CSS px;\n/// `clip` \u2014 its corner radius (x, CSS px), on (y \u2014 read by the chain's loop, not here), top feather (z, CSS px; \u2264 0 = a hard\n/// edge), w unused; `dpr` \u2014 the attachment's device px per CSS px, > 0 (below 1 too \u2014 petition I29); `grow` \u2014 device px the\n/// face is grown by, \u2265 0 (0 = the face as it is).\n/// \u2192 [0, 1]: 1 inside, 0 outside, a linear ramp one DEVICE px wide centred on the grown edge; times, with a feather, a\n/// smoothstep from 0 at the face's top edge to 1 `clip.z` CSS px below it.\nfn portal_cover_one(p_css: vec2f, portal: vec4f, clip: vec4f, dpr: f32, grow: f32) -> f32 {\n let px = 1.0 / max(dpr, 1e-6); // CSS px a device px \u2014 the attachment's own ratio, below 1 too (petition I29)\n let d = portal_sd(p_css - portal.xy, portal.zw, clip.x) - grow * px;\n let c = clamp(0.5 - d / px, 0.0, 1.0);\n if (clip.z <= 0.0) { return c; }\n let top = portal.y - portal.w;\n return c * smoothstep(top, top + clip.z, p_css.y);\n}\n\n/// The cover through the whole chain, its first face grown.\n/// `p_css` \u2014 the fragment's point, CSS px of the attachment; `portals`, `clips` \u2014 the chain (`u.portals`, `u.clips`): face `i`\n/// as `portal_cover_one` reads it, nearest first, the first `clips[i].y` < 0.5 ending it; `dpr` \u2014 device px per CSS px, > 0;\n/// `grow` \u2014 device px the FIRST face (the slot's own) is grown by, \u2265 0 \u2014 every face above it is taken as it is (the fill's one\n/// px, so the hole's edge partitions with the plate \u2014 PORTAL.md \xA710).\n/// \u2192 [0, 1]: the product of the faces' covers \u2014 a nested slot shows only where every face agrees; exactly 1 for an empty chain.\nfn portal_cover_grown(p_css: vec2f, portals: array<vec4f, PORTAL_CHAIN>, clips: array<vec4f, PORTAL_CHAIN>, dpr: f32, grow: f32) -> f32 {\n var c = 1.0;\n for (var i = 0u; i < PORTAL_CHAIN; i++) {\n if (clips[i].y < 0.5) { break; }\n c *= portal_cover_one(p_css, portals[i], clips[i], dpr, select(0.0, grow, i == 0u));\n }\n return c;\n}\n\n/// The cover a kind multiplies its output by: the slot's chain, no face grown.\n/// `p_css` \u2014 the fragment's point, CSS px of the attachment (`@builtin(position).xy / mat_dpr(u)`); `portals`, `clips` \u2014 the\n/// slot's chain, `u.portals` and `u.clips`; `dpr` \u2014 the attachment's device px per CSS px, `mat_dpr(u)`.\n/// \u2192 [0, 1]: 1 where the slot is wholly visible (everywhere, at the root), 0 outside its faces, a one-device-px ramp on each\n/// edge and a face's top feather where it has one.\nfn portal_cover(p_css: vec2f, portals: array<vec4f, PORTAL_CHAIN>, clips: array<vec4f, PORTAL_CHAIN>, dpr: f32) -> f32 {\n return portal_cover_grown(p_css, portals, clips, dpr, 0.0);\n}\n",
|
|
740
736
|
"primitives.wgsl": "// 2D primitives for the desk's objects \u2014 the note, the whiteboard, the photo,\n// the mini mat. Pure functions, exact Euclidean distances \u2014 which is what\n// everything downstream (1-px AA, the analytic shadows, the hit-test mirrors,\n// src/sdf.ts) relies on. Prefixed `sdf_` so this module can share a program\n// with the mat's module without a name clash. Moved out of shaders/card/ when\n// the card frame retired (2026-09-25).\n// (Lineage: research/sdf-card/src/sdf/primitives.wgsl.)\n//\n// THE KIT'S `sdf` PIECE (`kitWgsl([..., \"sdf\"])`; petition I31 \u2014 every declaration's `///` block says what it takes and gives).\n// A distance here is SIGNED \u2014 negative inside, 0 on the edge, positive outside \u2014 and in the units its point is given in: a kind\n// hands world units (one world unit is one CSS px at zoom 1), so a one-device-px AA band is `1 / (mat_zoom(u) \xB7 mat_dpr(u))`\n// world units wide. A shape is centred on its frame's origin unless it takes its own points; y runs down, as everywhere on\n// the desk, and no shape here cares (each is symmetric under the flip).\n\n/// An axis-aligned box centred on the origin.\n/// `p` \u2014 the point, in the box's own units (world units for a kind); `b` \u2014 the half extents, \u2265 0, in the same units.\n/// \u2192 the exact Euclidean signed distance in those units: negative inside (\u2212min(b.x, b.y) at the centre), 0 on the edge,\n/// positive outside.\nfn sdf_box(p: vec2f, b: vec2f) -> f32 {\n let q = abs(p) - b;\n return length(max(q, vec2f(0.0))) + min(max(q.x, q.y), 0.0);\n}\n\n/// A box centred on the origin with its four corners rounded \u2014 the card's outline; src/sdf.ts `sdRoundBox` is its CPU mirror,\n/// number for number.\n/// `p` \u2014 the point, in the box's units; `b` \u2014 the half extents, \u2265 0; `r` \u2014 the corner radius, clamped to [0, min(b.x, b.y)]\n/// (an oversized radius makes a stadium, never a dent).\n/// \u2192 the exact Euclidean signed distance, in `p`'s units: negative inside, 0 on the rounded edge, positive outside.\nfn sdf_round_box(p: vec2f, b: vec2f, r: f32) -> f32 {\n let rr = clamp(r, 0.0, min(b.x, b.y));\n let q = abs(p) - b + rr;\n return length(max(q, vec2f(0.0))) + min(max(q.x, q.y), 0.0) - rr;\n}\n\n/// A circle centred on the origin.\n/// `p` \u2014 the point; `r` \u2014 the radius, \u2265 0, in `p`'s units.\n/// \u2192 `length(p) \u2212 r`: exact, negative inside (\u2212r at the centre), positive outside.\nfn sdf_circle(p: vec2f, r: f32) -> f32 { return length(p) - r; }\n\n/// The segment from `a` to `b` \u2014 a line of zero width (subtract a half-width for a stroke with round caps).\n/// `p` \u2014 the point; `a`, `b` \u2014 the segment's ends, in `p`'s units; `a` \u2260 `b` (a zero-length segment divides by zero).\n/// \u2192 the exact UNSIGNED distance to the nearest point of the segment, \u2265 0, in `p`'s units.\nfn sdf_segment(p: vec2f, a: vec2f, b: vec2f) -> f32 {\n let pa = p - a;\n let ba = b - a;\n let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);\n return length(pa - ba * h);\n}\n\n/// An X centred on the origin (Quilez's rounded X): two bars along the diagonals, crossing at the origin.\n/// `p` \u2014 the point; `w` \u2014 the X's span: each arm runs from the origin to (\xB1w/2, \xB1w/2), \u2265 0; `r` \u2014 the bars' half thickness,\n/// \u2265 0, their ends round. All in `p`'s units.\n/// \u2192 the exact signed distance: negative inside the bars, positive outside.\nfn sdf_rounded_x(p: vec2f, w: f32, r: f32) -> f32 {\n let q = abs(p);\n return length(q - min(q.x + q.y, w) * 0.5) - r;\n}\n\n// Exact circular fillet CSG (Quilez). Unlike smin/smax these put a TRUE circle\n// of radius r on the seam. Exact when the operands are perpendicular\n// half-planes (the retired card frame's corner was built to satisfy it).\n//\n// The union form clamps its operands to >= -r so the fillet stays local: the\n// unclamped hg_sdf form keeps growing once both surfaces are more than r\n// inside, and the field goes badly wrong in the interior.\n\n/// The UNION of two shapes with a circular fillet of radius `r` on their seam.\n/// `a`, `b` \u2014 the two shapes' signed distances at one point, in one unit; `r` \u2014 the fillet's radius, \u2265 0, in that unit\n/// (0 = the plain `min(a, b)`).\n/// \u2192 the union's signed distance: exact where the operands are perpendicular half-planes, a bound elsewhere; the fillet stays\n/// local to the seam (each operand is clamped at \u2212r), and away from it the answer is `min(a, b)`.\nfn op_union_round(a: f32, b: f32, r: f32) -> f32 {\n let m = min(a, b);\n let A = max(a, -r);\n let B = max(b, -r);\n let u = max(vec2f(r - A, r - B), vec2f(0.0));\n return min(m, max(r, min(A, B)) - length(u));\n}\n\n/// The INTERSECTION of two shapes with a circular round of radius `r` on their crease.\n/// `a`, `b` \u2014 the two shapes' signed distances at one point, in one unit; `r` \u2014 the round's radius, \u2265 0, in that unit\n/// (0 = the plain `max(a, b)`).\n/// \u2192 the intersection's signed distance: exact where the operands are perpendicular half-planes, a bound elsewhere.\nfn op_isect_round(a: f32, b: f32, r: f32) -> f32 {\n let u = max(vec2f(r + a, r + b), vec2f(0.0));\n return min(-r, max(a, b)) + length(u);\n}\n",
|
|
741
|
-
"tray/tray-pass.wgsl": "// The pegboard
|
|
742
|
-
"tray/tray.wgsl": "// THE PEGBOARD TRAY (design-017 \xA75\u2013\xA76; design-018 \xA72\u2013\xA74) \u2014 the drawer, and the research board in it (research/sdf-pegboard, shader.js)\n// lying on the mat, shaded in CLOSED FORM for a head-on view: no march, no history, no jitter \u2014 every frame is final, so a scrolling\n// drawer is clean on every frame. Its edge is an arris in the face's material; its holes show the desk as drawn under it, black at the\n// board's shadow; under its top edge a clear HEADER (design-018 R4) where nothing hangs and no hole opens, then what hangs on it fades in\n// through the face's portal feather (every kind's `portal_cover`) and here, the accessories and the holes. A PURE module: the uniform blocks arrive as parameters \u2014 `u` the mat's struct carrying the DESK's light (the colour law\n// is mat.wgsl's own), `t` the tray's (src/tray/pass.ts) \u2014 and so does the blue noise.\n//\n// Board units are PITCHES (the research's 1). A board point is x from the drawer's left edge and Y down the board, Y split into a\n// whole row R (i32: the scroll's whole rows carried on the CPU plus the rows on screen) and fy \u2208 [0,1): nothing large ever enters\n// an f32, so the pattern is exact at any scroll (src/tray/lattice.ts is this arithmetic on the CPU, rounding half up as here).\n\n// ---- hashing: the research's pcg, on integer lattice points only (wrapping u32)\nfn peg_pcg(v: u32) -> u32 {\n let s = v * 747796405u + 2891336453u;\n let w = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u;\n return (w >> 22u) ^ w;\n}\n\n// Four independent uniforms for a lattice cell (the research's hash2i4).\nfn peg_hash4(x: u32, y: u32, seed: u32) -> vec4f {\n let h0 = peg_pcg(x ^ peg_pcg(y + seed));\n let h1 = peg_pcg(h0);\n let h2 = peg_pcg(h1);\n let h3 = peg_pcg(h2);\n return vec4f(f32(h0), f32(h1), f32(h2), f32(h3)) * 2.3283064365386963e-10;\n}\n\n// ---- the board point\nstruct PegPoint { x: f32, R: i32, fy: f32 }\n\n// The same point moved by `d` pitches \u2014 the move's rows re-carried into R, so R stays exact.\nfn peg_move(p: PegPoint, d: vec2f) -> PegPoint {\n let y = p.fy + d.y;\n let fl = floor(y);\n return PegPoint(p.x + d.x, p.R + i32(fl), y - fl);\n}\n\n// ---- value noise on the board (the research's noise2, its quintic fade): the value and its gradient in lattice units. The corners'\n// hashes come PRE-GATHERED (src/tray/pass.ts `hashTexels`: texel (i, j) holds the research's pcg hash of lattice points (i, j),\n// (i+1, j), (i, j+1), (i+1, j+1), 8 bits each, tiled every 256 cells) \u2014 one load where the pcg took eight rounds. Each band reads\n// it at its own offset (its seed); the lattice point is an integer, so the noise stays exact at any row.\nfn peg_vnoise(ht: texture_2d<f32>, x: u32, y: u32, f: vec2f, seed: u32) -> vec3f {\n let u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n let du = 30.0 * f * f * (f * (f - 2.0) + 1.0);\n let k = textureLoad(ht, vec2u((x + seed) & 255u, (y + ((seed * 2654435761u) >> 24u)) & 255u), 0);\n let a = k.r;\n let b = k.g;\n let c = k.b;\n let d = k.a;\n let v = mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n return vec3f(v, du.x * mix(b - a, d - c, u.y), du.y * mix(c - a, d - b, u.x));\n}\n\n// One band at (fx, fy) cells per pitch (integers): its lattice row is R\xB7fy plus the cells within the row, in wrapping u32 \u2014 seamless\n// and exact at any R. The value, and its gradient in pitches.\nfn peg_band(ht: texture_2d<f32>, p: PegPoint, fx: u32, fy: u32, seed: u32) -> vec3f {\n let X = p.x * f32(fx);\n let ix = floor(X);\n let Y = p.fy * f32(fy);\n let iy = floor(Y);\n let n = peg_vnoise(ht, u32(i32(ix)), u32(p.R) * fy + u32(i32(iy)), vec2f(X - ix, Y - iy), seed);\n return vec3f(n.x, n.y * f32(fx), n.z * f32(fy));\n}\n\n// \u230Aa / b\u230B for b > 0 (WGSL's `/` truncates toward zero).\nfn peg_floor_div(a: i32, b: i32) -> i32 {\n let q = a / b;\n return select(q, q - 1, (a % b != 0) && (a < 0));\n}\n\n// Value noise on a TURNED lattice, exact at any row: lattice coordinates M\xB7(x, Y)\xB7num/den, M = [[a, \u2212b], [b, a]] an INTEGER rotation\n// and scale (cells turned atan(b/a), num\xB7\u221A(a\xB2 + b\xB2)/den of them to a pitch), so no octave's cells line up with the board or with\n// another octave's \u2014 value noise's square cells stop reading as squares. The carried row splits R = den\xB7Q + r: Q's share is an\n// exact integer (\u2212b\xB7num\xB7Q, a\xB7num\xB7Q); only x, r and fy, all small, are ever a float. One pre-gathered load.\nfn peg_rot(ht: texture_2d<f32>, p: PegPoint, a: i32, b: i32, num: i32, den: i32, seed: u32) -> f32 {\n let q = peg_floor_div(p.R, den);\n let r = f32(p.R - q * den) + p.fy;\n let uv = vec2f(f32(a) * p.x - f32(b) * r, f32(b) * p.x + f32(a) * r) * (f32(num) / f32(den));\n let cell = floor(uv);\n let base = vec2i(i32(cell.x) - b * num * q, i32(cell.y) + a * num * q);\n return peg_vnoise(ht, u32(base.x), u32(base.y), uv - cell, seed).x;\n}\n\n// The research's \"fine detail fading with pixel footprint\", per band: F cycles per pitch are kept while a cycle spans \u2265 4 device\n// px and gone at 2, so a scrolling board never shimmers (a fleck by its thin side, not its cell). The footprint is the frame's, so\n// every band's share is a UNIFORM (pass.ts `keep` \u2014 `1 \u2212 smoothstep(\xBC, \xBD, fp\xB7F)`): the face's grain (\u2248 19) \xB7 85 \xB7 190, then\n// 210 \xB7 42 \xB7 105, the punched fibre's 9 \xB7 18 \xB7 36.\n\n// Sparse fibre flecks (the research's `flecks`): one randomly turned ellipse per jittered cell at F per pitch, signed \u2014 dark\n// negative, pale positive. A stroke's thin side is sub-pixel long before its cell is (\u2248 0.3 of it): its edge is widened by the pixel's\n// footprint in the ellipse's own measure and its peak lowered to keep its ink \u2014 analytic AA, the stroke a faint line, never a dash\n// (the research averaged 1024 jittered samples to the same end). `fp` = pitches per device px.\nfn peg_flecks(p: PegPoint, F: u32, density: f32, seed: u32, fp: f32) -> f32 {\n let X = p.x * f32(F);\n let ix = floor(X);\n let Y = p.fy * f32(F);\n let iy = floor(Y);\n let cx = u32(i32(ix));\n let cy = u32(p.R) * F + u32(i32(iy));\n let q = vec2f(X - ix, Y - iy);\n var acc = 0.0;\n for (var j = -1; j <= 1; j++) {\n for (var i = -1; i <= 1; i++) {\n let r = peg_hash4(cx + u32(i), cy + u32(j), seed);\n if (r.w > density) { continue; }\n let d = q - (vec2f(f32(i), f32(j)) + r.xy);\n let ang = r.z * 3.14159265;\n let ca = cos(ang);\n let sa = sin(ang);\n let dl = vec2f(d.x * ca + d.y * sa, -d.x * sa + d.y * ca);\n let wid = 0.10 + 0.10 * r.y;\n let e = length(vec2f(dl.x / (0.35 + 0.65 * r.x), dl.y / wid));\n let aa = fp * f32(F) / wid;\n acc += (1.0 - smoothstep(0.6 - aa, 1.0 + aa, e)) / (1.0 + 2.0 * aa) * select(-1.0, 0.8, r.z > 0.62);\n }\n }\n return clamp(acc, -1.0, 1.0);\n}\n\n// ---- the lattice (the research's cellCoord \xB7 sdStadiumV)\nstruct PegHole { q: vec2f, d: f32, g: vec2f }\n\n// The vertical stadium at an offset from its centre: the distance (negative inside) and its gradient, pointing out of the hole.\nfn peg_stadium(t: TrayUniforms, q: vec2f) -> vec3f {\n let e = vec2f(q.x, q.y - clamp(q.y, -t.hole.y, t.hole.y));\n let len = max(length(e), 1.0e-6);\n return vec3f(len - t.hole.x, e / len);\n}\n\n// The hole of the nearest row (a hole's half-height, 0.33, is under half a pitch): rows at Y = r + \xBE, odd rows shifted \xBD \u2014 the\n// stagger's parity from the exact row index. A hole whose centre lies within the solid border of a side is never punched.\nfn peg_hole(t: TrayUniforms, p: PegPoint) -> PegHole {\n let dr = floor(p.fy - t.depth.w + 0.5);\n let row = p.R + i32(dr);\n let shift = t.depth.z + select(0.0, 0.5, (row & 1) != 0);\n let hx = p.x - shift;\n let col = floor(hx + 0.5);\n var h: PegHole;\n h.q = vec2f(hx - col, p.fy - dr - t.depth.w);\n let s = peg_stadium(t, h.q);\n h.d = s.x;\n h.g = s.yz;\n let cx = col + shift;\n if ((cx < t.hole.w) || (cx > t.rect.z / t.view.w - t.hole.w)) { h.d = 1.0e3; }\n return h;\n}\n\n// ---- the materials (the research's boardMat \xB7 wallMat), linear albedo\n// The tempered face: the TONE drift \u2014 two smooth octaves on lattices turned against the board and each other (cells 2.24 and 0.89\n// of a pitch), the research's big and mid fbm as a UI pitch shows them \u2014 and the fine fibre GRAIN on a third (cells 0.05 of a pitch,\n// \u2248 19 a pitch: what survives of the research's fine grain where its footprint lets it), then at a reading pitch the research's fine\n// height grain and its bump, the fibre flecks \u2014 every band faded by its own footprint. Out: the grain as one factor on the face's\n// linear albedo (`g`: the albedo is face\xB7(1 + g)), the height's gradient (the bump), and whether the face is FLAT (every bump faded).\nstruct PegFace { g: f32, grad: vec2f, flat: bool }\nfn peg_face(ht: texture_2d<f32>, t: TrayUniforms, p: PegPoint, fp: f32) -> PegFace {\n let tone = 0.62 * (peg_rot(ht, p, 2, 1, 1, 5, 0x3a1u) - 0.5) + 0.38 * (peg_rot(ht, p, 1, -2, 1, 2, 0x5c7u) - 0.5);\n var grain = 0.0;\n let kg = t.keepFace.x;\n if (kg > 0.0) { grain = kg * (peg_rot(ht, p, 3, 1, 6, 1, 0x2b1u) - 0.5); }\n // the fine height (the research's faceHeight: 0.55 \xB7 85, 0.30 \xB7 190\xD770, 0.15 \xB7 60\xD7210) \u2014 at a UI pitch its footprint fades it\n var h = 0.0;\n var grad = vec2f(0.0);\n let k85 = t.keepFace.z;\n let k190 = t.keepFace.w;\n let k210 = t.keepFine.x;\n if (k85 > 0.0) { let n = peg_band(ht, p, 85u, 85u, 0x1f3u); h += 0.55 * k85 * (n.x - 0.5); grad += 0.55 * k85 * n.yz; }\n if (k190 > 0.0) { let n = peg_band(ht, p, 190u, 70u, 0x4a7u); h += 0.30 * k190 * (n.x - 0.5); grad += 0.30 * k190 * n.yz; }\n if (k210 > 0.0) { let n = peg_band(ht, p, 60u, 210u, 0x7c9u); h += 0.15 * k210 * (n.x - 0.5); grad += 0.15 * k210 * n.yz; }\n let k42 = t.keepFine.y;\n let k105 = t.keepFine.z;\n var fl = 0.0;\n if (k42 > 0.0) { fl += 0.30 * k42 * peg_flecks(p, 42u, 0.40, 0xa13u, fp); }\n if (k105 > 0.0) { fl += 0.18 * k105 * peg_flecks(p, 105u, 0.35, 0xc35u, fp); }\n var f: PegFace;\n f.g = (1.0 + 0.12 * tone + 0.08 * grain + 0.40 * h) * (1.0 + fl) - 1.0;\n f.grad = grad;\n f.flat = (k85 <= 0.0) && (k190 <= 0.0) && (k210 <= 0.0);\n return f;\n}\n\n// The paler, fuzzier punched fibre (the research's edge): its clumps at 9 \xB7 18 \xB7 36 per pitch, faded by the footprint.\nfn peg_edge(ht: texture_2d<f32>, t: TrayUniforms, p: PegPoint, fp: f32) -> vec3f {\n var clump = 0.0;\n let k9 = t.keepEdge.x;\n let k18 = t.keepEdge.y;\n let k36 = t.keepEdge.z;\n if (k9 > 0.0) { clump += 0.5 * k9 * (peg_band(ht, p, 9u, 9u, 0x3e5u).x - 0.5); }\n if (k18 > 0.0) { clump += 0.25 * k18 * (peg_band(ht, p, 18u, 18u, 0x5a9u).x - 0.5); }\n if (k36 > 0.0) { clump += 0.125 * k36 * (peg_band(ht, p, 36u, 36u, 0x7bdu).x - 0.5); }\n return t.edge.xyz * (1.0 + 0.30 * clump / 0.875);\n}\n\n// ---- the light: the research's HOME lamp (D-K3.4), in the desk's colour law and room\n// The desk's colour law on the tray (the paper's `paper_colour`, chain 0): by day the mat's grade on the sRGB albedo \u2014 lit (`gobo`\n// 1) the identity, a configured byte is the drawn byte; in shadow the room's \xBD and its touch of saturation \u2014 by night the Moon and\n// the eye (mat.wgsl `night_mat`) on the linear albedo; between, the cross-fade.\nfn tray_colour(u: MatUniforms, albedo: vec3f, gobo: f32, noise: f32) -> vec3f {\n let day = shade_mat(u, albedo, gobo);\n if (u.night.x <= 0.0) { return day; }\n let night = night_mat(u, srgb_to_linear(albedo), gobo, noise);\n if (u.night.x >= 1.0) { return night; }\n return mix(day, night, u.night.x);\n}\n\n// A surface under the lamp, its linear albedo and normal \u2192 the colour on screen. The relief is the paper's \u2014 `(N\xB7L/L_z)^(1/2.2)`, the\n// flat face exactly 1 \u2014 and only brightens; a normal turned from the lamp takes the room's grade instead (`gobo` = the Lambert term),\n// so a face turned away reads as a shadow does, never black. `vis`: the lamp seen at all. The glint is the photo's.\nfn tray_lit(u: MatUniforms, t: TrayUniforms, alb: vec3f, n: vec3f, vis: f32, noise: f32) -> vec3f {\n let L = t.lamp.xyz;\n let lam = clamp(dot(n, L) / max(L.z, 1.0e-3), 0.0, 1.6);\n let srgb = clamp(night_encode(alb) * pow(max(lam, 1.0), 1.0 / 2.2), vec3f(0.0), vec3f(1.0));\n var c = tray_colour(u, srgb, min(lam, 1.0) * vis, noise);\n let H = normalize(L + vec3f(0.0, 0.0, 1.0));\n let spec = pow(saturate(dot(n, H)), 48.0) * 0.08 * vis;\n let glint = mix(vec3f(1.0, 0.985, 0.95), vec3f(0.78, 0.86, 1.0) * 0.5, saturate(u.night.x));\n return c + glint * spec;\n}\n\n// ---- the board: the face and the rim fillet, and the desk through a hole\n// The front surface at a board point: the face, or \u2014 within k of a hole \u2014 the fillet. On smax(slab, \u2212hole, k) = 0 seen head-on the\n// surface's blend weight is h = \u221A(d/k) for d \u2208 [0,k], and its normal normalize(((h \u2212 1)\xB7\u2207d, h)) sweeps from the hole's wall to the\n// face: the rim's lit edge and its shadowed one, in the paler punched fibre where the normal is steep (the research's faceness).\nfn peg_surface(ht: texture_2d<f32>, u: MatUniforms, t: TrayUniforms, p: PegPoint, h: PegHole, fp: f32, noise: f32) -> vec3f {\n let k = t.hole.z;\n let face = peg_face(ht, t, p, fp);\n // the PLAIN FACE \u2014 clear of every fillet, its bump faded: N\xB7L is L_z exactly, the relief 1, the glint nothing; the grain's small\n // linear factor rides the configured byte to first order ((1 + g)^(1/2.4) \u2248 1 + g/2.4 \u2014 the byte itself where the grain is 0)\n if ((h.d >= k) && face.flat) {\n return tray_colour(u, clamp(t.faceSrgb.xyz * (1.0 + face.g / 2.4), vec3f(0.0), vec3f(1.0)), 1.0, noise);\n }\n var n = normalize(vec3f(-face.grad * 0.003, 1.0)); // the research's bump: 0.0030 per unit of the height's slope\n if (h.d < k) {\n let s = sqrt(max(h.d, 0.0) / k);\n n = normalize(vec3f((s - 1.0) * h.g, s));\n }\n let faceness = smoothstep(0.35, 0.75, n.z);\n var alb = t.face.xyz * (1.0 + face.g);\n if (faceness < 1.0) { alb = mix(peg_edge(ht, t, p, fp) * 0.86, alb, faceness); } // the punched rim, burnished at the front\n return tray_lit(u, t, alb, n, 1.0, noise);\n}\n\n// Through a hole: THE DESK AS DRAWN (design-018 \xA73). A hole is cut through the board, so it shows what lies under the drawer this\n// frame \u2014 the mat's green, its grid lines, the leaf shadows, anything laid there \u2014 in the board's shadow: premultiplied BLACK over the\n// frame, at the alpha that leaves `f` of the light. THE TWO PLANES (design-017 \xA76.4) give the lamp at the mat: the ray from the mat\n// toward the lamp crosses the board's back face at A and its front face at B; the mat is lit iff both lie in the SAME hole \u2014 B's, with\n// A tested against it (a convex prism holds the segment iff it holds both ends) \u2014 the penumbra from the SDF distance, widened by the\n// lamp's size over each plane's height. The room's light reaches the mat through the hole, cut by the cavity. f = a\xB7cav + (1 \u2212 a)\xB7vis,\n// `a` the room's share (the mat's own shade-in-shadow: shade_mat at gobo 0) \u2014 ENCODED as the mat's own chain encodes its shade (its\n// grade lives in pow-2.2 space), so the densest hole is the mat's shade under a leaf \u2014 over the desk dimmed as the rest of it is.\nfn peg_through(u: MatUniforms, t: TrayUniforms, p: PegPoint, h: PegHole, fp: f32) -> f32 {\n let L = t.lamp.xyz;\n let lz = max(L.z, 1.0e-3);\n let off = L.xy / lz;\n let thick = t.depth.x;\n let gap = t.depth.y;\n let hb = peg_hole(t, peg_move(p, off * (gap + thick)));\n let da = peg_stadium(t, hb.q - off * thick).x;\n // the penumbra: the lamp's angular radius over the ray's length to each plane's edge (height / L_z), and a pixel\n let wa = t.lamp.w * gap / lz + fp;\n let wb = t.lamp.w * (gap + thick) / lz + fp;\n let vis = smoothstep(-wb, wb, -hb.d) * smoothstep(-wa, wa, -da);\n let cav = mix(t.cavity.x, t.cavity.y, smoothstep(0.0, t.cavity.z, -h.d));\n let a = 1.0 - u.gobo.z * (1.0 - u.gobo.w);\n let f = a * cav + (1.0 - a) * vis;\n return 1.0 - (1.0 - t.dim) * pow(f, 1.0 / 2.2);\n}\n\n// ---- the drawer\nfn tray_erf(x: f32) -> f32 {\n let s = sign(x);\n let a = abs(x);\n let k = 1.0 / (1.0 + 0.3275911 * a);\n let y = 1.0 - (((((1.061405429 * k - 1.453152027) * k) + 1.421413741) * k - 0.284496736) * k + 0.254829592) * k * exp(-a * a);\n return s * y;\n}\n\n// Gaussian-blurred coverage of an outline at distance d (the shadow, straight out of the field \u2014 the paper's).\nfn tray_blur(d: f32, sigma: f32) -> f32 { return 0.5 - 0.5 * tray_erf(d / max(sigma * 1.4142136, 1.0e-4)); }\n\n// The drawer's outline (CSS px, negative inside): a box whose top corners round and whose bottom lies past the view \u2014 what is drawn\n// and what casts the shadows (the finger notch retired \u2014 design-018 \xA72).\nfn tray_outline(t: TrayUniforms, p: vec2f) -> f32 {\n let r = t.shape.x;\n let ext = vec2f(0.5 * t.rect.z, 0.5 * t.rect.w + r);\n return sdf_round_box(p - vec2f(t.rect.x + ext.x, t.rect.y + ext.y), ext, r);\n}\n\n// The board point under a pixel (CSS px) \u2014 the rows on screen added to the carried rows in i32.\nfn tray_point(t: TrayUniforms, p: vec2f) -> PegPoint {\n let ly = (p.y - t.rect.y) / t.view.w + t.frac;\n let fl = floor(ly);\n return PegPoint((p.x - t.rect.x) / t.view.w, t.rowBase + i32(fl), ly - fl);\n}\n\n// The board's EDGE (design-018 \xA72): an ARRIS \u2014 a quarter-round `A` px wide where the face meets the outline, in the FACE's own\n// material (its tone at the board point under it, so the grain runs on without a seam; not the paler punched fibre). Its normal turns\n// from the eye (on the face's side) to the outline's outward direction (at the outline): it catches the lamp where the outline faces\n// it \u2014 the top and the left \u2014 and falls to the room's shade on the far side. `o`: the outline's distance there (\u2212A \u2026 0).\nfn tray_arris(ht: texture_2d<f32>, u: MatUniforms, t: TrayUniforms, p: vec2f, o: f32, noise: f32) -> vec3f {\n let e = 0.5;\n let gx = tray_outline(t, p + vec2f(e, 0.0)) - tray_outline(t, p - vec2f(e, 0.0));\n let gy = tray_outline(t, p + vec2f(0.0, e)) - tray_outline(t, p - vec2f(0.0, e));\n let g = vec2f(gx, gy) / max(length(vec2f(gx, gy)), 1.0e-6);\n let s = clamp(1.0 + o / t.shape.y, 0.0, 1.0);\n let n = vec3f(s * g, sqrt(max(1.0 - s * s, 0.0)));\n let face = peg_face(ht, t, tray_point(t, p), t.fp);\n return tray_lit(u, t, t.face.xyz * (1.0 + face.g), n, 1.0, noise);\n}\n\n// The board to its edge, PREMULTIPLIED: the point under the carry, its hole \u2014 the front surface over it by its analytic coverage, and\n// where the surface is not, the desk seen through (black at the hole's alpha). `holes` scales every hole's OPENING: 1 as punched (the\n// board under the specimens), 0 CLOSED \u2014 the plain face, its fillets' relief flattened into it (the VEIL's board, `tray_veil`).\nfn tray_board(ht: texture_2d<f32>, u: MatUniforms, t: TrayUniforms, p: vec2f, noise: f32, holes: f32) -> vec4f {\n let pt = tray_point(t, p);\n let h = peg_hole(t, pt);\n let cg = clamp(0.5 + h.d / t.fp, 0.0, 1.0); // the surface's analytic coverage as punched\n let open = (1.0 - cg) * holes; // what of the pixel the hole opens\n var col = vec4f(0.0);\n if (holes < 1.0) {\n // closing: the plain face where the hole no longer opens, and the fillet (within `hole.z` of it) turning to the face\n var plain = h;\n plain.d = 1.0e3;\n let face = peg_surface(ht, u, t, pt, plain, t.fp, noise);\n var s = face;\n if (cg > 0.0 && h.d < t.hole.z) { s = mix(face, peg_surface(ht, u, t, pt, h, t.fp, noise), holes); }\n col = vec4f(s, 1.0) * cg + vec4f(face, 1.0) * ((1.0 - cg) * (1.0 - holes));\n } else if (cg > 0.0) {\n col = vec4f(peg_surface(ht, u, t, pt, h, t.fp, noise), 1.0) * cg;\n }\n if (open > 0.0) { col += vec4f(0.0, 0.0, 0.0, peg_through(u, t, pt, h, t.fp) * open); }\n return col;\n}\n\n// A pixel INSIDE the drawer's outline (`o` its distance there, `px` a device px in CSS px), premultiplied: the edge within `A` of the\n// outline, the board inside it with its holes at `holes` \u2014 blended across the pixel where they meet \u2014 and the edge's INNER SHADOW over\n// the board (D-R1.2): the window's lip on the lamp's side, black at up to `shape.z`, fading over `shape.w` px inside the edge, where the\n// outline faces the lamp (the push of the drawer's lamp shadow points away from it).\nfn tray_inside(ht: texture_2d<f32>, u: MatUniforms, t: TrayUniforms, p: vec2f, px: f32, o: f32, noise: f32, holes: f32) -> vec4f {\n let k = clamp(0.5 + (o + t.shape.y) / px, 0.0, 1.0);\n var col = vec4f(0.0);\n if (k > 0.0) { col = vec4f(tray_arris(ht, u, t, p, o, noise), 1.0); }\n if (k < 1.0) {\n var b = tray_board(ht, u, t, p, noise, holes);\n if (t.shape.z > 0.0 && -o - t.shape.y < t.shape.w) {\n let e = 0.5;\n let g = normalize(vec2f(tray_outline(t, p + vec2f(e, 0.0)) - tray_outline(t, p - vec2f(e, 0.0)), tray_outline(t, p + vec2f(0.0, e)) - tray_outline(t, p - vec2f(0.0, e))) + vec2f(1.0e-6, 0.0));\n let facing = max(dot(g, -normalize(t.shadow.zw)), 0.0);\n let sh = t.shape.z * facing * (1.0 - smoothstep(0.0, t.shape.w, -o - t.shape.y));\n b = vec4f(b.rgb * (1.0 - sh), 1.0 - (1.0 - b.a) * (1.0 - sh));\n }\n col = mix(b, col, k);\n }\n return col;\n}\n\n// One pixel of the tray UNDER its specimens (`frag` in device px), the view whole: the dim over the desk, the drawer's shadows on it,\n// the board to the outline and its edge, every hole as punched \u2014 premultiplied, each pixel drawn once. The board scrolls under its edge\n// (the drawer is a window onto it). The specimens hang over the board inside the edge and under the header (their face clip,\n// tray/specimens.ts `faceClip`); the VEIL (`tray_veil`) is laid over them all at the top.\nfn tray_drawer(u: MatUniforms, t: TrayUniforms, frag: vec2f, noise_tex: texture_2d<f32>, noise_samp: sampler, ht: texture_2d<f32>) -> vec4f {\n let dpr = t.view.z;\n let p = frag / dpr;\n let px = 1.0 / dpr;\n let dim = vec4f(0.0, 0.0, 0.0, t.dim);\n // beyond the shadows' reach: the dim alone\n let reach = 3.0 * max(t.shadow.x, t.room.x) + length(t.shadow.zw);\n if ((p.x < t.rect.x - reach) || (p.x > t.rect.x + t.rect.z + reach) || (p.y < t.rect.y - reach)) { return dim; }\n // the blue noise is the NIGHT's (the rods' snow, mat.wgsl `night_mat`): by day the board takes none \u2014 its own grain never bands,\n // and a scrolled board is then the same pixels, moved\n var bn = vec3f(0.5);\n if (u.night.x > 0.0) { bn = textureSampleLevel(noise_tex, noise_samp, frag * u.noise.z + u.noise.xy, 0.0).rgb; }\n // DEEP INSIDE \u2014 clear of the rounded corners, the edge, its inner shadow and two px more: no edge, no shadow, no outline to evaluate\n // (premultiplied \u2014 a hole's alpha is its shadow over the desk, no longer 1)\n let m = t.shape.y + t.shape.w + 2.0;\n if ((p.x > t.rect.x + m) && (p.x < t.rect.x + t.rect.z - m) && (p.y > t.rect.y + t.shape.x + m)) {\n return tray_board(ht, u, t, p, bn.y, 1.0);\n }\n let o = tray_outline(t, p);\n let cover = clamp(0.5 - o / px, 0.0, 1.0);\n // the shadows on the desk \u2014 the room's round the outline, the lamp's pushed along its ground direction, one over the other \u2014 where\n // the drawer does not cover them\n var under = dim;\n if (cover < 1.0) {\n let room = t.room.y * tray_blur(o, t.room.x);\n let lamp = t.shadow.y * tray_blur(tray_outline(t, p - t.shadow.zw), t.shadow.x);\n under = vec4f(0.0, 0.0, 0.0, 1.0 - (1.0 - room) * (1.0 - lamp) * (1.0 - t.dim));\n }\n if (cover <= 0.0) { return under; }\n return tray_inside(ht, u, t, p, px, o, bn.y, 1.0) * cover + under * (1.0 - cover);\n}\n\n// THE VEIL (design-018 rev 5 \u2014 James, 2026-09-28: \"at the edge fading, somehow the holes are displayed on top of objects\"): the PLAIN\n// board \u2014 its face with every hole closed, its edge, the edge's inner shadow: `tray_drawer`'s own pixel, less its holes \u2014 laid OVER\n// everything the drawer holds (the accessories, the specimens, their tags), whole in the HEADER (`fade.y` under the edge's inside) and\n// fading out over the ramp below it (`fade.x`): `1 \u2212 smoothstep(foot, foot + fade.x, y)`. What hangs there and the holes behind it fade\n// INTO THE BOARD together \u2014 a specimen never turns see-through, so no hole shows through it (the specimens are opaque to the face\n// clip's top; a hard clip at the header's foot lies under the veil's whole). Only pixels wholly inside the outline: its antialiased rim\n// is `tray_drawer`'s, and the veil there would lay the edge twice. A host's FOOT inset (petition I21, `fade.z` \u2014 0 none) is the same\n// mirrored at the board's bottom edge: whole in the foot, fading out over the ramp above its line (`smoothstep(line \u2212 fade.x, line, y)`;\n// the face's clip ends at the line, under the veil's whole) \u2014 the larger of the two ramps where a short drawer's meet.\nfn tray_veil(u: MatUniforms, t: TrayUniforms, frag: vec2f, noise_tex: texture_2d<f32>, noise_samp: sampler, ht: texture_2d<f32>) -> vec4f {\n let p = frag / t.view.z;\n let px = 1.0 / t.view.z;\n let foot = t.rect.y + t.shape.y + t.fade.y;\n var a = 1.0 - smoothstep(foot, foot + t.fade.x, p.y);\n if (t.fade.z > 0.0) {\n let line = t.rect.y + t.rect.w - t.fade.z;\n a = max(a, smoothstep(line - t.fade.x, line, p.y));\n }\n if (a <= 0.0) { return vec4f(0.0); }\n let o = tray_outline(t, p);\n if (o > -0.5 * px) { return vec4f(0.0); }\n var bn = vec3f(0.5);\n if (u.night.x > 0.0) { bn = textureSampleLevel(noise_tex, noise_samp, frag * u.noise.z + u.noise.xy, 0.0).rgb; }\n return tray_inside(ht, u, t, p, px, o, bn.y, 0.0) * a;\n}\n\n// ---- the accessories (K5a): what the specimens hang on, plugged into real holes \u2014 SK\xC5DIS's hook, shelf, clip and rail seen head-on, in\n// their powder coat under the lamp, each casting its shadow on the board. One instance per specimen (tray-pass.wgsl), its quad the\n// accessory and its shadow's reach; CSS px, the pitch `P` the unit of every length (kernel/tray.ts TRAY_ACCESSORY's numbers).\nfn acc_peg(a: TrayAccessory, i: u32) -> vec2f {\n if (i == 0u) { return a.pegs0.xy; }\n if (i == 1u) { return a.pegs0.zw; }\n if (i == 2u) { return a.pegs1.xy; }\n return a.pegs1.zw;\n}\n\n// The accessory's silhouette: its signed distance at `p` (negative inside) \u2014 every peg's PLUG in its hole, and the accessory's body.\nfn acc_sdf(a: TrayAccessory, p: vec2f, P: f32) -> f32 {\n let kind = u32(a.kind.x + 0.5);\n let n = min(u32(a.kind.y + 0.5), 4u);\n let r = a.rect;\n var d = 1.0e4;\n var lo = 1.0e9;\n var hi = -1.0e9;\n for (var i = 0u; i < n; i++) {\n let g = acc_peg(a, i);\n lo = min(lo, g.x);\n hi = max(hi, g.x);\n d = min(d, sdf_round_box(p - g, vec2f(0.1, 0.28) * P, 0.09 * P)); // the plug, in its hole\n if (kind == 0u) { d = min(d, sdf_segment(p, g, g + vec2f(0.0, 0.58 * P)) - 0.07 * P); } // a hook: down to its tip\n if (kind == 1u) { d = min(d, sdf_segment(p, g, vec2f(g.x, r.w + 0.1 * P)) - 0.06 * P); } // a shelf's bracket, up to the plank\n if (kind == 2u) { // a clip: its spring from the hole down over the specimen's top edge\n let top = g.y - 0.22 * P;\n let foot = max(g.y, r.y) + 0.5 * P;\n d = min(d, sdf_round_box(p - vec2f(g.x, 0.5 * (top + foot)), vec2f(0.24 * P, 0.5 * (foot - top)), 0.08 * P));\n }\n }\n if (kind == 1u) { // the shelf's plank, under the specimen and past its sides\n let hw = 0.5 * (r.z - r.x) + 0.3 * P;\n d = min(d, sdf_round_box(p - vec2f(0.5 * (r.x + r.z), r.w + 0.12 * P), vec2f(hw, 0.12 * P), 0.05 * P));\n }\n if (kind == 3u && n > 0u) { // the rail: a bar through its holes, past the outer two\n let y = acc_peg(a, 0u).y;\n d = min(d, sdf_round_box(p - vec2f(0.5 * (lo + hi), y), vec2f(0.5 * (hi - lo) + 0.4 * P, 0.11 * P), 0.11 * P));\n }\n return d;\n}\n\n// One pixel of an accessory's quad: its shadow on the board (the silhouette seen from the lamp, pushed off by how far it stands off the\n// board, softened by the lamp's size over that), then the accessory over it \u2014 its rounded relief from the silhouette's gradient, lit as\n// the board's edge is; both kept inside that edge. Premultiplied.\nfn tray_accessory(u: MatUniforms, t: TrayUniforms, a: TrayAccessory, frag: vec2f, noise_tex: texture_2d<f32>, noise_samp: sampler, ht: texture_2d<f32>) -> vec4f {\n let p = frag / t.view.z;\n let px = 1.0 / t.view.z;\n let P = t.view.w;\n // inside the board's edge \u2014 whole: in the header and its ramp the VEIL lays the plain board over it, as over the specimens (design-018\n // rev 5, `tray_veil`)\n let inside = clamp(0.5 - (tray_outline(t, p) + t.shape.y) / px, 0.0, 1.0);\n if (inside <= 0.0) { return vec4f(0.0); }\n let L = t.lamp.xyz;\n let lz = max(L.z, 1.0e-3);\n let off = L.xy / lz * a.kind.z;\n let sig = t.lamp.w * a.kind.z / lz + px;\n let ds = acc_sdf(a, p + off, P);\n let shadow = t.accessory.w * (1.0 - smoothstep(-sig, sig, ds));\n let d = acc_sdf(a, p, P);\n let c = clamp(0.5 - d / px, 0.0, 1.0);\n if ((c <= 0.0) && (shadow <= 1.0e-4)) { return vec4f(0.0); }\n var col = vec3f(0.0);\n if (c > 0.0) {\n let e = 0.5;\n let gx = acc_sdf(a, p + vec2f(e, 0.0), P) - acc_sdf(a, p - vec2f(e, 0.0), P);\n let gy = acc_sdf(a, p + vec2f(0.0, e), P) - acc_sdf(a, p - vec2f(0.0, e), P);\n let g = vec2f(gx, gy) / max(length(vec2f(gx, gy)), 1.0e-6);\n let s = clamp(-d / (0.07 * P), 0.0, 1.0);\n let n = normalize(vec3f(g * (1.0 - s) * 1.2, max(s, 0.2)));\n var bn = vec3f(0.5);\n if (u.night.x > 0.0) { bn = textureSampleLevel(noise_tex, noise_samp, frag * u.noise.z + u.noise.xy, 0.0).rgb; }\n col = tray_lit(u, t, t.accessory.xyz, n, 1.0, bn.y);\n }\n let body = c * inside;\n let under = shadow * inside;\n return vec4f(col * body, body + under * (1.0 - body));\n}\n"
|
|
737
|
+
"tray/tray-pass.wgsl": "// The pegboard drawer and its edge veil share one layout. The board is drawn under the specimens; the veil is drawn over them.\n\n@group(0) @binding(0) var<uniform> u: MatUniforms;\n@group(0) @binding(1) var<uniform> t: TrayUniforms;\n@group(0) @binding(2) var noise_tex: texture_2d<f32>;\n@group(0) @binding(3) var noise_samp: sampler;\n@group(0) @binding(4) var hash_tex: texture_2d<f32>;\n\n@vertex\nfn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {\n var corners = array<vec2f, 6>(vec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0), vec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0));\n let k = corners[vi % 6u];\n let vw = t.view.x;\n let vh = t.view.y;\n var lo = vec2f(0.0, 0.0);\n var hi = vec2f(vw, vh);\n // Dimming covers the view; otherwise cover only the drawer and its shadows.\n if (t.dim <= 0.0) {\n let reach = 3.0 * max(t.shadow.x, t.room.x) + length(t.shadow.zw);\n lo = vec2f(max(t.rect.x - reach, 0.0), max(t.rect.y - reach, 0.0));\n hi = vec2f(min(t.rect.x + t.rect.z + reach, vw), vh);\n }\n let css = mix(lo, hi, k);\n return vec4f(css.x / vw * 2.0 - 1.0, 1.0 - css.y / vh * 2.0, 0.0, 1.0);\n}\n\n@fragment\nfn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {\n return tray_drawer(u, t, pos.xy, noise_tex, noise_samp, hash_tex);\n}\n\n// THE VEIL (design-018 rev 5): one quad over the drawer's top \u2014 from its edge down through the header and its ramp \u2014 laid LAST, over\n// everything the drawer holds (tray.wgsl `tray_veil`: the plain board, whole in the header, fading out over the ramp). With a host's\n// FOOT inset (petition I21, `fade.z`) a second quad in the same draw, vertices 6\u201311: from the foot's ramp down to the board's bottom\n// edge (never above the first's foot), the plain board whole in the foot and fading out over the ramp above its line.\n@vertex\nfn vs_veil(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {\n var corners = array<vec2f, 6>(vec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0), vec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0));\n let k = corners[vi % 6u];\n let vw = t.view.x;\n let vh = t.view.y;\n let head = t.rect.y + t.shape.y + t.fade.y + t.fade.x + 1.0;\n var lo = clamp(vec2f(t.rect.x, t.rect.y), vec2f(0.0), vec2f(vw, vh));\n var hi = clamp(vec2f(t.rect.x + t.rect.z, head), vec2f(0.0), vec2f(vw, vh));\n if (vi >= 6u) {\n lo = clamp(vec2f(t.rect.x, max(t.rect.y + t.rect.w - t.fade.z - t.fade.w - 1.0, head)), vec2f(0.0), vec2f(vw, vh));\n hi = clamp(vec2f(t.rect.x + t.rect.z, t.rect.y + t.rect.w), vec2f(0.0), vec2f(vw, vh));\n }\n let css = mix(lo, hi, k);\n return vec4f(css.x / vw * 2.0 - 1.0, 1.0 - css.y / vh * 2.0, 0.0, 1.0);\n}\n\n@fragment\nfn fs_veil(@builtin(position) pos: vec4f) -> @location(0) vec4f {\n return tray_veil(u, t, pos.xy, noise_tex, noise_samp, hash_tex);\n}\n",
|
|
738
|
+
"tray/tray.wgsl": "// THE PEGBOARD TRAY (design-017 \xA75\u2013\xA76; design-018 \xA72\u2013\xA74) \u2014 the drawer, and the research board in it (research/sdf-pegboard, shader.js)\n// lying on the mat, shaded in CLOSED FORM for a head-on view: no march, no history, no jitter \u2014 every frame is final, so a scrolling\n// drawer is clean on every frame. Its edge is an arris in the face's material; its holes show the desk as drawn under it, black at the\n// board's shadow; under its top edge a clear HEADER (design-018 R4) where nothing hangs and no hole opens, then what hangs on it fades in\n// through the face's portal feather (every kind's `portal_cover`) and here, the holes. A PURE module: the uniform blocks arrive as parameters \u2014 `u` the mat's struct carrying the DESK's light (the colour law\n// is mat.wgsl's own), `t` the tray's (src/tray/pass.ts) \u2014 and so does the blue noise.\n//\n// Board units are PITCHES (the research's 1). A board point is x from the drawer's left edge and Y down the board, Y split into a\n// whole row R (i32: the scroll's whole rows carried on the CPU plus the rows on screen) and fy \u2208 [0,1): nothing large ever enters\n// an f32, so the pattern is exact at any scroll (src/tray/lattice.ts is this arithmetic on the CPU, rounding half up as here).\n\n// ---- hashing: the research's pcg, on integer lattice points only (wrapping u32)\nfn peg_pcg(v: u32) -> u32 {\n let s = v * 747796405u + 2891336453u;\n let w = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u;\n return (w >> 22u) ^ w;\n}\n\n// Four independent uniforms for a lattice cell (the research's hash2i4).\nfn peg_hash4(x: u32, y: u32, seed: u32) -> vec4f {\n let h0 = peg_pcg(x ^ peg_pcg(y + seed));\n let h1 = peg_pcg(h0);\n let h2 = peg_pcg(h1);\n let h3 = peg_pcg(h2);\n return vec4f(f32(h0), f32(h1), f32(h2), f32(h3)) * 2.3283064365386963e-10;\n}\n\n// ---- the board point\nstruct PegPoint { x: f32, R: i32, fy: f32 }\n\n// The same point moved by `d` pitches \u2014 the move's rows re-carried into R, so R stays exact.\nfn peg_move(p: PegPoint, d: vec2f) -> PegPoint {\n let y = p.fy + d.y;\n let fl = floor(y);\n return PegPoint(p.x + d.x, p.R + i32(fl), y - fl);\n}\n\n// ---- value noise on the board (the research's noise2, its quintic fade): the value and its gradient in lattice units. The corners'\n// hashes come PRE-GATHERED (src/tray/pass.ts `hashTexels`: texel (i, j) holds the research's pcg hash of lattice points (i, j),\n// (i+1, j), (i, j+1), (i+1, j+1), 8 bits each, tiled every 256 cells) \u2014 one load where the pcg took eight rounds. Each band reads\n// it at its own offset (its seed); the lattice point is an integer, so the noise stays exact at any row.\nfn peg_vnoise(ht: texture_2d<f32>, x: u32, y: u32, f: vec2f, seed: u32) -> vec3f {\n let u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n let du = 30.0 * f * f * (f * (f - 2.0) + 1.0);\n let k = textureLoad(ht, vec2u((x + seed) & 255u, (y + ((seed * 2654435761u) >> 24u)) & 255u), 0);\n let a = k.r;\n let b = k.g;\n let c = k.b;\n let d = k.a;\n let v = mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n return vec3f(v, du.x * mix(b - a, d - c, u.y), du.y * mix(c - a, d - b, u.x));\n}\n\n// One band at (fx, fy) cells per pitch (integers): its lattice row is R\xB7fy plus the cells within the row, in wrapping u32 \u2014 seamless\n// and exact at any R. The value, and its gradient in pitches.\nfn peg_band(ht: texture_2d<f32>, p: PegPoint, fx: u32, fy: u32, seed: u32) -> vec3f {\n let X = p.x * f32(fx);\n let ix = floor(X);\n let Y = p.fy * f32(fy);\n let iy = floor(Y);\n let n = peg_vnoise(ht, u32(i32(ix)), u32(p.R) * fy + u32(i32(iy)), vec2f(X - ix, Y - iy), seed);\n return vec3f(n.x, n.y * f32(fx), n.z * f32(fy));\n}\n\n// \u230Aa / b\u230B for b > 0 (WGSL's `/` truncates toward zero).\nfn peg_floor_div(a: i32, b: i32) -> i32 {\n let q = a / b;\n return select(q, q - 1, (a % b != 0) && (a < 0));\n}\n\n// Value noise on a TURNED lattice, exact at any row: lattice coordinates M\xB7(x, Y)\xB7num/den, M = [[a, \u2212b], [b, a]] an INTEGER rotation\n// and scale (cells turned atan(b/a), num\xB7\u221A(a\xB2 + b\xB2)/den of them to a pitch), so no octave's cells line up with the board or with\n// another octave's \u2014 value noise's square cells stop reading as squares. The carried row splits R = den\xB7Q + r: Q's share is an\n// exact integer (\u2212b\xB7num\xB7Q, a\xB7num\xB7Q); only x, r and fy, all small, are ever a float. One pre-gathered load.\nfn peg_rot(ht: texture_2d<f32>, p: PegPoint, a: i32, b: i32, num: i32, den: i32, seed: u32) -> f32 {\n let q = peg_floor_div(p.R, den);\n let r = f32(p.R - q * den) + p.fy;\n let uv = vec2f(f32(a) * p.x - f32(b) * r, f32(b) * p.x + f32(a) * r) * (f32(num) / f32(den));\n let cell = floor(uv);\n let base = vec2i(i32(cell.x) - b * num * q, i32(cell.y) + a * num * q);\n return peg_vnoise(ht, u32(base.x), u32(base.y), uv - cell, seed).x;\n}\n\n// The research's \"fine detail fading with pixel footprint\", per band: F cycles per pitch are kept while a cycle spans \u2265 4 device\n// px and gone at 2, so a scrolling board never shimmers (a fleck by its thin side, not its cell). The footprint is the frame's, so\n// every band's share is a UNIFORM (pass.ts `keep` \u2014 `1 \u2212 smoothstep(\xBC, \xBD, fp\xB7F)`): the face's grain (\u2248 19) \xB7 85 \xB7 190, then\n// 210 \xB7 42 \xB7 105, the punched fibre's 9 \xB7 18 \xB7 36.\n\n// Sparse fibre flecks (the research's `flecks`): one randomly turned ellipse per jittered cell at F per pitch, signed \u2014 dark\n// negative, pale positive. A stroke's thin side is sub-pixel long before its cell is (\u2248 0.3 of it): its edge is widened by the pixel's\n// footprint in the ellipse's own measure and its peak lowered to keep its ink \u2014 analytic AA, the stroke a faint line, never a dash\n// (the research averaged 1024 jittered samples to the same end). `fp` = pitches per device px.\nfn peg_flecks(p: PegPoint, F: u32, density: f32, seed: u32, fp: f32) -> f32 {\n let X = p.x * f32(F);\n let ix = floor(X);\n let Y = p.fy * f32(F);\n let iy = floor(Y);\n let cx = u32(i32(ix));\n let cy = u32(p.R) * F + u32(i32(iy));\n let q = vec2f(X - ix, Y - iy);\n var acc = 0.0;\n for (var j = -1; j <= 1; j++) {\n for (var i = -1; i <= 1; i++) {\n let r = peg_hash4(cx + u32(i), cy + u32(j), seed);\n if (r.w > density) { continue; }\n let d = q - (vec2f(f32(i), f32(j)) + r.xy);\n let ang = r.z * 3.14159265;\n let ca = cos(ang);\n let sa = sin(ang);\n let dl = vec2f(d.x * ca + d.y * sa, -d.x * sa + d.y * ca);\n let wid = 0.10 + 0.10 * r.y;\n let e = length(vec2f(dl.x / (0.35 + 0.65 * r.x), dl.y / wid));\n let aa = fp * f32(F) / wid;\n acc += (1.0 - smoothstep(0.6 - aa, 1.0 + aa, e)) / (1.0 + 2.0 * aa) * select(-1.0, 0.8, r.z > 0.62);\n }\n }\n return clamp(acc, -1.0, 1.0);\n}\n\n// ---- the lattice (the research's cellCoord \xB7 sdStadiumV)\nstruct PegHole { q: vec2f, d: f32, g: vec2f }\n\n// The vertical stadium at an offset from its centre: the distance (negative inside) and its gradient, pointing out of the hole.\nfn peg_stadium(t: TrayUniforms, q: vec2f) -> vec3f {\n let e = vec2f(q.x, q.y - clamp(q.y, -t.hole.y, t.hole.y));\n let len = max(length(e), 1.0e-6);\n return vec3f(len - t.hole.x, e / len);\n}\n\n// The hole of the nearest row (a hole's half-height, 0.33, is under half a pitch): rows at Y = r + \xBE, odd rows shifted \xBD \u2014 the\n// stagger's parity from the exact row index. A hole whose centre lies within the solid border of a side is never punched.\nfn peg_hole(t: TrayUniforms, p: PegPoint) -> PegHole {\n let dr = floor(p.fy - t.depth.w + 0.5);\n let row = p.R + i32(dr);\n let shift = t.depth.z + select(0.0, 0.5, (row & 1) != 0);\n let hx = p.x - shift;\n let col = floor(hx + 0.5);\n var h: PegHole;\n h.q = vec2f(hx - col, p.fy - dr - t.depth.w);\n let s = peg_stadium(t, h.q);\n h.d = s.x;\n h.g = s.yz;\n let cx = col + shift;\n if ((cx < t.hole.w) || (cx > t.rect.z / t.view.w - t.hole.w)) { h.d = 1.0e3; }\n return h;\n}\n\n// ---- the materials (the research's boardMat \xB7 wallMat), linear albedo\n// The tempered face: the TONE drift \u2014 two smooth octaves on lattices turned against the board and each other (cells 2.24 and 0.89\n// of a pitch), the research's big and mid fbm as a UI pitch shows them \u2014 and the fine fibre GRAIN on a third (cells 0.05 of a pitch,\n// \u2248 19 a pitch: what survives of the research's fine grain where its footprint lets it), then at a reading pitch the research's fine\n// height grain and its bump, the fibre flecks \u2014 every band faded by its own footprint. Out: the grain as one factor on the face's\n// linear albedo (`g`: the albedo is face\xB7(1 + g)), the height's gradient (the bump), and whether the face is FLAT (every bump faded).\nstruct PegFace { g: f32, grad: vec2f, flat: bool }\nfn peg_face(ht: texture_2d<f32>, t: TrayUniforms, p: PegPoint, fp: f32) -> PegFace {\n let tone = 0.62 * (peg_rot(ht, p, 2, 1, 1, 5, 0x3a1u) - 0.5) + 0.38 * (peg_rot(ht, p, 1, -2, 1, 2, 0x5c7u) - 0.5);\n var grain = 0.0;\n let kg = t.keepFace.x;\n if (kg > 0.0) { grain = kg * (peg_rot(ht, p, 3, 1, 6, 1, 0x2b1u) - 0.5); }\n // the fine height (the research's faceHeight: 0.55 \xB7 85, 0.30 \xB7 190\xD770, 0.15 \xB7 60\xD7210) \u2014 at a UI pitch its footprint fades it\n var h = 0.0;\n var grad = vec2f(0.0);\n let k85 = t.keepFace.z;\n let k190 = t.keepFace.w;\n let k210 = t.keepFine.x;\n if (k85 > 0.0) { let n = peg_band(ht, p, 85u, 85u, 0x1f3u); h += 0.55 * k85 * (n.x - 0.5); grad += 0.55 * k85 * n.yz; }\n if (k190 > 0.0) { let n = peg_band(ht, p, 190u, 70u, 0x4a7u); h += 0.30 * k190 * (n.x - 0.5); grad += 0.30 * k190 * n.yz; }\n if (k210 > 0.0) { let n = peg_band(ht, p, 60u, 210u, 0x7c9u); h += 0.15 * k210 * (n.x - 0.5); grad += 0.15 * k210 * n.yz; }\n let k42 = t.keepFine.y;\n let k105 = t.keepFine.z;\n var fl = 0.0;\n if (k42 > 0.0) { fl += 0.30 * k42 * peg_flecks(p, 42u, 0.40, 0xa13u, fp); }\n if (k105 > 0.0) { fl += 0.18 * k105 * peg_flecks(p, 105u, 0.35, 0xc35u, fp); }\n var f: PegFace;\n f.g = (1.0 + 0.12 * tone + 0.08 * grain + 0.40 * h) * (1.0 + fl) - 1.0;\n f.grad = grad;\n f.flat = (k85 <= 0.0) && (k190 <= 0.0) && (k210 <= 0.0);\n return f;\n}\n\n// The paler, fuzzier punched fibre (the research's edge): its clumps at 9 \xB7 18 \xB7 36 per pitch, faded by the footprint.\nfn peg_edge(ht: texture_2d<f32>, t: TrayUniforms, p: PegPoint, fp: f32) -> vec3f {\n var clump = 0.0;\n let k9 = t.keepEdge.x;\n let k18 = t.keepEdge.y;\n let k36 = t.keepEdge.z;\n if (k9 > 0.0) { clump += 0.5 * k9 * (peg_band(ht, p, 9u, 9u, 0x3e5u).x - 0.5); }\n if (k18 > 0.0) { clump += 0.25 * k18 * (peg_band(ht, p, 18u, 18u, 0x5a9u).x - 0.5); }\n if (k36 > 0.0) { clump += 0.125 * k36 * (peg_band(ht, p, 36u, 36u, 0x7bdu).x - 0.5); }\n return t.edge.xyz * (1.0 + 0.30 * clump / 0.875);\n}\n\n// ---- the light: the research's HOME lamp (D-K3.4), in the desk's colour law and room\n// The desk's colour law on the tray (the paper's `paper_colour`, chain 0): by day the mat's grade on the sRGB albedo \u2014 lit (`gobo`\n// 1) the identity, a configured byte is the drawn byte; in shadow the room's \xBD and its touch of saturation \u2014 by night the Moon and\n// the eye (mat.wgsl `night_mat`) on the linear albedo; between, the cross-fade.\nfn tray_colour(u: MatUniforms, albedo: vec3f, gobo: f32, noise: f32) -> vec3f {\n let day = shade_mat(u, albedo, gobo);\n if (u.night.x <= 0.0) { return day; }\n let night = night_mat(u, srgb_to_linear(albedo), gobo, noise);\n if (u.night.x >= 1.0) { return night; }\n return mix(day, night, u.night.x);\n}\n\n// A surface under the lamp, its linear albedo and normal \u2192 the colour on screen. The relief is the paper's \u2014 `(N\xB7L/L_z)^(1/2.2)`, the\n// flat face exactly 1 \u2014 and only brightens; a normal turned from the lamp takes the room's grade instead (`gobo` = the Lambert term),\n// so a face turned away reads as a shadow does, never black. `vis`: the lamp seen at all. The glint is the photo's.\nfn tray_lit(u: MatUniforms, t: TrayUniforms, alb: vec3f, n: vec3f, vis: f32, noise: f32) -> vec3f {\n let L = t.lamp.xyz;\n let lam = clamp(dot(n, L) / max(L.z, 1.0e-3), 0.0, 1.6);\n let srgb = clamp(night_encode(alb) * pow(max(lam, 1.0), 1.0 / 2.2), vec3f(0.0), vec3f(1.0));\n var c = tray_colour(u, srgb, min(lam, 1.0) * vis, noise);\n let H = normalize(L + vec3f(0.0, 0.0, 1.0));\n let spec = pow(saturate(dot(n, H)), 48.0) * 0.08 * vis;\n let glint = mix(vec3f(1.0, 0.985, 0.95), vec3f(0.78, 0.86, 1.0) * 0.5, saturate(u.night.x));\n return c + glint * spec;\n}\n\n// ---- the board: the face and the rim fillet, and the desk through a hole\n// The front surface at a board point: the face, or \u2014 within k of a hole \u2014 the fillet. On smax(slab, \u2212hole, k) = 0 seen head-on the\n// surface's blend weight is h = \u221A(d/k) for d \u2208 [0,k], and its normal normalize(((h \u2212 1)\xB7\u2207d, h)) sweeps from the hole's wall to the\n// face: the rim's lit edge and its shadowed one, in the paler punched fibre where the normal is steep (the research's faceness).\nfn peg_surface(ht: texture_2d<f32>, u: MatUniforms, t: TrayUniforms, p: PegPoint, h: PegHole, fp: f32, noise: f32) -> vec3f {\n let k = t.hole.z;\n let face = peg_face(ht, t, p, fp);\n // the PLAIN FACE \u2014 clear of every fillet, its bump faded: N\xB7L is L_z exactly, the relief 1, the glint nothing; the grain's small\n // linear factor rides the configured byte to first order ((1 + g)^(1/2.4) \u2248 1 + g/2.4 \u2014 the byte itself where the grain is 0)\n if ((h.d >= k) && face.flat) {\n return tray_colour(u, clamp(t.faceSrgb.xyz * (1.0 + face.g / 2.4), vec3f(0.0), vec3f(1.0)), 1.0, noise);\n }\n var n = normalize(vec3f(-face.grad * 0.003, 1.0)); // the research's bump: 0.0030 per unit of the height's slope\n if (h.d < k) {\n let s = sqrt(max(h.d, 0.0) / k);\n n = normalize(vec3f((s - 1.0) * h.g, s));\n }\n let faceness = smoothstep(0.35, 0.75, n.z);\n var alb = t.face.xyz * (1.0 + face.g);\n if (faceness < 1.0) { alb = mix(peg_edge(ht, t, p, fp) * 0.86, alb, faceness); } // the punched rim, burnished at the front\n return tray_lit(u, t, alb, n, 1.0, noise);\n}\n\n// Through a hole: THE DESK AS DRAWN (design-018 \xA73). A hole is cut through the board, so it shows what lies under the drawer this\n// frame \u2014 the mat's green, its grid lines, the leaf shadows, anything laid there \u2014 in the board's shadow: premultiplied BLACK over the\n// frame, at the alpha that leaves `f` of the light. THE TWO PLANES (design-017 \xA76.4) give the lamp at the mat: the ray from the mat\n// toward the lamp crosses the board's back face at A and its front face at B; the mat is lit iff both lie in the SAME hole \u2014 B's, with\n// A tested against it (a convex prism holds the segment iff it holds both ends) \u2014 the penumbra from the SDF distance, widened by the\n// lamp's size over each plane's height. The room's light reaches the mat through the hole, cut by the cavity. f = a\xB7cav + (1 \u2212 a)\xB7vis,\n// `a` the room's share (the mat's own shade-in-shadow: shade_mat at gobo 0) \u2014 ENCODED as the mat's own chain encodes its shade (its\n// grade lives in pow-2.2 space), so the densest hole is the mat's shade under a leaf \u2014 over the desk dimmed as the rest of it is.\nfn peg_through(u: MatUniforms, t: TrayUniforms, p: PegPoint, h: PegHole, fp: f32) -> f32 {\n let L = t.lamp.xyz;\n let lz = max(L.z, 1.0e-3);\n let off = L.xy / lz;\n let thick = t.depth.x;\n let gap = t.depth.y;\n let hb = peg_hole(t, peg_move(p, off * (gap + thick)));\n let da = peg_stadium(t, hb.q - off * thick).x;\n // the penumbra: the lamp's angular radius over the ray's length to each plane's edge (height / L_z), and a pixel\n let wa = t.lamp.w * gap / lz + fp;\n let wb = t.lamp.w * (gap + thick) / lz + fp;\n let vis = smoothstep(-wb, wb, -hb.d) * smoothstep(-wa, wa, -da);\n let cav = mix(t.cavity.x, t.cavity.y, smoothstep(0.0, t.cavity.z, -h.d));\n let a = 1.0 - u.gobo.z * (1.0 - u.gobo.w);\n let f = a * cav + (1.0 - a) * vis;\n return 1.0 - (1.0 - t.dim) * pow(f, 1.0 / 2.2);\n}\n\n// ---- the drawer\nfn tray_erf(x: f32) -> f32 {\n let s = sign(x);\n let a = abs(x);\n let k = 1.0 / (1.0 + 0.3275911 * a);\n let y = 1.0 - (((((1.061405429 * k - 1.453152027) * k) + 1.421413741) * k - 0.284496736) * k + 0.254829592) * k * exp(-a * a);\n return s * y;\n}\n\n// Gaussian-blurred coverage of an outline at distance d (the shadow, straight out of the field \u2014 the paper's).\nfn tray_blur(d: f32, sigma: f32) -> f32 { return 0.5 - 0.5 * tray_erf(d / max(sigma * 1.4142136, 1.0e-4)); }\n\n// The drawer's outline (CSS px, negative inside): a box whose top corners round and whose bottom lies past the view \u2014 what is drawn\n// and what casts the shadows (the finger notch retired \u2014 design-018 \xA72).\nfn tray_outline(t: TrayUniforms, p: vec2f) -> f32 {\n let r = t.shape.x;\n let ext = vec2f(0.5 * t.rect.z, 0.5 * t.rect.w + r);\n return sdf_round_box(p - vec2f(t.rect.x + ext.x, t.rect.y + ext.y), ext, r);\n}\n\n// The board point under a pixel (CSS px) \u2014 the rows on screen added to the carried rows in i32.\nfn tray_point(t: TrayUniforms, p: vec2f) -> PegPoint {\n let ly = (p.y - t.rect.y) / t.view.w + t.frac;\n let fl = floor(ly);\n return PegPoint((p.x - t.rect.x) / t.view.w, t.rowBase + i32(fl), ly - fl);\n}\n\n// The board's EDGE (design-018 \xA72): an ARRIS \u2014 a quarter-round `A` px wide where the face meets the outline, in the FACE's own\n// material (its tone at the board point under it, so the grain runs on without a seam; not the paler punched fibre). Its normal turns\n// from the eye (on the face's side) to the outline's outward direction (at the outline): it catches the lamp where the outline faces\n// it \u2014 the top and the left \u2014 and falls to the room's shade on the far side. `o`: the outline's distance there (\u2212A \u2026 0).\nfn tray_arris(ht: texture_2d<f32>, u: MatUniforms, t: TrayUniforms, p: vec2f, o: f32, noise: f32) -> vec3f {\n let e = 0.5;\n let gx = tray_outline(t, p + vec2f(e, 0.0)) - tray_outline(t, p - vec2f(e, 0.0));\n let gy = tray_outline(t, p + vec2f(0.0, e)) - tray_outline(t, p - vec2f(0.0, e));\n let g = vec2f(gx, gy) / max(length(vec2f(gx, gy)), 1.0e-6);\n let s = clamp(1.0 + o / t.shape.y, 0.0, 1.0);\n let n = vec3f(s * g, sqrt(max(1.0 - s * s, 0.0)));\n let face = peg_face(ht, t, tray_point(t, p), t.fp);\n return tray_lit(u, t, t.face.xyz * (1.0 + face.g), n, 1.0, noise);\n}\n\n// The board to its edge, PREMULTIPLIED: the point under the carry, its hole \u2014 the front surface over it by its analytic coverage, and\n// where the surface is not, the desk seen through (black at the hole's alpha). `holes` scales every hole's OPENING: 1 as punched (the\n// board under the specimens), 0 CLOSED \u2014 the plain face, its fillets' relief flattened into it (the VEIL's board, `tray_veil`).\nfn tray_board(ht: texture_2d<f32>, u: MatUniforms, t: TrayUniforms, p: vec2f, noise: f32, holes: f32) -> vec4f {\n let pt = tray_point(t, p);\n let h = peg_hole(t, pt);\n let cg = clamp(0.5 + h.d / t.fp, 0.0, 1.0); // the surface's analytic coverage as punched\n let open = (1.0 - cg) * holes; // what of the pixel the hole opens\n var col = vec4f(0.0);\n if (holes < 1.0) {\n // closing: the plain face where the hole no longer opens, and the fillet (within `hole.z` of it) turning to the face\n var plain = h;\n plain.d = 1.0e3;\n let face = peg_surface(ht, u, t, pt, plain, t.fp, noise);\n var s = face;\n if (cg > 0.0 && h.d < t.hole.z) { s = mix(face, peg_surface(ht, u, t, pt, h, t.fp, noise), holes); }\n col = vec4f(s, 1.0) * cg + vec4f(face, 1.0) * ((1.0 - cg) * (1.0 - holes));\n } else if (cg > 0.0) {\n col = vec4f(peg_surface(ht, u, t, pt, h, t.fp, noise), 1.0) * cg;\n }\n if (open > 0.0) { col += vec4f(0.0, 0.0, 0.0, peg_through(u, t, pt, h, t.fp) * open); }\n return col;\n}\n\n// A pixel INSIDE the drawer's outline (`o` its distance there, `px` a device px in CSS px), premultiplied: the edge within `A` of the\n// outline, the board inside it with its holes at `holes` \u2014 blended across the pixel where they meet \u2014 and the edge's INNER SHADOW over\n// the board (D-R1.2): the window's lip on the lamp's side, black at up to `shape.z`, fading over `shape.w` px inside the edge, where the\n// outline faces the lamp (the push of the drawer's lamp shadow points away from it).\nfn tray_inside(ht: texture_2d<f32>, u: MatUniforms, t: TrayUniforms, p: vec2f, px: f32, o: f32, noise: f32, holes: f32) -> vec4f {\n let k = clamp(0.5 + (o + t.shape.y) / px, 0.0, 1.0);\n var col = vec4f(0.0);\n if (k > 0.0) { col = vec4f(tray_arris(ht, u, t, p, o, noise), 1.0); }\n if (k < 1.0) {\n var b = tray_board(ht, u, t, p, noise, holes);\n if (t.shape.z > 0.0 && -o - t.shape.y < t.shape.w) {\n let e = 0.5;\n let g = normalize(vec2f(tray_outline(t, p + vec2f(e, 0.0)) - tray_outline(t, p - vec2f(e, 0.0)), tray_outline(t, p + vec2f(0.0, e)) - tray_outline(t, p - vec2f(0.0, e))) + vec2f(1.0e-6, 0.0));\n let facing = max(dot(g, -normalize(t.shadow.zw)), 0.0);\n let sh = t.shape.z * facing * (1.0 - smoothstep(0.0, t.shape.w, -o - t.shape.y));\n b = vec4f(b.rgb * (1.0 - sh), 1.0 - (1.0 - b.a) * (1.0 - sh));\n }\n col = mix(b, col, k);\n }\n return col;\n}\n\n// One pixel of the tray UNDER its specimens (`frag` in device px), the view whole: the dim over the desk, the drawer's shadows on it,\n// the board to the outline and its edge, every hole as punched \u2014 premultiplied, each pixel drawn once. The board scrolls under its edge\n// (the drawer is a window onto it). The specimens hang over the board inside the edge and under the header (their face clip,\n// tray/specimens.ts `faceClip`); the VEIL (`tray_veil`) is laid over them all at the top.\nfn tray_drawer(u: MatUniforms, t: TrayUniforms, frag: vec2f, noise_tex: texture_2d<f32>, noise_samp: sampler, ht: texture_2d<f32>) -> vec4f {\n let dpr = t.view.z;\n let p = frag / dpr;\n let px = 1.0 / dpr;\n let dim = vec4f(0.0, 0.0, 0.0, t.dim);\n // beyond the shadows' reach: the dim alone\n let reach = 3.0 * max(t.shadow.x, t.room.x) + length(t.shadow.zw);\n if ((p.x < t.rect.x - reach) || (p.x > t.rect.x + t.rect.z + reach) || (p.y < t.rect.y - reach)) { return dim; }\n // the blue noise is the NIGHT's (the rods' snow, mat.wgsl `night_mat`): by day the board takes none \u2014 its own grain never bands,\n // and a scrolled board is then the same pixels, moved\n var bn = vec3f(0.5);\n if (u.night.x > 0.0) { bn = textureSampleLevel(noise_tex, noise_samp, frag * u.noise.z + u.noise.xy, 0.0).rgb; }\n // DEEP INSIDE \u2014 clear of the rounded corners, the edge, its inner shadow and two px more: no edge, no shadow, no outline to evaluate\n // (premultiplied \u2014 a hole's alpha is its shadow over the desk, no longer 1)\n let m = t.shape.y + t.shape.w + 2.0;\n if ((p.x > t.rect.x + m) && (p.x < t.rect.x + t.rect.z - m) && (p.y > t.rect.y + t.shape.x + m)) {\n return tray_board(ht, u, t, p, bn.y, 1.0);\n }\n let o = tray_outline(t, p);\n let cover = clamp(0.5 - o / px, 0.0, 1.0);\n // the shadows on the desk \u2014 the room's round the outline, the lamp's pushed along its ground direction, one over the other \u2014 where\n // the drawer does not cover them\n var under = dim;\n if (cover < 1.0) {\n let room = t.room.y * tray_blur(o, t.room.x);\n let lamp = t.shadow.y * tray_blur(tray_outline(t, p - t.shadow.zw), t.shadow.x);\n under = vec4f(0.0, 0.0, 0.0, 1.0 - (1.0 - room) * (1.0 - lamp) * (1.0 - t.dim));\n }\n if (cover <= 0.0) { return under; }\n return tray_inside(ht, u, t, p, px, o, bn.y, 1.0) * cover + under * (1.0 - cover);\n}\n\n// THE VEIL (design-018 rev 5 \u2014 James, 2026-09-28: \"at the edge fading, somehow the holes are displayed on top of objects\"): the PLAIN\n// board \u2014 its face with every hole closed, its edge, the edge's inner shadow: `tray_drawer`'s own pixel, less its holes \u2014 laid OVER\n// everything the drawer holds (the specimens and their tags), whole in the HEADER (`fade.y` under the edge's inside) and\n// fading out over the ramp below it (`fade.x`): `1 \u2212 smoothstep(foot, foot + fade.x, y)`. What hangs there and the holes behind it fade\n// INTO THE BOARD together \u2014 a specimen never turns see-through, so no hole shows through it (the specimens are opaque to the face\n// clip's top; a hard clip at the header's foot lies under the veil's whole). Only pixels wholly inside the outline: its antialiased rim\n// is `tray_drawer`'s, and the veil there would lay the edge twice. A host's FOOT inset (petition I21, `fade.z` \u2014 0 none) is the same\n// mirrored at the board's bottom edge: whole in the foot, fading out over the ramp above its line (`smoothstep(line \u2212 fade.w, line, y)`;\n// the face's clip ends at the line, under the veil's whole) \u2014 the larger of the two ramps where a short drawer's meet.\nfn tray_veil(u: MatUniforms, t: TrayUniforms, frag: vec2f, noise_tex: texture_2d<f32>, noise_samp: sampler, ht: texture_2d<f32>) -> vec4f {\n let p = frag / t.view.z;\n let px = 1.0 / t.view.z;\n let foot = t.rect.y + t.shape.y + t.fade.y;\n var a = 1.0 - smoothstep(foot, foot + t.fade.x, p.y);\n if (t.fade.z > 0.0) {\n let line = t.rect.y + t.rect.w - t.fade.z;\n var bottom = step(line, p.y);\n if (t.fade.w > 0.0) { bottom = smoothstep(line - t.fade.w, line, p.y); }\n a = max(a, bottom);\n }\n if (a <= 0.0) { return vec4f(0.0); }\n let o = tray_outline(t, p);\n if (o > -0.5 * px) { return vec4f(0.0); }\n var bn = vec3f(0.5);\n if (u.night.x > 0.0) { bn = textureSampleLevel(noise_tex, noise_samp, frag * u.noise.z + u.noise.xy, 0.0).rgb; }\n return tray_inside(ht, u, t, p, px, o, bn.y, 0.0) * a;\n}\n"
|
|
743
739
|
};
|
|
744
740
|
|
|
745
741
|
// ../desk/src/shaders.ts
|
|
@@ -2524,4 +2520,4 @@ export {
|
|
|
2524
2520
|
writeChanged,
|
|
2525
2521
|
lineWeight
|
|
2526
2522
|
};
|
|
2527
|
-
//# sourceMappingURL=chunk-
|
|
2523
|
+
//# sourceMappingURL=chunk-VZEZ7CYW.js.map
|