@jbrowse/render-core 5.0.0-beta.10 → 5.0.0-beta.13

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 (97) hide show
  1. package/esm/RenderCanvas.d.ts +1 -1
  2. package/esm/RenderCanvas.js +1 -1
  3. package/esm/canvas2dUtils.js +1 -1
  4. package/esm/createRenderingBackend.d.ts +2 -2
  5. package/esm/createRenderingBackend.js +2 -2
  6. package/esm/graphicsCapabilities.js +1 -1
  7. package/esm/hal/createHal.js +1 -1
  8. package/esm/hal/deviceGpuCache.js +8 -11
  9. package/esm/hal/types.d.ts +0 -2
  10. package/esm/hal/webgl2Hal.d.ts +1 -1
  11. package/esm/hal/webgl2Hal.js +4 -4
  12. package/esm/hal/webgpuHal.js +2 -3
  13. package/esm/marks/barMark.js +4 -2
  14. package/esm/marks/index.d.ts +4 -4
  15. package/esm/marks/index.js +3 -3
  16. package/esm/marks/lineMark.d.ts +6 -0
  17. package/esm/marks/lineMark.js +34 -21
  18. package/esm/marks/linkMark.d.ts +21 -1
  19. package/esm/marks/linkMark.js +25 -6
  20. package/esm/marks/markPlan.d.ts +4 -3
  21. package/esm/marks/markPlan.js +5 -4
  22. package/esm/marks/markRamp.d.ts +4 -2
  23. package/esm/marks/markRamp.js +14 -6
  24. package/esm/marks/rowLane.d.ts +2 -0
  25. package/esm/marks/rowLane.js +7 -3
  26. package/esm/marks/rowSpanIndex.d.ts +3 -2
  27. package/esm/marks/rowSpanIndex.js +18 -8
  28. package/esm/marks/spanMark.d.ts +4 -3
  29. package/esm/marks/spanMark.js +3 -3
  30. package/esm/marks/types.d.ts +4 -1
  31. package/esm/marks/types.js +3 -1
  32. package/esm/shaders/barMark.glsl.generated.d.ts +1 -1
  33. package/esm/shaders/barMark.glsl.generated.js +1 -1
  34. package/esm/shaders/barMark.wgsl.generated.d.ts +1 -1
  35. package/esm/shaders/barMark.wgsl.generated.js +1 -1
  36. package/esm/shaders/curveDistance.js.generated.d.ts +1 -0
  37. package/esm/shaders/curveDistance.js.generated.js +9 -0
  38. package/esm/shaders/insertionGlyph.generated.d.ts +6 -0
  39. package/esm/shaders/insertionGlyph.generated.js +8 -0
  40. package/esm/shaders/lineCenterMark.glsl.generated.d.ts +2 -2
  41. package/esm/shaders/lineCenterMark.glsl.generated.js +2 -2
  42. package/esm/shaders/lineCenterMark.iface.generated.d.ts +11 -9
  43. package/esm/shaders/lineCenterMark.iface.generated.js +20 -18
  44. package/esm/shaders/lineCenterMark.wgsl.generated.d.ts +1 -1
  45. package/esm/shaders/lineCenterMark.wgsl.generated.js +1 -1
  46. package/esm/shaders/lineCommon.js.generated.d.ts +1 -0
  47. package/esm/shaders/lineCommon.js.generated.js +20 -0
  48. package/esm/shaders/lineStepMark.glsl.generated.d.ts +2 -2
  49. package/esm/shaders/lineStepMark.glsl.generated.js +2 -2
  50. package/esm/shaders/lineStepMark.iface.generated.d.ts +11 -9
  51. package/esm/shaders/lineStepMark.iface.generated.js +20 -18
  52. package/esm/shaders/lineStepMark.wgsl.generated.d.ts +1 -1
  53. package/esm/shaders/lineStepMark.wgsl.generated.js +1 -1
  54. package/esm/shaders/linkMark.glsl.generated.d.ts +1 -1
  55. package/esm/shaders/linkMark.glsl.generated.js +1 -1
  56. package/esm/shaders/linkMark.js.generated.d.ts +2 -1
  57. package/esm/shaders/linkMark.js.generated.js +3 -3
  58. package/esm/shaders/linkMark.wgsl.generated.d.ts +1 -1
  59. package/esm/shaders/linkMark.wgsl.generated.js +1 -1
  60. package/esm/shaders/pointMark.glsl.generated.d.ts +1 -1
  61. package/esm/shaders/pointMark.glsl.generated.js +1 -1
  62. package/esm/shaders/pointMark.wgsl.generated.d.ts +1 -1
  63. package/esm/shaders/pointMark.wgsl.generated.js +1 -1
  64. package/esm/shaders/spanMark.glsl.generated.d.ts +1 -1
  65. package/esm/shaders/spanMark.glsl.generated.js +1 -1
  66. package/esm/shaders/spanMark.wgsl.generated.d.ts +1 -1
  67. package/esm/shaders/spanMark.wgsl.generated.js +1 -1
  68. package/esm/slangPass.d.ts +10 -9
  69. package/esm/slangPass.js +2 -2
  70. package/package.json +3 -3
  71. package/src/shaders/barMark.glsl.generated.ts +1 -1
  72. package/src/shaders/barMark.wgsl.generated.ts +1 -1
  73. package/src/shaders/capsule.slang +1 -1
  74. package/src/shaders/curveDistance.js.generated.ts +12 -0
  75. package/src/shaders/curveDistance.slang +1 -1
  76. package/src/shaders/insertionGlyph.generated.ts +14 -0
  77. package/src/shaders/insertionGlyph.slang +59 -0
  78. package/src/shaders/lineCenterMark.glsl.generated.ts +2 -2
  79. package/src/shaders/lineCenterMark.iface.generated.ts +21 -18
  80. package/src/shaders/lineCenterMark.slang +5 -1
  81. package/src/shaders/lineCenterMark.wgsl.generated.ts +1 -1
  82. package/src/shaders/lineCommon.js.generated.ts +20 -0
  83. package/src/shaders/lineCommon.slang +14 -4
  84. package/src/shaders/lineStepMark.glsl.generated.ts +2 -2
  85. package/src/shaders/lineStepMark.iface.generated.ts +21 -18
  86. package/src/shaders/lineStepMark.slang +5 -1
  87. package/src/shaders/lineStepMark.wgsl.generated.ts +1 -1
  88. package/src/shaders/linkMark.glsl.generated.ts +1 -1
  89. package/src/shaders/linkMark.js.generated.ts +3 -3
  90. package/src/shaders/linkMark.slang +10 -7
  91. package/src/shaders/linkMark.wgsl.generated.ts +1 -1
  92. package/src/shaders/pointMark.glsl.generated.ts +1 -1
  93. package/src/shaders/pointMark.wgsl.generated.ts +1 -1
  94. package/src/shaders/rowRect.slang +2 -2
  95. package/src/shaders/rowTable.slang +5 -1
  96. package/src/shaders/spanMark.glsl.generated.ts +1 -1
  97. package/src/shaders/spanMark.wgsl.generated.ts +1 -1
@@ -25,7 +25,7 @@ export interface RenderCanvasHandle {
25
25
  * render-prop body on the same value for the displays on the chrome, which it
26
26
  * does *not* get for free from the `renderError` unmount (that covers only a
27
27
  * reported loss — see the `canvasKey` note in useRenderingBackend.ts).
28
- * GPU_RENDERING.md said so and ended with "any new consumer rendering its own
28
+ * GPU_DISPLAY_LIFECYCLE.md said so and ended with "any new consumer rendering its own
29
29
  * banner must too" — a rule enforced by remembering to read the doc. Rendering
30
30
  * the element here makes it structural instead: there is no way to mount this
31
31
  * canvas without the key.
@@ -16,7 +16,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
16
16
  * render-prop body on the same value for the displays on the chrome, which it
17
17
  * does *not* get for free from the `renderError` unmount (that covers only a
18
18
  * reported loss — see the `canvasKey` note in useRenderingBackend.ts).
19
- * GPU_RENDERING.md said so and ended with "any new consumer rendering its own
19
+ * GPU_DISPLAY_LIFECYCLE.md said so and ended with "any new consumer rendering its own
20
20
  * banner must too" — a rule enforced by remembering to read the doc. Rendering
21
21
  * the element here makes it structural instead: there is no way to mount this
22
22
  * canvas without the key.
@@ -139,7 +139,7 @@ export function prepareCanvas(canvas, ctx, canvasWidth, canvasHeight) {
139
139
  // `getContext('2d')` + null-guard + `prepareCanvas` ritual that every
140
140
  // standalone 2D overlay otherwise hand-rolls in its draw effect — and the
141
141
  // `prepareCanvas` DPR step is easy to omit, which renders blurry on Retina
142
- // (see agent-docs/reference/GPU_RENDERING.md §"Canvas scaling & hi-DPI").
142
+ // (see agent-docs/reference/GPU_SHADERS.md §"Canvas scaling & hi-DPI").
143
143
  // Returning the
144
144
  // prepared ctx structurally prevents that omission.
145
145
  export function getPreparedCanvas2D(canvas, width, height) {
@@ -40,9 +40,9 @@ export declare function createRenderingBackend<TRenderingBackend>(canvas: HTMLCa
40
40
  * `DisplayChrome` machinery as a GPU display — the lifecycle is backend-
41
41
  * agnostic, so nothing downstream knows or cares there's no HAL.
42
42
  *
43
- * Reference: `plugins/sequence`'s `SequenceRenderer`. Promote to the dual-path
43
+ * No in-tree display uses it. Promote to the dual-path
44
44
  * `createRenderingBackend` only once a profile shows Canvas2D can't keep 60fps
45
45
  * at the display's real feature counts (≳100K features/frame — see
46
- * RFC-001 §3a).
46
+ * agent-docs/reference/PLUGIN_ABI_STABILITY.md §Canvas2D or GPU).
47
47
  */
48
48
  export declare function createCanvas2DBackend<TRenderingBackend>(canvas: HTMLCanvasElement, createCanvas2DRenderingBackend: (canvas: HTMLCanvasElement) => TRenderingBackend): Promise<TRenderingBackend>;
@@ -50,10 +50,10 @@ export async function createRenderingBackend(canvas, { passes, sampleCount = der
50
50
  * `DisplayChrome` machinery as a GPU display — the lifecycle is backend-
51
51
  * agnostic, so nothing downstream knows or cares there's no HAL.
52
52
  *
53
- * Reference: `plugins/sequence`'s `SequenceRenderer`. Promote to the dual-path
53
+ * No in-tree display uses it. Promote to the dual-path
54
54
  * `createRenderingBackend` only once a profile shows Canvas2D can't keep 60fps
55
55
  * at the display's real feature counts (≳100K features/frame — see
56
- * RFC-001 §3a).
56
+ * agent-docs/reference/PLUGIN_ABI_STABILITY.md §Canvas2D or GPU).
57
57
  */
58
58
  export function createCanvas2DBackend(canvas, createCanvas2DRenderingBackend) {
59
59
  return Promise.resolve(createCanvas2DRenderingBackend(canvas));
@@ -54,7 +54,7 @@ async function probeWebgpu() {
54
54
  * moment this returns and the browser reclaims the context on GC, the same
55
55
  * release the HAL relies on. A page that then reaches the 16-context ceiling
56
56
  * evicts this one first (it is the oldest, and nothing draws to it or
57
- * re-acquires it), so the eviction cascade in GPU_CONTEXT_BUDGET.md cannot
57
+ * re-acquires it), so the eviction cascade in GPU_PORTABILITY.md cannot
58
58
  * start here.
59
59
  *
60
60
  * The driver string comes off the same context, so it is free where it matters:
@@ -64,7 +64,7 @@ async function climbLadder(canvas, { passes, sampleCount, failures }) {
64
64
  // volvox tracks, and *no scroll churn at all* — headless Chrome (SwiftShader),
65
65
  // three runs each: WebGL2 blocks the main thread for 1.3-5.5 s in a single
66
66
  // task and 2.1-8.8 s in total, while Canvas2D never exceeds 339 ms and never
67
- // once produces a task over 500 ms. GPU_CONTEXT_BUDGET.md has the churn case
67
+ // once produces a task over 500 ms. GPU_PORTABILITY.md has the churn case
68
68
  // at ~25x; this is the floor, and it is the load-time pipeline build rather
69
69
  // than the per-pass rebuild the churn number measures.
70
70
  //
@@ -28,21 +28,18 @@ export function bindGroupLayoutEntries(bindings) {
28
28
  });
29
29
  }
30
30
  function gpuBlendState(bs) {
31
- // WebGPU rejects any factor but 'one' under max and min, which ignore them.
32
- // Alpha takes the same equation, as webgl2Hal's applyBlendState does.
31
+ // WebGPU rejects any factor but 'one' under max, which ignores them. Alpha
32
+ // accumulates as webgl2Hal's applyBlendState does.
33
33
  const max = { srcFactor: 'one', dstFactor: 'one', operation: 'max' };
34
- const min = { srcFactor: 'one', dstFactor: 'one', operation: 'min' };
35
34
  const behind = { srcFactor: 'one-minus-dst-alpha', dstFactor: 'one' };
36
35
  return bs.op === 'max'
37
36
  ? { color: max, alpha: max }
38
- : bs.op === 'min'
39
- ? { color: min, alpha: min }
40
- : bs.op === 'behind'
41
- ? { color: behind, alpha: behind }
42
- : {
43
- color: { srcFactor: bs.srcFactor, dstFactor: bs.dstFactor },
44
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' },
45
- };
37
+ : bs.op === 'behind'
38
+ ? { color: behind, alpha: behind }
39
+ : {
40
+ color: { srcFactor: bs.srcFactor, dstFactor: bs.dstFactor },
41
+ alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' },
42
+ };
46
43
  }
47
44
  export function pipelineRecipe(desc, wgslSource, sampleCount) {
48
45
  return {
@@ -38,8 +38,6 @@ export type BlendState = {
38
38
  dstFactor: BlendFactor;
39
39
  } | {
40
40
  op: 'max';
41
- } | {
42
- op: 'min';
43
41
  } | {
44
42
  op: 'behind';
45
43
  };
@@ -43,7 +43,7 @@ export declare class WebGL2Hal extends GpuHalBase<RegionPassBuffer> implements G
43
43
  * owns (`antialias` above one sample), so this HAL holds no separate target —
44
44
  * releasing its samples means dropping the context, which costs a re-acquire
45
45
  * against a hard 16-context ceiling and a full shader recompile per display.
46
- * `agent-docs/reference/GPU_CONTEXT_BUDGET.md` measured that trade and
46
+ * `agent-docs/reference/GPU_PORTABILITY.md` measured that trade and
47
47
  * declined it; what makes the WebGPU side worth doing is that it pays
48
48
  * neither.
49
49
  */
@@ -181,6 +181,9 @@ export class WebGL2Hal extends GpuHalBase {
181
181
  // Programs link on first draw (`getPass`). The first links here as a
182
182
  // canary, so a GL stack that cannot compile our shaders throws while
183
183
  // `createGpuHal` can still fall back to Canvas2D.
184
+ // Starting every pass's compile here was a loss (headed Chrome, Intel UHD
185
+ // 630): the GPU process ran all 17 programs before the 10 a first BAM draw
186
+ // needs, and a first visit drew at 1094 ms against 907.
184
187
  const canary = descriptors[0];
185
188
  if (canary) {
186
189
  this.passes.set(canary.id, {
@@ -308,7 +311,7 @@ export class WebGL2Hal extends GpuHalBase {
308
311
  * owns (`antialias` above one sample), so this HAL holds no separate target —
309
312
  * releasing its samples means dropping the context, which costs a re-acquire
310
313
  * against a hard 16-context ceiling and a full shader recompile per display.
311
- * `agent-docs/reference/GPU_CONTEXT_BUDGET.md` measured that trade and
314
+ * `agent-docs/reference/GPU_PORTABILITY.md` measured that trade and
312
315
  * declined it; what makes the WebGPU side worth doing is that it pays
313
316
  * neither.
314
317
  */
@@ -458,9 +461,6 @@ export class WebGL2Hal extends GpuHalBase {
458
461
  if (bs?.op === 'max') {
459
462
  gl.blendEquation(gl.MAX);
460
463
  }
461
- else if (bs?.op === 'min') {
462
- gl.blendEquation(gl.MIN);
463
- }
464
464
  else if (bs?.op === 'behind') {
465
465
  gl.blendEquation(gl.FUNC_ADD);
466
466
  gl.blendFuncSeparate(gl.ONE_MINUS_DST_ALPHA, gl.ONE, gl.ONE_MINUS_DST_ALPHA, gl.ONE);
@@ -60,9 +60,8 @@ async function buildPipeline(device, recipe, passId) {
60
60
  multisample: recipe.sampleCount === 1 ? undefined : { count: recipe.sampleCount },
61
61
  });
62
62
  }
63
- // Every declared pass up front, unlike WebGL2's first-draw link; see
64
- // ARCHITECTURAL_LIMITS.md §"Every WebGPU display resolves its whole pass list
65
- // before it can paint". Each pass compiles as soon as its own WGSL arrives.
63
+ // Every declared pass up front, unlike WebGL2's first-draw link. Each pass
64
+ // compiles as soon as its own WGSL arrives.
66
65
  async function resolvePipelines(device, descriptors, sampleCount) {
67
66
  const built = await Promise.all(descriptors.map(async (desc) => {
68
67
  const { WGSL_SOURCE } = await desc.source.wgsl();
@@ -94,6 +94,8 @@ export const barMark = {
94
94
  const yScale = valueScaleUniforms(params);
95
95
  const table = params.rowTable;
96
96
  const [domainMin, domainMax] = params.domain;
97
+ const { seamPx } = params;
98
+ const seamLeft = block.reversed ? seamPx : 0;
97
99
  let clipped = false;
98
100
  for (let i = 0; i < count; i++) {
99
101
  const slot = rowSlot(row, i, table);
@@ -104,7 +106,7 @@ export const barMark = {
104
106
  const r = barRect(bpToPx, x[i], x2[i], y[i], top, band, params, yScale);
105
107
  if (r) {
106
108
  setFill(rowColor(color[i], row, i, table));
107
- ctx.fillRect(r.left, r.top, r.width + params.seamPx, r.height);
109
+ ctx.fillRect(r.left - seamLeft, r.top, r.width + seamPx, r.height);
108
110
  }
109
111
  clipped ||=
110
112
  clipSide(y[i], domainMin, domainMax, yScale.valueScaleType) !== 0;
@@ -123,7 +125,7 @@ export const barMark = {
123
125
  const { top, band } = barBand(params, frame.canvasHeight, slot);
124
126
  const s = stripRect(bpToPx, x[i], x2[i], y[i], top, band, params, yScale);
125
127
  if (s) {
126
- ctx.fillRect(s.left, s.top, s.width + params.seamPx, s.height);
128
+ ctx.fillRect(s.left - seamLeft, s.top, s.width + seamPx, s.height);
127
129
  }
128
130
  }
129
131
  },
@@ -8,15 +8,15 @@ export { pointMark } from './pointMark.ts';
8
8
  export { ruleMark } from './ruleMark.ts';
9
9
  export { spanMark } from './spanMark.ts';
10
10
  export { lineCenterMark, lineMarkOf, lineStepMark } from './lineMark.ts';
11
- export { linkFeet, linkMark, LINK_ELSEWHERE, LINK_NO_REGION, } from './linkMark.ts';
11
+ export { linkApex, linkFeet, linkMark, LINK_ELSEWHERE, LINK_NO_REGION, } from './linkMark.ts';
12
12
  export { ellipseDistance, ellipseNearest } from './ellipseDistance.ts';
13
13
  export { appendGlyph, pointInsetPx } from './glyphPaint.ts';
14
14
  export { recordPath } from './pathRecorder.ts';
15
15
  export { abgrToCssRgba, makeAbgrFill } from './colorFill.ts';
16
16
  export { RAMP_NOT_A_NUMBER_COLOR, RAMP_NO_VALUE_BITS, RAMP_NO_VALUE_COLOR, } from '../shaders/markColor.generated.ts';
17
- export { instanceColor, isThreshold, keepRampValues, rampUniforms, rampValueBits, thresholdBandOf, } from './markRamp.ts';
17
+ export { instanceColor, isThreshold, keepRampValues, paintColors, rampUniforms, rampValueBits, thresholdBandOf, } from './markRamp.ts';
18
18
  export { HIDDEN_ROW, NO_ROW_COLOR, RowKeys, buildRowTable } from './rowTable.ts';
19
- export { keySlot, rowColor, rowSlot } from './rowLane.ts';
19
+ export { keySlot, rowColor, rowSlot, rowTableKeys, rowTableTextures, } from './rowLane.ts';
20
20
  export { rowSpanIndex, spansInRow } from './rowSpanIndex.ts';
21
21
  export type { MarkPlan } from './markPlan.ts';
22
22
  export type { MarkInstance } from './markInk.ts';
@@ -27,7 +27,7 @@ export type { PointChannels, PointParams } from './pointMark.ts';
27
27
  export type { RuleChannels, RuleParams } from './ruleMark.ts';
28
28
  export type { SpanChannels, SpanParams } from './spanMark.ts';
29
29
  export type { LineChannels, LineInterpolate, LineParams } from './lineMark.ts';
30
- export type { LinkChannels, LinkParams, LinkRegion, LinkSizeScale, } from './linkMark.ts';
30
+ export type { LinkApex, LinkChannels, LinkParams, LinkRegion, LinkSizeScale, } from './linkMark.ts';
31
31
  export type { RowTable } from './rowTable.ts';
32
32
  export type { RowSpanIndex } from './rowSpanIndex.ts';
33
33
  export type { ColorChannel } from './markRamp.ts';
@@ -8,13 +8,13 @@ export { pointMark } from './pointMark.js';
8
8
  export { ruleMark } from './ruleMark.js';
9
9
  export { spanMark } from './spanMark.js';
10
10
  export { lineCenterMark, lineMarkOf, lineStepMark } from './lineMark.js';
11
- export { linkFeet, linkMark, LINK_ELSEWHERE, LINK_NO_REGION, } from './linkMark.js';
11
+ export { linkApex, linkFeet, linkMark, LINK_ELSEWHERE, LINK_NO_REGION, } from './linkMark.js';
12
12
  export { ellipseDistance, ellipseNearest } from './ellipseDistance.js';
13
13
  export { appendGlyph, pointInsetPx } from './glyphPaint.js';
14
14
  export { recordPath } from './pathRecorder.js';
15
15
  export { abgrToCssRgba, makeAbgrFill } from './colorFill.js';
16
16
  export { RAMP_NOT_A_NUMBER_COLOR, RAMP_NO_VALUE_BITS, RAMP_NO_VALUE_COLOR, } from '../shaders/markColor.generated.js';
17
- export { instanceColor, isThreshold, keepRampValues, rampUniforms, rampValueBits, thresholdBandOf, } from './markRamp.js';
17
+ export { instanceColor, isThreshold, keepRampValues, paintColors, rampUniforms, rampValueBits, thresholdBandOf, } from './markRamp.js';
18
18
  export { HIDDEN_ROW, NO_ROW_COLOR, RowKeys, buildRowTable } from './rowTable.js';
19
- export { keySlot, rowColor, rowSlot } from './rowLane.js';
19
+ export { keySlot, rowColor, rowSlot, rowTableKeys, rowTableTextures, } from './rowLane.js';
20
20
  export { rowSpanIndex, spansInRow } from './rowSpanIndex.js';
@@ -20,6 +20,12 @@ export interface LineChannels extends ColorChannel, RowChannel {
20
20
  export interface LineParams extends RowParams, MarkValueScale {
21
21
  /** The quantitative colour scale, for a line whose colour is a ramp or a threshold. */
22
22
  colorScale?: MarkColorScale;
23
+ /**
24
+ * Whether the colour field is the plotted `y`. Only then does a threshold's
25
+ * cut sit on the y scale, so the line changes colour where it crosses one;
26
+ * otherwise each instance takes its own value's colour.
27
+ */
28
+ colorFromY?: boolean;
23
29
  /** The value a step drops to across a gap. */
24
30
  origin: number;
25
31
  /** Stroke width in CSS px. */
@@ -2,6 +2,7 @@ import { bpRangeXTuple } from '../blockClipUtils.js';
2
2
  import { CappedPath, getDpr, makeBpMapper } from '../canvas2dUtils.js';
3
3
  import * as centerShader from '../shaders/lineCenterMark.generated.js';
4
4
  import { GAP_Y, NO_PREV_X } from '../shaders/lineCommon.generated.js';
5
+ import { lineColorAlongY } from '../shaders/lineCommon.js.generated.js';
5
6
  import * as stepShader from '../shaders/lineStepMark.generated.js';
6
7
  import { valueToYPxScaled } from '../shaders/pointMark.js.generated.js';
7
8
  import { rowBandTopPx } from '../shaders/rowTable.js.generated.js';
@@ -9,7 +10,7 @@ import { normalizeScoreUnclamped } from '../shaders/scoreScale.js.generated.js';
9
10
  import { slangPass } from '../slangPass.js';
10
11
  import { abgrToCssRgba } from './colorFill.js';
11
12
  import { colorBits, isThreshold, paintColors, rampUniforms, } from './markRamp.js';
12
- import { bandHeightPx, rowColor, rowLane, rowSlot, rowTableKeys, rowTableTextures, } from './rowLane.js';
13
+ import { bandHeightPx, rowColor, rowColorOverride, rowLane, rowSlot, rowTableKeys, rowTableTextures, } from './rowLane.js';
13
14
  import { valueScaleUniforms } from './valueScale.js';
14
15
  const NO_GAP = Number.POSITIVE_INFINITY;
15
16
  function sameRow(row, a, b) {
@@ -66,6 +67,7 @@ function writeLineUniforms(scratch, clip, block, frame, params) {
66
67
  domainMax: params.domain[1],
67
68
  ...valueScaleUniforms(params),
68
69
  ...rampUniforms(params.colorScale),
70
+ colorFromY: params.colorFromY ? 1 : 0,
69
71
  rowHeight,
70
72
  rowBandPx: params.rowBandPx ?? rowHeight,
71
73
  rowOffsetPx: params.rowOffsetPx ?? 0,
@@ -281,26 +283,36 @@ function traceCenter(pen, c, g, rows, indices) {
281
283
  pen.lineTo(cx, cy, colorY);
282
284
  }
283
285
  }
284
- // Under a threshold the line is stroked once per band per row, each pass
285
- // keeping the parts of the polyline inside its band; otherwise once, in each
286
- // instance's own colour.
286
+ // Along y, the line is stroked once per band per row, each pass keeping the
287
+ // parts of the polyline inside its band; otherwise once, in each instance's
288
+ // own colour. A row the table recolours takes its override either way.
287
289
  function paintLine(ctx, c, g, params, trace) {
288
290
  const { row, count } = c;
289
291
  const scale = params.colorScale;
290
- if (scale && isThreshold(scale) && c.colorValue) {
291
- const bySlot = new Map();
292
- for (let i = 0; i < count; i++) {
293
- const slot = rowSlot(row, i, g.table);
294
- if (slot !== undefined) {
295
- const held = bySlot.get(slot);
296
- if (held) {
297
- held.push(i);
298
- }
299
- else {
300
- bySlot.set(slot, [i]);
301
- }
292
+ const bySlot = new Map();
293
+ const own = [];
294
+ const { rampMode } = rampUniforms(scale);
295
+ const colorFromY = params.colorFromY ? 1 : 0;
296
+ for (let i = 0; i < count; i++) {
297
+ const slot = rowSlot(row, i, g.table);
298
+ if (slot === undefined) {
299
+ continue;
300
+ }
301
+ const overridden = rowColorOverride(row, i, g.table) !== undefined;
302
+ if (lineColorAlongY(rampMode, colorFromY, overridden)) {
303
+ const held = bySlot.get(slot);
304
+ if (held) {
305
+ held.push(i);
306
+ }
307
+ else {
308
+ bySlot.set(slot, [i]);
302
309
  }
303
310
  }
311
+ else {
312
+ own.push(i);
313
+ }
314
+ }
315
+ if (scale && isThreshold(scale)) {
304
316
  const { cuts, colors } = scale;
305
317
  for (const [slot, indices] of bySlot) {
306
318
  const cutYs = cuts.map(cut => colorYPx(g, cut, slot));
@@ -311,12 +323,13 @@ function paintLine(ctx, c, g, params, trace) {
311
323
  pen.end();
312
324
  }
313
325
  }
314
- return;
315
326
  }
316
- const color = paintColors(c, count, scale);
317
- const pen = new InstancePen(ctx, i => rowColor(color[i], row, i, g.table));
318
- trace(pen, Array.from({ length: count }, (_, i) => i));
319
- pen.end();
327
+ if (own.length > 0) {
328
+ const color = paintColors(c, count, scale);
329
+ const pen = new InstancePen(ctx, i => rowColor(color[i], row, i, g.table));
330
+ trace(pen, own);
331
+ pen.end();
332
+ }
320
333
  }
321
334
  function paintStep(ctx, c, block, frame, params) {
322
335
  if (c.count === 0) {
@@ -1,7 +1,8 @@
1
1
  import { LINK_ELSEWHERE, LINK_NO_REGION } from '../shaders/linkMark.consts.generated.ts';
2
+ import type { RenderBlock } from '../renderBlock.ts';
2
3
  import type { ColorChannel } from './markRamp.ts';
3
4
  import type { RowChannel, RowParams } from './rowLane.ts';
4
- import type { MarkColorScale, MarkShape } from './types.ts';
5
+ import type { MarkFrame, MarkColorScale, MarkShape } from './types.ts';
5
6
  import type { MarkValueScale } from './valueScale.ts';
6
7
  /**
7
8
  * The `link` shape's channels: a stroked curve from `x`, on the displayed
@@ -72,5 +73,24 @@ export interface LinkParams extends RowParams, MarkValueScale {
72
73
  /** How far inside the band the value range ends, the axis's own. */
73
74
  insetPx?: number;
74
75
  }
76
+ /**
77
+ * Where an instance's curve peaks as the painter and the shader draw it: the
78
+ * centreline's apex in canvas px and how far it stands off its baseline.
79
+ */
80
+ export interface LinkApex {
81
+ x: number;
82
+ y: number;
83
+ rise: number;
84
+ /** The curve's half-span at its baseline; 0 for a line or a stem, which enclose nothing. */
85
+ halfWidth: number;
86
+ strokePx: number;
87
+ /** Canvas y per px toward the baseline: 1 standing on the band's foot, -1 hanging from its top. */
88
+ inward: 1 | -1;
89
+ }
90
+ /**
91
+ * Instance `i`'s apex, undefined where it draws none on the band: no curve
92
+ * at all, or a far pair's circle, which the band clips to its legs.
93
+ */
94
+ export declare function linkApex(c: LinkChannels, block: RenderBlock, frame: MarkFrame, params: LinkParams, i: number): LinkApex | undefined;
75
95
  export declare const linkMark: MarkShape<LinkChannels, LinkParams>;
76
96
  export { LINK_ELSEWHERE, LINK_NO_REGION };
@@ -1,9 +1,9 @@
1
1
  import { getDpr } from '../canvas2dUtils.js';
2
2
  import { SCALE_TYPE_LOG } from '../scoreScale.js';
3
- import { distToWideCirclePx } from '../shaders/curveDistance.js.generated.js';
4
- import { LINK_CURVE_SEGMENTS, LINK_ELSEWHERE, LINK_FAR_SCREEN_WIDTHS, LINK_FOOT_PX, LINK_MAX_REGIONS, LINK_NO_REGION, LINK_NO_SIZE, LINK_LINE_MIN_PX, LINK_SHAPE_ARC, LINK_SHAPE_DOME, LINK_SHAPE_LINE, LINK_SIZE_CONSTANT, LINK_SIZE_SCALED, LINK_STEM_PX, } from '../shaders/linkMark.consts.generated.js';
3
+ import { distToWideCirclePx, legSweepAngle, } from '../shaders/curveDistance.js.generated.js';
4
+ import { LINK_CURVE_SEGMENTS, LINK_ELSEWHERE, LINK_FOOT_PX, LINK_MAX_REGIONS, LINK_NO_REGION, LINK_NO_SIZE, LINK_LINE_MIN_PX, LINK_SHAPE_ARC, LINK_SHAPE_DOME, LINK_SHAPE_LINE, LINK_SIZE_CONSTANT, LINK_SIZE_SCALED, LINK_STEM_PX, } from '../shaders/linkMark.consts.generated.js';
5
5
  import * as shader from '../shaders/linkMark.generated.js';
6
- import { linkApexPx, linkBaseYPx, linkFootDir, linkFootLenPx, linkRadiiPx, linkStrokeWidthPx, linkValuePx, } from '../shaders/linkMark.js.generated.js';
6
+ import { linkApexPx, linkBaseYPx, linkFootDir, linkFootLenPx, linkIsFar, linkRadiiPx, linkStrokeWidthPx, linkValuePx, } from '../shaders/linkMark.js.generated.js';
7
7
  import { slangPass } from '../slangPass.js';
8
8
  import { abgrToCssRgba } from './colorFill.js';
9
9
  import { ellipseNearest } from './ellipseDistance.js';
@@ -164,7 +164,7 @@ function placeCurve(c, g, i) {
164
164
  g.x2Px = regionPx(regions, region, c.x2[i]);
165
165
  const pairHalf = Math.abs(g.x2Px - g.xPx) / 2;
166
166
  const valuePx = linkValuePx(c.y ? c.y[i] : 0, g.domainMin, g.domainMax, g.band, g.scaleType, g.insetPx, g.symlogConstant);
167
- const apex = linkApexPx(pairHalf, g.reach, g.shape, g.valued, valuePx);
167
+ const apex = linkApexPx(pairHalf, g.reach, g.strokePx / 2, g.shape, g.valued, valuePx);
168
168
  if (g.shape === LINK_SHAPE_LINE) {
169
169
  g.kind = KIND_LINE;
170
170
  g.rx = Math.max(2 * pairHalf, LINK_LINE_MIN_PX);
@@ -174,15 +174,34 @@ function placeCurve(c, g, i) {
174
174
  const [rx, ry] = linkRadiiPx(pairHalf, apex, g.screenW);
175
175
  g.rx = rx;
176
176
  g.ry = ry;
177
- if (2 * pairHalf > LINK_FAR_SCREEN_WIDTHS * g.screenW) {
177
+ if (linkIsFar(pairHalf, g.screenW)) {
178
178
  g.kind = KIND_CIRCLE;
179
- g.legSweep = Math.asin(Math.min(1, Math.max(0, (g.reach + g.strokePx / 2) / Math.max(rx, 1e-6))));
179
+ g.legSweep = legSweepAngle(rx, g.reach + g.strokePx / 2);
180
180
  g.legHeight = rx * Math.sin(g.legSweep);
181
181
  }
182
182
  else {
183
183
  g.kind = KIND_ELLIPSE;
184
184
  }
185
185
  }
186
+ /**
187
+ * Instance `i`'s apex, undefined where it draws none on the band: no curve
188
+ * at all, or a far pair's circle, which the band clips to its legs.
189
+ */
190
+ export function linkApex(c, block, frame, params, i) {
191
+ const g = linkFrame(block, frame, params);
192
+ placeCurve(c, g, i);
193
+ if (g.kind === KIND_NONE || g.kind === KIND_CIRCLE) {
194
+ return undefined;
195
+ }
196
+ return {
197
+ x: (g.xPx + g.x2Px) / 2,
198
+ y: yAt(g, g.ry),
199
+ rise: g.ry,
200
+ halfWidth: g.kind === KIND_ELLIPSE ? g.rx : 0,
201
+ strokePx: g.strokePx,
202
+ inward: g.reverse ? -1 : 1,
203
+ };
204
+ }
186
205
  function sizeLane(c) {
187
206
  const lane = new Float32Array(c.count);
188
207
  for (let i = 0; i < c.count; i++) {
@@ -18,8 +18,9 @@ export interface MarkPlan<TRegion, TState extends MarkFrame> {
18
18
  }
19
19
  /**
20
20
  * Resolve which marks draw this frame. Throws for an enabled mark the plan
21
- * form cannot carry — one with a `band` or a `paintsBlock` — since a plan
22
- * neither scissors nor asks the block anything, and without the throw the GPU
23
- * would draw what Canvas2D declines.
21
+ * form cannot carry — one with a `band`, a `paintsBlock` or a per-draw
22
+ * `verticesPerInstance` — since a plan neither scissors, asks the block
23
+ * anything nor sizes a draw, and without the throw the GPU would draw what
24
+ * Canvas2D declines, or a chevron a line.
24
25
  */
25
26
  export declare function planMarks<TRegion, TState extends MarkFrame>(marks: readonly Mark<TRegion, TState>[], state: TState): MarkPlan<TRegion, TState>;
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Resolve which marks draw this frame. Throws for an enabled mark the plan
3
- * form cannot carry — one with a `band` or a `paintsBlock` — since a plan
4
- * neither scissors nor asks the block anything, and without the throw the GPU
5
- * would draw what Canvas2D declines.
3
+ * form cannot carry — one with a `band`, a `paintsBlock` or a per-draw
4
+ * `verticesPerInstance` — since a plan neither scissors, asks the block
5
+ * anything nor sizes a draw, and without the throw the GPU would draw what
6
+ * Canvas2D declines, or a chevron a line.
6
7
  */
7
8
  export function planMarks(marks, state) {
8
9
  const enabled = [];
@@ -10,7 +11,7 @@ export function planMarks(marks, state) {
10
11
  for (const mark of marks) {
11
12
  if (!mark.enabled || mark.enabled(state)) {
12
13
  if (!mark.planned) {
13
- throw new Error(`mark ${mark.pass.id} declares a band or a block gate, which a frame plan does not carry; draw it through drawMarks`);
14
+ throw new Error(`mark ${mark.pass.id} declares a band, a block gate or a per-draw vertex count, which a frame plan does not carry; draw it through drawMarks`);
14
15
  }
15
16
  enabled.push(mark);
16
17
  passes.push(mark.planned);
@@ -56,8 +56,10 @@ export declare function rampValueBits(values: Float32Array): Uint32Array<ArrayBu
56
56
  */
57
57
  export declare function keepRampValues(values: Float32Array, keep: (value: number, index: number) => boolean): Float32Array<ArrayBuffer>;
58
58
  /**
59
- * The interval a value is in on a threshold: how many of the ascending cuts it
60
- * is at or past, `thresholdIndex`'s rule and the shader's `thresholdBand`.
59
+ * The interval a lane value is in on a threshold: how many of the ascending
60
+ * cuts it is at or past, `thresholdIndex`'s rule and the shader's
61
+ * `thresholdBand`. The value is a float32 and so is each cut in the shader's
62
+ * uniform, so a cut compares as one here: 0.7 off a lane is at a cut of 0.7.
61
63
  */
62
64
  export declare function thresholdBandOf(value: number, cuts: readonly number[]): number;
63
65
  /**
@@ -99,13 +99,15 @@ export function keepRampValues(values, keep) {
99
99
  return new Float32Array(kept.buffer, kept.byteOffset, kept.length);
100
100
  }
101
101
  /**
102
- * The interval a value is in on a threshold: how many of the ascending cuts it
103
- * is at or past, `thresholdIndex`'s rule and the shader's `thresholdBand`.
102
+ * The interval a lane value is in on a threshold: how many of the ascending
103
+ * cuts it is at or past, `thresholdIndex`'s rule and the shader's
104
+ * `thresholdBand`. The value is a float32 and so is each cut in the shader's
105
+ * uniform, so a cut compares as one here: 0.7 off a lane is at a cut of 0.7.
104
106
  */
105
107
  export function thresholdBandOf(value, cuts) {
106
108
  let band = 0;
107
109
  for (const cut of cuts) {
108
- if (value >= cut) {
110
+ if (value >= Math.fround(cut)) {
109
111
  band++;
110
112
  }
111
113
  }
@@ -150,9 +152,7 @@ export function paintColors(c, count, scale) {
150
152
  const colors = new Uint32Array(count);
151
153
  const bits = rampValueBits(colorValue);
152
154
  const colorOf = isThreshold(scale)
153
- ? (value) => Number.isFinite(value)
154
- ? scale.colors[thresholdBandOf(value, scale.cuts)]
155
- : scale.colors[value > 0 ? scale.cuts.length : 0]
155
+ ? thresholdColorOf(scale)
156
156
  : rampColorOf(scale);
157
157
  for (let i = 0; i < count; i++) {
158
158
  const value = colorValue[i];
@@ -175,6 +175,14 @@ function constantColors(c, color, count) {
175
175
  c.constantBake = colors;
176
176
  return colors;
177
177
  }
178
+ // The cuts the shader's uniform block holds, so Canvas2D paints a value past
179
+ // the last of them in the band the GPU does.
180
+ function thresholdColorOf(scale) {
181
+ const cuts = scale.cuts.slice(0, MAX_COLOR_CUTS);
182
+ return (value) => Number.isFinite(value)
183
+ ? scale.colors[thresholdBandOf(value, cuts)]
184
+ : scale.colors[value > 0 ? cuts.length : 0];
185
+ }
178
186
  function rampColorOf(ramp) {
179
187
  const [min, max] = ramp.domain;
180
188
  const log = ramp.scale === 'log';
@@ -35,5 +35,7 @@ export declare function bandHeightPx(params: RowParams, canvasHeight: number): n
35
35
  export declare function keySlot(key: number, table: RowTable | undefined): number | undefined;
36
36
  /** The slot instance `i` is drawn on, undefined where the table hides its key. */
37
37
  export declare function rowSlot(row: Uint32Array | undefined, i: number, table: RowTable | undefined): number | undefined;
38
+ /** Instance `i`'s key's colour override, undefined where it keeps its own. */
39
+ export declare function rowColorOverride(row: Uint32Array | undefined, i: number, table: RowTable | undefined): number | undefined;
38
40
  /** The colour instance `i` is drawn in: its key's override, else `color`. */
39
41
  export declare function rowColor(color: number, row: Uint32Array | undefined, i: number, table: RowTable | undefined): number;
@@ -33,9 +33,13 @@ export function keySlot(key, table) {
33
33
  export function rowSlot(row, i, table) {
34
34
  return keySlot(rowKey(row, i), table);
35
35
  }
36
- /** The colour instance `i` is drawn in: its key's override, else `color`. */
37
- export function rowColor(color, row, i, table) {
36
+ /** Instance `i`'s key's colour override, undefined where it keeps its own. */
37
+ export function rowColorOverride(row, i, table) {
38
38
  const key = rowKey(row, i);
39
39
  const override = table && key < table.keys ? table.color[key] : NO_ROW_COLOR;
40
- return override >>> 24 === 0 ? color : override;
40
+ return override >>> 24 === 0 ? undefined : override;
41
+ }
42
+ /** The colour instance `i` is drawn in: its key's override, else `color`. */
43
+ export function rowColor(color, row, i, table) {
44
+ return rowColorOverride(row, i, table) ?? color;
41
45
  }
@@ -16,8 +16,9 @@ export interface RowSpanIndex {
16
16
  }
17
17
  /**
18
18
  * The index over `count` spans from `x` to `x2`, each in the row `row` names
19
- * (row 0 where there is no lane). A counting sort by row, a sort of each row
20
- * the input left out of start order, and one pass for the reach.
19
+ * (row 0 where there is no lane), less those on `HIDDEN_ROW`, which no row
20
+ * holds. A counting sort by row, a sort of each row the input left out of
21
+ * start order, and one pass for the reach.
21
22
  */
22
23
  export declare function rowSpanIndex(x: Uint32Array, x2: Uint32Array, row: Uint32Array | undefined, count?: number): RowSpanIndex;
23
24
  /**