@jbrowse/render-core 5.0.0-beta.11 → 5.0.0-beta.14

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 (190) hide show
  1. package/esm/OverlayCanvas.d.ts +3 -2
  2. package/esm/OverlayCanvas.js +4 -2
  3. package/esm/RenderCanvas.d.ts +4 -3
  4. package/esm/RenderCanvas.js +4 -2
  5. package/esm/ScrollLockedOverlay.d.ts +2 -2
  6. package/esm/ScrollLockedOverlay.js +3 -2
  7. package/esm/blockClipUtils.d.ts +5 -5
  8. package/esm/blockClipUtils.js +4 -3
  9. package/esm/canvas2dUtils.d.ts +15 -21
  10. package/esm/canvas2dUtils.js +25 -55
  11. package/esm/colorRampLut.d.ts +2 -2
  12. package/esm/colorRampLut.js +2 -2
  13. package/esm/coverageBand.d.ts +0 -1
  14. package/esm/coverageBand.js +0 -5
  15. package/esm/coverageBandBuffers.d.ts +8 -5
  16. package/esm/createRenderingBackend.d.ts +1 -1
  17. package/esm/displayPhase.d.ts +5 -9
  18. package/esm/displayPhase.js +2 -2
  19. package/esm/hal/mockHal.d.ts +3 -4
  20. package/esm/hal/mockHal.js +5 -4
  21. package/esm/hal/types.d.ts +5 -5
  22. package/esm/hal/webgl2Hal.d.ts +4 -1
  23. package/esm/hal/webgl2Hal.js +18 -6
  24. package/esm/hal/webgpuHal.js +2 -2
  25. package/esm/linkFeet.d.ts +4 -0
  26. package/esm/linkFeet.js +7 -0
  27. package/esm/marks/barMark.d.ts +1 -1
  28. package/esm/marks/barMark.js +4 -2
  29. package/esm/marks/colorFill.d.ts +4 -4
  30. package/esm/marks/colorFill.js +4 -4
  31. package/esm/marks/connectorMark.d.ts +48 -0
  32. package/esm/marks/connectorMark.js +285 -0
  33. package/esm/marks/drawAgainstHit.d.ts +1 -1
  34. package/esm/marks/drawAgainstHit.js +4 -4
  35. package/esm/marks/glyphPaint.d.ts +5 -4
  36. package/esm/marks/glyphPaint.js +17 -7
  37. package/esm/marks/index.d.ts +8 -5
  38. package/esm/marks/index.js +6 -4
  39. package/esm/marks/lineMark.d.ts +47 -7
  40. package/esm/marks/lineMark.js +53 -39
  41. package/esm/marks/linkMark.d.ts +43 -6
  42. package/esm/marks/linkMark.js +118 -21
  43. package/esm/marks/markBackend.d.ts +1 -1
  44. package/esm/marks/markBackend.js +1 -1
  45. package/esm/marks/markPlan.d.ts +4 -3
  46. package/esm/marks/markPlan.js +5 -4
  47. package/esm/marks/markRamp.d.ts +11 -11
  48. package/esm/marks/markRamp.js +9 -9
  49. package/esm/marks/pointMark.d.ts +11 -5
  50. package/esm/marks/pointMark.js +34 -9
  51. package/esm/marks/rowLane.d.ts +4 -2
  52. package/esm/marks/rowLane.js +7 -3
  53. package/esm/marks/rowTable.d.ts +5 -5
  54. package/esm/marks/rowTable.js +4 -4
  55. package/esm/marks/ruleMark.js +1 -0
  56. package/esm/marks/spanMark.d.ts +8 -7
  57. package/esm/marks/spanMark.js +3 -3
  58. package/esm/marks/types.d.ts +8 -5
  59. package/esm/marks/types.js +8 -1
  60. package/esm/perRegionRenderingBackend.d.ts +1 -1
  61. package/esm/perRegionRenderingBackend.js +1 -1
  62. package/esm/regionDataMap.d.ts +2 -2
  63. package/esm/regionDataMap.js +2 -2
  64. package/esm/shaders/barMark.glsl.generated.d.ts +1 -1
  65. package/esm/shaders/barMark.glsl.generated.js +1 -1
  66. package/esm/shaders/barMark.wgsl.generated.d.ts +1 -1
  67. package/esm/shaders/barMark.wgsl.generated.js +1 -1
  68. package/esm/shaders/capsule.js.generated.d.ts +1 -1
  69. package/esm/shaders/capsule.js.generated.js +2 -2
  70. package/esm/shaders/connectorMark.consts.generated.d.ts +11 -0
  71. package/esm/shaders/connectorMark.consts.generated.js +13 -0
  72. package/esm/shaders/{coverageIndicator.generated.d.ts → connectorMark.generated.d.ts} +2 -1
  73. package/esm/shaders/connectorMark.generated.js +8 -0
  74. package/esm/shaders/connectorMark.glsl.generated.d.ts +2 -0
  75. package/esm/shaders/connectorMark.glsl.generated.js +4 -0
  76. package/esm/shaders/connectorMark.iface.generated.d.ts +88 -0
  77. package/esm/shaders/connectorMark.iface.generated.js +1194 -0
  78. package/esm/shaders/connectorMark.js.generated.d.ts +5 -0
  79. package/esm/shaders/connectorMark.js.generated.js +47 -0
  80. package/esm/shaders/connectorMark.wgsl.generated.d.ts +1 -0
  81. package/esm/shaders/connectorMark.wgsl.generated.js +3 -0
  82. package/esm/shaders/coverageInterbase.glsl.generated.d.ts +1 -1
  83. package/esm/shaders/coverageInterbase.glsl.generated.js +1 -1
  84. package/esm/shaders/coverageInterbase.wgsl.generated.d.ts +1 -1
  85. package/esm/shaders/coverageInterbase.wgsl.generated.js +1 -1
  86. package/esm/shaders/curveDistance.js.generated.d.ts +4 -0
  87. package/esm/shaders/curveDistance.js.generated.js +113 -0
  88. package/esm/shaders/insertionGlyph.generated.d.ts +6 -0
  89. package/esm/shaders/insertionGlyph.generated.js +8 -0
  90. package/esm/shaders/lineCenterMark.glsl.generated.d.ts +2 -2
  91. package/esm/shaders/lineCenterMark.glsl.generated.js +2 -2
  92. package/esm/shaders/lineCenterMark.iface.generated.d.ts +11 -9
  93. package/esm/shaders/lineCenterMark.iface.generated.js +20 -18
  94. package/esm/shaders/lineCenterMark.wgsl.generated.d.ts +1 -1
  95. package/esm/shaders/lineCenterMark.wgsl.generated.js +1 -1
  96. package/esm/shaders/lineCommon.js.generated.d.ts +1 -0
  97. package/esm/shaders/lineCommon.js.generated.js +20 -0
  98. package/esm/shaders/lineStepMark.glsl.generated.d.ts +2 -2
  99. package/esm/shaders/lineStepMark.glsl.generated.js +2 -2
  100. package/esm/shaders/lineStepMark.iface.generated.d.ts +11 -9
  101. package/esm/shaders/lineStepMark.iface.generated.js +20 -18
  102. package/esm/shaders/lineStepMark.wgsl.generated.d.ts +1 -1
  103. package/esm/shaders/lineStepMark.wgsl.generated.js +1 -1
  104. package/esm/shaders/linkMark.consts.generated.d.ts +1 -0
  105. package/esm/shaders/linkMark.consts.generated.js +1 -0
  106. package/esm/shaders/linkMark.glsl.generated.d.ts +2 -2
  107. package/esm/shaders/linkMark.glsl.generated.js +2 -2
  108. package/esm/shaders/linkMark.js.generated.d.ts +4 -2
  109. package/esm/shaders/linkMark.js.generated.js +33 -4
  110. package/esm/shaders/linkMark.wgsl.generated.d.ts +1 -1
  111. package/esm/shaders/linkMark.wgsl.generated.js +1 -1
  112. package/esm/shaders/pointMark.glsl.generated.d.ts +1 -1
  113. package/esm/shaders/pointMark.glsl.generated.js +1 -1
  114. package/esm/shaders/pointMark.iface.generated.d.ts +10 -8
  115. package/esm/shaders/pointMark.iface.generated.js +18 -16
  116. package/esm/shaders/pointMark.wgsl.generated.d.ts +1 -1
  117. package/esm/shaders/pointMark.wgsl.generated.js +1 -1
  118. package/esm/shaders/spanMark.glsl.generated.d.ts +1 -1
  119. package/esm/shaders/spanMark.glsl.generated.js +1 -1
  120. package/esm/shaders/spanMark.wgsl.generated.d.ts +1 -1
  121. package/esm/shaders/spanMark.wgsl.generated.js +1 -1
  122. package/esm/slangPass.d.ts +5 -3
  123. package/esm/slangPass.js +2 -2
  124. package/package.json +7 -4
  125. package/src/shaders/arcHull.test.ts +87 -0
  126. package/src/shaders/barMark.glsl.generated.ts +1 -1
  127. package/src/shaders/barMark.slang +2 -2
  128. package/src/shaders/barMark.wgsl.generated.ts +1 -1
  129. package/src/shaders/capsule.js.generated.ts +2 -2
  130. package/src/shaders/capsule.slang +69 -24
  131. package/src/shaders/clipStrip.slang +1 -1
  132. package/src/shaders/colorPack.slang +2 -2
  133. package/src/shaders/colorRampLut.slang +7 -7
  134. package/src/shaders/connectorMark.consts.generated.ts +24 -0
  135. package/src/shaders/connectorMark.generated.ts +12 -0
  136. package/src/shaders/connectorMark.glsl.generated.ts +6 -0
  137. package/src/shaders/connectorMark.iface.generated.ts +1270 -0
  138. package/src/shaders/connectorMark.js.generated.ts +52 -0
  139. package/src/shaders/connectorMark.slang +287 -0
  140. package/src/shaders/connectorMark.wgsl.generated.ts +4 -0
  141. package/src/shaders/coverageBand.slang +10 -12
  142. package/src/shaders/coverageInterbase.glsl.generated.ts +1 -1
  143. package/src/shaders/coverageInterbase.slang +24 -21
  144. package/src/shaders/coverageInterbase.wgsl.generated.ts +1 -1
  145. package/src/shaders/coverageSnp.slang +1 -1
  146. package/src/shaders/curveDistance.js.generated.ts +112 -0
  147. package/src/shaders/curveDistance.slang +104 -1
  148. package/src/shaders/farDome.test.ts +117 -0
  149. package/src/shaders/hpClampFloor.test.ts +0 -4
  150. package/src/shaders/insertionGlyph.generated.ts +14 -0
  151. package/src/shaders/insertionGlyph.slang +59 -0
  152. package/src/shaders/lineCenterMark.glsl.generated.ts +2 -2
  153. package/src/shaders/lineCenterMark.iface.generated.ts +21 -18
  154. package/src/shaders/lineCenterMark.slang +19 -23
  155. package/src/shaders/lineCenterMark.wgsl.generated.ts +1 -1
  156. package/src/shaders/lineCommon.js.generated.ts +20 -0
  157. package/src/shaders/lineCommon.slang +18 -8
  158. package/src/shaders/lineStepMark.glsl.generated.ts +2 -2
  159. package/src/shaders/lineStepMark.iface.generated.ts +21 -18
  160. package/src/shaders/lineStepMark.slang +10 -6
  161. package/src/shaders/lineStepMark.wgsl.generated.ts +1 -1
  162. package/src/shaders/linkMark.consts.generated.ts +2 -0
  163. package/src/shaders/linkMark.glsl.generated.ts +2 -2
  164. package/src/shaders/linkMark.js.generated.ts +30 -4
  165. package/src/shaders/linkMark.slang +67 -24
  166. package/src/shaders/linkMark.wgsl.generated.ts +1 -1
  167. package/src/shaders/markColor.slang +10 -10
  168. package/src/shaders/pointMark.glsl.generated.ts +1 -1
  169. package/src/shaders/pointMark.iface.generated.ts +19 -16
  170. package/src/shaders/pointMark.slang +12 -8
  171. package/src/shaders/pointMark.wgsl.generated.ts +1 -1
  172. package/src/shaders/rowRect.slang +3 -3
  173. package/src/shaders/rowTable.slang +12 -8
  174. package/src/shaders/scoreScale.slang +1 -1
  175. package/src/shaders/spanMark.glsl.generated.ts +1 -1
  176. package/src/shaders/spanMark.slang +5 -5
  177. package/src/shaders/spanMark.wgsl.generated.ts +1 -1
  178. package/src/shaders/viewRegion.slang +13 -0
  179. package/esm/shaders/coverageIndicator.generated.js +0 -7
  180. package/esm/shaders/coverageIndicator.glsl.generated.d.ts +0 -2
  181. package/esm/shaders/coverageIndicator.glsl.generated.js +0 -4
  182. package/esm/shaders/coverageIndicator.iface.generated.d.ts +0 -85
  183. package/esm/shaders/coverageIndicator.iface.generated.js +0 -114
  184. package/esm/shaders/coverageIndicator.wgsl.generated.d.ts +0 -1
  185. package/esm/shaders/coverageIndicator.wgsl.generated.js +0 -3
  186. package/src/shaders/coverageIndicator.generated.ts +0 -11
  187. package/src/shaders/coverageIndicator.glsl.generated.ts +0 -6
  188. package/src/shaders/coverageIndicator.iface.generated.ts +0 -172
  189. package/src/shaders/coverageIndicator.slang +0 -61
  190. package/src/shaders/coverageIndicator.wgsl.generated.ts +0 -4
@@ -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, {
@@ -301,7 +304,15 @@ export class WebGL2Hal extends GpuHalBase {
301
304
  if (this.disposed) {
302
305
  return { x: getDpr(), y: getDpr() };
303
306
  }
304
- return syncCanvasSize(this.canvas, width, height).scale;
307
+ // Resized past 7680x4320 px of area, Chrome shrinks the drawing buffer and
308
+ // leaves canvas.width/height at the requested size, so the scale comes from
309
+ // the buffer. A lost context reads 0x0 and falls back to syncCanvasSize's.
310
+ const { scale } = syncCanvasSize(this.canvas, width, height);
311
+ const { drawingBufferWidth: bufferW, drawingBufferHeight: bufferH } = this.gl;
312
+ return {
313
+ x: width > 0 && bufferW > 0 ? bufferW / width : scale.x,
314
+ y: height > 0 && bufferH > 0 ? bufferH / height : scale.y,
315
+ };
305
316
  }
306
317
  /**
307
318
  * Nothing to give back. WebGL2 multisamples in the drawing buffer the context
@@ -351,23 +362,24 @@ export class WebGL2Hal extends GpuHalBase {
351
362
  const gl = this.gl;
352
363
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
353
364
  gl.disable(gl.SCISSOR_TEST);
354
- gl.viewport(0, 0, this.canvas.width, this.canvas.height);
365
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
355
366
  gl.clearColor(clearR, clearG, clearB, clearA);
356
367
  gl.clear(gl.COLOR_BUFFER_BIT);
357
368
  }
358
369
  setScissor(x, y, w, h) {
359
370
  const gl = this.gl;
360
371
  gl.enable(gl.SCISSOR_TEST);
361
- gl.scissor(x, this.canvas.height - y - h, w, h);
372
+ gl.scissor(x, gl.drawingBufferHeight - y - h, w, h);
362
373
  }
363
374
  clearScissor() {
364
375
  this.gl.disable(this.gl.SCISSOR_TEST);
365
376
  }
366
377
  setViewport(x, y, w, h) {
367
- this.gl.viewport(x, this.canvas.height - y - h, w, h);
378
+ this.gl.viewport(x, this.gl.drawingBufferHeight - y - h, w, h);
368
379
  }
369
380
  clearViewport() {
370
- this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
381
+ const gl = this.gl;
382
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
371
383
  }
372
384
  drawPass(passId, regionKey, bufferPassId, verticesPerInstance) {
373
385
  const gl = this.gl;
@@ -410,7 +422,7 @@ export class WebGL2Hal extends GpuHalBase {
410
422
  endFrame() {
411
423
  const gl = this.gl;
412
424
  gl.disable(gl.SCISSOR_TEST);
413
- gl.viewport(0, 0, this.canvas.width, this.canvas.height);
425
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
414
426
  }
415
427
  releaseResources() {
416
428
  const gl = this.gl;
@@ -113,7 +113,7 @@ export class WebGPUHal extends GpuHalBase {
113
113
  // at 1 there is no MSAA texture at all rather than a smaller one, which is
114
114
  // the whole point of the knob.
115
115
  sampleCount;
116
- // The multisampled colour attachment, resolved into the canvas texture at the
116
+ // The multisampled color attachment, resolved into the canvas texture at the
117
117
  // end of the frame. Null for the life of the HAL when `sampleCount` is 1.
118
118
  msaaTexture = null;
119
119
  msaaView = null;
@@ -395,7 +395,7 @@ export class WebGPUHal extends GpuHalBase {
395
395
  const existing = byName.get(binding.name);
396
396
  if (existing) {
397
397
  // Same hazard as a buffer, and no longer a hypothetical one: every
398
- // `uploadTexture` in tree is a colour ramp, and since the ramp became a
398
+ // `uploadTexture` in tree is a color ramp, and since the ramp became a
399
399
  // `defineMark` `texture` they are all written by `GpuMarkBackend`'s
400
400
  // `bindRamp` from inside `drawRegion` — mid-frame, between an encoded
401
401
  // draw and its submit. The deferral is what makes replacing one there
@@ -0,0 +1,4 @@
1
+ export declare const LINK_FOOT_FORWARD = 1;
2
+ export declare const LINK_FOOT_REVERSE = 2;
3
+ /** The `feet` lane's value for a tick at each foot, by genomic direction. */
4
+ export declare function linkFeet(xDir: number, x2Dir: number): number;
@@ -0,0 +1,7 @@
1
+ export const LINK_FOOT_FORWARD = 1;
2
+ export const LINK_FOOT_REVERSE = 2;
3
+ /** The `feet` lane's value for a tick at each foot, by genomic direction. */
4
+ export function linkFeet(xDir, x2Dir) {
5
+ const bits = (d) => d > 0 ? LINK_FOOT_FORWARD : d < 0 ? LINK_FOOT_REVERSE : 0;
6
+ return bits(xDir) | (bits(x2Dir) << 2);
7
+ }
@@ -13,7 +13,7 @@ export interface BarChannels extends ColorChannel, RowChannel {
13
13
  count: number;
14
14
  }
15
15
  export interface BarParams extends RowParams, MarkValueScale {
16
- /** The quantitative colour scale, for a bar whose colour is a ramp or a threshold. */
16
+ /** The quantitative color scale, for a bar whose color is a ramp or a threshold. */
17
17
  colorScale?: MarkColorScale;
18
18
  /** The value bars grow from; a bar below it hangs down. */
19
19
  origin: number;
@@ -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
  },
@@ -3,13 +3,13 @@
3
3
  *
4
4
  * `@jbrowse/core/util/colorBits` re-exports `abgrToCssRgba` from here: core
5
5
  * depends on render-core, never the reverse. The direction that genuinely
6
- * needs core is resolving a CSS colour to a packed number, which is why a
7
- * shape takes packed colours and the display resolves them.
6
+ * needs core is resolving a CSS color to a packed number, which is why a
7
+ * shape takes packed colors and the display resolves them.
8
8
  */
9
9
  export declare function abgrToCssRgba(c: number): string;
10
10
  /**
11
- * A fill-style setter that only touches `ctx.fillStyle` when the colour
12
- * changes. Most of a painting is runs of one colour, and both the string
11
+ * A fill-style setter that only touches `ctx.fillStyle` when the color
12
+ * changes. Most of a painting is runs of one color, and both the string
13
13
  * allocation and the context write cost more than the comparison.
14
14
  */
15
15
  export declare function makeAbgrFill(ctx: {
@@ -3,16 +3,16 @@
3
3
  *
4
4
  * `@jbrowse/core/util/colorBits` re-exports `abgrToCssRgba` from here: core
5
5
  * depends on render-core, never the reverse. The direction that genuinely
6
- * needs core is resolving a CSS colour to a packed number, which is why a
7
- * shape takes packed colours and the display resolves them.
6
+ * needs core is resolving a CSS color to a packed number, which is why a
7
+ * shape takes packed colors and the display resolves them.
8
8
  */
9
9
  export function abgrToCssRgba(c) {
10
10
  const a = ((c >>> 24) & 255) / 255;
11
11
  return `rgba(${c & 255},${(c >>> 8) & 255},${(c >>> 16) & 255},${a})`;
12
12
  }
13
13
  /**
14
- * A fill-style setter that only touches `ctx.fillStyle` when the colour
15
- * changes. Most of a painting is runs of one colour, and both the string
14
+ * A fill-style setter that only touches `ctx.fillStyle` when the color
15
+ * changes. Most of a painting is runs of one color, and both the string
16
16
  * allocation and the context write cost more than the comparison.
17
17
  */
18
18
  export function makeAbgrFill(ctx) {
@@ -0,0 +1,48 @@
1
+ import type { RenderBlock } from '../renderBlock.ts';
2
+ import type { LinkRegion } from './linkMark.ts';
3
+ import type { MarkShape } from './types.ts';
4
+ /**
5
+ * The `connector` shape's channels: a stroked cubic from `x` on the displayed
6
+ * region whose payload holds it, at row `row`'s centre, to `x2` on the
7
+ * displayed region `x2Region` names, at row `row2`'s. `bend` is the depth in
8
+ * px a dipping connector reaches below its ends, or `CONNECTOR_BOW` for one
9
+ * that bows up over its row; `width` the stroke in CSS px; `color` ABGR with
10
+ * its alpha; `bits` the `CONNECTOR_*` flags: each end's strand, a split
11
+ * junction's leading second end, a straight segment, a dash, an arrowhead.
12
+ */
13
+ export interface ConnectorChannels {
14
+ x: Uint32Array;
15
+ x2: Uint32Array;
16
+ x2Region: Uint32Array;
17
+ row: Uint32Array;
18
+ row2: Uint32Array;
19
+ bend: Float32Array;
20
+ width: Float32Array;
21
+ color: Uint32Array;
22
+ bits: Uint8Array;
23
+ count: number;
24
+ }
25
+ export interface ConnectorParams {
26
+ /** The view's displayed regions, indexed as `x2Region` and the block's own index are. */
27
+ regions: readonly LinkRegion[];
28
+ /** Canvas y of row 0's centre. */
29
+ rowOffsetPx: number;
30
+ /** Canvas px between two rows' centres. */
31
+ rowPitchPx: number;
32
+ /** How far an arrowhead's arms reach back and off the row. */
33
+ arrowPx: number;
34
+ /** A dashed connector's `[dash, gap]` in CSS px, starting on a dash. */
35
+ strokeDash?: readonly [number, number];
36
+ maxHandlePx?: number;
37
+ }
38
+ /**
39
+ * A connector's two ends in canvas px, for a gesture that acts on the nearer
40
+ * one; undefined where the instance places nowhere.
41
+ */
42
+ export declare function connectorEnds(c: ConnectorChannels, block: RenderBlock, params: ConnectorParams, i: number): {
43
+ x: number;
44
+ y: number;
45
+ x2: number;
46
+ y2: number;
47
+ } | undefined;
48
+ export declare const connectorMark: MarkShape<ConnectorChannels, ConnectorParams>;
@@ -0,0 +1,285 @@
1
+ import { getDpr } from '../canvas2dUtils.js';
2
+ import { CONNECTOR_ARROW, CONNECTOR_CURVE_SEGMENTS, CONNECTOR_DASHED, CONNECTOR_LEADING_2, CONNECTOR_MAX_HANDLE_PX, CONNECTOR_MAX_REACH_PX, CONNECTOR_MINUS_1, CONNECTOR_MINUS_2, CONNECTOR_STRAIGHT, } from '../shaders/connectorMark.consts.generated.js';
3
+ import * as shader from '../shaders/connectorMark.generated.js';
4
+ import { connectorBudgetPx, connectorHandlePx, connectorLiftPx, connectorStrokePx, connectorTangent, } from '../shaders/connectorMark.js.generated.js';
5
+ import { slangPass } from '../slangPass.js';
6
+ import { abgrToCssRgba } from './colorFill.js';
7
+ import { regionPx, viewRegionUniforms } from './linkMark.js';
8
+ import { nearestInk } from './markHit.js';
9
+ function newPlaced() {
10
+ return {
11
+ x0: 0,
12
+ y0: 0,
13
+ x1: 0,
14
+ y1: 0,
15
+ x2: 0,
16
+ y2: 0,
17
+ x3: 0,
18
+ y3: 0,
19
+ strokePx: 0,
20
+ arrow: false,
21
+ back: 0,
22
+ };
23
+ }
24
+ const strandOf = (bits, bit) => ((bits & bit) !== 0 ? -1 : 1);
25
+ // Places instance `i` into `p` as the shader does, or answers false where its
26
+ // far end lies on no displayed region the table holds.
27
+ function place(c, params, own, i, p, dpr) {
28
+ const { regions } = params;
29
+ const far = c.x2Region[i];
30
+ const ownRegion = regions[own];
31
+ const farRegion = regions[far];
32
+ if (!ownRegion || !farRegion) {
33
+ return false;
34
+ }
35
+ const bits = c.bits[i];
36
+ p.x0 = regionPx(regions, own, c.x[i]);
37
+ p.y0 = params.rowOffsetPx + c.row[i] * params.rowPitchPx;
38
+ p.x3 = regionPx(regions, far, c.x2[i]);
39
+ p.y3 = params.rowOffsetPx + c.row2[i] * params.rowPitchPx;
40
+ p.strokePx = connectorStrokePx(c.width[i], dpr);
41
+ const reversed2 = farRegion.signedPxPerBp < 0;
42
+ const strand2 = strandOf(bits, CONNECTOR_MINUS_2);
43
+ p.arrow = (bits & CONNECTOR_ARROW) !== 0;
44
+ p.back = (reversed2 ? -strand2 : strand2) * -params.arrowPx;
45
+ if ((bits & CONNECTOR_STRAIGHT) !== 0) {
46
+ p.x1 = p.x0;
47
+ p.y1 = p.y0;
48
+ p.x2 = p.x3;
49
+ p.y2 = p.y3;
50
+ return true;
51
+ }
52
+ const dx = p.x3 - p.x0;
53
+ const dy = p.y3 - p.y0;
54
+ const budget = connectorBudgetPx(dx, dy);
55
+ const handle = connectorHandlePx(budget, params.maxHandlePx ?? CONNECTOR_MAX_HANDLE_PX);
56
+ const lift = connectorLiftPx(budget, dy, c.bend[i]);
57
+ const t1 = connectorTangent(strandOf(bits, CONNECTOR_MINUS_1), 0, ownRegion.signedPxPerBp < 0 ? 1 : 0);
58
+ const t2 = connectorTangent(strand2, (bits & CONNECTOR_LEADING_2) !== 0 ? 1 : 0, reversed2 ? 1 : 0);
59
+ p.x1 = p.x0 + handle * t1;
60
+ p.y1 = p.y0 - lift;
61
+ p.x2 = p.x3 + handle * t2;
62
+ p.y2 = p.y3 - lift;
63
+ return true;
64
+ }
65
+ // The curve's point at `t`, written into `out`.
66
+ function cubicAt(p, t, out) {
67
+ const s = 1 - t;
68
+ const a = s * s * s;
69
+ const b = 3 * s * s * t;
70
+ const d = 3 * s * t * t;
71
+ const e = t * t * t;
72
+ out.x = a * p.x0 + b * p.x1 + d * p.x2 + e * p.x3;
73
+ out.y = a * p.y0 + b * p.y1 + d * p.y2 + e * p.y3;
74
+ }
75
+ // A connector whose ends both lie further off the canvas than any curve can
76
+ // reach back onto it draws nothing there.
77
+ function offCanvas(p, frame) {
78
+ const reach = CONNECTOR_MAX_REACH_PX + p.strokePx;
79
+ return (Math.max(p.y0, p.y3) + reach < 0 ||
80
+ Math.min(p.y0, p.y3) - reach > frame.canvasHeight);
81
+ }
82
+ function traceCurve(ctx, p) {
83
+ ctx.moveTo(p.x0, p.y0);
84
+ ctx.bezierCurveTo(p.x1, p.y1, p.x2, p.y2, p.x3, p.y3);
85
+ }
86
+ function traceArrow(ctx, p, arrowPx) {
87
+ ctx.moveTo(p.x3 + p.back, p.y3 - arrowPx);
88
+ ctx.lineTo(p.x3, p.y3);
89
+ ctx.moveTo(p.x3 + p.back, p.y3 + arrowPx);
90
+ ctx.lineTo(p.x3, p.y3);
91
+ }
92
+ const svgNumber = (n) => Math.round(n * 100) / 100;
93
+ function svgPathOf(p, arrowPx) {
94
+ const parts = [];
95
+ const pt = (x, y) => `${svgNumber(x)} ${svgNumber(y)}`;
96
+ const sink = {
97
+ moveTo: (x, y) => parts.push(`M${pt(x, y)}`),
98
+ lineTo: (x, y) => parts.push(`L${pt(x, y)}`),
99
+ bezierCurveTo: (x1, y1, x2, y2, x, y) => parts.push(`C${pt(x1, y1)} ${pt(x2, y2)} ${pt(x, y)}`),
100
+ };
101
+ traceCurve(sink, p);
102
+ if (p.arrow) {
103
+ traceArrow(sink, p, arrowPx);
104
+ }
105
+ return parts.join('');
106
+ }
107
+ const POINT = { x: 0, y: 0 };
108
+ // The box the stroke lies in: the hull of the curve's chords, its arrow, and
109
+ // half the stroke.
110
+ function inkBox(p, arrowPx) {
111
+ let left = Math.min(p.x0, p.x3);
112
+ let right = Math.max(p.x0, p.x3);
113
+ let top = Math.min(p.y0, p.y3);
114
+ let bottom = Math.max(p.y0, p.y3);
115
+ for (let k = 1; k < CONNECTOR_CURVE_SEGMENTS; k++) {
116
+ cubicAt(p, k / CONNECTOR_CURVE_SEGMENTS, POINT);
117
+ left = Math.min(left, POINT.x);
118
+ right = Math.max(right, POINT.x);
119
+ top = Math.min(top, POINT.y);
120
+ bottom = Math.max(bottom, POINT.y);
121
+ }
122
+ if (p.arrow) {
123
+ left = Math.min(left, p.x3 + p.back);
124
+ right = Math.max(right, p.x3 + p.back);
125
+ top = Math.min(top, p.y3 - arrowPx);
126
+ bottom = Math.max(bottom, p.y3 + arrowPx);
127
+ }
128
+ const half = p.strokePx / 2;
129
+ return {
130
+ left: left - half,
131
+ top: top - half,
132
+ width: right - left + p.strokePx,
133
+ height: bottom - top + p.strokePx,
134
+ };
135
+ }
136
+ // The nearest point to (px, py) on segment (ax, ay)-(bx, by), into `best`
137
+ // where it is nearer than what `best` holds.
138
+ function nearSegment(ax, ay, bx, by, px, py, best) {
139
+ const dx = bx - ax;
140
+ const dy = by - ay;
141
+ const lenSq = dx * dx + dy * dy;
142
+ const t = lenSq > 0
143
+ ? Math.min(1, Math.max(0, ((px - ax) * dx + (py - ay) * dy) / lenSq))
144
+ : 0;
145
+ const x = ax + t * dx;
146
+ const y = ay + t * dy;
147
+ const distSq = (px - x) ** 2 + (py - y) ** 2;
148
+ if (distSq < best.distSq) {
149
+ best.x = x;
150
+ best.y = y;
151
+ best.distSq = distSq;
152
+ }
153
+ }
154
+ // The nearest point of the drawn ink's centreline, over the chords the GPU
155
+ // strip is built from.
156
+ function nearestOnConnector(p, arrowPx, px, py) {
157
+ const best = { x: p.x0, y: p.y0, distSq: Infinity };
158
+ let ax = p.x0;
159
+ let ay = p.y0;
160
+ for (let k = 1; k <= CONNECTOR_CURVE_SEGMENTS; k++) {
161
+ cubicAt(p, k / CONNECTOR_CURVE_SEGMENTS, POINT);
162
+ nearSegment(ax, ay, POINT.x, POINT.y, px, py, best);
163
+ ax = POINT.x;
164
+ ay = POINT.y;
165
+ }
166
+ if (p.arrow) {
167
+ for (const sign of [-1, 1]) {
168
+ nearSegment(p.x3 + p.back, p.y3 + sign * arrowPx, p.x3, p.y3, px, py, best);
169
+ }
170
+ }
171
+ return best;
172
+ }
173
+ /**
174
+ * A connector's two ends in canvas px, for a gesture that acts on the nearer
175
+ * one; undefined where the instance places nowhere.
176
+ */
177
+ export function connectorEnds(c, block, params, i) {
178
+ const p = newPlaced();
179
+ return place(c, params, block.displayedRegionIndex, i, p, getDpr())
180
+ ? { x: p.x0, y: p.y0, x2: p.x3, y2: p.y3 }
181
+ : undefined;
182
+ }
183
+ const PLACED = newPlaced();
184
+ export const connectorMark = {
185
+ id: 'connector',
186
+ spansView: true,
187
+ pass: {
188
+ ...slangPass({ id: 'connector', mod: shader }),
189
+ pack: c => shader.packInstances({
190
+ x: c.x,
191
+ x2: c.x2,
192
+ x2Region: c.x2Region,
193
+ row: c.row,
194
+ row2: c.row2,
195
+ bend: c.bend,
196
+ width: c.width,
197
+ color: c.color,
198
+ bits: c.bits,
199
+ }, c.count),
200
+ },
201
+ writeUniforms(scratch, _clip, block, frame, params) {
202
+ shader.writeUniforms(scratch, {
203
+ canvasHeight: frame.canvasHeight,
204
+ canvasWidth: frame.canvasWidth,
205
+ devicePixelRatio: getDpr(),
206
+ rowOffsetPx: params.rowOffsetPx,
207
+ rowPitchPx: params.rowPitchPx,
208
+ maxHandlePx: params.maxHandlePx ?? CONNECTOR_MAX_HANDLE_PX,
209
+ dashPx: params.strokeDash?.[0] ?? 0,
210
+ gapPx: params.strokeDash?.[1] ?? 0,
211
+ arrowPx: params.arrowPx,
212
+ zero: 0,
213
+ ...viewRegionUniforms(params.regions, block.displayedRegionIndex),
214
+ });
215
+ },
216
+ paintsBlock(block, _frame, params) {
217
+ return params.regions[block.displayedRegionIndex] !== undefined;
218
+ },
219
+ paintBlock(ctx, c, block, frame, params) {
220
+ const own = block.displayedRegionIndex;
221
+ const dpr = getDpr();
222
+ const dash = params.strokeDash ? [...params.strokeDash] : [];
223
+ ctx.lineCap = 'butt';
224
+ for (let i = 0; i < c.count; i++) {
225
+ if (!place(c, params, own, i, PLACED, dpr) || offCanvas(PLACED, frame)) {
226
+ continue;
227
+ }
228
+ ctx.lineWidth = PLACED.strokePx;
229
+ ctx.strokeStyle = abgrToCssRgba(c.color[i]);
230
+ ctx.setLineDash((c.bits[i] & CONNECTOR_DASHED) !== 0 ? dash : []);
231
+ ctx.beginPath();
232
+ traceCurve(ctx, PLACED);
233
+ ctx.stroke();
234
+ if (PLACED.arrow) {
235
+ ctx.setLineDash([]);
236
+ ctx.beginPath();
237
+ traceArrow(ctx, PLACED, params.arrowPx);
238
+ ctx.stroke();
239
+ }
240
+ }
241
+ ctx.setLineDash([]);
242
+ },
243
+ ink(c, block, _frame, params, i) {
244
+ const p = newPlaced();
245
+ if (!place(c, params, block.displayedRegionIndex, i, p, getDpr())) {
246
+ return undefined;
247
+ }
248
+ return {
249
+ ...inkBox(p, params.arrowPx),
250
+ stroke: {
251
+ d: svgPathOf(p, params.arrowPx),
252
+ widthPx: p.strokePx,
253
+ originX: 0,
254
+ originY: 0,
255
+ },
256
+ };
257
+ },
258
+ hitNearest(c, block, _frame, params, xPx, yPx, candidates, maxDistSq) {
259
+ const own = block.displayedRegionIndex;
260
+ const dpr = getDpr();
261
+ const reach = Math.sqrt(maxDistSq);
262
+ return nearestInk(candidates, maxDistSq, i => {
263
+ if (!place(c, params, own, i, PLACED, dpr)) {
264
+ return undefined;
265
+ }
266
+ const box = inkBox(PLACED, params.arrowPx);
267
+ if (xPx < box.left - reach ||
268
+ xPx > box.left + box.width + reach ||
269
+ yPx < box.top - reach ||
270
+ yPx > box.top + box.height + reach) {
271
+ return undefined;
272
+ }
273
+ const near = nearestOnConnector(PLACED, params.arrowPx, xPx, yPx);
274
+ const dist = Math.sqrt(near.distSq);
275
+ const half = PLACED.strokePx / 2;
276
+ if (dist <= half) {
277
+ return { x: xPx, y: yPx, distSq: 0 };
278
+ }
279
+ const t = half / dist;
280
+ const x = near.x + (xPx - near.x) * t;
281
+ const y = near.y + (yPx - near.y) * t;
282
+ return { x, y, distSq: (xPx - x) ** 2 + (yPx - y) ** 2 };
283
+ });
284
+ },
285
+ };
@@ -65,7 +65,7 @@ export declare function inkViolations<C, S extends MarkFrame>(mark: Mark<C, S>,
65
65
  * **What the painter drew for instance `i` has to be attributed to `i`,** and
66
66
  * there are two ways to get that. A mark whose every instance is one `fillRect`
67
67
  * gets it positionally, which is the default and needs nothing. A mark that
68
- * batches a colour run into one path (`point`'s glyphs), or that skips an
68
+ * batches a color run into one path (`point`'s glyphs), or that skips an
69
69
  * instance the view has scrolled past (`cell`'s off-canvas rows), cannot be read
70
70
  * that way at all — the batch has fewer rects than instances, or the wrong ones.
71
71
  * `sliceOne` is the way in for those: hand back one instance's channels and the
@@ -3,7 +3,7 @@ const IDENTITY = [1, 0, 0, 1, 0, 0];
3
3
  /**
4
4
  * A `MarkContext2D` that records every rect a painter fills, in paint order and
5
5
  * in CANVAS coordinates — `fillRect` and path `rect` alike, so a shape that
6
- * batches by colour records the same list as one that fills per instance. A
6
+ * batches by color records the same list as one that fills per instance. A
7
7
  * path traced with `moveTo`/`lineTo`/`bezierCurveTo` and filled records its
8
8
  * bounding box, the extent a shape whose hit test answers a polygon's box
9
9
  * (the variant inversion triangle) is checked against; a curve contributes its control
@@ -12,9 +12,9 @@ const IDENTITY = [1, 0, 0, 1, 0, 0];
12
12
  * A **stroked** path records one rect per `moveTo`/`lineTo` edge, each the
13
13
  * edge's bounding box widened by half the current `lineWidth` — the extent a
14
14
  * round cap reaches on all four sides. Per edge rather than per `stroke()`,
15
- * because a painter that batches a colour run into one path (dotplot's) would
15
+ * because a painter that batches a color run into one path (dotplot's) would
16
16
  * otherwise record the run's hull as a single instance. `fillStyle` on such a
17
- * record is the `strokeStyle` it went down in; the field is the colour of the
17
+ * record is the `strokeStyle` it went down in; the field is the color of the
18
18
  * ink either way.
19
19
  *
20
20
  * `save`/`restore` bracket the transform and the styles the way a real context
@@ -281,7 +281,7 @@ function hitViolations(hit, boxes, x, y) {
281
281
  * **What the painter drew for instance `i` has to be attributed to `i`,** and
282
282
  * there are two ways to get that. A mark whose every instance is one `fillRect`
283
283
  * gets it positionally, which is the default and needs nothing. A mark that
284
- * batches a colour run into one path (`point`'s glyphs), or that skips an
284
+ * batches a color run into one path (`point`'s glyphs), or that skips an
285
285
  * instance the view has scrolled past (`cell`'s off-canvas rows), cannot be read
286
286
  * that way at all — the batch has fewer rects than instances, or the wrong ones.
287
287
  * `sliceOne` is the way in for those: hand back one instance's channels and the
@@ -3,11 +3,12 @@ import type { InkRect, MarkContext2D } from './types.ts';
3
3
  export type GlyphPath = Pick<MarkContext2D, 'moveTo' | 'lineTo' | 'closePath' | 'rect' | 'arc'>;
4
4
  /**
5
5
  * Append one of the `point` shape's glyphs to the current path, centred on
6
- * `(cx, y)`.
6
+ * `(cx, y)`. The triangle is `triangleHeight` tall, `diameter` unless the
7
+ * mark draws one wider than it is tall.
7
8
  *
8
9
  * The disc/crisp-square split and the square's snap are pointGlyph.slang's
9
10
  * (adr-051), so the painter cannot drift from the shader about where the split
10
- * is. The caller owns `beginPath`/`fill`, so a run of one colour can batch into
11
+ * is. The caller owns `beginPath`/`fill`, so a run of one color can batch into
11
12
  * a single fill.
12
13
  *
13
14
  * Its own module, not `pointMark.ts`'s: this is the half of the shape a
@@ -15,7 +16,7 @@ export type GlyphPath = Pick<MarkContext2D, 'moveTo' | 'lineTo' | 'closePath' |
15
16
  * hand that consumer the shader's WGSL and GLSL strings. Wiggle's scatter mode
16
17
  * is that consumer.
17
18
  */
18
- export declare function appendGlyph(ctx: GlyphPath, glyph: number, cx: number, y: number, diameter: number): void;
19
+ export declare function appendGlyph(ctx: GlyphPath, glyph: number, cx: number, y: number, diameter: number, triangleHeight?: number): void;
19
20
  /**
20
21
  * Room a glyph needs at each end of a plot whose display wants its topmost and
21
22
  * bottommost points drawn whole: a bar's datum is its top edge and never
@@ -33,4 +34,4 @@ export declare function pointInsetPx(diameter: number): number;
33
34
  * diamond is scaled by `DIAMOND_GLYPH_SCALE` and the small square snaps to
34
35
  * the pixel grid, so neither is the diameter about the centre.
35
36
  */
36
- export declare function glyphBox(glyph: number, cx: number, y: number, diameter: number): InkRect;
37
+ export declare function glyphBox(glyph: number, cx: number, y: number, diameter: number, triangleHeight?: number): InkRect;
@@ -3,11 +3,12 @@ import { crispSquareTopLeftPx } from '../shaders/pointGlyph.js.generated.js';
3
3
  import { DIAMOND_GLYPH_SCALE, GLYPH_DIAMOND, GLYPH_SQUARE, GLYPH_TRIANGLE, } from '../shaders/pointMark.consts.generated.js';
4
4
  /**
5
5
  * Append one of the `point` shape's glyphs to the current path, centred on
6
- * `(cx, y)`.
6
+ * `(cx, y)`. The triangle is `triangleHeight` tall, `diameter` unless the
7
+ * mark draws one wider than it is tall.
7
8
  *
8
9
  * The disc/crisp-square split and the square's snap are pointGlyph.slang's
9
10
  * (adr-051), so the painter cannot drift from the shader about where the split
10
- * is. The caller owns `beginPath`/`fill`, so a run of one colour can batch into
11
+ * is. The caller owns `beginPath`/`fill`, so a run of one color can batch into
11
12
  * a single fill.
12
13
  *
13
14
  * Its own module, not `pointMark.ts`'s: this is the half of the shape a
@@ -15,12 +16,13 @@ import { DIAMOND_GLYPH_SCALE, GLYPH_DIAMOND, GLYPH_SQUARE, GLYPH_TRIANGLE, } fro
15
16
  * hand that consumer the shader's WGSL and GLSL strings. Wiggle's scatter mode
16
17
  * is that consumer.
17
18
  */
18
- export function appendGlyph(ctx, glyph, cx, y, diameter) {
19
+ export function appendGlyph(ctx, glyph, cx, y, diameter, triangleHeight = diameter) {
19
20
  const r = diameter / 2;
20
21
  if (glyph === GLYPH_TRIANGLE) {
21
- ctx.moveTo(cx - r, y - r);
22
- ctx.lineTo(cx + r, y - r);
23
- ctx.lineTo(cx, y + r);
22
+ const ry = triangleHeight / 2;
23
+ ctx.moveTo(cx - r, y - ry);
24
+ ctx.lineTo(cx + r, y - ry);
25
+ ctx.lineTo(cx, y + ry);
24
26
  ctx.closePath();
25
27
  }
26
28
  else if (glyph === GLYPH_DIAMOND) {
@@ -58,8 +60,16 @@ export function pointInsetPx(diameter) {
58
60
  * diamond is scaled by `DIAMOND_GLYPH_SCALE` and the small square snaps to
59
61
  * the pixel grid, so neither is the diameter about the centre.
60
62
  */
61
- export function glyphBox(glyph, cx, y, diameter) {
63
+ export function glyphBox(glyph, cx, y, diameter, triangleHeight = diameter) {
62
64
  const r = diameter / 2;
65
+ if (glyph === GLYPH_TRIANGLE) {
66
+ return {
67
+ left: cx - r,
68
+ top: y - triangleHeight / 2,
69
+ width: diameter,
70
+ height: triangleHeight,
71
+ };
72
+ }
63
73
  if (glyph === GLYPH_DIAMOND) {
64
74
  const ri = r * DIAMOND_GLYPH_SCALE;
65
75
  return { left: cx - ri, top: y - ri, width: 2 * ri, height: 2 * ri };