@jbrowse/render-core 5.0.0-beta.2 → 5.0.0-beta.4

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 (114) hide show
  1. package/esm/RenderLifecycleMixin.d.ts +20 -1
  2. package/esm/RenderLifecycleMixin.js +25 -2
  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/coverageBand.d.ts +0 -1
  10. package/esm/coverageBand.js +0 -1
  11. package/esm/coverageBandBuffers.d.ts +0 -6
  12. package/esm/coverageBandBuffers.js +1 -13
  13. package/esm/createRenderingBackend.d.ts +14 -18
  14. package/esm/createRenderingBackend.js +4 -2
  15. package/esm/encodeMemo.d.ts +14 -0
  16. package/esm/encodeMemo.js +48 -0
  17. package/esm/gpuDevice.d.ts +9 -0
  18. package/esm/gpuDevice.js +12 -3
  19. package/esm/graphicsCapabilities.d.ts +18 -15
  20. package/esm/graphicsCapabilities.js +23 -16
  21. package/esm/hal/createHal.d.ts +1 -2
  22. package/esm/hal/createHal.js +14 -4
  23. package/esm/hal/deviceGpuCache.js +2 -2
  24. package/esm/hal/gpuHalBase.d.ts +1 -1
  25. package/esm/hal/gpuHalBase.js +2 -3
  26. package/esm/hal/mockHal.d.ts +0 -1
  27. package/esm/hal/mockHal.js +0 -10
  28. package/esm/hal/regionRegistry.d.ts +0 -1
  29. package/esm/hal/regionRegistry.js +3 -15
  30. package/esm/hal/types.d.ts +5 -1
  31. package/esm/hal/webgl2Hal.d.ts +1 -1
  32. package/esm/hal/webgl2Hal.js +2 -2
  33. package/esm/hal/webgpuHal.d.ts +2 -2
  34. package/esm/hal/webgpuHal.js +19 -16
  35. package/esm/installUpload.d.ts +4 -1
  36. package/esm/installUpload.js +7 -39
  37. package/esm/instanceCache.d.ts +10 -7
  38. package/esm/instanceCache.js +14 -15
  39. package/esm/mapUploadSync.d.ts +0 -8
  40. package/esm/mapUploadSync.js +0 -1
  41. package/esm/marks/colorFill.d.ts +17 -0
  42. package/esm/marks/colorFill.js +26 -0
  43. package/esm/marks/drawAgainstHit.d.ts +47 -0
  44. package/esm/marks/drawAgainstHit.js +140 -0
  45. package/esm/marks/glyphPaint.d.ts +16 -0
  46. package/esm/marks/glyphPaint.js +41 -0
  47. package/esm/marks/index.d.ts +9 -0
  48. package/esm/marks/index.js +6 -0
  49. package/esm/marks/markBackend.d.ts +87 -0
  50. package/esm/marks/markBackend.js +137 -0
  51. package/esm/marks/markHit.d.ts +25 -0
  52. package/esm/marks/markHit.js +46 -0
  53. package/esm/marks/markPaint.d.ts +13 -0
  54. package/esm/marks/markPaint.js +18 -0
  55. package/esm/marks/pointMark.d.ts +24 -0
  56. package/esm/marks/pointMark.js +91 -0
  57. package/esm/marks/spanMark.d.ts +45 -0
  58. package/esm/marks/spanMark.js +59 -0
  59. package/esm/marks/types.d.ts +216 -0
  60. package/esm/marks/types.js +146 -0
  61. package/esm/perRegionRenderingBackend.d.ts +15 -3
  62. package/esm/perRegionRenderingBackend.js +26 -3
  63. package/esm/regionDataMap.d.ts +2 -2
  64. package/esm/regionDataMap.js +2 -2
  65. package/esm/renderBlock.d.ts +14 -0
  66. package/esm/renderBlock.js +25 -0
  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/pointMark.consts.generated.d.ts +4 -0
  78. package/esm/shaders/pointMark.consts.generated.js +6 -0
  79. package/esm/shaders/pointMark.generated.d.ts +5 -0
  80. package/esm/shaders/pointMark.generated.js +7 -0
  81. package/esm/shaders/pointMark.iface.generated.d.ts +55 -0
  82. package/esm/shaders/pointMark.iface.generated.js +100 -0
  83. package/esm/shaders/pointMark.js.generated.d.ts +2 -0
  84. package/esm/shaders/pointMark.js.generated.js +19 -0
  85. package/esm/shaders/spanMark.generated.d.ts +4 -0
  86. package/esm/shaders/spanMark.generated.js +6 -0
  87. package/esm/shaders/spanMark.iface.generated.d.ts +58 -0
  88. package/esm/shaders/spanMark.iface.generated.js +122 -0
  89. package/esm/sharedBackendKey.d.ts +18 -0
  90. package/esm/{keyedRenderingBackend.js → sharedBackendKey.js} +5 -0
  91. package/esm/slangPass.d.ts +2 -0
  92. package/esm/slangPass.js +2 -0
  93. package/package.json +14 -4
  94. package/src/shaders/capsule.js.generated.ts +14 -0
  95. package/src/shaders/capsule.slang +12 -4
  96. package/src/shaders/colorPack.slang +5 -1
  97. package/src/shaders/coverageBand.slang +38 -15
  98. package/src/shaders/coverageBar.generated.ts +2 -2
  99. package/src/shaders/coverageBar.slang +9 -0
  100. package/src/shaders/coverageMod.generated.ts +2 -2
  101. package/src/shaders/coverageSnp.generated.ts +2 -2
  102. package/src/shaders/hpmath.slang +7 -1
  103. package/src/shaders/pointMark.consts.generated.ts +10 -0
  104. package/src/shaders/pointMark.generated.ts +12 -0
  105. package/src/shaders/pointMark.iface.generated.ts +146 -0
  106. package/src/shaders/pointMark.js.generated.ts +25 -0
  107. package/src/shaders/pointMark.slang +205 -0
  108. package/src/shaders/rowRect.slang +7 -8
  109. package/src/shaders/spanMark.generated.ts +10 -0
  110. package/src/shaders/spanMark.iface.generated.ts +168 -0
  111. package/src/shaders/spanMark.slang +22 -0
  112. package/esm/globalRenderingBackend.d.ts +0 -90
  113. package/esm/globalRenderingBackend.js +0 -53
  114. package/esm/keyedRenderingBackend.d.ts +0 -41
@@ -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,59 @@
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
+ import { inkOnRect, nearestInk } from './markHit.js';
8
+ export const spanMark = {
9
+ id: 'span',
10
+ pass: {
11
+ ...slangPass({ id: 'span', 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
+ // CSS px, not physical: `extendToMinWidthX` divides `minCellPx` by this
19
+ // to reach clip space, so a CSS width is what makes the floor a CSS-pixel
20
+ // one and matches the painter's `Math.max` below.
21
+ minCellDenomPx: clip.scissorW,
22
+ minCellPx: params.minWidthPx,
23
+ zero: 0,
24
+ rowHeight: params.rowHeight,
25
+ rowProportion: params.rowProportion,
26
+ scrollTop: params.scrollTop,
27
+ });
28
+ },
29
+ paintBlock(ctx, channels, block, _frame, params) {
30
+ const { x, x2, row, color, count } = channels;
31
+ const { rowHeight, rowProportion, minWidthPx, seamPx, scrollTop } = params;
32
+ const h = drawnRowHeightPx(rowHeight, rowProportion);
33
+ const offset = rowBandOffsetPx(rowHeight, rowProportion);
34
+ const bpToPx = makeBpMapper(block);
35
+ const setFill = makeAbgrFill(ctx);
36
+ for (let i = 0; i < count; i++) {
37
+ const xa = bpToPx(x[i]);
38
+ const xb = bpToPx(x2[i]);
39
+ const width = Math.max(minWidthPx, Math.abs(xb - xa));
40
+ setFill(color[i]);
41
+ ctx.fillRect(spanLeft(xa, xb, width), offset + rowHeight * row[i] - scrollTop, width + seamPx, h);
42
+ }
43
+ },
44
+ hitNearest(channels, block, _frame, params, xPx, yPx, candidates, maxDistSq) {
45
+ const { x, x2, row } = channels;
46
+ const { rowHeight, rowProportion, minWidthPx, scrollTop } = params;
47
+ const h = drawnRowHeightPx(rowHeight, rowProportion);
48
+ const offset = rowBandOffsetPx(rowHeight, rowProportion);
49
+ const bpToPx = makeBpMapper(block);
50
+ return nearestInk(candidates, maxDistSq, i => {
51
+ const xa = bpToPx(x[i]);
52
+ const xb = bpToPx(x2[i]);
53
+ const width = Math.max(minWidthPx, Math.abs(xb - xa));
54
+ // the rect `paintBlock` fills, less `seamPx`, which is painter-only
55
+ // overdraw
56
+ return inkOnRect(xPx, yPx, spanLeft(xa, xb, width), offset + rowHeight * row[i] - scrollTop, width, h);
57
+ });
58
+ },
59
+ };
@@ -0,0 +1,216 @@
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, and so does `hitNearest`: a block with no ink on it has nothing
73
+ * to be near. Optional: a shape whose every block can carry ink leaves it
74
+ * off.
75
+ *
76
+ * Placement is one use: the canvas continuation marker exists only where a
77
+ * block meets a canvas edge, and an interior block would otherwise shade a
78
+ * whole pileup's worth of vertices to draw nothing. Settings are the other,
79
+ * and the general one: the coverage band's layers read `hasDomain` off
80
+ * `params` so an unresolved autoscale draws no depth-scaled layer rather
81
+ * than bars of arbitrary height, and `showInterbase` off it so the histogram
82
+ * and its triangles turn off together.
83
+ */
84
+ paintsBlock?(block: RenderBlock, frame: MarkFrame, params: TParams): boolean;
85
+ /**
86
+ * The nearest drawn ink to `(xPx, yPx)` among `candidates`, or undefined if
87
+ * nothing beats `maxDistSq`.
88
+ *
89
+ * The candidate set is the caller's, not the shape's: a display with a
90
+ * worker-built index (GWAS's Flatbush over (bp, value)) hands in what the
91
+ * index answered, and one without hands in every instance. What the shape
92
+ * owns is where its ink actually is — the half that drifts from the painter.
93
+ *
94
+ * Distance 0 means the point is on the ink `paintBlock` fills, edges
95
+ * included, and a caller wanting containment alone passes a bound that
96
+ * admits nothing else (`Number.MIN_VALUE`). Only a strictly nearer candidate
97
+ * replaces the best, so on a tie the first candidate wins — a caller that
98
+ * iterates back to front gets the last-painted mark, which is the one on
99
+ * top. `drawAgainstHit.ts` is the sweep that holds a shape to this.
100
+ *
101
+ * Optional, because a shape earns it from a consumer like any other member.
102
+ * MAF answers a hit from row/column arithmetic over the alignment rather than
103
+ * from `span`'s channels, so it reads none.
104
+ */
105
+ hitNearest?(channels: TChannels, block: RenderBlock, frame: MarkFrame, params: TParams, xPx: number, yPx: number, candidates: Iterable<number>, maxDistSq: number): MarkHit | undefined;
106
+ }
107
+ /**
108
+ * Which uniform struct is standing in the HAL's current slot, so the next mark
109
+ * of the SAME block can draw off it instead of packing and staging the same
110
+ * bytes again. Compared by reference and never called or read for its values:
111
+ * one writer plus one `params` lens is one struct, because `writeUniforms` sees
112
+ * nothing but the block, the clip, the frame and what that lens returned.
113
+ *
114
+ * A display declaring several marks over one writer and one lens is the common
115
+ * shape, not an edge case — the coverage band is five, the feature glyphs are
116
+ * five — and every mark after the first was restating the previous one's write.
117
+ *
118
+ * **The caller owns one of these per block and hands the same one to every mark
119
+ * of that block.** A stale one drawn against the next block's clip is silently
120
+ * the wrong geometry, so it is a fresh object per block rather than a field
121
+ * that has to be remembered to reset. A caller that writes uniforms of its own
122
+ * between marks (alignments' per-section pileup write) starts a new one after
123
+ * it, or hands none at all.
124
+ */
125
+ export interface StagedUniforms {
126
+ writer: unknown;
127
+ params: unknown;
128
+ }
129
+ /**
130
+ * A shape bound to one display's region payload and render state — what
131
+ * `defineMark` returns and what a display actually holds.
132
+ *
133
+ * The shape's own generics are erased here on purpose: a display draws a list
134
+ * of marks over heterogeneous shapes, and only the binding knows how to reach
135
+ * either one's channels.
136
+ */
137
+ export interface Mark<TRegion, TState extends MarkFrame> {
138
+ readonly pass: InstancePass<TRegion>;
139
+ /**
140
+ * The pass id whose instance buffer this mark draws from, for a mark that
141
+ * is registered but never uploaded to: the canvas chevrons ride the line
142
+ * buffer and the continuation markers ride rect's. `defineMark` refuses the
143
+ * pairing unless the two passes declare one instance struct.
144
+ */
145
+ readonly bufferOf?: string;
146
+ /**
147
+ * The 256-entry RGBA colour ramp this mark's pass samples this frame, or
148
+ * undefined when it samples none. `createMarkBackend` uploads it per pass and
149
+ * only when the bytes' identity moves, and binds an inert table for the
150
+ * undefined case — a shader owns its sampler unconditionally, and a textured
151
+ * pass with no texture never draws on the WebGPU HAL.
152
+ */
153
+ readonly texture?: (state: TState, region: TRegion) => Uint8Array | undefined;
154
+ drawRegion(hal: GpuHal, scratch: ArrayBuffer, block: RenderBlock, clip: BlockClipResult, region: TRegion, state: TState, regionKey: number, staged?: StagedUniforms): void;
155
+ paintBlock(ctx: MarkContext2D, region: TRegion, block: RenderBlock, state: TState): void;
156
+ hitNearest?(region: TRegion, block: RenderBlock, state: TState, xPx: number, yPx: number, candidates: Iterable<number>, maxDistSq: number): MarkHit | undefined;
157
+ }
158
+ /**
159
+ * `{ shape, channels }` over typed arrays — the declaration a display writes in
160
+ * place of a `GpuXxxRenderer` class, a `Canvas2DXxxRenderer` class, a factory
161
+ * and a pass list.
162
+ *
163
+ * `channels` and `params` are projections, not copies: `channels` names which
164
+ * of the region's arrays feed the shape's lanes, and `params` names which of
165
+ * the render state's values reach the uniforms — and which of the region's,
166
+ * for a value the payload carries rather than the frame (the variant matrix's
167
+ * column count). Both run once per block per frame, never per instance.
168
+ *
169
+ * **`channels` answers `undefined` for a region this mark has nothing in.**
170
+ * That is how one mark list serves a display whose cells are a union — synteny
171
+ * draws ribbon cells, outline cells and glyph-lane cells off one list, and a
172
+ * region key holds exactly one of the three. The mark then packs an empty
173
+ * buffer (which every HAL reads as the release), draws nothing and paints
174
+ * nothing, so the kind test is stated once per mark instead of once per
175
+ * backend.
176
+ *
177
+ * **`params` is handed the block**, because the key a display's per-track
178
+ * state is filed under is the block's `displayedRegionIndex` and nothing else
179
+ * in the lens's reach says which track this is. Every method the lens feeds
180
+ * already receives the block; the lens receiving it is what lets it stay a
181
+ * pick rather than pushing the lookup into the shape.
182
+ *
183
+ * `bufferOf` names the mark whose uploaded buffer this one draws from, so the
184
+ * declaration says what the HAL's `drawPass(id, region, bufferPassId)` says:
185
+ * the chevron mark's channels are the line mark's, packed once under the line
186
+ * pass.
187
+ *
188
+ * `texture` is the colour ramp the mark's pass samples, for a shape that
189
+ * resolves a per-instance scalar through a LUT (wiggle density, and the same
190
+ * 256-entry table HiC and LD bind). It is state, not a channel: the backend
191
+ * uploads it once per pass and re-uploads only when the bytes' identity moves,
192
+ * so a ramp change costs one texture and no instance byte.
193
+ *
194
+ * **Per pass per FRAME, though the lens is handed a region too.** LD reads its
195
+ * ramp off the payload's own metric — a pre-computed file with no D' column
196
+ * downgrades the request, and the metric that arrived is the authoritative one
197
+ * — which is exact because LD holds one region. A display with several regions
198
+ * wanting different ramps for one pass would re-upload the texture per region
199
+ * per frame; correct, since each recorded draw keeps the table bound when it
200
+ * was encoded, but a texture per region per frame is not what this is for.
201
+ *
202
+ * `band` is the strip of the canvas the mark is clipped to, for a display that
203
+ * stacks bands on one canvas (MAF's coverage strip over its rows viewport).
204
+ * The GPU scissors to it and Canvas2D clips to it, a zero-height band draws
205
+ * nothing, and the shape still places Y against the whole canvas — the band is
206
+ * a clip, never an offset, which is what lets one `scrollTop` serve both
207
+ * backends. A mark without one paints wherever its shape puts ink.
208
+ */
209
+ export declare function defineMark<TRegion, TState extends MarkFrame, TChannels, TParams>(spec: {
210
+ shape: MarkShape<TChannels, TParams>;
211
+ channels: (region: TRegion) => TChannels | undefined;
212
+ params: (state: TState, region: TRegion, block: RenderBlock) => TParams;
213
+ bufferOf?: Mark<TRegion, TState>;
214
+ band?: (state: TState) => MarkBand;
215
+ texture?: (state: TState, region: TRegion) => Uint8Array | undefined;
216
+ }): Mark<TRegion, TState>;
@@ -0,0 +1,146 @@
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
+ * **Per pass per FRAME, though the lens is handed a region too.** LD reads its
39
+ * ramp off the payload's own metric — a pre-computed file with no D' column
40
+ * downgrades the request, and the metric that arrived is the authoritative one
41
+ * — which is exact because LD holds one region. A display with several regions
42
+ * wanting different ramps for one pass would re-upload the texture per region
43
+ * per frame; correct, since each recorded draw keeps the table bound when it
44
+ * was encoded, but a texture per region per frame is not what this is for.
45
+ *
46
+ * `band` is the strip of the canvas the mark is clipped to, for a display that
47
+ * stacks bands on one canvas (MAF's coverage strip over its rows viewport).
48
+ * The GPU scissors to it and Canvas2D clips to it, a zero-height band draws
49
+ * nothing, and the shape still places Y against the whole canvas — the band is
50
+ * a clip, never an offset, which is what lets one `scrollTop` serve both
51
+ * backends. A mark without one paints wherever its shape puts ink.
52
+ */
53
+ export function defineMark(spec) {
54
+ const { shape, channels, params, band, texture } = spec;
55
+ const lender = spec.bufferOf?.pass;
56
+ if (lender &&
57
+ (lender.instanceStride !== shape.pass.instanceStride ||
58
+ JSON.stringify(lender.vertexAttributes) !==
59
+ JSON.stringify(shape.pass.vertexAttributes))) {
60
+ throw new Error(`mark ${shape.id} draws off ${lender.id}'s buffer but declares a different instance struct`);
61
+ }
62
+ const bufferOf = lender?.id;
63
+ return {
64
+ pass: {
65
+ ...shape.pass,
66
+ pack: region => {
67
+ const c = channels(region);
68
+ return c === undefined ? NOTHING : shape.pass.pack(c);
69
+ },
70
+ },
71
+ bufferOf,
72
+ texture,
73
+ drawRegion(hal, scratch, block, clip, region, state, regionKey, staged) {
74
+ const strip = band?.(state);
75
+ const scissor = strip
76
+ ? devicePxBand(strip.top, strip.height, clip.scaleY, clip.pxH)
77
+ : undefined;
78
+ if (scissor?.height !== 0 && channels(region) !== undefined) {
79
+ const p = params(state, region, block);
80
+ if (!shape.paintsBlock || shape.paintsBlock(block, state, p)) {
81
+ if (scissor) {
82
+ hal.setScissor(clip.pxX, scissor.top, clip.pxW, scissor.height);
83
+ }
84
+ if (staged?.writer !== shape.writeUniforms ||
85
+ staged.params !== params) {
86
+ shape.writeUniforms(scratch, clip, block, state, p);
87
+ hal.writeUniforms(scratch);
88
+ if (staged) {
89
+ staged.writer = shape.writeUniforms;
90
+ staged.params = params;
91
+ }
92
+ }
93
+ hal.drawPass(shape.pass.id, regionKey, bufferOf);
94
+ if (scissor) {
95
+ hal.setScissor(clip.pxX, 0, clip.pxW, clip.pxH);
96
+ }
97
+ }
98
+ }
99
+ },
100
+ paintBlock(ctx, region, block, state) {
101
+ const strip = band?.(state);
102
+ const c = channels(region);
103
+ if ((!strip || strip.height > 0) && c !== undefined) {
104
+ const p = params(state, region, block);
105
+ if (!shape.paintsBlock || shape.paintsBlock(block, state, p)) {
106
+ const paint = () => {
107
+ shape.paintBlock(ctx, c, block, state, p);
108
+ };
109
+ if (strip) {
110
+ withClip(ctx, 0, strip.top, state.canvasWidth, strip.height, paint);
111
+ }
112
+ else {
113
+ paint();
114
+ }
115
+ }
116
+ }
117
+ },
118
+ hitNearest: shape.hitNearest
119
+ ? (region, block, state, xPx, yPx, candidates, maxDistSq) => {
120
+ const strip = band?.(state);
121
+ const c = channels(region);
122
+ let hit;
123
+ // The three gates `paintBlock` takes, in its order — the band on its
124
+ // HEIGHT, as there, so a band both backends decline answers nothing
125
+ // either. The cursor being inside the strip is a fourth, and the ink
126
+ // being inside it the fifth: a band is a CLIP, so ink outside one was
127
+ // never drawn and cannot be the nearest thing to anything.
128
+ if ((!strip || strip.height > 0) &&
129
+ c !== undefined &&
130
+ !bandExcludes(strip, yPx)) {
131
+ const p = params(state, region, block);
132
+ if (!shape.paintsBlock || shape.paintsBlock(block, state, p)) {
133
+ hit = shape.hitNearest(c, block, state, p, xPx, yPx, candidates, maxDistSq);
134
+ }
135
+ }
136
+ return hit && !bandExcludes(strip, hit.y) ? hit : undefined;
137
+ }
138
+ : undefined,
139
+ };
140
+ }
141
+ // What a mark with nothing in this region packs. An empty pack IS the release
142
+ // — every HAL deletes the pass's prior buffer before it looks at the count.
143
+ const NOTHING = new ArrayBuffer(0);
144
+ function bandExcludes(strip, yPx) {
145
+ return (strip !== undefined && (yPx < strip.top || yPx > strip.top + strip.height));
146
+ }
@@ -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
  }
@@ -2,6 +2,7 @@ import { clipBlock } from './blockClipUtils.js';
2
2
  import { prepareCanvas } from './canvas2dUtils.js';
3
3
  import { uploadPass } from './instancePass.js';
4
4
  import { Canvas2DRenderingBackendBase, GpuRenderingBackendBase, } from './renderingBackendBase.js';
5
+ const TRANSPARENT = [0, 0, 0, 0];
5
6
  /**
6
7
  * Canvas2D-side base for `PerRegionRenderingBackend` implementations. Inherits
7
8
  * the `canvas` + 2D context (and no-op `dispose`) from
@@ -21,9 +22,26 @@ export class Canvas2DPerRegionRenderingBackend extends Canvas2DRenderingBackendB
21
22
  release() { }
22
23
  renderBlocks(blocks, regions, state) {
23
24
  prepareCanvas(this.canvas, this.ctx, state.canvasWidth, state.canvasHeight);
25
+ const clear = this.clearColor(state);
26
+ if (clear) {
27
+ const [r, g, b, a] = clear;
28
+ this.ctx.fillStyle = `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`;
29
+ this.ctx.fillRect(0, 0, state.canvasWidth, state.canvasHeight);
30
+ }
24
31
  this.draw(blocks, regions, state);
25
32
  return blocks.some(block => regions.has(block.displayedRegionIndex));
26
33
  }
34
+ /**
35
+ * What the frame is painted onto before the blocks are drawn — the Canvas2D
36
+ * half of the GPU base's `beginFrame` clear. Undefined, the default, leaves
37
+ * `prepareCanvas`'s `clearRect` as the whole of it, which is transparent.
38
+ * Synteny is the one display that overrides: its indel wedges pre-blend
39
+ * against a KNOWN ground and only agree with the base ribbon beside them over
40
+ * a destination that really is that colour.
41
+ */
42
+ clearColor(_state) {
43
+ return undefined;
44
+ }
27
45
  }
28
46
  /**
29
47
  * GPU-side base for `PerRegionRenderingBackend` implementations. Inherits the
@@ -70,9 +88,10 @@ export class GpuPerRegionRenderingBackend extends GpuRenderingBackendBase {
70
88
  // window, so a track dragged past the clamp draws at reduced resolution
71
89
  // instead of asking for a viewport its target cannot hold.
72
90
  const scale = this.hal.resize(canvasWidth, canvasHeight);
73
- // Always pair beginFrame/endFrame so the canvas clears to transparent even
74
- // when every block is skipped (e.g. all regions pruned by a density gate).
75
- this.hal.beginFrame(0, 0, 0, 0);
91
+ // Always pair beginFrame/endFrame so the canvas clears even when every
92
+ // block is skipped (e.g. all regions pruned by a density gate).
93
+ const [clearR, clearG, clearB, clearA] = this.clearColor(state);
94
+ this.hal.beginFrame(clearR, clearG, clearB, clearA);
76
95
  for (const block of blocks) {
77
96
  const region = regions.get(block.displayedRegionIndex);
78
97
  if (region !== undefined) {
@@ -90,4 +109,8 @@ export class GpuPerRegionRenderingBackend extends GpuRenderingBackendBase {
90
109
  this.hal.endFrame();
91
110
  return painted;
92
111
  }
112
+ /** @see Canvas2DPerRegionRenderingBackend.clearColor */
113
+ clearColor(_state) {
114
+ return TRANSPARENT;
115
+ }
93
116
  }
@@ -17,8 +17,8 @@ export declare function isCheckedAtTheStore(map: ReadonlyMap<unknown, unknown>):
17
17
  * observable object graph — the stored value is not the object the worker
18
18
  * produced. A multi-wiggle region is one atom per field per source, so a
19
19
  * thousand-sample track pays ~18k on every pan; MAF pays the whole set again
20
- * for every cached region on every row reorder, since `placeFetchedRows`
21
- * re-places them all.
20
+ * for every cached region on every row reorder, since a reorder re-places
21
+ * them all.
22
22
  * - **Read.** Each field access goes through `getObservablePropValue_`. Hot
23
23
  * loops that hoist their typed arrays are fine, but the ones that don't were
24
24
  * paying it per iteration.
@@ -27,8 +27,8 @@ export function isCheckedAtTheStore(map) {
27
27
  * observable object graph — the stored value is not the object the worker
28
28
  * produced. A multi-wiggle region is one atom per field per source, so a
29
29
  * thousand-sample track pays ~18k on every pan; MAF pays the whole set again
30
- * for every cached region on every row reorder, since `placeFetchedRows`
31
- * re-places them all.
30
+ * for every cached region on every row reorder, since a reorder re-places
31
+ * them all.
32
32
  * - **Read.** Each field access goes through `getObservablePropValue_`. Hot
33
33
  * loops that hoist their typed arrays are fine, but the ones that don't were
34
34
  * paying it per iteration.
@@ -23,3 +23,17 @@ export interface RenderBlock extends BpRegionBounds {
23
23
  export declare function buildRenderBlocks(regions: (BpRegionBounds & {
24
24
  displayedRegionIndex: number;
25
25
  })[]): RenderBlock[];
26
+ /**
27
+ * One display key's whole canvas as a single block.
28
+ *
29
+ * For a display whose x axis is not the block's bp span: a dotplot segment, a
30
+ * synteny ribbon, a Hi-C or LD diamond each read their screen x off the
31
+ * payload's own coordinates through a transform the shader and the painter both
32
+ * apply — a `panPx` fold from the fetch-time base for the first two, the
33
+ * diagonal `viewScale`/`viewOffsetX` for the last two. So the block carries
34
+ * nothing but its key and the identity bp span that keeps `clipBlock`
35
+ * well-formed.
36
+ */
37
+ export declare function canvasWideBlock(displayedRegionIndex: number, canvasWidth: number): RenderBlock;
38
+ /** {@link canvasWideBlock} per key, in the keys' own order. */
39
+ export declare function canvasWideBlocks(keys: Iterable<number>, canvasWidth: number): RenderBlock[];
@@ -8,3 +8,28 @@ export function buildRenderBlocks(regions) {
8
8
  reversed: r.reversed ?? false,
9
9
  }));
10
10
  }
11
+ /**
12
+ * One display key's whole canvas as a single block.
13
+ *
14
+ * For a display whose x axis is not the block's bp span: a dotplot segment, a
15
+ * synteny ribbon, a Hi-C or LD diamond each read their screen x off the
16
+ * payload's own coordinates through a transform the shader and the painter both
17
+ * apply — a `panPx` fold from the fetch-time base for the first two, the
18
+ * diagonal `viewScale`/`viewOffsetX` for the last two. So the block carries
19
+ * nothing but its key and the identity bp span that keeps `clipBlock`
20
+ * well-formed.
21
+ */
22
+ export function canvasWideBlock(displayedRegionIndex, canvasWidth) {
23
+ return {
24
+ displayedRegionIndex,
25
+ start: 0,
26
+ end: canvasWidth,
27
+ screenStartPx: 0,
28
+ screenEndPx: canvasWidth,
29
+ reversed: false,
30
+ };
31
+ }
32
+ /** {@link canvasWideBlock} per key, in the keys' own order. */
33
+ export function canvasWideBlocks(keys, canvasWidth) {
34
+ return [...keys].map(key => canvasWideBlock(key, canvasWidth));
35
+ }