@jbrowse/render-core 5.0.0-beta.7 → 5.0.0-beta.9

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 (174) hide show
  1. package/esm/RenderCanvas.d.ts +2 -2
  2. package/esm/RenderLifecycleMixin.d.ts +7 -6
  3. package/esm/RenderLifecycleMixin.js +7 -6
  4. package/esm/canvas2dUtils.d.ts +58 -17
  5. package/esm/canvas2dUtils.js +82 -30
  6. package/esm/computePipeline.d.ts +4 -3
  7. package/esm/computePipeline.js +21 -14
  8. package/esm/coverageBand.d.ts +3 -3
  9. package/esm/coverageBand.js +2 -2
  10. package/esm/coverageBandBuffers.d.ts +2 -3
  11. package/esm/createRenderingBackend.d.ts +1 -1
  12. package/esm/displayPhase.d.ts +79 -109
  13. package/esm/displayPhase.js +42 -83
  14. package/esm/gpuDevice.d.ts +11 -10
  15. package/esm/gpuDevice.js +10 -11
  16. package/esm/graphicsCapabilities.d.ts +6 -7
  17. package/esm/graphicsCapabilities.js +3 -4
  18. package/esm/hal/createHal.js +1 -1
  19. package/esm/hal/deviceGpuCache.d.ts +25 -30
  20. package/esm/hal/deviceGpuCache.js +85 -97
  21. package/esm/hal/index.d.ts +2 -2
  22. package/esm/hal/index.js +1 -1
  23. package/esm/hal/mockHal.d.ts +8 -6
  24. package/esm/hal/mockHal.js +7 -2
  25. package/esm/hal/passIds.d.ts +8 -7
  26. package/esm/hal/passIds.js +11 -10
  27. package/esm/hal/types.d.ts +28 -12
  28. package/esm/hal/webgl2Hal.d.ts +11 -3
  29. package/esm/hal/webgl2Hal.js +116 -145
  30. package/esm/hal/webgpuHal.d.ts +8 -8
  31. package/esm/hal/webgpuHal.js +97 -124
  32. package/esm/instancePass.d.ts +2 -1
  33. package/esm/mapUploadSync.d.ts +2 -2
  34. package/esm/mapUploadSync.js +2 -2
  35. package/esm/marks/barMark.d.ts +6 -3
  36. package/esm/marks/barMark.js +19 -13
  37. package/esm/marks/drawAgainstHit.d.ts +58 -39
  38. package/esm/marks/drawAgainstHit.js +136 -96
  39. package/esm/marks/glyphPaint.d.ts +14 -2
  40. package/esm/marks/glyphPaint.js +16 -2
  41. package/esm/marks/index.d.ts +4 -2
  42. package/esm/marks/index.js +3 -2
  43. package/esm/marks/markBackend.d.ts +2 -2
  44. package/esm/marks/markBackend.js +2 -2
  45. package/esm/marks/markHit.d.ts +2 -2
  46. package/esm/marks/markHit.js +2 -2
  47. package/esm/marks/markPlan.d.ts +2 -2
  48. package/esm/marks/markPlan.js +2 -2
  49. package/esm/marks/markRamp.d.ts +1 -3
  50. package/esm/marks/markRamp.js +1 -5
  51. package/esm/marks/nearestMarkHit.d.ts +49 -0
  52. package/esm/marks/nearestMarkHit.js +126 -0
  53. package/esm/marks/pointMark.d.ts +9 -2
  54. package/esm/marks/pointMark.js +36 -31
  55. package/esm/marks/rowLane.d.ts +17 -0
  56. package/esm/marks/rowLane.js +11 -0
  57. package/esm/marks/spanMark.d.ts +6 -26
  58. package/esm/marks/spanMark.js +34 -31
  59. package/esm/marks/types.d.ts +51 -181
  60. package/esm/marks/types.js +91 -131
  61. package/esm/namedReactions.d.ts +3 -4
  62. package/esm/namedReactions.js +3 -4
  63. package/esm/scoreScale.d.ts +20 -0
  64. package/esm/scoreScale.js +77 -0
  65. package/esm/shaders/barMark.generated.d.ts +2 -3
  66. package/esm/shaders/barMark.generated.js +4 -3
  67. package/esm/shaders/barMark.glsl.generated.d.ts +2 -0
  68. package/esm/shaders/barMark.glsl.generated.js +4 -0
  69. package/esm/shaders/barMark.iface.generated.d.ts +12 -6
  70. package/esm/shaders/barMark.iface.generated.js +27 -15
  71. package/esm/shaders/barMark.wgsl.generated.d.ts +1 -0
  72. package/esm/shaders/barMark.wgsl.generated.js +3 -0
  73. package/esm/shaders/coverageBar.generated.d.ts +2 -3
  74. package/esm/shaders/coverageBar.generated.js +4 -3
  75. package/esm/shaders/coverageBar.glsl.generated.d.ts +2 -0
  76. package/esm/shaders/coverageBar.glsl.generated.js +4 -0
  77. package/esm/shaders/coverageBar.iface.generated.js +1 -1
  78. package/esm/shaders/coverageBar.wgsl.generated.d.ts +1 -0
  79. package/esm/shaders/coverageBar.wgsl.generated.js +3 -0
  80. package/esm/shaders/coverageIndicator.generated.d.ts +2 -3
  81. package/esm/shaders/coverageIndicator.generated.js +4 -3
  82. package/esm/shaders/coverageIndicator.glsl.generated.d.ts +2 -0
  83. package/esm/shaders/coverageIndicator.glsl.generated.js +4 -0
  84. package/esm/shaders/coverageIndicator.iface.generated.js +1 -1
  85. package/esm/shaders/coverageIndicator.wgsl.generated.d.ts +1 -0
  86. package/esm/shaders/coverageIndicator.wgsl.generated.js +3 -0
  87. package/esm/shaders/coverageInterbase.generated.d.ts +2 -3
  88. package/esm/shaders/coverageInterbase.generated.js +4 -3
  89. package/esm/shaders/coverageInterbase.glsl.generated.d.ts +2 -0
  90. package/esm/shaders/coverageInterbase.glsl.generated.js +4 -0
  91. package/esm/shaders/coverageInterbase.iface.generated.js +1 -1
  92. package/esm/shaders/coverageInterbase.wgsl.generated.d.ts +1 -0
  93. package/esm/shaders/coverageInterbase.wgsl.generated.js +3 -0
  94. package/esm/shaders/coverageMod.generated.d.ts +2 -3
  95. package/esm/shaders/coverageMod.generated.js +4 -3
  96. package/esm/shaders/coverageMod.glsl.generated.d.ts +2 -0
  97. package/esm/shaders/coverageMod.glsl.generated.js +4 -0
  98. package/esm/shaders/coverageMod.iface.generated.js +1 -1
  99. package/esm/shaders/coverageMod.wgsl.generated.d.ts +1 -0
  100. package/esm/shaders/coverageMod.wgsl.generated.js +3 -0
  101. package/esm/shaders/coverageSnp.generated.d.ts +2 -3
  102. package/esm/shaders/coverageSnp.generated.js +4 -3
  103. package/esm/shaders/coverageSnp.glsl.generated.d.ts +2 -0
  104. package/esm/shaders/coverageSnp.glsl.generated.js +4 -0
  105. package/esm/shaders/coverageSnp.iface.generated.js +1 -1
  106. package/esm/shaders/coverageSnp.wgsl.generated.d.ts +1 -0
  107. package/esm/shaders/coverageSnp.wgsl.generated.js +3 -0
  108. package/esm/shaders/pointMark.generated.d.ts +2 -3
  109. package/esm/shaders/pointMark.generated.js +4 -3
  110. package/esm/shaders/pointMark.glsl.generated.d.ts +2 -0
  111. package/esm/shaders/pointMark.glsl.generated.js +4 -0
  112. package/esm/shaders/pointMark.iface.generated.d.ts +11 -3
  113. package/esm/shaders/pointMark.iface.generated.js +23 -9
  114. package/esm/shaders/pointMark.js.generated.d.ts +1 -1
  115. package/esm/shaders/pointMark.js.generated.js +51 -29
  116. package/esm/shaders/pointMark.wgsl.generated.d.ts +1 -0
  117. package/esm/shaders/pointMark.wgsl.generated.js +3 -0
  118. package/esm/shaders/scoreScale.js.generated.js +46 -22
  119. package/esm/shaders/spanMark.generated.d.ts +2 -3
  120. package/esm/shaders/spanMark.generated.js +4 -3
  121. package/esm/shaders/spanMark.glsl.generated.d.ts +2 -0
  122. package/esm/shaders/spanMark.glsl.generated.js +4 -0
  123. package/esm/shaders/spanMark.iface.generated.js +1 -1
  124. package/esm/shaders/spanMark.wgsl.generated.d.ts +1 -0
  125. package/esm/shaders/spanMark.wgsl.generated.js +3 -0
  126. package/esm/slangPass.d.ts +9 -10
  127. package/esm/slangPass.js +9 -3
  128. package/esm/useGraphicsCapabilities.d.ts +1 -1
  129. package/esm/useGraphicsCapabilities.js +1 -1
  130. package/esm/webgpuUtils.d.ts +3 -11
  131. package/esm/webgpuUtils.js +12 -33
  132. package/package.json +3 -2
  133. package/src/shaders/barMark.generated.ts +6 -5
  134. package/src/shaders/barMark.glsl.generated.ts +6 -0
  135. package/src/shaders/barMark.iface.generated.ts +31 -15
  136. package/src/shaders/barMark.slang +11 -3
  137. package/src/shaders/barMark.wgsl.generated.ts +4 -0
  138. package/src/shaders/colorPack.slang +3 -2
  139. package/src/shaders/coverageBand.slang +5 -11
  140. package/src/shaders/coverageBar.generated.ts +6 -5
  141. package/src/shaders/coverageBar.glsl.generated.ts +6 -0
  142. package/src/shaders/coverageBar.iface.generated.ts +1 -1
  143. package/src/shaders/coverageBar.slang +1 -1
  144. package/src/shaders/coverageBar.wgsl.generated.ts +4 -0
  145. package/src/shaders/coverageIndicator.generated.ts +6 -5
  146. package/src/shaders/coverageIndicator.glsl.generated.ts +6 -0
  147. package/src/shaders/coverageIndicator.iface.generated.ts +1 -1
  148. package/src/shaders/coverageIndicator.wgsl.generated.ts +4 -0
  149. package/src/shaders/coverageInterbase.generated.ts +6 -5
  150. package/src/shaders/coverageInterbase.glsl.generated.ts +6 -0
  151. package/src/shaders/coverageInterbase.iface.generated.ts +1 -1
  152. package/src/shaders/coverageInterbase.wgsl.generated.ts +4 -0
  153. package/src/shaders/coverageMod.generated.ts +6 -5
  154. package/src/shaders/coverageMod.glsl.generated.ts +6 -0
  155. package/src/shaders/coverageMod.iface.generated.ts +1 -1
  156. package/src/shaders/coverageMod.wgsl.generated.ts +4 -0
  157. package/src/shaders/coverageSnp.generated.ts +6 -5
  158. package/src/shaders/coverageSnp.glsl.generated.ts +6 -0
  159. package/src/shaders/coverageSnp.iface.generated.ts +1 -1
  160. package/src/shaders/coverageSnp.wgsl.generated.ts +4 -0
  161. package/src/shaders/hpmath.slang +18 -6
  162. package/src/shaders/pointMark.generated.ts +6 -6
  163. package/src/shaders/pointMark.glsl.generated.ts +6 -0
  164. package/src/shaders/pointMark.iface.generated.ts +28 -9
  165. package/src/shaders/pointMark.js.generated.ts +48 -29
  166. package/src/shaders/pointMark.slang +18 -5
  167. package/src/shaders/pointMark.wgsl.generated.ts +4 -0
  168. package/src/shaders/scoreScale.js.generated.ts +43 -22
  169. package/src/shaders/scoreScale.slang +37 -53
  170. package/src/shaders/spanMark.generated.ts +6 -5
  171. package/src/shaders/spanMark.glsl.generated.ts +6 -0
  172. package/src/shaders/spanMark.iface.generated.ts +1 -1
  173. package/src/shaders/spanMark.wgsl.generated.ts +4 -0
  174. package/src/shaders/valueScale.slang +8 -45
@@ -0,0 +1,126 @@
1
+ import { bpAtPxExact } from '../canvas2dUtils.js';
2
+ import { denormalizeScore, scaleTypeCode } from '../scoreScale.js';
3
+ const EVERY_VALUE = [-Infinity, Infinity];
4
+ /**
5
+ * The instance nearest `(xPx, yPx)` within `radiusPx`, over the blocks the
6
+ * radius reaches. Marks are asked last to first and only a strictly nearer hit
7
+ * replaces the best, so a tie goes to the mark on top. `candidates` names what
8
+ * a mark is asked about in a block: what an index finds in the reach, or
9
+ * `backToFront` over every instance. `undefined` leaves the mark out, as a
10
+ * closed gate does.
11
+ */
12
+ export function nearestMarkHit(marks, blocks, regionOf, state, xPx, yPx, { radiusPx, candidates, }) {
13
+ let best;
14
+ let bestDistSq = radiusPx * radiusPx;
15
+ for (const block of blocks) {
16
+ const region = regionOf(block.displayedRegionIndex);
17
+ const widthPx = block.screenEndPx - block.screenStartPx;
18
+ if (region === undefined || widthPx <= 0) {
19
+ continue;
20
+ }
21
+ const halfBp = (radiusPx * (block.end - block.start)) / widthPx;
22
+ const cursorBp = bpAtPxExact(xPx, block);
23
+ const bpMin = cursorBp - halfBp;
24
+ const bpMax = cursorBp + halfBp;
25
+ if (bpMax < block.start || bpMin > block.end) {
26
+ continue;
27
+ }
28
+ for (let m = marks.length - 1; m >= 0; m--) {
29
+ const mark = marks[m];
30
+ if (!mark.hitNearest) {
31
+ continue;
32
+ }
33
+ const values = mark.valueWindow
34
+ ? mark.valueWindow(region, block, state, yPx, radiusPx)
35
+ : EVERY_VALUE;
36
+ const asked = values &&
37
+ candidates(region, m, {
38
+ block,
39
+ bpMin,
40
+ bpMax,
41
+ valueMin: values[0],
42
+ valueMax: values[1],
43
+ });
44
+ const hit = asked &&
45
+ mark.hitNearest(region, block, state, xPx, yPx, asked, bestDistSq);
46
+ if (hit) {
47
+ bestDistSq = hit.distSq;
48
+ best = {
49
+ mark: m,
50
+ block,
51
+ region,
52
+ index: hit.index,
53
+ x: hit.x,
54
+ y: hit.y,
55
+ distSq: hit.distSq,
56
+ };
57
+ }
58
+ }
59
+ }
60
+ return best;
61
+ }
62
+ /** `[end - 1 .. start]`: every instance as candidates, back to front. */
63
+ export function backToFront(start, end) {
64
+ return new ReverseRange(start, end);
65
+ }
66
+ // A class rather than a generator: `for..of` steps this in a few ns where a
67
+ // generator yields in tens.
68
+ class ReverseRange {
69
+ i;
70
+ start;
71
+ result = { value: 0, done: false };
72
+ constructor(start, end) {
73
+ this.start = start;
74
+ this.i = end;
75
+ }
76
+ [Symbol.iterator]() {
77
+ return this;
78
+ }
79
+ next() {
80
+ const r = this.result;
81
+ if (this.i > this.start) {
82
+ this.i--;
83
+ r.value = this.i;
84
+ r.done = false;
85
+ }
86
+ else {
87
+ r.done = true;
88
+ }
89
+ return r;
90
+ }
91
+ }
92
+ /**
93
+ * A value-scaled shape's `valueWindow`, read back through `denormalizeScore`
94
+ * inside each `rowHeight` band the radius touches — the whole canvas for a
95
+ * shape with no rows. An end within reach of a band edge opens to infinity,
96
+ * where an out-of-domain value clamps, so a cursor near the seam reaches the
97
+ * neighbouring band too. A point passes its `insetPx`; a bar passes its
98
+ * `origin`, and its window opens away from the origin on the cursor's side.
99
+ */
100
+ export function valueWindow(yPx, radiusPx, { canvasHeight }, scale) {
101
+ const { domain, insetPx = 0, origin } = scale;
102
+ const code = scaleTypeCode(scale.scaleType);
103
+ const height = scale.rowHeight ?? canvasHeight;
104
+ const inset = Math.min(insetPx, height / 2);
105
+ const bands = height > 0 ? Math.ceil(canvasHeight / height) : 1;
106
+ const bandAt = (y) => bands > 1 ? Math.max(0, Math.min(bands - 1, Math.floor(y / height))) : 0;
107
+ const lastBand = bandAt(yPx + radiusPx);
108
+ let valueMin = Infinity;
109
+ let valueMax = -Infinity;
110
+ for (let b = bandAt(yPx - radiusPx); b <= lastBand; b++) {
111
+ const top = b * height;
112
+ const valueAt = (y) => denormalizeScore(1 - (y - top - inset) / (height - 2 * inset), domain[0], domain[1], code, 1);
113
+ const lo = yPx + radiusPx >= top + height - inset
114
+ ? -Infinity
115
+ : valueAt(yPx + radiusPx);
116
+ const hi = yPx - radiusPx <= top + inset ? Infinity : valueAt(yPx - radiusPx);
117
+ const [bandMin, bandMax] = origin === undefined
118
+ ? [lo, hi]
119
+ : valueAt(yPx) >= origin
120
+ ? [lo, Infinity]
121
+ : [-Infinity, hi];
122
+ valueMin = Math.min(valueMin, bandMin);
123
+ valueMax = Math.max(valueMax, bandMax);
124
+ }
125
+ return [valueMin, valueMax];
126
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ColorChannel } from './markRamp.ts';
2
+ import type { RowChannel, RowParams } from './rowLane.ts';
2
3
  import type { MarkRamp, MarkShape, MarkValueScaleType } from './types.ts';
3
4
  /**
4
5
  * The `point` shape's channels: a glyph per instance at `x`, on the `y` scale,
@@ -8,14 +9,14 @@ import type { MarkRamp, MarkShape, MarkValueScaleType } from './types.ts';
8
9
  * one array holds SNPs (`x2 === x + 1`) and structural variants together, and
9
10
  * the shader, the painter and the hit test all take the same branch off it.
10
11
  */
11
- export interface PointChannels extends ColorChannel {
12
+ export interface PointChannels extends ColorChannel, RowChannel {
12
13
  x: Uint32Array;
13
14
  x2: Uint32Array;
14
15
  y: Float32Array;
15
16
  glyph: Uint8Array;
16
17
  count: number;
17
18
  }
18
- export interface PointParams {
19
+ export interface PointParams extends RowParams {
19
20
  /** `[min, max]` `y` is read through. */
20
21
  domain: [number, number];
21
22
  /** How that domain is read; linear when absent. */
@@ -24,5 +25,11 @@ export interface PointParams {
24
25
  ramp?: MarkRamp;
25
26
  /** Glyph diameter in CSS px. */
26
27
  diameterPx: number;
28
+ /**
29
+ * How far inside the plot the value range ends, so a point at a domain
30
+ * endpoint draws whole — `glyphPaint`'s `pointInsetPx`, and the same number
31
+ * the display's axis takes as its offset. Absent centres it on the edge.
32
+ */
33
+ insetPx?: number;
27
34
  }
28
35
  export declare const pointMark: MarkShape<PointChannels, PointParams>;
@@ -1,18 +1,20 @@
1
1
  import { bpRangeXTuple } from '../blockClipUtils.js';
2
- import { getDpr, makeBpMapper } from '../canvas2dUtils.js';
2
+ import { CappedPath, getDpr, makeBpMapper } from '../canvas2dUtils.js';
3
+ import { scaleTypeCode } from '../scoreScale.js';
3
4
  import * as shader from '../shaders/pointMark.generated.js';
4
- import { pointDrawsBar, valueToYPxScaled, } from '../shaders/pointMark.js.generated.js';
5
+ import { pointDrawsBar, pointYPx } from '../shaders/pointMark.js.generated.js';
5
6
  import { slangPass } from '../slangPass.js';
6
7
  import { abgrToCssRgba } from './colorFill.js';
7
8
  import { appendGlyph, glyphBox } from './glyphPaint.js';
8
9
  import { inkAtPoint, inkOnRect, nearestInk } from './markHit.js';
9
- import { colorBits, paintColors, rampUniforms, valueScaleTypeCode, } from './markRamp.js';
10
- import { blockPx } from './spanMark.js';
10
+ import { colorBits, paintColors, rampUniforms } from './markRamp.js';
11
+ import { valueWindow } from './nearestMarkHit.js';
12
+ import { bandHeightPx, bandTopPx, rowLane } from './rowLane.js';
11
13
  export const pointMark = {
12
14
  id: 'point',
13
15
  pass: {
14
16
  ...slangPass({ id: 'point', mod: shader }),
15
- pack: c => shader.packInstances({ ...c, color: colorBits(c) }, c.count),
17
+ pack: c => shader.packInstances({ ...c, color: colorBits(c), row: rowLane(c.row, c.count) }, c.count),
16
18
  },
17
19
  writeUniforms(scratch, clip, block, frame, params) {
18
20
  shader.writeUniforms(scratch, {
@@ -20,7 +22,7 @@ export const pointMark = {
20
22
  canvasHeight: frame.canvasHeight,
21
23
  domainMin: params.domain[0],
22
24
  domainMax: params.domain[1],
23
- valueScaleType: valueScaleTypeCode(params.scaleType),
25
+ valueScaleType: scaleTypeCode(params.scaleType),
24
26
  ...rampUniforms(params.ramp),
25
27
  zero: 0,
26
28
  // viewportWidth and radiusPx stay in CSS units to match canvasHeight:
@@ -28,41 +30,39 @@ export const pointMark = {
28
30
  // stretched ellipses on hi-DPI.
29
31
  viewportWidth: clip.scissorW,
30
32
  radiusPx: params.diameterPx / 2,
33
+ rowHeight: bandHeightPx(params, frame.canvasHeight),
34
+ insetPx: params.insetPx ?? 0,
31
35
  devicePixelRatio: getDpr(),
32
36
  });
33
37
  },
34
38
  paintBlock(ctx, channels, block, frame, params) {
35
- const { x, x2, y, glyph, count } = channels;
39
+ const { x, x2, y, glyph, row, count } = channels;
36
40
  if (count === 0) {
37
41
  return;
38
42
  }
39
43
  const color = paintColors(channels, count, params.ramp);
40
- const { diameterPx, domain } = params;
44
+ const { diameterPx, domain, insetPx = 0 } = params;
41
45
  const r = diameterPx / 2;
42
- const canvasHeight = frame.canvasHeight;
46
+ const band = bandHeightPx(params, frame.canvasHeight);
43
47
  const domainMin = domain[0];
44
48
  const domainMax = domain[1];
45
- const st = valueScaleTypeCode(params.scaleType);
46
- // The per-block closure, not the six-argument `bpToScreenPx`: that spelling
47
- // re-derives the region span and the block width at every call, and this
48
- // loop makes two calls per instance. Measured at 1.67x on 100K points.
49
+ const st = scaleTypeCode(params.scaleType);
49
50
  const bpToPx = makeBpMapper(block);
50
- // Batched by colour: a run of one colour is one fillStyle write and one
51
- // fill() over a shared path, which is most of a painting.
52
51
  let current = color[0];
53
52
  ctx.fillStyle = abgrToCssRgba(current);
54
- ctx.beginPath();
53
+ const path = new CappedPath(ctx, 'fill');
55
54
  for (let i = 0; i < count; i++) {
56
55
  const abgr = color[i];
57
56
  if (abgr !== current) {
58
- ctx.fill();
57
+ path.flush();
59
58
  current = abgr;
60
59
  ctx.fillStyle = abgrToCssRgba(abgr);
61
- ctx.beginPath();
62
60
  }
61
+ path.add();
63
62
  const xStart = bpToPx(x[i]);
64
63
  const xEnd = bpToPx(x2[i]);
65
- const yPx = valueToYPxScaled(y[i], domainMin, domainMax, canvasHeight, st);
64
+ const yPx = bandTopPx(row, i, band) +
65
+ pointYPx(y[i], domainMin, domainMax, band, st, insetPx);
66
66
  const widthPx = Math.abs(xEnd - xStart);
67
67
  if (pointDrawsBar(widthPx, r)) {
68
68
  ctx.rect(Math.min(xStart, xEnd), yPx - r, widthPx, diameterPx);
@@ -71,19 +71,22 @@ export const pointMark = {
71
71
  appendGlyph(ctx, glyph[i], xStart, yPx, diameterPx);
72
72
  }
73
73
  }
74
- ctx.fill();
74
+ path.flush();
75
75
  },
76
76
  // A bar is the rect the painter fills, unpadded on every side; a glyph is
77
77
  // the box `appendGlyph` paints inside.
78
78
  ink(channels, block, frame, params, i) {
79
- const { x, x2, y, glyph } = channels;
80
- const { diameterPx, domain } = params;
81
- const xStart = blockPx(block, x[i]);
82
- const xEnd = blockPx(block, x2[i]);
83
- const cy = valueToYPxScaled(y[i], domain[0], domain[1], frame.canvasHeight, valueScaleTypeCode(params.scaleType));
79
+ const { x, x2, y, glyph, row } = channels;
80
+ const { diameterPx, domain, insetPx = 0 } = params;
81
+ const bpToPx = makeBpMapper(block);
82
+ const xStart = bpToPx(x[i]);
83
+ const xEnd = bpToPx(x2[i]);
84
+ const r = diameterPx / 2;
85
+ const band = bandHeightPx(params, frame.canvasHeight);
86
+ const cy = bandTopPx(row, i, band) +
87
+ pointYPx(y[i], domain[0], domain[1], band, scaleTypeCode(params.scaleType), insetPx);
84
88
  const lo = Math.min(xStart, xEnd);
85
89
  const hi = Math.max(xStart, xEnd);
86
- const r = diameterPx / 2;
87
90
  return pointDrawsBar(hi - lo, r)
88
91
  ? { left: lo, top: cy - r, width: hi - lo, height: diameterPx }
89
92
  : glyphBox(glyph[i], xStart, cy, diameterPx);
@@ -93,17 +96,18 @@ export const pointMark = {
93
96
  // one under the cursor is the nearest CENTRE, which no box can say. A bar
94
97
  // is grabbed anywhere inside the rect it fills, as the derived test would.
95
98
  hitNearest(channels, block, frame, params, xPx, yPx, candidates, maxDistSq) {
96
- const { x, x2, y } = channels;
99
+ const { x, x2, y, row } = channels;
97
100
  const bpToPx = makeBpMapper(block);
98
- const { diameterPx, domain } = params;
101
+ const { diameterPx, domain, insetPx = 0 } = params;
99
102
  const domainMin = domain[0];
100
103
  const domainMax = domain[1];
101
- const canvasHeight = frame.canvasHeight;
102
- const st = valueScaleTypeCode(params.scaleType);
104
+ const band = bandHeightPx(params, frame.canvasHeight);
105
+ const st = scaleTypeCode(params.scaleType);
103
106
  return nearestInk(candidates, maxDistSq, i => {
104
107
  const xStart = bpToPx(x[i]);
105
108
  const xEnd = bpToPx(x2[i]);
106
- const cy = valueToYPxScaled(y[i], domainMin, domainMax, canvasHeight, st);
109
+ const cy = bandTopPx(row, i, band) +
110
+ pointYPx(y[i], domainMin, domainMax, band, st, insetPx);
107
111
  const lo = Math.min(xStart, xEnd);
108
112
  const hi = Math.max(xStart, xEnd);
109
113
  return pointDrawsBar(hi - lo, diameterPx / 2)
@@ -111,4 +115,5 @@ export const pointMark = {
111
115
  : inkAtPoint(xPx, yPx, xStart, cy);
112
116
  });
113
117
  },
118
+ valueWindow,
114
119
  };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The `row` lane a value shape may carry: which band an instance stands in,
3
+ * `rowHeight` px each, the value scale ruling every band on its own. A caller
4
+ * that declares no rows is on row 0 with the whole canvas for a band, which is
5
+ * the same arithmetic, so the shaders take the lane unconditionally.
6
+ */
7
+ export interface RowChannel {
8
+ row?: Uint32Array;
9
+ }
10
+ export interface RowParams {
11
+ /** CSS px per row band; the canvas height when absent. */
12
+ rowHeight?: number;
13
+ }
14
+ /** The lane the instance buffer packs: zeros where the caller sent none. */
15
+ export declare function rowLane(row: Uint32Array | undefined, count: number): Uint32Array<ArrayBufferLike>;
16
+ export declare function bandHeightPx(params: RowParams, canvasHeight: number): number;
17
+ export declare function bandTopPx(row: Uint32Array | undefined, i: number, band: number): number;
@@ -0,0 +1,11 @@
1
+ const NO_ROWS = new Uint32Array(0);
2
+ /** The lane the instance buffer packs: zeros where the caller sent none. */
3
+ export function rowLane(row, count) {
4
+ return row ?? (count === 0 ? NO_ROWS : new Uint32Array(count));
5
+ }
6
+ export function bandHeightPx(params, canvasHeight) {
7
+ return params.rowHeight ?? canvasHeight;
8
+ }
9
+ export function bandTopPx(row, i, band) {
10
+ return row === undefined ? 0 : band * row[i];
11
+ }
@@ -1,4 +1,3 @@
1
- import type { RenderBlock } from '../renderBlock.ts';
2
1
  import type { MarkShape } from './types.ts';
3
2
  /**
4
3
  * The `span` shape's channels: a coloured rectangle from `x` to `x2` on the
@@ -8,12 +7,7 @@ export interface SpanChannels {
8
7
  x: Uint32Array;
9
8
  x2: Uint32Array;
10
9
  row: Uint32Array;
11
- /**
12
- * Packed ABGR, resolved in the worker — a span's colour has no ramp arm.
13
- * `rowRect`'s uniform struct is shared with four other consumers, and a
14
- * sampler on it would move all of them for a channel no config asks for
15
- * on a span (ADR-113).
16
- */
10
+ /** Packed ABGR, resolved in the worker: a span has no ramp arm (ADR-113). */
17
11
  color: Uint32Array;
18
12
  count: number;
19
13
  }
@@ -23,31 +17,17 @@ export interface SpanParams {
23
17
  /** Fraction of the row the rect fills, leaving inter-row gaps. */
24
18
  rowProportion: number;
25
19
  /**
26
- * Narrowest a span is painted, in CSS px. Zero is a no-op and is what a
27
- * caller whose marks TILE writes — widening a sub-pixel cell inside a run
28
- * paints ink the data does not contain. A caller whose marks are sparse
29
- * intervals floors, because at chromosome zoom the alternative is a smudge.
20
+ * Narrowest a span is painted, in CSS px. A caller whose marks tile writes
21
+ * zero; one whose marks are sparse intervals floors them.
30
22
  */
31
23
  minWidthPx: number;
32
24
  /**
33
- * CSS px of overlap added to each span's right edge by the **painter only**.
34
- *
35
- * The GPU pass needs none: abutting quads share an exact clip-space edge and
36
- * the rasterizer fills it once. Canvas2D antialiases each `fillRect`
37
- * independently, so two runs meeting on a fractional pixel each take partial
38
- * coverage of it and a hairline of background shows through. Which callers
39
- * want it splits on the same axis `minWidthPx` does — MAF's cells tile, so it
40
- * pads; the multi-row painter's features are sparse intervals with background
41
- * between them by right, so it writes 0.
42
- *
43
- * Padding the drawn width and not the anchor is what keeps it growing
44
- * rightward on both orientations, so a reversed block's spans stay exact
45
- * mirrors of a forward block's. Wiggle's `WIGGLE_FUDGE_FACTOR` is the same
46
- * rule spelled per display; the shader must not grow a matching pad.
25
+ * CSS px the **painter alone** adds to each span's right edge, closing the
26
+ * hairline two antialiased `fillRect`s leave where tiling runs meet on a
27
+ * fractional pixel. The GPU pass needs none, and the ink excludes it.
47
28
  */
48
29
  seamPx: number;
49
30
  /** Rows-area scroll offset in CSS px; 0 for a canvas sized to its content. */
50
31
  scrollTop: number;
51
32
  }
52
- export declare function blockPx(block: RenderBlock, bp: number): number;
53
33
  export declare const spanMark: MarkShape<SpanChannels, SpanParams>;
@@ -1,14 +1,34 @@
1
1
  import { bpRangeXTuple } from '../blockClipUtils.js';
2
- import { bpToScreenPx, makeBpMapper, spanLeft } from '../canvas2dUtils.js';
2
+ import { bpProjection, projectBp, spanLeft } from '../canvas2dUtils.js';
3
3
  import { drawnRowHeightPx, rowBandOffsetPx, } from '../shaders/rowRect.js.generated.js';
4
4
  import * as shader from '../shaders/spanMark.generated.js';
5
5
  import { slangPass } from '../slangPass.js';
6
6
  import { makeAbgrFill } from './colorFill.js';
7
- // Per instance rather than through `makeBpMapper`: `ink` is asked one
8
- // instance at a time, and a closure per ask is the allocation the mapper
9
- // exists to hoist out of a loop.
10
- export function blockPx(block, bp) {
11
- return bpToScreenPx(bp, block.start, block.end, block.screenStartPx, block.screenEndPx, block.reversed);
7
+ function spanFrame(block, params) {
8
+ const { rowHeight, rowProportion } = params;
9
+ const { originPx, startBp, spanBp, signedSpanPx } = bpProjection(block);
10
+ return {
11
+ originPx,
12
+ startBp,
13
+ spanBp,
14
+ signedSpanPx,
15
+ rowHeight,
16
+ scrollTop: params.scrollTop,
17
+ bandOffsetPx: rowBandOffsetPx(rowHeight, rowProportion),
18
+ minWidthPx: params.minWidthPx,
19
+ left: 0,
20
+ top: 0,
21
+ width: 0,
22
+ height: drawnRowHeightPx(rowHeight, rowProportion),
23
+ };
24
+ }
25
+ function placeSpan(c, g, i) {
26
+ const xa = projectBp(g, c.x[i]);
27
+ const xb = projectBp(g, c.x2[i]);
28
+ const width = Math.max(g.minWidthPx, Math.abs(xb - xa));
29
+ g.left = spanLeft(xa, xb, width);
30
+ g.top = g.bandOffsetPx + g.rowHeight * c.row[i] - g.scrollTop;
31
+ g.width = width;
12
32
  }
13
33
  export const spanMark = {
14
34
  id: 'span',
@@ -20,9 +40,6 @@ export const spanMark = {
20
40
  shader.writeUniforms(scratch, {
21
41
  bpRangeX: bpRangeXTuple(clip, block.reversed),
22
42
  canvasHeight: frame.canvasHeight,
23
- // CSS px, not physical: `extendToMinWidthX` divides `minCellPx` by this
24
- // to reach clip space, so a CSS width is what makes the floor a CSS-pixel
25
- // one and matches the painter's `Math.max` below.
26
43
  minCellDenomPx: clip.scissorW,
27
44
  minCellPx: params.minWidthPx,
28
45
  zero: 0,
@@ -32,33 +49,19 @@ export const spanMark = {
32
49
  });
33
50
  },
34
51
  paintBlock(ctx, channels, block, _frame, params) {
35
- const { x, x2, row, color, count } = channels;
36
- const { rowHeight, rowProportion, minWidthPx, seamPx, scrollTop } = params;
37
- const h = drawnRowHeightPx(rowHeight, rowProportion);
38
- const offset = rowBandOffsetPx(rowHeight, rowProportion);
39
- const bpToPx = makeBpMapper(block);
52
+ const { color, count } = channels;
53
+ const { seamPx } = params;
54
+ const g = spanFrame(block, params);
40
55
  const setFill = makeAbgrFill(ctx);
41
56
  for (let i = 0; i < count; i++) {
42
- const xa = bpToPx(x[i]);
43
- const xb = bpToPx(x2[i]);
44
- const width = Math.max(minWidthPx, Math.abs(xb - xa));
57
+ placeSpan(channels, g, i);
45
58
  setFill(color[i]);
46
- ctx.fillRect(spanLeft(xa, xb, width), offset + rowHeight * row[i] - scrollTop, width + seamPx, h);
59
+ ctx.fillRect(g.left, g.top, g.width + seamPx, g.height);
47
60
  }
48
61
  },
49
62
  ink(channels, block, _frame, params, i) {
50
- const { x, x2, row } = channels;
51
- const { rowHeight, rowProportion, minWidthPx, scrollTop } = params;
52
- const xa = blockPx(block, x[i]);
53
- const xb = blockPx(block, x2[i]);
54
- const width = Math.max(minWidthPx, Math.abs(xb - xa));
55
- return {
56
- left: spanLeft(xa, xb, width),
57
- top: rowBandOffsetPx(rowHeight, rowProportion) +
58
- rowHeight * row[i] -
59
- scrollTop,
60
- width,
61
- height: drawnRowHeightPx(rowHeight, rowProportion),
62
- };
63
+ const g = spanFrame(block, params);
64
+ placeSpan(channels, g, i);
65
+ return { left: g.left, top: g.top, width: g.width, height: g.height };
63
66
  },
64
67
  };