/** * WGSL every shader shares, and the machinery that specializes a shader for a * blend mode. Sources are built in TypeScript so a variant is a string the * registry composes, rather than one file per combination. */ /** * The group 0 uniform block every mark shader binds, laid out the way * BufferManager writes it, with `dpi()` for the shaders that measure in device * pixels. */ export declare function uniformBlock(): string; /** Canvas pixels to clip space. y flips because canvas coordinates grow down. */ export declare const TO_NDC = "fn toNdc(p: vec2, resolution: vec2) -> vec2 {\n var q = p / resolution;\n q.y = 1.0 - q.y;\n return q * 2.0 - 1.0;\n}"; /** * Every shader ends this way: `blendAdjust` for the mode, then one fragment * entry point per colour function. A colour function returns straight alpha and * the entry point drops a fragment covering nothing before weighting the rest. * * The discard is not an optimization. Marks grow their geometry past the shape * so an analytic edge is not clipped, and those empty fragments still change * the destination under a multiply or a min. */ export declare function fragmentTail(blend: string, entries?: Record): string; /** A segment direction that survives a zero-length segment, and its normal. */ export declare const SEGMENT_NORMAL = "fn safeDirection(d: vec2) -> vec2 {\n return select(vec2(1.0, 0.0), normalize(d), length(d) > 1e-9);\n}\n\nfn normalAt(d: vec2) -> vec2 {\n let dir = safeDirection(d);\n return vec2(-dir.y, dir.x);\n}"; /** * The stroke over the fill, each taking its true share of the pixel. * Thresholding instead would hand the whole pixel to one of them, which drops * the inner half of any stroke thin enough to straddle a pixel boundary. * * Over, not side by side. Canvas fills the whole shape and then strokes on top, * so a stroke that is translucent shows the fill through it and one that is * fully transparent leaves the fill untouched. Giving the stroke band to the * stroke alone instead ate a ring off every such shape: a vega legend swatch is * `stroke: transparent` with a width of 1.5, which came out 8px across where * canvas draws 10. An opaque stroke covers the fill under it either way, so * nothing that was already right moves. */ export declare const FILL_STROKE_SHARE = "fn fillStrokeShare(fill: vec4, stroke: vec4, fillCov: f32, strokeCov: f32) -> vec4 {\n let sa = stroke.a * strokeCov;\n let fa = fill.a * fillCov * (1.0 - sa);\n let a = sa + fa;\n return vec4((stroke.rgb * sa + fill.rgb * fa) / max(a, 1e-6), a);\n}"; /** * Fraction of the pixel covered by an axis-aligned box, computed the way canvas * does it rather than from MSAA samples. Two abutting rects then produce * complementary coverage, so the seam is the faint one canvas leaves and not a * whole missing sample. A deliberate gap between them is preserved exactly, * because the geometry is untouched. lo/hi are in device pixels. * * Taking the difference of the two edges rather than the distance to the * nearer one is what keeps a box thinner than a pixel honest: the near edge * alone reports a 0.2 px border as 0.6 covered. */ export declare const BOX_COVERAGE = "fn boxCoverage(p: vec2, lo: vec2, hi: vec2) -> f32 {\n let cx = clamp(hi.x - p.x + 0.5, 0.0, 1.0) - clamp(lo.x - p.x + 0.5, 0.0, 1.0);\n let cy = clamp(hi.y - p.y + 0.5, 0.0, 1.0) - clamp(lo.y - p.y + 0.5, 0.0, 1.0);\n return cx * cy;\n}"; /** The two triangles of a unit square, for a vertex stage that indexes its corners. */ export declare const UNIT_QUAD = "array(\n vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),\n vec2(1.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 1.0),\n )"; /** * Straight colour out of a premultiplied texel. Textures stay premultiplied so * filtering does not darken an edge, and the blend state takes straight alpha. */ export declare const UNPREMULTIPLY = "fn unpremultiply(c: vec4) -> vec3 {\n return c.rgb / max(c.a, 1e-6);\n}"; /** Builds one shader source. `arg` names a shape, curve or other sub-variant. */ export type ShaderBuilder = (blend: string, arg?: string) => string;