@jbrowse/render-core 5.0.0-beta.1 → 5.0.0-beta.3

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.
Files changed (120) hide show
  1. package/esm/RenderLifecycleMixin.d.ts +25 -6
  2. package/esm/RenderLifecycleMixin.js +30 -7
  3. package/esm/canvas2dUtils.d.ts +25 -0
  4. package/esm/canvas2dUtils.js +27 -0
  5. package/esm/colorRampLut.d.ts +4 -4
  6. package/esm/colorRampLut.js +4 -4
  7. package/esm/computePipeline.d.ts +13 -0
  8. package/esm/computePipeline.js +55 -0
  9. package/esm/contractReports.d.ts +54 -0
  10. package/esm/contractReports.js +112 -0
  11. package/esm/coverageBand.d.ts +0 -1
  12. package/esm/coverageBand.js +0 -1
  13. package/esm/coverageBandBuffers.d.ts +0 -6
  14. package/esm/coverageBandBuffers.js +1 -13
  15. package/esm/createRenderingBackend.d.ts +14 -18
  16. package/esm/createRenderingBackend.js +4 -2
  17. package/esm/displayPhase.d.ts +10 -1
  18. package/esm/displayPhase.js +2 -1
  19. package/esm/encodeMemo.d.ts +14 -0
  20. package/esm/encodeMemo.js +48 -0
  21. package/esm/gpuDevice.d.ts +9 -0
  22. package/esm/gpuDevice.js +12 -3
  23. package/esm/graphicsCapabilities.d.ts +18 -15
  24. package/esm/graphicsCapabilities.js +23 -16
  25. package/esm/hal/createHal.d.ts +1 -2
  26. package/esm/hal/createHal.js +14 -4
  27. package/esm/hal/deviceGpuCache.js +2 -2
  28. package/esm/hal/gpuHalBase.d.ts +1 -1
  29. package/esm/hal/gpuHalBase.js +2 -3
  30. package/esm/hal/mockHal.d.ts +0 -1
  31. package/esm/hal/mockHal.js +0 -10
  32. package/esm/hal/regionRegistry.d.ts +0 -1
  33. package/esm/hal/regionRegistry.js +3 -15
  34. package/esm/hal/types.d.ts +5 -1
  35. package/esm/hal/webgl2Hal.d.ts +1 -1
  36. package/esm/hal/webgl2Hal.js +2 -2
  37. package/esm/hal/webgpuHal.d.ts +2 -2
  38. package/esm/hal/webgpuHal.js +13 -10
  39. package/esm/installUpload.d.ts +4 -1
  40. package/esm/installUpload.js +10 -41
  41. package/esm/instanceCache.d.ts +10 -7
  42. package/esm/instanceCache.js +14 -15
  43. package/esm/mapUploadSync.d.ts +0 -8
  44. package/esm/mapUploadSync.js +0 -1
  45. package/esm/marks/colorFill.d.ts +17 -0
  46. package/esm/marks/colorFill.js +26 -0
  47. package/esm/marks/drawAgainstHit.d.ts +47 -0
  48. package/esm/marks/drawAgainstHit.js +140 -0
  49. package/esm/marks/glyphPaint.d.ts +16 -0
  50. package/esm/marks/glyphPaint.js +41 -0
  51. package/esm/marks/index.d.ts +9 -0
  52. package/esm/marks/index.js +6 -0
  53. package/esm/marks/markBackend.d.ts +87 -0
  54. package/esm/marks/markBackend.js +133 -0
  55. package/esm/marks/markPaint.d.ts +13 -0
  56. package/esm/marks/markPaint.js +18 -0
  57. package/esm/marks/pointMark.d.ts +24 -0
  58. package/esm/marks/pointMark.js +98 -0
  59. package/esm/marks/spanMark.d.ts +45 -0
  60. package/esm/marks/spanMark.js +71 -0
  61. package/esm/marks/types.d.ts +206 -0
  62. package/esm/marks/types.js +125 -0
  63. package/esm/perRegionRenderingBackend.d.ts +15 -3
  64. package/esm/perRegionRenderingBackend.js +26 -3
  65. package/esm/regionDataMap.d.ts +2 -2
  66. package/esm/regionDataMap.js +25 -23
  67. package/esm/renderingBackendBase.d.ts +11 -14
  68. package/esm/renderingBackendBase.js +7 -7
  69. package/esm/shaders/capsule.js.generated.d.ts +1 -0
  70. package/esm/shaders/capsule.js.generated.js +11 -0
  71. package/esm/shaders/coverageBar.generated.d.ts +2 -2
  72. package/esm/shaders/coverageBar.generated.js +2 -2
  73. package/esm/shaders/coverageMod.generated.d.ts +2 -2
  74. package/esm/shaders/coverageMod.generated.js +2 -2
  75. package/esm/shaders/coverageSnp.generated.d.ts +2 -2
  76. package/esm/shaders/coverageSnp.generated.js +2 -2
  77. package/esm/shaders/hpmath.js.generated.d.ts +1 -0
  78. package/esm/shaders/hpmath.js.generated.js +4 -1
  79. package/esm/shaders/pointMark.consts.generated.d.ts +4 -0
  80. package/esm/shaders/pointMark.consts.generated.js +6 -0
  81. package/esm/shaders/pointMark.generated.d.ts +5 -0
  82. package/esm/shaders/pointMark.generated.js +7 -0
  83. package/esm/shaders/pointMark.iface.generated.d.ts +55 -0
  84. package/esm/shaders/pointMark.iface.generated.js +100 -0
  85. package/esm/shaders/pointMark.js.generated.d.ts +1 -0
  86. package/esm/shaders/pointMark.js.generated.js +16 -0
  87. package/esm/shaders/rowRect.generated.d.ts +0 -1
  88. package/esm/shaders/rowRect.generated.js +0 -1
  89. package/esm/shaders/spanMark.generated.d.ts +4 -0
  90. package/esm/shaders/spanMark.generated.js +6 -0
  91. package/esm/shaders/spanMark.iface.generated.d.ts +58 -0
  92. package/esm/shaders/spanMark.iface.generated.js +122 -0
  93. package/esm/sharedBackendKey.d.ts +18 -0
  94. package/esm/{keyedRenderingBackend.js → sharedBackendKey.js} +5 -0
  95. package/esm/slangPass.d.ts +2 -0
  96. package/esm/slangPass.js +2 -0
  97. package/package.json +14 -4
  98. package/src/shaders/capsule.js.generated.ts +14 -0
  99. package/src/shaders/capsule.slang +12 -4
  100. package/src/shaders/colorPack.slang +5 -1
  101. package/src/shaders/coverageBand.slang +38 -15
  102. package/src/shaders/coverageBar.generated.ts +2 -2
  103. package/src/shaders/coverageBar.slang +9 -0
  104. package/src/shaders/coverageMod.generated.ts +2 -2
  105. package/src/shaders/coverageSnp.generated.ts +2 -2
  106. package/src/shaders/hpmath.js.generated.ts +5 -1
  107. package/src/shaders/hpmath.slang +29 -10
  108. package/src/shaders/pointMark.consts.generated.ts +10 -0
  109. package/src/shaders/pointMark.generated.ts +12 -0
  110. package/src/shaders/pointMark.iface.generated.ts +146 -0
  111. package/src/shaders/pointMark.js.generated.ts +21 -0
  112. package/src/shaders/pointMark.slang +192 -0
  113. package/src/shaders/rowRect.generated.ts +0 -2
  114. package/src/shaders/rowRect.slang +33 -44
  115. package/src/shaders/spanMark.generated.ts +10 -0
  116. package/src/shaders/spanMark.iface.generated.ts +168 -0
  117. package/src/shaders/spanMark.slang +22 -0
  118. package/esm/globalRenderingBackend.d.ts +0 -90
  119. package/esm/globalRenderingBackend.js +0 -53
  120. package/esm/keyedRenderingBackend.d.ts +0 -41
@@ -0,0 +1,18 @@
1
+ import { forEachClippedBlock } from '../canvas2dUtils.js';
2
+ /**
3
+ * Paint one frame's blocks with a mark list — the Canvas2D half of a
4
+ * declaration, and the SVG export path, since `MarkContext2D` is satisfied by
5
+ * both a real 2D context and the SVG one.
6
+ *
7
+ * Its own module, away from the backend that calls it: an SVG export runs with
8
+ * no backend attached at all (and must, or a headless export would depend on a
9
+ * GPU device having been probed), so the path that draws must not pull the HAL
10
+ * in behind it.
11
+ */
12
+ export function paintMarkBlocks(ctx, marks, regions, blocks, state) {
13
+ forEachClippedBlock(ctx, blocks, state.canvasWidth, state.canvasHeight, block => regions.get(block.displayedRegionIndex), (region, block) => {
14
+ for (const mark of marks) {
15
+ mark.paintBlock(ctx, region, block, state);
16
+ }
17
+ });
18
+ }
@@ -0,0 +1,24 @@
1
+ import type { MarkShape } from './types.ts';
2
+ /**
3
+ * The `point` shape's channels: a glyph per instance at `x`, on the `y` scale,
4
+ * widening to a bar where `x2 - x` is wider than the glyph.
5
+ *
6
+ * `x2` is a channel rather than an option because the widening is per instance:
7
+ * one array holds SNPs (`x2 === x + 1`) and structural variants together, and
8
+ * the shader, the painter and the hit test all take the same branch off it.
9
+ */
10
+ export interface PointChannels {
11
+ x: Uint32Array;
12
+ x2: Uint32Array;
13
+ y: Float32Array;
14
+ color: Uint32Array;
15
+ glyph: Uint8Array;
16
+ count: number;
17
+ }
18
+ export interface PointParams {
19
+ /** `[min, max]` of the linear scale `y` is read through. */
20
+ domain: [number, number];
21
+ /** Glyph diameter in CSS px. */
22
+ diameterPx: number;
23
+ }
24
+ export declare const pointMark: MarkShape<PointChannels, PointParams>;
@@ -0,0 +1,98 @@
1
+ import { bpRangeXTuple } from '../blockClipUtils.js';
2
+ import { getDpr, makeBpMapper } from '../canvas2dUtils.js';
3
+ import * as shader from '../shaders/pointMark.generated.js';
4
+ import { valueToYPx } from '../shaders/pointMark.js.generated.js';
5
+ import { slangPass } from '../slangPass.js';
6
+ import { abgrToCssRgba } from './colorFill.js';
7
+ import { appendGlyph } from './glyphPaint.js';
8
+ export const pointMark = {
9
+ id: 'point',
10
+ pass: {
11
+ ...slangPass({ id: 'point', mod: shader }),
12
+ pack: c => shader.packInstances(c, c.count),
13
+ },
14
+ writeUniforms(scratch, clip, block, frame, params) {
15
+ shader.writeUniforms(scratch, {
16
+ bpRangeX: bpRangeXTuple(clip, block.reversed),
17
+ canvasHeight: frame.canvasHeight,
18
+ domainMin: params.domain[0],
19
+ domainMax: params.domain[1],
20
+ zero: 0,
21
+ // viewportWidth and radiusPx stay in CSS units to match canvasHeight:
22
+ // mixing a DPR-scaled radius with a CSS-scaled height draws vertically
23
+ // stretched ellipses on hi-DPI.
24
+ viewportWidth: clip.scissorW,
25
+ radiusPx: params.diameterPx / 2,
26
+ devicePixelRatio: getDpr(),
27
+ });
28
+ },
29
+ paintBlock(ctx, channels, block, frame, params) {
30
+ const { x, x2, y, color, glyph, count } = channels;
31
+ if (count === 0) {
32
+ return;
33
+ }
34
+ const { diameterPx, domain } = params;
35
+ const r = diameterPx / 2;
36
+ const canvasHeight = frame.canvasHeight;
37
+ const domainMin = domain[0];
38
+ const domainMax = domain[1];
39
+ // The per-block closure, not the six-argument `bpToScreenPx`: that spelling
40
+ // re-derives the region span and the block width at every call, and this
41
+ // loop makes two calls per instance. Measured at 1.67x on 100K points.
42
+ const bpToPx = makeBpMapper(block);
43
+ // Batched by colour: a run of one colour is one fillStyle write and one
44
+ // fill() over a shared path, which is most of a painting.
45
+ let current = color[0];
46
+ ctx.fillStyle = abgrToCssRgba(current);
47
+ ctx.beginPath();
48
+ for (let i = 0; i < count; i++) {
49
+ const abgr = color[i];
50
+ if (abgr !== current) {
51
+ ctx.fill();
52
+ current = abgr;
53
+ ctx.fillStyle = abgrToCssRgba(abgr);
54
+ ctx.beginPath();
55
+ }
56
+ const xStart = bpToPx(x[i]);
57
+ const xEnd = bpToPx(x2[i]);
58
+ const yPx = valueToYPx(y[i], domainMin, domainMax, canvasHeight);
59
+ const widthPx = Math.abs(xEnd - xStart);
60
+ if (widthPx > diameterPx) {
61
+ ctx.rect(Math.min(xStart, xEnd), yPx - r, widthPx, diameterPx);
62
+ }
63
+ else {
64
+ appendGlyph(ctx, glyph[i], xStart, yPx, diameterPx);
65
+ }
66
+ }
67
+ ctx.fill();
68
+ },
69
+ hitNearest(channels, block, frame, params, xPx, yPx, candidates, maxDistSq) {
70
+ const { x, x2, y } = channels;
71
+ const bpToPx = makeBpMapper(block);
72
+ const { diameterPx, domain } = params;
73
+ const domainMin = domain[0];
74
+ const domainMax = domain[1];
75
+ const canvasHeight = frame.canvasHeight;
76
+ let best;
77
+ let bestDistSq = maxDistSq;
78
+ for (const i of candidates) {
79
+ const xStart = bpToPx(x[i]);
80
+ const xEnd = bpToPx(x2[i]);
81
+ // The bar branch, in the same words the shader and the painter take it:
82
+ // a mark with extent is grabbed at the nearest point along its span, a
83
+ // glyph at its centre.
84
+ const lo = Math.min(xStart, xEnd);
85
+ const hi = Math.max(xStart, xEnd);
86
+ const ptX = hi - lo > diameterPx ? Math.max(lo, Math.min(xPx, hi)) : xStart;
87
+ const ptY = valueToYPx(y[i], domainMin, domainMax, canvasHeight);
88
+ const dx = xPx - ptX;
89
+ const dy = yPx - ptY;
90
+ const distSq = dx * dx + dy * dy;
91
+ if (distSq < bestDistSq) {
92
+ bestDistSq = distSq;
93
+ best = { index: i, x: ptX, y: ptY, distSq };
94
+ }
95
+ }
96
+ return best;
97
+ },
98
+ };
@@ -0,0 +1,45 @@
1
+ import type { MarkShape } from './types.ts';
2
+ /**
3
+ * The `span` shape's channels: a coloured rectangle from `x` to `x2` on the
4
+ * band belonging to `row`.
5
+ */
6
+ export interface SpanChannels {
7
+ x: Uint32Array;
8
+ x2: Uint32Array;
9
+ row: Uint32Array;
10
+ color: Uint32Array;
11
+ count: number;
12
+ }
13
+ export interface SpanParams {
14
+ /** CSS px per row. */
15
+ rowHeight: number;
16
+ /** Fraction of the row the rect fills, leaving inter-row gaps. */
17
+ rowProportion: number;
18
+ /**
19
+ * Narrowest a span is painted, in CSS px. Zero is a no-op and is what a
20
+ * caller whose marks TILE writes — widening a sub-pixel cell inside a run
21
+ * paints ink the data does not contain. A caller whose marks are sparse
22
+ * intervals floors, because at chromosome zoom the alternative is a smudge.
23
+ */
24
+ minWidthPx: number;
25
+ /**
26
+ * CSS px of overlap added to each span's right edge by the **painter only**.
27
+ *
28
+ * The GPU pass needs none: abutting quads share an exact clip-space edge and
29
+ * the rasterizer fills it once. Canvas2D antialiases each `fillRect`
30
+ * independently, so two runs meeting on a fractional pixel each take partial
31
+ * coverage of it and a hairline of background shows through. Which callers
32
+ * want it splits on the same axis `minWidthPx` does — MAF's cells tile, so it
33
+ * pads; the multi-row painter's features are sparse intervals with background
34
+ * between them by right, so it writes 0.
35
+ *
36
+ * Padding the drawn width and not the anchor is what keeps it growing
37
+ * rightward on both orientations, so a reversed block's spans stay exact
38
+ * mirrors of a forward block's. Wiggle's `WIGGLE_FUDGE_FACTOR` is the same
39
+ * rule spelled per display; the shader must not grow a matching pad.
40
+ */
41
+ seamPx: number;
42
+ /** Rows-area scroll offset in CSS px; 0 for a canvas sized to its content. */
43
+ scrollTop: number;
44
+ }
45
+ export declare const spanMark: MarkShape<SpanChannels, SpanParams>;
@@ -0,0 +1,71 @@
1
+ import { bpRangeXTuple } from '../blockClipUtils.js';
2
+ import { makeBpMapper, spanLeft } from '../canvas2dUtils.js';
3
+ import { drawnRowHeightPx, rowBandOffsetPx, } from '../shaders/rowRect.js.generated.js';
4
+ import * as shader from '../shaders/spanMark.generated.js';
5
+ import { slangPass } from '../slangPass.js';
6
+ import { makeAbgrFill } from './colorFill.js';
7
+ export const spanMark = {
8
+ id: 'span',
9
+ pass: {
10
+ ...slangPass({ id: 'span', mod: shader }),
11
+ pack: c => shader.packInstances(c, c.count),
12
+ },
13
+ writeUniforms(scratch, clip, block, frame, params) {
14
+ shader.writeUniforms(scratch, {
15
+ bpRangeX: bpRangeXTuple(clip, block.reversed),
16
+ canvasHeight: frame.canvasHeight,
17
+ // CSS px, not physical: `extendToMinWidthX` divides `minCellPx` by this
18
+ // to reach clip space, so a CSS width is what makes the floor a CSS-pixel
19
+ // one and matches the painter's `Math.max` below.
20
+ minCellDenomPx: clip.scissorW,
21
+ minCellPx: params.minWidthPx,
22
+ zero: 0,
23
+ rowHeight: params.rowHeight,
24
+ rowProportion: params.rowProportion,
25
+ scrollTop: params.scrollTop,
26
+ });
27
+ },
28
+ paintBlock(ctx, channels, block, _frame, params) {
29
+ const { x, x2, row, color, count } = channels;
30
+ const { rowHeight, rowProportion, minWidthPx, seamPx, scrollTop } = params;
31
+ const h = drawnRowHeightPx(rowHeight, rowProportion);
32
+ const offset = rowBandOffsetPx(rowHeight, rowProportion);
33
+ const bpToPx = makeBpMapper(block);
34
+ const setFill = makeAbgrFill(ctx);
35
+ for (let i = 0; i < count; i++) {
36
+ const xa = bpToPx(x[i]);
37
+ const xb = bpToPx(x2[i]);
38
+ const width = Math.max(minWidthPx, Math.abs(xb - xa));
39
+ setFill(color[i]);
40
+ ctx.fillRect(spanLeft(xa, xb, width), offset + rowHeight * row[i] - scrollTop, width + seamPx, h);
41
+ }
42
+ },
43
+ hitNearest(channels, block, _frame, params, xPx, yPx, candidates, maxDistSq) {
44
+ const { x, x2, row } = channels;
45
+ const { rowHeight, rowProportion, minWidthPx, scrollTop } = params;
46
+ const h = drawnRowHeightPx(rowHeight, rowProportion);
47
+ const offset = rowBandOffsetPx(rowHeight, rowProportion);
48
+ const bpToPx = makeBpMapper(block);
49
+ let best;
50
+ let bestDistSq = maxDistSq;
51
+ for (const i of candidates) {
52
+ const xa = bpToPx(x[i]);
53
+ const xb = bpToPx(x2[i]);
54
+ const width = Math.max(minWidthPx, Math.abs(xb - xa));
55
+ const left = spanLeft(xa, xb, width);
56
+ const top = offset + rowHeight * row[i] - scrollTop;
57
+ // the rect `paintBlock` fills, less `seamPx`, which is painter-only
58
+ // overdraw; distance 0 inside it, else to its nearest edge
59
+ const nx = Math.min(Math.max(xPx, left), left + width);
60
+ const ny = Math.min(Math.max(yPx, top), top + h);
61
+ const dx = xPx - nx;
62
+ const dy = yPx - ny;
63
+ const distSq = dx * dx + dy * dy;
64
+ if (distSq < bestDistSq) {
65
+ bestDistSq = distSq;
66
+ best = { index: i, x: nx, y: ny, distSq };
67
+ }
68
+ }
69
+ return best;
70
+ },
71
+ };
@@ -0,0 +1,206 @@
1
+ import type { BlockClipResult } from '../blockClipUtils.ts';
2
+ import type { ClipContext2D } from '../canvas2dUtils.ts';
3
+ import type { GpuHal } from '../hal/index.ts';
4
+ import type { InstancePass } from '../instancePass.ts';
5
+ import type { RenderBlock } from '../renderBlock.ts';
6
+ import type { FrameDimensions } from '../renderingBackendBase.ts';
7
+ /**
8
+ * The 2D-context subset a shape's painter needs, structural for the same reason
9
+ * `ClipContext2D` is: render-core must not depend on `@jbrowse/core`, where the
10
+ * real `Ctx2D = CanvasRenderingContext2D | SvgCanvas` union lives. Both members
11
+ * satisfy it, so a display passes its `Ctx2D` straight in — which is also what
12
+ * makes one painter the on-screen fallback and the SVG export path.
13
+ */
14
+ export interface MarkContext2D extends ClipContext2D {
15
+ fillStyle: string | CanvasGradient | CanvasPattern;
16
+ strokeStyle: string | CanvasGradient | CanvasPattern;
17
+ lineWidth: number;
18
+ lineCap?: CanvasLineCap;
19
+ lineJoin?: CanvasLineJoin;
20
+ fillRect(x: number, y: number, w: number, h: number): void;
21
+ strokeRect(x: number, y: number, w: number, h: number): void;
22
+ translate(x: number, y: number): void;
23
+ scale(x: number, y: number): void;
24
+ rotate(angle: number): void;
25
+ moveTo(x: number, y: number): void;
26
+ lineTo(x: number, y: number): void;
27
+ bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void;
28
+ arc(x: number, y: number, radius: number, startAngle: number, endAngle: number): void;
29
+ ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number): void;
30
+ setLineDash(segments: number[]): void;
31
+ closePath(): void;
32
+ fill(): void;
33
+ stroke(): void;
34
+ }
35
+ export type MarkFrame = FrameDimensions;
36
+ /** A horizontal strip of the canvas, in CSS px down from its top edge. */
37
+ export interface MarkBand {
38
+ top: number;
39
+ height: number;
40
+ }
41
+ /** Where a shape's ink sits nearest a cursor, and how far that is. */
42
+ export interface MarkHit {
43
+ index: number;
44
+ x: number;
45
+ y: number;
46
+ distSq: number;
47
+ }
48
+ /**
49
+ * A shape: one hand-written `.slang`, one packer, one Canvas2D painter and one
50
+ * hit test, all reading the same channel arrays.
51
+ *
52
+ * `TChannels` is the shape's own vocabulary — parallel typed arrays plus a
53
+ * count. `TParams` is everything else the drawing needs, which reaches the GPU
54
+ * as uniforms and the painter as arguments; the two must be the same values or
55
+ * the backends diverge, which is what having one object per shape prevents.
56
+ *
57
+ * There is no `MarkShape` constructor. A shape is written out once and
58
+ * admitted by a consumer rather than by completeness — the ADR-040 bar. It
59
+ * lives here when two displays share it (`span`) or its geometry is generic
60
+ * (`point`); a display whose shader's generated twins feed its own hit test
61
+ * keeps the shape beside them (variants' `cellMark`) and still declares it
62
+ * through `defineMark`.
63
+ */
64
+ export interface MarkShape<TChannels, TParams> {
65
+ readonly id: string;
66
+ readonly pass: InstancePass<TChannels>;
67
+ writeUniforms(scratch: ArrayBuffer, clip: BlockClipResult, block: RenderBlock, frame: MarkFrame, params: TParams): void;
68
+ paintBlock(ctx: MarkContext2D, channels: TChannels, block: RenderBlock, frame: MarkFrame, params: TParams): void;
69
+ /**
70
+ * The shape's draw predicate over (block, frame, params) — not over its
71
+ * instances, which it is never handed. Both backends skip the block when it
72
+ * says no. Optional: a shape whose every block can carry ink leaves it off.
73
+ *
74
+ * Placement is one use: the canvas continuation marker exists only where a
75
+ * block meets a canvas edge, and an interior block would otherwise shade a
76
+ * whole pileup's worth of vertices to draw nothing. Settings are the other,
77
+ * and the general one: the coverage band's layers read `hasDomain` off
78
+ * `params` so an unresolved autoscale draws no depth-scaled layer rather
79
+ * than bars of arbitrary height, and `showInterbase` off it so the histogram
80
+ * and its triangles turn off together.
81
+ */
82
+ paintsBlock?(block: RenderBlock, frame: MarkFrame, params: TParams): boolean;
83
+ /**
84
+ * The nearest drawn ink to `(xPx, yPx)` among `candidates`, or undefined if
85
+ * nothing beats `maxDistSq`.
86
+ *
87
+ * The candidate set is the caller's, not the shape's: a display with a
88
+ * worker-built index (GWAS's Flatbush over (bp, value)) hands in what the
89
+ * index answered, and one without hands in every instance. What the shape
90
+ * owns is where its ink actually is — the half that drifts from the painter.
91
+ *
92
+ * Distance 0 means the point is on the ink `paintBlock` fills, edges
93
+ * included, and a caller wanting containment alone passes a bound that
94
+ * admits nothing else (`Number.MIN_VALUE`). Only a strictly nearer candidate
95
+ * replaces the best, so on a tie the first candidate wins — a caller that
96
+ * iterates back to front gets the last-painted mark, which is the one on
97
+ * top. `drawAgainstHit.ts` is the sweep that holds a shape to this.
98
+ *
99
+ * Optional, because a shape earns it from a consumer like any other member.
100
+ * MAF answers a hit from row/column arithmetic over the alignment rather than
101
+ * from `span`'s channels, so it reads none.
102
+ */
103
+ hitNearest?(channels: TChannels, block: RenderBlock, frame: MarkFrame, params: TParams, xPx: number, yPx: number, candidates: Iterable<number>, maxDistSq: number): MarkHit | undefined;
104
+ }
105
+ /**
106
+ * Which uniform struct is standing in the HAL's current slot, so the next mark
107
+ * of the SAME block can draw off it instead of packing and staging the same
108
+ * bytes again. Compared by reference and never called or read for its values:
109
+ * one writer plus one `params` lens is one struct, because `writeUniforms` sees
110
+ * nothing but the block, the clip, the frame and what that lens returned.
111
+ *
112
+ * A display declaring several marks over one writer and one lens is the common
113
+ * shape, not an edge case — the coverage band is five, the feature glyphs are
114
+ * five — and every mark after the first was restating the previous one's write.
115
+ *
116
+ * **The caller owns one of these per block and hands the same one to every mark
117
+ * of that block.** A stale one drawn against the next block's clip is silently
118
+ * the wrong geometry, so it is a fresh object per block rather than a field
119
+ * that has to be remembered to reset. A caller that writes uniforms of its own
120
+ * between marks (alignments' per-section pileup write) starts a new one after
121
+ * it, or hands none at all.
122
+ */
123
+ export interface StagedUniforms {
124
+ writer: unknown;
125
+ params: unknown;
126
+ }
127
+ /**
128
+ * A shape bound to one display's region payload and render state — what
129
+ * `defineMark` returns and what a display actually holds.
130
+ *
131
+ * The shape's own generics are erased here on purpose: a display draws a list
132
+ * of marks over heterogeneous shapes, and only the binding knows how to reach
133
+ * either one's channels.
134
+ */
135
+ export interface Mark<TRegion, TState extends MarkFrame> {
136
+ readonly pass: InstancePass<TRegion>;
137
+ /**
138
+ * The pass id whose instance buffer this mark draws from, for a mark that
139
+ * is registered but never uploaded to: the canvas chevrons ride the line
140
+ * buffer and the continuation markers ride rect's. `defineMark` refuses the
141
+ * pairing unless the two passes declare one instance struct.
142
+ */
143
+ readonly bufferOf?: string;
144
+ /**
145
+ * The 256-entry RGBA colour ramp this mark's pass samples this frame, or
146
+ * undefined when it samples none. `createMarkBackend` uploads it per pass and
147
+ * only when the bytes' identity moves, and binds an inert table for the
148
+ * undefined case — a shader owns its sampler unconditionally, and a textured
149
+ * pass with no texture never draws on the WebGPU HAL.
150
+ */
151
+ readonly texture?: (state: TState, region: TRegion) => Uint8Array | undefined;
152
+ drawRegion(hal: GpuHal, scratch: ArrayBuffer, block: RenderBlock, clip: BlockClipResult, region: TRegion, state: TState, regionKey: number, staged?: StagedUniforms): void;
153
+ paintBlock(ctx: MarkContext2D, region: TRegion, block: RenderBlock, state: TState): void;
154
+ hitNearest?(region: TRegion, block: RenderBlock, state: TState, xPx: number, yPx: number, candidates: Iterable<number>, maxDistSq: number): MarkHit | undefined;
155
+ }
156
+ /**
157
+ * `{ shape, channels }` over typed arrays — the declaration a display writes in
158
+ * place of a `GpuXxxRenderer` class, a `Canvas2DXxxRenderer` class, a factory
159
+ * and a pass list.
160
+ *
161
+ * `channels` and `params` are projections, not copies: `channels` names which
162
+ * of the region's arrays feed the shape's lanes, and `params` names which of
163
+ * the render state's values reach the uniforms — and which of the region's,
164
+ * for a value the payload carries rather than the frame (the variant matrix's
165
+ * column count). Both run once per block per frame, never per instance.
166
+ *
167
+ * **`channels` answers `undefined` for a region this mark has nothing in.**
168
+ * That is how one mark list serves a display whose cells are a union — synteny
169
+ * draws ribbon cells, outline cells and glyph-lane cells off one list, and a
170
+ * region key holds exactly one of the three. The mark then packs an empty
171
+ * buffer (which every HAL reads as the release), draws nothing and paints
172
+ * nothing, so the kind test is stated once per mark instead of once per
173
+ * backend.
174
+ *
175
+ * **`params` is handed the block**, because the key a display's per-track
176
+ * state is filed under is the block's `displayedRegionIndex` and nothing else
177
+ * in the lens's reach says which track this is. Every method the lens feeds
178
+ * already receives the block; the lens receiving it is what lets it stay a
179
+ * pick rather than pushing the lookup into the shape.
180
+ *
181
+ * `bufferOf` names the mark whose uploaded buffer this one draws from, so the
182
+ * declaration says what the HAL's `drawPass(id, region, bufferPassId)` says:
183
+ * the chevron mark's channels are the line mark's, packed once under the line
184
+ * pass.
185
+ *
186
+ * `texture` is the colour ramp the mark's pass samples, for a shape that
187
+ * resolves a per-instance scalar through a LUT (wiggle density, and the same
188
+ * 256-entry table HiC and LD bind). It is state, not a channel: the backend
189
+ * uploads it once per pass and re-uploads only when the bytes' identity moves,
190
+ * so a ramp change costs one texture and no instance byte.
191
+ *
192
+ * `band` is the strip of the canvas the mark is clipped to, for a display that
193
+ * stacks bands on one canvas (MAF's coverage strip over its rows viewport).
194
+ * The GPU scissors to it and Canvas2D clips to it, a zero-height band draws
195
+ * nothing, and the shape still places Y against the whole canvas — the band is
196
+ * a clip, never an offset, which is what lets one `scrollTop` serve both
197
+ * backends. A mark without one paints wherever its shape puts ink.
198
+ */
199
+ export declare function defineMark<TRegion, TState extends MarkFrame, TChannels, TParams>(spec: {
200
+ shape: MarkShape<TChannels, TParams>;
201
+ channels: (region: TRegion) => TChannels | undefined;
202
+ params: (state: TState, region: TRegion, block: RenderBlock) => TParams;
203
+ bufferOf?: Mark<TRegion, TState>;
204
+ band?: (state: TState) => MarkBand;
205
+ texture?: (state: TState, region: TRegion) => Uint8Array | undefined;
206
+ }): Mark<TRegion, TState>;
@@ -0,0 +1,125 @@
1
+ import { devicePxBand, withClip } from '../canvas2dUtils.js';
2
+ /**
3
+ * `{ shape, channels }` over typed arrays — the declaration a display writes in
4
+ * place of a `GpuXxxRenderer` class, a `Canvas2DXxxRenderer` class, a factory
5
+ * and a pass list.
6
+ *
7
+ * `channels` and `params` are projections, not copies: `channels` names which
8
+ * of the region's arrays feed the shape's lanes, and `params` names which of
9
+ * the render state's values reach the uniforms — and which of the region's,
10
+ * for a value the payload carries rather than the frame (the variant matrix's
11
+ * column count). Both run once per block per frame, never per instance.
12
+ *
13
+ * **`channels` answers `undefined` for a region this mark has nothing in.**
14
+ * That is how one mark list serves a display whose cells are a union — synteny
15
+ * draws ribbon cells, outline cells and glyph-lane cells off one list, and a
16
+ * region key holds exactly one of the three. The mark then packs an empty
17
+ * buffer (which every HAL reads as the release), draws nothing and paints
18
+ * nothing, so the kind test is stated once per mark instead of once per
19
+ * backend.
20
+ *
21
+ * **`params` is handed the block**, because the key a display's per-track
22
+ * state is filed under is the block's `displayedRegionIndex` and nothing else
23
+ * in the lens's reach says which track this is. Every method the lens feeds
24
+ * already receives the block; the lens receiving it is what lets it stay a
25
+ * pick rather than pushing the lookup into the shape.
26
+ *
27
+ * `bufferOf` names the mark whose uploaded buffer this one draws from, so the
28
+ * declaration says what the HAL's `drawPass(id, region, bufferPassId)` says:
29
+ * the chevron mark's channels are the line mark's, packed once under the line
30
+ * pass.
31
+ *
32
+ * `texture` is the colour ramp the mark's pass samples, for a shape that
33
+ * resolves a per-instance scalar through a LUT (wiggle density, and the same
34
+ * 256-entry table HiC and LD bind). It is state, not a channel: the backend
35
+ * uploads it once per pass and re-uploads only when the bytes' identity moves,
36
+ * so a ramp change costs one texture and no instance byte.
37
+ *
38
+ * `band` is the strip of the canvas the mark is clipped to, for a display that
39
+ * stacks bands on one canvas (MAF's coverage strip over its rows viewport).
40
+ * The GPU scissors to it and Canvas2D clips to it, a zero-height band draws
41
+ * nothing, and the shape still places Y against the whole canvas — the band is
42
+ * a clip, never an offset, which is what lets one `scrollTop` serve both
43
+ * backends. A mark without one paints wherever its shape puts ink.
44
+ */
45
+ export function defineMark(spec) {
46
+ const { shape, channels, params, band, texture } = spec;
47
+ const lender = spec.bufferOf?.pass;
48
+ if (lender &&
49
+ (lender.instanceStride !== shape.pass.instanceStride ||
50
+ JSON.stringify(lender.vertexAttributes) !==
51
+ JSON.stringify(shape.pass.vertexAttributes))) {
52
+ throw new Error(`mark ${shape.id} draws off ${lender.id}'s buffer but declares a different instance struct`);
53
+ }
54
+ const bufferOf = lender?.id;
55
+ return {
56
+ pass: {
57
+ ...shape.pass,
58
+ pack: region => {
59
+ const c = channels(region);
60
+ return c === undefined ? NOTHING : shape.pass.pack(c);
61
+ },
62
+ },
63
+ bufferOf,
64
+ texture,
65
+ drawRegion(hal, scratch, block, clip, region, state, regionKey, staged) {
66
+ const strip = band?.(state);
67
+ const scissor = strip
68
+ ? devicePxBand(strip.top, strip.height, clip.scaleY, clip.pxH)
69
+ : undefined;
70
+ if (scissor?.height !== 0 && channels(region) !== undefined) {
71
+ const p = params(state, region, block);
72
+ if (!shape.paintsBlock || shape.paintsBlock(block, state, p)) {
73
+ if (scissor) {
74
+ hal.setScissor(clip.pxX, scissor.top, clip.pxW, scissor.height);
75
+ }
76
+ if (staged?.writer !== shape.writeUniforms ||
77
+ staged.params !== params) {
78
+ shape.writeUniforms(scratch, clip, block, state, p);
79
+ hal.writeUniforms(scratch);
80
+ if (staged) {
81
+ staged.writer = shape.writeUniforms;
82
+ staged.params = params;
83
+ }
84
+ }
85
+ hal.drawPass(shape.pass.id, regionKey, bufferOf);
86
+ if (scissor) {
87
+ hal.setScissor(clip.pxX, 0, clip.pxW, clip.pxH);
88
+ }
89
+ }
90
+ }
91
+ },
92
+ paintBlock(ctx, region, block, state) {
93
+ const strip = band?.(state);
94
+ const c = channels(region);
95
+ if ((!strip || strip.height > 0) && c !== undefined) {
96
+ const p = params(state, region, block);
97
+ if (!shape.paintsBlock || shape.paintsBlock(block, state, p)) {
98
+ const paint = () => {
99
+ shape.paintBlock(ctx, c, block, state, p);
100
+ };
101
+ if (strip) {
102
+ withClip(ctx, 0, strip.top, state.canvasWidth, strip.height, paint);
103
+ }
104
+ else {
105
+ paint();
106
+ }
107
+ }
108
+ }
109
+ },
110
+ hitNearest: shape.hitNearest
111
+ ? (region, block, state, xPx, yPx, candidates, maxDistSq) => {
112
+ const c = channels(region);
113
+ return c === undefined || bandExcludes(band?.(state), yPx)
114
+ ? undefined
115
+ : shape.hitNearest(c, block, state, params(state, region, block), xPx, yPx, candidates, maxDistSq);
116
+ }
117
+ : undefined,
118
+ };
119
+ }
120
+ // What a mark with nothing in this region packs. An empty pack IS the release
121
+ // — every HAL deletes the pass's prior buffer before it looks at the count.
122
+ const NOTHING = new ArrayBuffer(0);
123
+ function bandExcludes(strip, yPx) {
124
+ return (strip !== undefined && (yPx < strip.top || yPx > strip.top + strip.height));
125
+ }
@@ -4,6 +4,8 @@ import type { InstancePass } from './instancePass.ts';
4
4
  import type { RenderBlock } from './renderBlock.ts';
5
5
  import type { FrameDimensions, RenderingBackend } from './renderingBackendBase.ts';
6
6
  export type { FrameDimensions } from './renderingBackendBase.ts';
7
+ /** Straight RGBA, each channel 0-1 — what `beginFrame` takes. */
8
+ export type ClearColor = readonly [number, number, number, number];
7
9
  /**
8
10
  * Shared contract for per-region streamed GPU backends.
9
11
  *
@@ -12,9 +14,8 @@ export type { FrameDimensions } from './renderingBackendBase.ts';
12
14
  * against a data map passed back through `renderBlocks` each frame.
13
15
  *
14
16
  * `UploadData` is what the upload-side autorun pushes per region;
15
- * `RenderData` is what the render-side reads. They default to the same
16
- * type — MAF is the lone case where they diverge (upload payload also
17
- * carries a pre-encoded GPU buffer that the render side doesn't need).
17
+ * `RenderData` is what the render-side reads. They default to the same type,
18
+ * and every per-region display keeps the default today.
18
19
  *
19
20
  * Renderers are stateless: they keep no per-region cache. Canvas2D backends
20
21
  * stub `upload` / `release` as no-ops since they read everything from the
@@ -58,6 +59,15 @@ export declare abstract class Canvas2DPerRegionRenderingBackend<UploadData, Rend
58
59
  upload(): void;
59
60
  release(): void;
60
61
  renderBlocks(blocks: Block[], regions: ReadonlyMap<number, RenderData>, state: RenderState): boolean;
62
+ /**
63
+ * What the frame is painted onto before the blocks are drawn — the Canvas2D
64
+ * half of the GPU base's `beginFrame` clear. Undefined, the default, leaves
65
+ * `prepareCanvas`'s `clearRect` as the whole of it, which is transparent.
66
+ * Synteny is the one display that overrides: its indel wedges pre-blend
67
+ * against a KNOWN ground and only agree with the base ribbon beside them over
68
+ * a destination that really is that colour.
69
+ */
70
+ protected clearColor(_state: RenderState): ClearColor | undefined;
61
71
  /**
62
72
  * Paint the visible blocks into `this.ctx` (already DPR-prepared). Delegates
63
73
  * to the plugin's pure `drawXxxBlocks(ctx, regions, blocks, state)` function.
@@ -111,4 +121,6 @@ export declare abstract class GpuPerRegionRenderingBackend<UploadData, RenderSta
111
121
  * those whose draw state is fully in the uploaded buffer).
112
122
  */
113
123
  protected abstract drawRegion(block: Block, clip: BlockClipResult, region: RenderData, state: RenderState): void;
124
+ /** @see Canvas2DPerRegionRenderingBackend.clearColor */
125
+ protected clearColor(_state: RenderState): ClearColor;
114
126
  }