@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.
- package/esm/RenderLifecycleMixin.d.ts +20 -1
- package/esm/RenderLifecycleMixin.js +25 -2
- package/esm/canvas2dUtils.d.ts +25 -0
- package/esm/canvas2dUtils.js +27 -0
- package/esm/colorRampLut.d.ts +4 -4
- package/esm/colorRampLut.js +4 -4
- package/esm/computePipeline.d.ts +13 -0
- package/esm/computePipeline.js +55 -0
- package/esm/coverageBand.d.ts +0 -1
- package/esm/coverageBand.js +0 -1
- package/esm/coverageBandBuffers.d.ts +0 -6
- package/esm/coverageBandBuffers.js +1 -13
- package/esm/createRenderingBackend.d.ts +14 -18
- package/esm/createRenderingBackend.js +4 -2
- package/esm/encodeMemo.d.ts +14 -0
- package/esm/encodeMemo.js +48 -0
- package/esm/gpuDevice.d.ts +9 -0
- package/esm/gpuDevice.js +12 -3
- package/esm/graphicsCapabilities.d.ts +18 -15
- package/esm/graphicsCapabilities.js +23 -16
- package/esm/hal/createHal.d.ts +1 -2
- package/esm/hal/createHal.js +14 -4
- package/esm/hal/deviceGpuCache.js +2 -2
- package/esm/hal/gpuHalBase.d.ts +1 -1
- package/esm/hal/gpuHalBase.js +2 -3
- package/esm/hal/mockHal.d.ts +0 -1
- package/esm/hal/mockHal.js +0 -10
- package/esm/hal/regionRegistry.d.ts +0 -1
- package/esm/hal/regionRegistry.js +3 -15
- package/esm/hal/types.d.ts +5 -1
- package/esm/hal/webgl2Hal.d.ts +1 -1
- package/esm/hal/webgl2Hal.js +2 -2
- package/esm/hal/webgpuHal.d.ts +2 -2
- package/esm/hal/webgpuHal.js +19 -16
- package/esm/installUpload.d.ts +4 -1
- package/esm/installUpload.js +7 -39
- package/esm/instanceCache.d.ts +10 -7
- package/esm/instanceCache.js +14 -15
- package/esm/mapUploadSync.d.ts +0 -8
- package/esm/mapUploadSync.js +0 -1
- package/esm/marks/colorFill.d.ts +17 -0
- package/esm/marks/colorFill.js +26 -0
- package/esm/marks/drawAgainstHit.d.ts +47 -0
- package/esm/marks/drawAgainstHit.js +140 -0
- package/esm/marks/glyphPaint.d.ts +16 -0
- package/esm/marks/glyphPaint.js +41 -0
- package/esm/marks/index.d.ts +9 -0
- package/esm/marks/index.js +6 -0
- package/esm/marks/markBackend.d.ts +87 -0
- package/esm/marks/markBackend.js +137 -0
- package/esm/marks/markHit.d.ts +25 -0
- package/esm/marks/markHit.js +46 -0
- package/esm/marks/markPaint.d.ts +13 -0
- package/esm/marks/markPaint.js +18 -0
- package/esm/marks/pointMark.d.ts +24 -0
- package/esm/marks/pointMark.js +91 -0
- package/esm/marks/spanMark.d.ts +45 -0
- package/esm/marks/spanMark.js +59 -0
- package/esm/marks/types.d.ts +216 -0
- package/esm/marks/types.js +146 -0
- package/esm/perRegionRenderingBackend.d.ts +15 -3
- package/esm/perRegionRenderingBackend.js +26 -3
- package/esm/regionDataMap.d.ts +2 -2
- package/esm/regionDataMap.js +2 -2
- package/esm/renderBlock.d.ts +14 -0
- package/esm/renderBlock.js +25 -0
- package/esm/renderingBackendBase.d.ts +11 -14
- package/esm/renderingBackendBase.js +7 -7
- package/esm/shaders/capsule.js.generated.d.ts +1 -0
- package/esm/shaders/capsule.js.generated.js +11 -0
- package/esm/shaders/coverageBar.generated.d.ts +2 -2
- package/esm/shaders/coverageBar.generated.js +2 -2
- package/esm/shaders/coverageMod.generated.d.ts +2 -2
- package/esm/shaders/coverageMod.generated.js +2 -2
- package/esm/shaders/coverageSnp.generated.d.ts +2 -2
- package/esm/shaders/coverageSnp.generated.js +2 -2
- package/esm/shaders/pointMark.consts.generated.d.ts +4 -0
- package/esm/shaders/pointMark.consts.generated.js +6 -0
- package/esm/shaders/pointMark.generated.d.ts +5 -0
- package/esm/shaders/pointMark.generated.js +7 -0
- package/esm/shaders/pointMark.iface.generated.d.ts +55 -0
- package/esm/shaders/pointMark.iface.generated.js +100 -0
- package/esm/shaders/pointMark.js.generated.d.ts +2 -0
- package/esm/shaders/pointMark.js.generated.js +19 -0
- package/esm/shaders/spanMark.generated.d.ts +4 -0
- package/esm/shaders/spanMark.generated.js +6 -0
- package/esm/shaders/spanMark.iface.generated.d.ts +58 -0
- package/esm/shaders/spanMark.iface.generated.js +122 -0
- package/esm/sharedBackendKey.d.ts +18 -0
- package/esm/{keyedRenderingBackend.js → sharedBackendKey.js} +5 -0
- package/esm/slangPass.d.ts +2 -0
- package/esm/slangPass.js +2 -0
- package/package.json +14 -4
- package/src/shaders/capsule.js.generated.ts +14 -0
- package/src/shaders/capsule.slang +12 -4
- package/src/shaders/colorPack.slang +5 -1
- package/src/shaders/coverageBand.slang +38 -15
- package/src/shaders/coverageBar.generated.ts +2 -2
- package/src/shaders/coverageBar.slang +9 -0
- package/src/shaders/coverageMod.generated.ts +2 -2
- package/src/shaders/coverageSnp.generated.ts +2 -2
- package/src/shaders/hpmath.slang +7 -1
- package/src/shaders/pointMark.consts.generated.ts +10 -0
- package/src/shaders/pointMark.generated.ts +12 -0
- package/src/shaders/pointMark.iface.generated.ts +146 -0
- package/src/shaders/pointMark.js.generated.ts +25 -0
- package/src/shaders/pointMark.slang +205 -0
- package/src/shaders/rowRect.slang +7 -8
- package/src/shaders/spanMark.generated.ts +10 -0
- package/src/shaders/spanMark.iface.generated.ts +168 -0
- package/src/shaders/spanMark.slang +22 -0
- package/esm/globalRenderingBackend.d.ts +0 -90
- package/esm/globalRenderingBackend.js +0 -53
- package/esm/keyedRenderingBackend.d.ts +0 -41
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `MarkContext2D` that records every rect a painter fills, in paint order —
|
|
3
|
+
* `fillRect` and path `rect` alike, so a shape that batches by colour records
|
|
4
|
+
* the same list as one that fills per instance. A path traced with
|
|
5
|
+
* `moveTo`/`lineTo` and filled records its bounding box, which is what a
|
|
6
|
+
* shape whose hit test answers a polygon's box (the variant inversion
|
|
7
|
+
* triangle) is held to.
|
|
8
|
+
*
|
|
9
|
+
* A **stroked** path records one rect per `moveTo`/`lineTo` edge, each the
|
|
10
|
+
* edge's bounding box widened by half the current `lineWidth` — the extent a
|
|
11
|
+
* round cap reaches on all four sides. Per edge rather than per `stroke()`,
|
|
12
|
+
* because a painter that batches a colour run into one path (dotplot's) would
|
|
13
|
+
* otherwise record the run's hull as a single instance. `fillStyle` on such a
|
|
14
|
+
* record is the `strokeStyle` it went down in; the field is the colour of the
|
|
15
|
+
* ink either way.
|
|
16
|
+
*/
|
|
17
|
+
export function recordingContext() {
|
|
18
|
+
const calls = [];
|
|
19
|
+
const points = [];
|
|
20
|
+
const edges = [];
|
|
21
|
+
const ctx = {
|
|
22
|
+
fillStyle: '',
|
|
23
|
+
strokeStyle: '',
|
|
24
|
+
lineWidth: 1,
|
|
25
|
+
lineCap: 'butt',
|
|
26
|
+
save() { },
|
|
27
|
+
restore() { },
|
|
28
|
+
beginPath() {
|
|
29
|
+
points.length = 0;
|
|
30
|
+
edges.length = 0;
|
|
31
|
+
},
|
|
32
|
+
clip() { },
|
|
33
|
+
moveTo(x, y) {
|
|
34
|
+
points.push([x, y]);
|
|
35
|
+
},
|
|
36
|
+
lineTo(x, y) {
|
|
37
|
+
const from = points.at(-1);
|
|
38
|
+
if (from) {
|
|
39
|
+
edges.push([from[0], from[1], x, y]);
|
|
40
|
+
}
|
|
41
|
+
points.push([x, y]);
|
|
42
|
+
},
|
|
43
|
+
arc() { },
|
|
44
|
+
closePath() { },
|
|
45
|
+
fill() {
|
|
46
|
+
if (points.length > 0) {
|
|
47
|
+
const xs = points.map(p => p[0]);
|
|
48
|
+
const ys = points.map(p => p[1]);
|
|
49
|
+
const x = Math.min(...xs);
|
|
50
|
+
const y = Math.min(...ys);
|
|
51
|
+
calls.push({
|
|
52
|
+
x,
|
|
53
|
+
y,
|
|
54
|
+
w: Math.max(...xs) - x,
|
|
55
|
+
h: Math.max(...ys) - y,
|
|
56
|
+
fillStyle: this.fillStyle,
|
|
57
|
+
});
|
|
58
|
+
points.length = 0;
|
|
59
|
+
edges.length = 0;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
stroke() {
|
|
63
|
+
const half = this.lineWidth / 2;
|
|
64
|
+
for (const [ax, ay, bx, by] of edges) {
|
|
65
|
+
const x = Math.min(ax, bx) - half;
|
|
66
|
+
const y = Math.min(ay, by) - half;
|
|
67
|
+
calls.push({
|
|
68
|
+
x,
|
|
69
|
+
y,
|
|
70
|
+
w: Math.max(ax, bx) + half - x,
|
|
71
|
+
h: Math.max(ay, by) + half - y,
|
|
72
|
+
fillStyle: this.strokeStyle,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
points.length = 0;
|
|
76
|
+
edges.length = 0;
|
|
77
|
+
},
|
|
78
|
+
rect(x, y, w, h) {
|
|
79
|
+
calls.push({ x, y, w, h, fillStyle: this.fillStyle });
|
|
80
|
+
},
|
|
81
|
+
fillRect(x, y, w, h) {
|
|
82
|
+
calls.push({ x, y, w, h, fillStyle: this.fillStyle });
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
return { ctx: ctx, calls };
|
|
86
|
+
}
|
|
87
|
+
function contains(r, x, y) {
|
|
88
|
+
return x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The draw-against-hit gate for a shape whose instances paint as rects: paint
|
|
92
|
+
* the block, then walk it in `step` px and hold `hitNearest` to what the
|
|
93
|
+
* painter recorded. A hit at distance 0 has to sit on the rect of the index it
|
|
94
|
+
* names, and a point on a rect has to answer that rect — the last-painted one
|
|
95
|
+
* where rects overlap, since the sweep hands candidates in back to front.
|
|
96
|
+
*
|
|
97
|
+
* Returns the violations rather than asserting, so a test reads as
|
|
98
|
+
* `expect(sweep(...)).toEqual([])`. `maxDistSq` is the caller's, because a
|
|
99
|
+
* shape whose hit target is wider than its ink (`point`) needs a bound that
|
|
100
|
+
* admits the whole rect.
|
|
101
|
+
*/
|
|
102
|
+
export function sweepDrawAgainstHit(shape, channels, block, frame, params, { maxDistSq, step = 0.5 }) {
|
|
103
|
+
const violations = [];
|
|
104
|
+
const { ctx, calls: rects } = recordingContext();
|
|
105
|
+
shape.paintBlock(ctx, channels, block, frame, params);
|
|
106
|
+
if (rects.length !== channels.count) {
|
|
107
|
+
return [`painted ${rects.length} rects for ${channels.count} instances`];
|
|
108
|
+
}
|
|
109
|
+
const candidates = Array.from({ length: channels.count }, (_, k) => channels.count - 1 - k);
|
|
110
|
+
let yMin = Infinity;
|
|
111
|
+
let yMax = -Infinity;
|
|
112
|
+
for (const r of rects) {
|
|
113
|
+
yMin = Math.min(yMin, r.y);
|
|
114
|
+
yMax = Math.max(yMax, r.y + r.h);
|
|
115
|
+
}
|
|
116
|
+
const x0 = Math.min(block.screenStartPx, block.screenEndPx) - 1;
|
|
117
|
+
const x1 = Math.max(block.screenStartPx, block.screenEndPx) + 1;
|
|
118
|
+
for (let y = Math.floor(yMin) - 1; y <= yMax + 1; y += step) {
|
|
119
|
+
for (let x = x0; x <= x1; x += step) {
|
|
120
|
+
let expected = -1;
|
|
121
|
+
for (let k = rects.length - 1; k >= 0; k--) {
|
|
122
|
+
if (contains(rects[k], x, y)) {
|
|
123
|
+
expected = k;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const hit = shape.hitNearest(channels, block, frame, params, x, y, candidates, maxDistSq);
|
|
128
|
+
if (hit && hit.distSq === 0 && !contains(rects[hit.index], x, y)) {
|
|
129
|
+
violations.push(`(${x}, ${y}) answered ${hit.index} at distance 0, off its rect`);
|
|
130
|
+
}
|
|
131
|
+
if (expected !== -1 && hit?.index !== expected) {
|
|
132
|
+
violations.push(`(${x}, ${y}) is on rect ${expected}, answered ${hit?.index ?? 'nothing'}`);
|
|
133
|
+
}
|
|
134
|
+
if (violations.length >= 10) {
|
|
135
|
+
return violations;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return violations;
|
|
140
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MarkContext2D } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Append one of the `point` shape's glyphs to the current path, centred on
|
|
4
|
+
* `(cx, y)`.
|
|
5
|
+
*
|
|
6
|
+
* The disc/crisp-square split and the square's snap are pointGlyph.slang's
|
|
7
|
+
* (adr-051), so the painter cannot drift from the shader about where the split
|
|
8
|
+
* is. The caller owns `beginPath`/`fill`, which is what lets a run of one
|
|
9
|
+
* colour batch into a single fill.
|
|
10
|
+
*
|
|
11
|
+
* Its own module, not `pointMark.ts`'s: this is the half of the shape a
|
|
12
|
+
* consumer can want without the pass, and importing it through the shape would
|
|
13
|
+
* hand that consumer the shader's WGSL and GLSL strings. Wiggle's scatter mode
|
|
14
|
+
* is that consumer.
|
|
15
|
+
*/
|
|
16
|
+
export declare function appendGlyph(ctx: MarkContext2D, glyph: number, cx: number, y: number, diameter: number): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SMALL_POINT_MAX_DIAMETER } from '../shaders/pointGlyph.generated.js';
|
|
2
|
+
import { crispSquareTopLeftPx } from '../shaders/pointGlyph.js.generated.js';
|
|
3
|
+
import { DIAMOND_GLYPH_SCALE, GLYPH_DIAMOND, GLYPH_TRIANGLE, } from '../shaders/pointMark.consts.generated.js';
|
|
4
|
+
/**
|
|
5
|
+
* Append one of the `point` shape's glyphs to the current path, centred on
|
|
6
|
+
* `(cx, y)`.
|
|
7
|
+
*
|
|
8
|
+
* The disc/crisp-square split and the square's snap are pointGlyph.slang's
|
|
9
|
+
* (adr-051), so the painter cannot drift from the shader about where the split
|
|
10
|
+
* is. The caller owns `beginPath`/`fill`, which is what lets a run of one
|
|
11
|
+
* colour batch into a single fill.
|
|
12
|
+
*
|
|
13
|
+
* Its own module, not `pointMark.ts`'s: this is the half of the shape a
|
|
14
|
+
* consumer can want without the pass, and importing it through the shape would
|
|
15
|
+
* hand that consumer the shader's WGSL and GLSL strings. Wiggle's scatter mode
|
|
16
|
+
* is that consumer.
|
|
17
|
+
*/
|
|
18
|
+
export function appendGlyph(ctx, glyph, cx, y, diameter) {
|
|
19
|
+
const r = diameter / 2;
|
|
20
|
+
if (glyph === GLYPH_TRIANGLE) {
|
|
21
|
+
ctx.moveTo(cx - r, y - r);
|
|
22
|
+
ctx.lineTo(cx + r, y - r);
|
|
23
|
+
ctx.lineTo(cx, y + r);
|
|
24
|
+
ctx.closePath();
|
|
25
|
+
}
|
|
26
|
+
else if (glyph === GLYPH_DIAMOND) {
|
|
27
|
+
const ri = r * DIAMOND_GLYPH_SCALE;
|
|
28
|
+
ctx.moveTo(cx, y - ri);
|
|
29
|
+
ctx.lineTo(cx + ri, y);
|
|
30
|
+
ctx.lineTo(cx, y + ri);
|
|
31
|
+
ctx.lineTo(cx - ri, y);
|
|
32
|
+
ctx.closePath();
|
|
33
|
+
}
|
|
34
|
+
else if (diameter <= SMALL_POINT_MAX_DIAMETER) {
|
|
35
|
+
ctx.rect(crispSquareTopLeftPx(cx, diameter), crispSquareTopLeftPx(y, diameter), diameter, diameter);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
ctx.moveTo(cx + r, y);
|
|
39
|
+
ctx.arc(cx, y, r, 0, Math.PI * 2);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { defineMark } from './types.ts';
|
|
2
|
+
export { paintMarkBlocks } from './markPaint.ts';
|
|
3
|
+
export { pointMark } from './pointMark.ts';
|
|
4
|
+
export { spanMark } from './spanMark.ts';
|
|
5
|
+
export { appendGlyph } from './glyphPaint.ts';
|
|
6
|
+
export { abgrToCssRgba, makeAbgrFill } from './colorFill.ts';
|
|
7
|
+
export type { Mark, MarkBand, MarkContext2D, MarkFrame, MarkHit, MarkShape, StagedUniforms, } from './types.ts';
|
|
8
|
+
export type { PointChannels, PointParams } from './pointMark.ts';
|
|
9
|
+
export type { SpanChannels, SpanParams } from './spanMark.ts';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { defineMark } from './types.js';
|
|
2
|
+
export { paintMarkBlocks } from './markPaint.js';
|
|
3
|
+
export { pointMark } from './pointMark.js';
|
|
4
|
+
export { spanMark } from './spanMark.js';
|
|
5
|
+
export { appendGlyph } from './glyphPaint.js';
|
|
6
|
+
export { abgrToCssRgba, makeAbgrFill } from './colorFill.js';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Canvas2DPerRegionRenderingBackend, GpuPerRegionRenderingBackend } from '../perRegionRenderingBackend.ts';
|
|
2
|
+
import type { BlockClipResult } from '../blockClipUtils.ts';
|
|
3
|
+
import type { GpuHal } from '../hal/index.ts';
|
|
4
|
+
import type { SampleCount } from '../hal/types.ts';
|
|
5
|
+
import type { InstancePass } from '../instancePass.ts';
|
|
6
|
+
import type { ClearColor, PerRegionRenderingBackend } from '../perRegionRenderingBackend.ts';
|
|
7
|
+
import type { RenderBlock } from '../renderBlock.ts';
|
|
8
|
+
import type { FrameDimensions } from '../renderingBackendBase.ts';
|
|
9
|
+
import type { Mark } from './types.ts';
|
|
10
|
+
/** Pack and upload one region's buffers for a mark list. */
|
|
11
|
+
export declare function uploadMarks<TRegion, TState extends FrameDimensions>(hal: GpuHal, regionKey: number, marks: readonly Mark<TRegion, TState>[], data: TRegion): void;
|
|
12
|
+
/**
|
|
13
|
+
* Draw one already-clipped block through a mark list.
|
|
14
|
+
*
|
|
15
|
+
* **The viewport is this helper's, the scissor is the caller's.** Every shape
|
|
16
|
+
* maps bp across the block column, so nobody wants a viewport narrower than the
|
|
17
|
+
* clip; a caller does want a narrower scissor, which is how alignments clips a
|
|
18
|
+
* coverage strip inside the column it hands over. `GpuPerRegionRenderingBackend`
|
|
19
|
+
* sets both per block already, so for a backend on that scaffold the viewport
|
|
20
|
+
* set here is the same rect twice.
|
|
21
|
+
*
|
|
22
|
+
* A caller that narrows it must hand over marks declaring no `band`, and that
|
|
23
|
+
* is the one way the two clips do not compose: a banded mark scissors to its
|
|
24
|
+
* strip and hands the BLOCK COLUMN back rather than what the caller had. The
|
|
25
|
+
* split is deliberate — `render-core/CLAUDE.md` §Drawing has which displays
|
|
26
|
+
* take which side — but nothing enforces it, so it is stated at both.
|
|
27
|
+
*/
|
|
28
|
+
export declare function drawMarks<TRegion, TState extends FrameDimensions>(hal: GpuHal, scratch: ArrayBuffer, marks: readonly Mark<TRegion, TState>[], block: RenderBlock, clip: BlockClipResult, region: TRegion, state: TState, regionKey: number): void;
|
|
29
|
+
/**
|
|
30
|
+
* The HAL-side half of `createMarkBackend`, exported so a display's mark list
|
|
31
|
+
* can be driven against `MockHal`: which marks upload, which draw off another's
|
|
32
|
+
* buffer, and which blocks a shape declines.
|
|
33
|
+
*/
|
|
34
|
+
export declare class GpuMarkBackend<TRegion, TState extends FrameDimensions> extends GpuPerRegionRenderingBackend<TRegion, TState> {
|
|
35
|
+
private marks;
|
|
36
|
+
private clear?;
|
|
37
|
+
protected regionPasses: InstancePass<TRegion>[];
|
|
38
|
+
constructor(hal: GpuHal, marks: readonly Mark<TRegion, TState>[], clear?: ((state: TState) => ClearColor) | undefined);
|
|
39
|
+
protected clearColor(state: TState): ClearColor;
|
|
40
|
+
private boundRamps;
|
|
41
|
+
private bindRamp;
|
|
42
|
+
protected drawRegion(block: RenderBlock, clip: BlockClipResult, region: TRegion, state: TState): void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The Canvas2D half, exported for the same reason `GpuMarkBackend` is: a
|
|
46
|
+
* display's frame scaffold — the ground it clears to, the blocks it paints — is
|
|
47
|
+
* a property of the pair, and a suite pinning it on one backend has to pin it
|
|
48
|
+
* on the other.
|
|
49
|
+
*/
|
|
50
|
+
export declare class Canvas2DMarkBackend<TRegion, TState extends FrameDimensions> extends Canvas2DPerRegionRenderingBackend<TRegion, TState> {
|
|
51
|
+
private marks;
|
|
52
|
+
private clear?;
|
|
53
|
+
constructor(canvas: HTMLCanvasElement, marks: readonly Mark<TRegion, TState>[], clear?: ((state: TState) => ClearColor) | undefined);
|
|
54
|
+
protected clearColor(state: TState): ClearColor | undefined;
|
|
55
|
+
protected draw(blocks: RenderBlock[], regions: ReadonlyMap<number, TRegion>, state: TState): void;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build a display's rendering backend from its mark declarations.
|
|
59
|
+
*
|
|
60
|
+
* **This is the only module on the mark path that reaches the HAL**, which is
|
|
61
|
+
* why it is not on the `marks` subpath: a display's declaration, its painter
|
|
62
|
+
* and its hit test are all things a state model can legitimately reach, and a
|
|
63
|
+
* state model is eager (ADR-091). Reaching the backend costs the GPU stack at
|
|
64
|
+
* plugin-install time, so it is a separate import a display makes once, from
|
|
65
|
+
* the factory the component hands to `DisplayChrome`.
|
|
66
|
+
*
|
|
67
|
+
* This is the whole of what a display used to spell as a `GpuXxxRenderer`
|
|
68
|
+
* class, a `Canvas2DXxxRenderer` class, a pass list and a factory: the passes
|
|
69
|
+
* are the marks' own and each backend walks the same list.
|
|
70
|
+
*
|
|
71
|
+
* One uniform buffer serves every mark because each writes its own layout into
|
|
72
|
+
* the scratch immediately before its own `drawPass`, which is what the HAL's
|
|
73
|
+
* write-then-draw ordering already guarantees for a multi-pass renderer — or
|
|
74
|
+
* draws off the struct the previous mark of the same block already staged, per
|
|
75
|
+
* `StagedUniforms`.
|
|
76
|
+
*/
|
|
77
|
+
export declare function createMarkBackend<TRegion, TState extends FrameDimensions>(canvas: HTMLCanvasElement, marks: readonly Mark<TRegion, TState>[], opts?: {
|
|
78
|
+
sampleCount?: SampleCount;
|
|
79
|
+
/**
|
|
80
|
+
* What the frame is cleared to, when transparent is the wrong answer. The
|
|
81
|
+
* GPU side hands it to `beginFrame` and the Canvas2D side fills it after
|
|
82
|
+
* `prepareCanvas`, so both backends composite over the same colour — which
|
|
83
|
+
* synteny needs, its indel wedges being pre-blended against the band's
|
|
84
|
+
* ground rather than composited over it.
|
|
85
|
+
*/
|
|
86
|
+
clearColor?: (state: TState) => ClearColor;
|
|
87
|
+
}): Promise<PerRegionRenderingBackend<TRegion, TState>>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { COLOR_RAMP_LUT_ENTRIES, uploadColorRampLut } from '../colorRampLut.js';
|
|
2
|
+
import { createRenderingBackend } from '../createRenderingBackend.js';
|
|
3
|
+
import { uploadPass } from '../instancePass.js';
|
|
4
|
+
import { Canvas2DPerRegionRenderingBackend, GpuPerRegionRenderingBackend, } from '../perRegionRenderingBackend.js';
|
|
5
|
+
import { paintMarkBlocks } from './markPaint.js';
|
|
6
|
+
// The passes a mark list owns a buffer for: the whole list minus those drawing
|
|
7
|
+
// off another's (`bufferOf`). Uploading to a borrowed pass is silent and its
|
|
8
|
+
// buffer is the lender's, so the skip is stated once, here — module-local,
|
|
9
|
+
// because `marks/backend` is a published subpath and nothing outside wants it.
|
|
10
|
+
function ownedPasses(marks) {
|
|
11
|
+
return marks.filter(m => !m.bufferOf).map(m => m.pass);
|
|
12
|
+
}
|
|
13
|
+
/** Pack and upload one region's buffers for a mark list. */
|
|
14
|
+
export function uploadMarks(hal, regionKey, marks, data) {
|
|
15
|
+
for (const pass of ownedPasses(marks)) {
|
|
16
|
+
uploadPass(hal, regionKey, pass, data);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Draw one already-clipped block through a mark list.
|
|
21
|
+
*
|
|
22
|
+
* **The viewport is this helper's, the scissor is the caller's.** Every shape
|
|
23
|
+
* maps bp across the block column, so nobody wants a viewport narrower than the
|
|
24
|
+
* clip; a caller does want a narrower scissor, which is how alignments clips a
|
|
25
|
+
* coverage strip inside the column it hands over. `GpuPerRegionRenderingBackend`
|
|
26
|
+
* sets both per block already, so for a backend on that scaffold the viewport
|
|
27
|
+
* set here is the same rect twice.
|
|
28
|
+
*
|
|
29
|
+
* A caller that narrows it must hand over marks declaring no `band`, and that
|
|
30
|
+
* is the one way the two clips do not compose: a banded mark scissors to its
|
|
31
|
+
* strip and hands the BLOCK COLUMN back rather than what the caller had. The
|
|
32
|
+
* split is deliberate — `render-core/CLAUDE.md` §Drawing has which displays
|
|
33
|
+
* take which side — but nothing enforces it, so it is stated at both.
|
|
34
|
+
*/
|
|
35
|
+
export function drawMarks(hal, scratch, marks, block, clip, region, state, regionKey) {
|
|
36
|
+
hal.setViewport(clip.pxX, 0, clip.pxW, clip.pxH);
|
|
37
|
+
// One call is one block, which is exactly the life of a `StagedUniforms`
|
|
38
|
+
const staged = { writer: undefined, params: undefined };
|
|
39
|
+
for (const mark of marks) {
|
|
40
|
+
mark.drawRegion(hal, scratch, block, clip, region, state, regionKey, staged);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// What a textured pass binds while its mark names no ramp: a shader declares
|
|
44
|
+
// its sampler unconditionally, and a textured pass with no texture never draws
|
|
45
|
+
// on the WebGPU HAL.
|
|
46
|
+
const INERT_RAMP = new Uint8Array(COLOR_RAMP_LUT_ENTRIES * 4);
|
|
47
|
+
/**
|
|
48
|
+
* The HAL-side half of `createMarkBackend`, exported so a display's mark list
|
|
49
|
+
* can be driven against `MockHal`: which marks upload, which draw off another's
|
|
50
|
+
* buffer, and which blocks a shape declines.
|
|
51
|
+
*/
|
|
52
|
+
export class GpuMarkBackend extends GpuPerRegionRenderingBackend {
|
|
53
|
+
marks;
|
|
54
|
+
clear;
|
|
55
|
+
// The base's upload loop already IS the mark walk, so this backend names its
|
|
56
|
+
// passes instead of overriding `upload` with a second spelling of it.
|
|
57
|
+
regionPasses;
|
|
58
|
+
constructor(hal, marks, clear) {
|
|
59
|
+
super(hal);
|
|
60
|
+
this.marks = marks;
|
|
61
|
+
this.clear = clear;
|
|
62
|
+
this.regionPasses = ownedPasses(marks);
|
|
63
|
+
}
|
|
64
|
+
clearColor(state) {
|
|
65
|
+
return this.clear ? this.clear(state) : super.clearColor(state);
|
|
66
|
+
}
|
|
67
|
+
// The ramp each textured pass holds, by the table's identity — the mirror of
|
|
68
|
+
// the one texture the HAL keeps per pass, so an unchanged ramp costs a frame
|
|
69
|
+
// nothing and a backend rebuilt after context loss re-uploads. Per pass and
|
|
70
|
+
// per backend, never per region.
|
|
71
|
+
boundRamps = new Map();
|
|
72
|
+
bindRamp(mark, region, state) {
|
|
73
|
+
if (mark.texture) {
|
|
74
|
+
const ramp = mark.texture(state, region) ?? INERT_RAMP;
|
|
75
|
+
if (ramp !== this.boundRamps.get(mark.pass.id)) {
|
|
76
|
+
uploadColorRampLut(this.hal, ramp, [mark.pass.id]);
|
|
77
|
+
this.boundRamps.set(mark.pass.id, ramp);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
drawRegion(block, clip, region, state) {
|
|
82
|
+
for (const mark of this.marks) {
|
|
83
|
+
this.bindRamp(mark, region, state);
|
|
84
|
+
}
|
|
85
|
+
drawMarks(this.hal, this.uniformData, this.marks, block, clip, region, state, block.displayedRegionIndex);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The Canvas2D half, exported for the same reason `GpuMarkBackend` is: a
|
|
90
|
+
* display's frame scaffold — the ground it clears to, the blocks it paints — is
|
|
91
|
+
* a property of the pair, and a suite pinning it on one backend has to pin it
|
|
92
|
+
* on the other.
|
|
93
|
+
*/
|
|
94
|
+
export class Canvas2DMarkBackend extends Canvas2DPerRegionRenderingBackend {
|
|
95
|
+
marks;
|
|
96
|
+
clear;
|
|
97
|
+
constructor(canvas, marks, clear) {
|
|
98
|
+
super(canvas);
|
|
99
|
+
this.marks = marks;
|
|
100
|
+
this.clear = clear;
|
|
101
|
+
}
|
|
102
|
+
clearColor(state) {
|
|
103
|
+
return this.clear ? this.clear(state) : super.clearColor(state);
|
|
104
|
+
}
|
|
105
|
+
draw(blocks, regions, state) {
|
|
106
|
+
paintMarkBlocks(this.ctx, this.marks, regions, blocks, state);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build a display's rendering backend from its mark declarations.
|
|
111
|
+
*
|
|
112
|
+
* **This is the only module on the mark path that reaches the HAL**, which is
|
|
113
|
+
* why it is not on the `marks` subpath: a display's declaration, its painter
|
|
114
|
+
* and its hit test are all things a state model can legitimately reach, and a
|
|
115
|
+
* state model is eager (ADR-091). Reaching the backend costs the GPU stack at
|
|
116
|
+
* plugin-install time, so it is a separate import a display makes once, from
|
|
117
|
+
* the factory the component hands to `DisplayChrome`.
|
|
118
|
+
*
|
|
119
|
+
* This is the whole of what a display used to spell as a `GpuXxxRenderer`
|
|
120
|
+
* class, a `Canvas2DXxxRenderer` class, a pass list and a factory: the passes
|
|
121
|
+
* are the marks' own and each backend walks the same list.
|
|
122
|
+
*
|
|
123
|
+
* One uniform buffer serves every mark because each writes its own layout into
|
|
124
|
+
* the scratch immediately before its own `drawPass`, which is what the HAL's
|
|
125
|
+
* write-then-draw ordering already guarantees for a multi-pass renderer — or
|
|
126
|
+
* draws off the struct the previous mark of the same block already staged, per
|
|
127
|
+
* `StagedUniforms`.
|
|
128
|
+
*/
|
|
129
|
+
export function createMarkBackend(canvas, marks, opts = {}) {
|
|
130
|
+
const { clearColor } = opts;
|
|
131
|
+
return createRenderingBackend(canvas, {
|
|
132
|
+
passes: marks.map(m => m.pass),
|
|
133
|
+
sampleCount: opts.sampleCount,
|
|
134
|
+
createGpuBackend: hal => new GpuMarkBackend(hal, marks, clearColor),
|
|
135
|
+
createCanvas2DBackend: c => new Canvas2DMarkBackend(c, marks, clearColor),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MarkHit } from './types.ts';
|
|
2
|
+
/** One instance's ink measured against a cursor — a `MarkHit` before it is one. */
|
|
3
|
+
export type InkHit = Omit<MarkHit, 'index'>;
|
|
4
|
+
/**
|
|
5
|
+
* Where a rect's ink sits nearest `(xPx, yPx)`, and how far: the point itself
|
|
6
|
+
* where the cursor is inside, else the nearest point on the edge.
|
|
7
|
+
*/
|
|
8
|
+
export declare function inkOnRect(xPx: number, yPx: number, left: number, top: number, width: number, height: number): InkHit;
|
|
9
|
+
/** The same for a mark with no extent to clamp into. */
|
|
10
|
+
export declare function inkAtPoint(xPx: number, yPx: number, x: number, y: number): InkHit;
|
|
11
|
+
/**
|
|
12
|
+
* The nearest of `candidates` by where each one's ink is, or undefined when
|
|
13
|
+
* none beats `maxDistSq`.
|
|
14
|
+
*
|
|
15
|
+
* This is the whole of every shape's `hitNearest` but the geometry: `inkAt`
|
|
16
|
+
* answers where instance `i` put ink and how far that is, and this keeps the
|
|
17
|
+
* closest. Only a STRICTLY nearer candidate replaces the best, so on a tie the
|
|
18
|
+
* first wins — which is what makes a caller that iterates back to front get the
|
|
19
|
+
* mark on top, and it is a rule that used to be spelled out five times over.
|
|
20
|
+
*
|
|
21
|
+
* `inkAt` may answer `undefined` for an instance that drew nothing at all, as
|
|
22
|
+
* against one that drew somewhere far away: a bar clipped out of its band has
|
|
23
|
+
* no ink to be near.
|
|
24
|
+
*/
|
|
25
|
+
export declare function nearestInk(candidates: Iterable<number>, maxDistSq: number, inkAt: (index: number) => InkHit | undefined): MarkHit | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a rect's ink sits nearest `(xPx, yPx)`, and how far: the point itself
|
|
3
|
+
* where the cursor is inside, else the nearest point on the edge.
|
|
4
|
+
*/
|
|
5
|
+
export function inkOnRect(xPx, yPx, left, top, width, height) {
|
|
6
|
+
const x = Math.min(Math.max(xPx, left), left + width);
|
|
7
|
+
const y = Math.min(Math.max(yPx, top), top + height);
|
|
8
|
+
const dx = xPx - x;
|
|
9
|
+
const dy = yPx - y;
|
|
10
|
+
return { x, y, distSq: dx * dx + dy * dy };
|
|
11
|
+
}
|
|
12
|
+
/** The same for a mark with no extent to clamp into. */
|
|
13
|
+
export function inkAtPoint(xPx, yPx, x, y) {
|
|
14
|
+
const dx = xPx - x;
|
|
15
|
+
const dy = yPx - y;
|
|
16
|
+
return { x, y, distSq: dx * dx + dy * dy };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The nearest of `candidates` by where each one's ink is, or undefined when
|
|
20
|
+
* none beats `maxDistSq`.
|
|
21
|
+
*
|
|
22
|
+
* This is the whole of every shape's `hitNearest` but the geometry: `inkAt`
|
|
23
|
+
* answers where instance `i` put ink and how far that is, and this keeps the
|
|
24
|
+
* closest. Only a STRICTLY nearer candidate replaces the best, so on a tie the
|
|
25
|
+
* first wins — which is what makes a caller that iterates back to front get the
|
|
26
|
+
* mark on top, and it is a rule that used to be spelled out five times over.
|
|
27
|
+
*
|
|
28
|
+
* `inkAt` may answer `undefined` for an instance that drew nothing at all, as
|
|
29
|
+
* against one that drew somewhere far away: a bar clipped out of its band has
|
|
30
|
+
* no ink to be near.
|
|
31
|
+
*/
|
|
32
|
+
export function nearestInk(candidates, maxDistSq, inkAt) {
|
|
33
|
+
let best;
|
|
34
|
+
let bestDistSq = maxDistSq;
|
|
35
|
+
for (const index of candidates) {
|
|
36
|
+
const ink = inkAt(index);
|
|
37
|
+
if (ink && ink.distSq < bestDistSq) {
|
|
38
|
+
bestDistSq = ink.distSq;
|
|
39
|
+
// Field by field, not `{ index, ...ink }`, which is both slower and one
|
|
40
|
+
// shape away from minting a hidden class per ink source and making every
|
|
41
|
+
// downstream `hit.x` read polymorphic.
|
|
42
|
+
best = { index, x: ink.x, y: ink.y, distSq: ink.distSq };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return best;
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RenderBlock } from '../renderBlock.ts';
|
|
2
|
+
import type { Mark, MarkContext2D, MarkFrame } from './types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Paint one frame's blocks with a mark list — the Canvas2D half of a
|
|
5
|
+
* declaration, and the SVG export path, since `MarkContext2D` is satisfied by
|
|
6
|
+
* both a real 2D context and the SVG one.
|
|
7
|
+
*
|
|
8
|
+
* Its own module, away from the backend that calls it: an SVG export runs with
|
|
9
|
+
* no backend attached at all (and must, or a headless export would depend on a
|
|
10
|
+
* GPU device having been probed), so the path that draws must not pull the HAL
|
|
11
|
+
* in behind it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function paintMarkBlocks<TRegion, TState extends MarkFrame>(ctx: MarkContext2D, marks: readonly Mark<TRegion, TState>[], regions: ReadonlyMap<number, TRegion>, blocks: RenderBlock[], state: TState): void;
|
|
@@ -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,91 @@
|
|
|
1
|
+
import { bpRangeXTuple } from '../blockClipUtils.js';
|
|
2
|
+
import { getDpr, makeBpMapper } from '../canvas2dUtils.js';
|
|
3
|
+
import * as shader from '../shaders/pointMark.generated.js';
|
|
4
|
+
import { pointDrawsBar, 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
|
+
import { inkAtPoint, inkOnRect, nearestInk } from './markHit.js';
|
|
9
|
+
export const pointMark = {
|
|
10
|
+
id: 'point',
|
|
11
|
+
pass: {
|
|
12
|
+
...slangPass({ id: 'point', mod: shader }),
|
|
13
|
+
pack: c => shader.packInstances(c, c.count),
|
|
14
|
+
},
|
|
15
|
+
writeUniforms(scratch, clip, block, frame, params) {
|
|
16
|
+
shader.writeUniforms(scratch, {
|
|
17
|
+
bpRangeX: bpRangeXTuple(clip, block.reversed),
|
|
18
|
+
canvasHeight: frame.canvasHeight,
|
|
19
|
+
domainMin: params.domain[0],
|
|
20
|
+
domainMax: params.domain[1],
|
|
21
|
+
zero: 0,
|
|
22
|
+
// viewportWidth and radiusPx stay in CSS units to match canvasHeight:
|
|
23
|
+
// mixing a DPR-scaled radius with a CSS-scaled height draws vertically
|
|
24
|
+
// stretched ellipses on hi-DPI.
|
|
25
|
+
viewportWidth: clip.scissorW,
|
|
26
|
+
radiusPx: params.diameterPx / 2,
|
|
27
|
+
devicePixelRatio: getDpr(),
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
paintBlock(ctx, channels, block, frame, params) {
|
|
31
|
+
const { x, x2, y, color, glyph, count } = channels;
|
|
32
|
+
if (count === 0) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const { diameterPx, domain } = params;
|
|
36
|
+
const r = diameterPx / 2;
|
|
37
|
+
const canvasHeight = frame.canvasHeight;
|
|
38
|
+
const domainMin = domain[0];
|
|
39
|
+
const domainMax = domain[1];
|
|
40
|
+
// The per-block closure, not the six-argument `bpToScreenPx`: that spelling
|
|
41
|
+
// re-derives the region span and the block width at every call, and this
|
|
42
|
+
// loop makes two calls per instance. Measured at 1.67x on 100K points.
|
|
43
|
+
const bpToPx = makeBpMapper(block);
|
|
44
|
+
// Batched by colour: a run of one colour is one fillStyle write and one
|
|
45
|
+
// fill() over a shared path, which is most of a painting.
|
|
46
|
+
let current = color[0];
|
|
47
|
+
ctx.fillStyle = abgrToCssRgba(current);
|
|
48
|
+
ctx.beginPath();
|
|
49
|
+
for (let i = 0; i < count; i++) {
|
|
50
|
+
const abgr = color[i];
|
|
51
|
+
if (abgr !== current) {
|
|
52
|
+
ctx.fill();
|
|
53
|
+
current = abgr;
|
|
54
|
+
ctx.fillStyle = abgrToCssRgba(abgr);
|
|
55
|
+
ctx.beginPath();
|
|
56
|
+
}
|
|
57
|
+
const xStart = bpToPx(x[i]);
|
|
58
|
+
const xEnd = bpToPx(x2[i]);
|
|
59
|
+
const yPx = valueToYPx(y[i], domainMin, domainMax, canvasHeight);
|
|
60
|
+
const widthPx = Math.abs(xEnd - xStart);
|
|
61
|
+
if (pointDrawsBar(widthPx, r)) {
|
|
62
|
+
ctx.rect(Math.min(xStart, xEnd), yPx - r, widthPx, diameterPx);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
appendGlyph(ctx, glyph[i], xStart, yPx, diameterPx);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
ctx.fill();
|
|
69
|
+
},
|
|
70
|
+
hitNearest(channels, block, frame, params, xPx, yPx, candidates, maxDistSq) {
|
|
71
|
+
const { x, x2, y } = channels;
|
|
72
|
+
const bpToPx = makeBpMapper(block);
|
|
73
|
+
const { diameterPx, domain } = params;
|
|
74
|
+
const domainMin = domain[0];
|
|
75
|
+
const domainMax = domain[1];
|
|
76
|
+
const canvasHeight = frame.canvasHeight;
|
|
77
|
+
return nearestInk(candidates, maxDistSq, i => {
|
|
78
|
+
const xStart = bpToPx(x[i]);
|
|
79
|
+
const xEnd = bpToPx(x2[i]);
|
|
80
|
+
const cy = valueToYPx(y[i], domainMin, domainMax, canvasHeight);
|
|
81
|
+
// A bar is the rect the painter fills, unpadded on every side, so the
|
|
82
|
+
// drawn bar and this box are one rectangle. A glyph has no rect to clamp
|
|
83
|
+
// into and offers its centre.
|
|
84
|
+
const lo = Math.min(xStart, xEnd);
|
|
85
|
+
const hi = Math.max(xStart, xEnd);
|
|
86
|
+
return pointDrawsBar(hi - lo, diameterPx / 2)
|
|
87
|
+
? inkOnRect(xPx, yPx, lo, cy - diameterPx / 2, hi - lo, diameterPx)
|
|
88
|
+
: inkAtPoint(xPx, yPx, xStart, cy);
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
};
|