@jbrowse/render-core 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/esm/OverlayCanvas.d.ts +29 -0
- package/esm/OverlayCanvas.js +43 -0
- package/esm/RenderCanvas.d.ts +76 -0
- package/esm/RenderCanvas.js +38 -0
- package/esm/RenderLifecycleMixin.d.ts +180 -0
- package/esm/RenderLifecycleMixin.js +279 -0
- package/esm/ScrollLockedOverlay.d.ts +28 -0
- package/esm/ScrollLockedOverlay.js +42 -0
- package/esm/blockClipUtils.d.ts +27 -0
- package/esm/blockClipUtils.js +101 -0
- package/esm/canvas2dUtils.d.ts +372 -0
- package/esm/canvas2dUtils.js +564 -0
- package/esm/canvasContext.d.ts +68 -0
- package/esm/canvasContext.js +74 -0
- package/esm/colorRampLut.d.ts +17 -0
- package/esm/colorRampLut.js +25 -0
- package/esm/coverageBand.d.ts +107 -0
- package/esm/coverageBand.js +109 -0
- package/esm/coverageBandBuffers.d.ts +30 -0
- package/esm/coverageBandBuffers.js +13 -0
- package/esm/createRenderingBackend.d.ts +52 -0
- package/esm/createRenderingBackend.js +58 -0
- package/esm/displayPhase.d.ts +155 -0
- package/esm/displayPhase.js +97 -0
- package/esm/globalRenderingBackend.d.ts +90 -0
- package/esm/globalRenderingBackend.js +53 -0
- package/esm/gpuDevice.d.ts +91 -0
- package/esm/gpuDevice.js +277 -0
- package/esm/graphicsCapabilities.d.ts +98 -0
- package/esm/graphicsCapabilities.js +165 -0
- package/esm/hal/createHal.d.ts +22 -0
- package/esm/hal/createHal.js +91 -0
- package/esm/hal/deviceGpuCache.d.ts +36 -0
- package/esm/hal/deviceGpuCache.js +120 -0
- package/esm/hal/gpuHalBase.d.ts +55 -0
- package/esm/hal/gpuHalBase.js +81 -0
- package/esm/hal/index.d.ts +7 -0
- package/esm/hal/index.js +5 -0
- package/esm/hal/mockHal.d.ts +153 -0
- package/esm/hal/mockHal.js +297 -0
- package/esm/hal/oomReporter.d.ts +15 -0
- package/esm/hal/oomReporter.js +22 -0
- package/esm/hal/passIds.d.ts +22 -0
- package/esm/hal/passIds.js +36 -0
- package/esm/hal/regionRegistry.d.ts +11 -0
- package/esm/hal/regionRegistry.js +66 -0
- package/esm/hal/types.d.ts +116 -0
- package/esm/hal/types.js +1 -0
- package/esm/hal/webgl2Hal.d.ts +47 -0
- package/esm/hal/webgl2Hal.js +490 -0
- package/esm/hal/webgpuHal.d.ts +169 -0
- package/esm/hal/webgpuHal.js +722 -0
- package/esm/installUpload.d.ts +91 -0
- package/esm/installUpload.js +94 -0
- package/esm/instanceCache.d.ts +43 -0
- package/esm/instanceCache.js +53 -0
- package/esm/instancePass.d.ts +50 -0
- package/esm/instancePass.js +34 -0
- package/esm/keyedRenderingBackend.d.ts +41 -0
- package/esm/keyedRenderingBackend.js +19 -0
- package/esm/mapUploadSync.d.ts +31 -0
- package/esm/mapUploadSync.js +45 -0
- package/esm/namedReactions.d.ts +29 -0
- package/esm/namedReactions.js +62 -0
- package/esm/perRegionRenderingBackend.d.ts +114 -0
- package/esm/perRegionRenderingBackend.js +93 -0
- package/esm/recoveryBudget.d.ts +40 -0
- package/esm/recoveryBudget.js +56 -0
- package/esm/regionDataMap.d.ts +60 -0
- package/esm/regionDataMap.js +98 -0
- package/esm/renderBlock.d.ts +25 -0
- package/esm/renderBlock.js +10 -0
- package/esm/renderingBackendBase.d.ts +71 -0
- package/esm/renderingBackendBase.js +50 -0
- package/esm/shaders/antialias.js.generated.d.ts +4 -0
- package/esm/shaders/antialias.js.generated.js +25 -0
- package/esm/shaders/capsule.generated.d.ts +1 -0
- package/esm/shaders/capsule.generated.js +3 -0
- package/esm/shaders/colorRampLut.generated.d.ts +1 -0
- package/esm/shaders/colorRampLut.generated.js +3 -0
- package/esm/shaders/coverageBand.generated.d.ts +2 -0
- package/esm/shaders/coverageBand.generated.js +4 -0
- package/esm/shaders/coverageBar.generated.d.ts +4 -0
- package/esm/shaders/coverageBar.generated.js +6 -0
- package/esm/shaders/coverageBar.iface.generated.d.ts +85 -0
- package/esm/shaders/coverageBar.iface.generated.js +114 -0
- package/esm/shaders/coverageIndicator.generated.d.ts +4 -0
- package/esm/shaders/coverageIndicator.generated.js +6 -0
- package/esm/shaders/coverageIndicator.iface.generated.d.ts +85 -0
- package/esm/shaders/coverageIndicator.iface.generated.js +114 -0
- package/esm/shaders/coverageInterbase.generated.d.ts +4 -0
- package/esm/shaders/coverageInterbase.generated.js +6 -0
- package/esm/shaders/coverageInterbase.iface.generated.d.ts +93 -0
- package/esm/shaders/coverageInterbase.iface.generated.js +134 -0
- package/esm/shaders/coverageMod.generated.d.ts +4 -0
- package/esm/shaders/coverageMod.generated.js +6 -0
- package/esm/shaders/coverageMod.iface.generated.d.ts +97 -0
- package/esm/shaders/coverageMod.iface.generated.js +144 -0
- package/esm/shaders/coverageSnp.generated.d.ts +4 -0
- package/esm/shaders/coverageSnp.generated.js +6 -0
- package/esm/shaders/coverageSnp.iface.generated.d.ts +97 -0
- package/esm/shaders/coverageSnp.iface.generated.js +144 -0
- package/esm/shaders/hpmath.js.generated.d.ts +3 -0
- package/esm/shaders/hpmath.js.generated.js +48 -0
- package/esm/shaders/pointGlyph.generated.d.ts +1 -0
- package/esm/shaders/pointGlyph.generated.js +3 -0
- package/esm/shaders/pointGlyph.js.generated.d.ts +1 -0
- package/esm/shaders/pointGlyph.js.generated.js +7 -0
- package/esm/shaders/rowRect.generated.d.ts +3 -0
- package/esm/shaders/rowRect.generated.js +5 -0
- package/esm/shaders/rowRect.js.generated.d.ts +2 -0
- package/esm/shaders/rowRect.js.generated.js +13 -0
- package/esm/shaders/scoreScale.js.generated.d.ts +1 -0
- package/esm/shaders/scoreScale.js.generated.js +46 -0
- package/esm/slangPass.d.ts +49 -0
- package/esm/slangPass.js +22 -0
- package/esm/useEventCallback.d.ts +1 -0
- package/esm/useEventCallback.js +19 -0
- package/esm/useGraphicsCapabilities.d.ts +17 -0
- package/esm/useGraphicsCapabilities.js +38 -0
- package/esm/useRenderingBackend.d.ts +83 -0
- package/esm/useRenderingBackend.js +422 -0
- package/esm/useTabVisibilityRerender.d.ts +13 -0
- package/esm/useTabVisibilityRerender.js +39 -0
- package/esm/webgpuUtils.d.ts +23 -0
- package/esm/webgpuUtils.js +76 -0
- package/package.json +91 -0
- package/src/shaders/antialias.js.generated.ts +33 -0
- package/src/shaders/antialias.slang +112 -0
- package/src/shaders/capsule.generated.ts +4 -0
- package/src/shaders/capsule.slang +62 -0
- package/src/shaders/colorPack.slang +26 -0
- package/src/shaders/colorRampLut.generated.ts +4 -0
- package/src/shaders/colorRampLut.slang +51 -0
- package/src/shaders/coverageBand.generated.ts +6 -0
- package/src/shaders/coverageBand.slang +276 -0
- package/src/shaders/coverageBar.generated.ts +10 -0
- package/src/shaders/coverageBar.iface.generated.ts +172 -0
- package/src/shaders/coverageBar.slang +37 -0
- package/src/shaders/coverageIndicator.generated.ts +10 -0
- package/src/shaders/coverageIndicator.iface.generated.ts +172 -0
- package/src/shaders/coverageIndicator.slang +61 -0
- package/src/shaders/coverageInterbase.generated.ts +10 -0
- package/src/shaders/coverageInterbase.iface.generated.ts +198 -0
- package/src/shaders/coverageInterbase.slang +63 -0
- package/src/shaders/coverageMod.generated.ts +10 -0
- package/src/shaders/coverageMod.iface.generated.ts +211 -0
- package/src/shaders/coverageMod.slang +36 -0
- package/src/shaders/coverageSnp.generated.ts +10 -0
- package/src/shaders/coverageSnp.iface.generated.ts +211 -0
- package/src/shaders/coverageSnp.slang +52 -0
- package/src/shaders/diagonalGrid.slang +25 -0
- package/src/shaders/glyphEdgeAlpha.test.ts +241 -0
- package/src/shaders/hpmath.js.generated.ts +47 -0
- package/src/shaders/hpmath.slang +226 -0
- package/src/shaders/pointGlyph.generated.ts +4 -0
- package/src/shaders/pointGlyph.js.generated.ts +9 -0
- package/src/shaders/pointGlyph.slang +93 -0
- package/src/shaders/rowRect.generated.ts +8 -0
- package/src/shaders/rowRect.js.generated.ts +17 -0
- package/src/shaders/rowRect.slang +160 -0
- package/src/shaders/scoreScale.js.generated.ts +51 -0
- package/src/shaders/scoreScale.slang +68 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// `glyphEdgeAlpha` (antialias.slang) sizes the antialiasing ramp on a glyph's
|
|
2
|
+
// edge from the SDF's own gradient, so that one ramp covers one output pixel for
|
|
3
|
+
// every shape the Manhattan and wiggle point modes draw. Two things have to hold
|
|
4
|
+
// for that, and neither is checked by the compiler:
|
|
5
|
+
//
|
|
6
|
+
// 1. The gradient really is what the reasoning assumes. Unit for the disc and
|
|
7
|
+
// for each of the triangle's three edges — but NOT for the diamond, whose
|
|
8
|
+
// `1 - (|x| + |y|)` is the L1 norm and therefore sqrt(2) times the true
|
|
9
|
+
// perpendicular distance to its edge. That sqrt(2) is exactly why the ramp
|
|
10
|
+
// is measured rather than assumed, and a shape added later with its own
|
|
11
|
+
// scaling would be silently mis-sized by any constant.
|
|
12
|
+
//
|
|
13
|
+
// 2. The ramp fits in the pad the quad leaves it. `discExpand` grows the quad
|
|
14
|
+
// by aaHalfPx(dpr) CSS px past the glyph edge; a ramp reaching further is
|
|
15
|
+
// clipped by the rasterizer and the fade ends on a hard edge. This is what
|
|
16
|
+
// the previous `fwidth`-as-half-width spelling got wrong, and it got it
|
|
17
|
+
// wrong worst on the diagonals, where fwidth overshoots most.
|
|
18
|
+
//
|
|
19
|
+
// The shader isn't unit-testable, so this models it. Same approach, and same
|
|
20
|
+
// reason, as LinearSyntenyDisplay/shaders/syntenyFillPad.test.ts and
|
|
21
|
+
// DotplotDisplay/shaders/dotplotCapsulePad.test.ts.
|
|
22
|
+
// SYNC: keep in step with antialias.slang's glyphEdgeAlpha and with the SDFs
|
|
23
|
+
// in manhattan.slang.
|
|
24
|
+
import { aaHalfPx } from './antialias.js.generated.ts'
|
|
25
|
+
|
|
26
|
+
const INV_SQRT5 = 1 / Math.sqrt(5)
|
|
27
|
+
|
|
28
|
+
// The four SDFs manhattan.slang selects between, over quad-local `p`. Positive
|
|
29
|
+
// inside, zero on the edge. SHAPE_BAR's constant 1 is the degenerate case the
|
|
30
|
+
// ramp's floor exists for.
|
|
31
|
+
const SDFS = {
|
|
32
|
+
disc: (x: number, y: number) => 1 - Math.hypot(x, y),
|
|
33
|
+
diamond: (x: number, y: number) => 1 - (Math.abs(x) + Math.abs(y)),
|
|
34
|
+
triangle: (x: number, y: number) =>
|
|
35
|
+
Math.min(1 - y, (2 * x + y + 1) * INV_SQRT5, (-2 * x + y + 1) * INV_SQRT5),
|
|
36
|
+
bar: () => 1,
|
|
37
|
+
} as const
|
|
38
|
+
|
|
39
|
+
// |grad sdf| per unit of p, centrally differenced. Away from creases this is the
|
|
40
|
+
// exact analytic gradient.
|
|
41
|
+
function gradMag(sdf: (x: number, y: number) => number, x: number, y: number) {
|
|
42
|
+
const h = 1e-6
|
|
43
|
+
return Math.hypot(
|
|
44
|
+
(sdf(x + h, y) - sdf(x - h, y)) / (2 * h),
|
|
45
|
+
(sdf(x, y + h) - sdf(x, y - h)) / (2 * h),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Points ON each shape's edge, parameterised so the sweep covers every facing —
|
|
50
|
+
// including the diagonals, which is where fwidth overshot and where the disc's
|
|
51
|
+
// quad has least room to spare.
|
|
52
|
+
function edgePoints(shape: keyof typeof SDFS, n = 64): [number, number][] {
|
|
53
|
+
const out: [number, number][] = []
|
|
54
|
+
for (let i = 0; i < n; i++) {
|
|
55
|
+
// Half-offset so no sample lands on an axis, i.e. on one of the diamond's
|
|
56
|
+
// four corners — `|y|` has a kink at y = 0, so a central difference there
|
|
57
|
+
// reports a gradient of 1 rather than the facet's sqrt(2). The corners are a
|
|
58
|
+
// property of the shape, not of the ramp, and the shader meets them on 1-2
|
|
59
|
+
// pixels per glyph.
|
|
60
|
+
const a = ((i + 0.5) / n) * Math.PI * 2
|
|
61
|
+
const c = Math.cos(a)
|
|
62
|
+
const s = Math.sin(a)
|
|
63
|
+
if (shape === 'disc') {
|
|
64
|
+
out.push([c, s])
|
|
65
|
+
} else if (shape === 'diamond') {
|
|
66
|
+
// scale the ray to the L1 unit ball
|
|
67
|
+
const k = 1 / (Math.abs(c) + Math.abs(s))
|
|
68
|
+
out.push([c * k, s * k])
|
|
69
|
+
} else if (shape === 'triangle') {
|
|
70
|
+
// walk the three edges rather than a ray, so corners are approached but
|
|
71
|
+
// the samples themselves stay on a single facet
|
|
72
|
+
const t = (i / n) * 3
|
|
73
|
+
const seg = Math.floor(t)
|
|
74
|
+
const f = 0.08 + (t - seg) * 0.84 // keep clear of the creases
|
|
75
|
+
const verts: [number, number][] = [
|
|
76
|
+
[-1, 1],
|
|
77
|
+
[1, 1],
|
|
78
|
+
[0, -1],
|
|
79
|
+
]
|
|
80
|
+
const [ax, ay] = verts[seg]!
|
|
81
|
+
const [bx, by] = verts[(seg + 1) % 3]!
|
|
82
|
+
out.push([ax + (bx - ax) * f, ay + (by - ay) * f])
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const RADII_PX = [1.5, 2, 4, 10]
|
|
89
|
+
const DPRS = [1, 2]
|
|
90
|
+
|
|
91
|
+
describe('glyph SDF gradients', () => {
|
|
92
|
+
test('the disc and the triangle carry unit gradients', () => {
|
|
93
|
+
for (const [x, y] of edgePoints('disc')) {
|
|
94
|
+
expect(gradMag(SDFS.disc, x, y)).toBeCloseTo(1, 5)
|
|
95
|
+
}
|
|
96
|
+
for (const [x, y] of edgePoints('triangle')) {
|
|
97
|
+
expect(gradMag(SDFS.triangle, x, y)).toBeCloseTo(1, 5)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('the diamond does not — its L1 norm carries sqrt(2)', () => {
|
|
102
|
+
// The trap a hard-coded ramp width would fall into: this SDF reports a
|
|
103
|
+
// distance sqrt(2) times the true perpendicular one, so a constant sized for
|
|
104
|
+
// the disc would make the diamond's edge sqrt(2) too soft.
|
|
105
|
+
for (const [x, y] of edgePoints('diamond')) {
|
|
106
|
+
expect(gradMag(SDFS.diamond, x, y)).toBeCloseTo(Math.SQRT2, 5)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('the bar is constant, which is what the ramp floor is for', () => {
|
|
111
|
+
expect(gradMag(SDFS.bar, 0.3, -0.4)).toBe(0)
|
|
112
|
+
// max(0, 1e-5) is a positive ramp width, and a coverage of 1 over a ramp
|
|
113
|
+
// that narrow clamps to 1 — the solid fill SHAPE_BAR wants.
|
|
114
|
+
expect(Math.max(0, 1e-5)).toBeGreaterThan(0)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// The screen-space width of the ramp glyphEdgeAlpha produces, in DEVICE px, and
|
|
119
|
+
// how far it reaches outside the glyph edge in CSS px.
|
|
120
|
+
//
|
|
121
|
+
// `p` units relate to pixels through the glyph's radius: |p| = 1 sits at
|
|
122
|
+
// radiusPx CSS px from the center, so one device px is 1/(radiusPx*dpr) of p.
|
|
123
|
+
function rampGeometry(
|
|
124
|
+
sdf: (x: number, y: number) => number,
|
|
125
|
+
x: number,
|
|
126
|
+
y: number,
|
|
127
|
+
radiusPx: number,
|
|
128
|
+
dpr: number,
|
|
129
|
+
// how the shader turns its measured derivative into the ramp's half-width.
|
|
130
|
+
// `aaRamp` takes the measurement as the FULL ramp width, so the half is half
|
|
131
|
+
// of it; the retired spelling used fwidth's overshooting value as the half.
|
|
132
|
+
halfRampOf = (measuredPerDevicePx: number) => 0.5 * measuredPerDevicePx,
|
|
133
|
+
// what the shader differentiates WITH. These are two separate quantities and
|
|
134
|
+
// conflating them is what hides the bug: the ramp's width in coverage units
|
|
135
|
+
// comes from whatever the shader measured, but converting that back to a
|
|
136
|
+
// screen distance is the TRUE gradient's job. Divide by the measured value and
|
|
137
|
+
// fwidth's overshoot cancels itself out and looks correct.
|
|
138
|
+
measure = gradMag,
|
|
139
|
+
) {
|
|
140
|
+
const trueG = gradMag(sdf, x, y)
|
|
141
|
+
const pPerDevicePx = 1 / (radiusPx * dpr)
|
|
142
|
+
const halfRamp = halfRampOf(measure(sdf, x, y) * pPerDevicePx)
|
|
143
|
+
// Back to screen distance: coverage changes by trueG per unit of p, so the
|
|
144
|
+
// half-ramp spans halfRamp/trueG of p, i.e. that times radiusPx CSS px.
|
|
145
|
+
const halfRampCssPx = (halfRamp / trueG) * radiusPx
|
|
146
|
+
return { widthDevicePx: 2 * halfRampCssPx * dpr, reachCssPx: halfRampCssPx }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// fwidth = |ddx| + |ddy|, which is what the retired spelling measured with.
|
|
150
|
+
function fwidthMag(
|
|
151
|
+
sdf: (x: number, y: number) => number,
|
|
152
|
+
x: number,
|
|
153
|
+
y: number,
|
|
154
|
+
) {
|
|
155
|
+
const h = 1e-6
|
|
156
|
+
return (
|
|
157
|
+
Math.abs((sdf(x + h, y) - sdf(x - h, y)) / (2 * h)) +
|
|
158
|
+
Math.abs((sdf(x, y + h) - sdf(x, y - h)) / (2 * h))
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
describe('glyphEdgeAlpha ramp', () => {
|
|
163
|
+
test.each(['disc', 'diamond', 'triangle'] as const)(
|
|
164
|
+
'is exactly one output pixel wide all the way round: %s',
|
|
165
|
+
shape => {
|
|
166
|
+
for (const [x, y] of edgePoints(shape)) {
|
|
167
|
+
for (const radiusPx of RADII_PX) {
|
|
168
|
+
for (const dpr of DPRS) {
|
|
169
|
+
const { widthDevicePx } = rampGeometry(
|
|
170
|
+
SDFS[shape],
|
|
171
|
+
x,
|
|
172
|
+
y,
|
|
173
|
+
radiusPx,
|
|
174
|
+
dpr,
|
|
175
|
+
)
|
|
176
|
+
expect(widthDevicePx).toBeCloseTo(1, 6)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
// EQUALS, not "fits inside". The measured ramp reaches exactly half an output
|
|
184
|
+
// pixel past the edge whatever the shape — that is what taking the gradient
|
|
185
|
+
// buys — so the pad `discExpand` leaves is knowable exactly, and a pad merely
|
|
186
|
+
// big enough is one nobody can tell from a pad four times too big. `<=` held
|
|
187
|
+
// for any constant at or above 0.5 and is how a flat 1.0 CSS px survived.
|
|
188
|
+
test.each(['disc', 'diamond', 'triangle'] as const)(
|
|
189
|
+
'the pad discExpand leaves is exactly the ramp reach: %s',
|
|
190
|
+
shape => {
|
|
191
|
+
for (const [x, y] of edgePoints(shape)) {
|
|
192
|
+
for (const radiusPx of RADII_PX) {
|
|
193
|
+
for (const dpr of DPRS) {
|
|
194
|
+
const { reachCssPx } = rampGeometry(
|
|
195
|
+
SDFS[shape],
|
|
196
|
+
x,
|
|
197
|
+
y,
|
|
198
|
+
radiusPx,
|
|
199
|
+
dpr,
|
|
200
|
+
)
|
|
201
|
+
expect(reachCssPx).toBeCloseTo(aaHalfPx(dpr), 9)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
test('fwidth as a half-width was both too wide and clipped (do not reintroduce)', () => {
|
|
209
|
+
// The retired spelling, on the worst facing it has: a disc's 45-degree
|
|
210
|
+
// point, at dpr 1. fwidth reads sqrt(2) there instead of 1, and was then
|
|
211
|
+
// used whole as the half-width.
|
|
212
|
+
const diag = Math.SQRT1_2
|
|
213
|
+
const retired = rampGeometry(
|
|
214
|
+
SDFS.disc,
|
|
215
|
+
diag,
|
|
216
|
+
diag,
|
|
217
|
+
4,
|
|
218
|
+
1,
|
|
219
|
+
g => g, // used whole, not halved
|
|
220
|
+
fwidthMag,
|
|
221
|
+
)
|
|
222
|
+
expect(retired.widthDevicePx).toBeCloseTo(2 * Math.SQRT2, 6)
|
|
223
|
+
expect(retired.reachCssPx).toBeGreaterThan(aaHalfPx(1))
|
|
224
|
+
|
|
225
|
+
// and on an axis-aligned facing it was merely 2x too wide, which is what
|
|
226
|
+
// made the fade vary with position around one circle.
|
|
227
|
+
const axis = rampGeometry(SDFS.disc, 1, 0, 4, 1, g => g, fwidthMag)
|
|
228
|
+
expect(axis.widthDevicePx).toBeCloseTo(2, 6)
|
|
229
|
+
expect(retired.widthDevicePx / axis.widthDevicePx).toBeCloseTo(
|
|
230
|
+
Math.SQRT2,
|
|
231
|
+
6,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
// both are one pixel now, at every facing
|
|
235
|
+
expect(rampGeometry(SDFS.disc, diag, diag, 4, 1).widthDevicePx).toBeCloseTo(
|
|
236
|
+
1,
|
|
237
|
+
6,
|
|
238
|
+
)
|
|
239
|
+
expect(rampGeometry(SDFS.disc, 1, 0, 4, 1).widthDevicePx).toBeCloseTo(1, 6)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// AUTO-GENERATED by packages/shader-tools/src/shader-codegen from hpmath.slang.
|
|
2
|
+
// Do not edit. Run `pnpm gen:shaders` to regenerate.
|
|
3
|
+
|
|
4
|
+
// Scalar twins of hpmath.slang, transliterated from slangc's WGSL so
|
|
5
|
+
// the Canvas2D and SVG paths run the shader's own math. See adr-051.
|
|
6
|
+
|
|
7
|
+
export function snapBoxHeightPx(heightPx: number): number {
|
|
8
|
+
let hPx = Math.floor((heightPx + 0.5))
|
|
9
|
+
let _t0: boolean
|
|
10
|
+
if (((hPx - (2.0 * Math.floor((hPx * 0.5)))) == 0.0)) {
|
|
11
|
+
_t0 = (hPx >= 2.0)
|
|
12
|
+
} else {
|
|
13
|
+
_t0 = false
|
|
14
|
+
}
|
|
15
|
+
if (_t0) {
|
|
16
|
+
_t0 = (hPx <= 4.0)
|
|
17
|
+
} else {
|
|
18
|
+
_t0 = false
|
|
19
|
+
}
|
|
20
|
+
let _t1: number
|
|
21
|
+
if (_t0) {
|
|
22
|
+
_t1 = (hPx + 1.0)
|
|
23
|
+
} else {
|
|
24
|
+
_t1 = hPx
|
|
25
|
+
}
|
|
26
|
+
return _t1
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function snapBoxCenterYPx(centerY: number, heightPx: number, scrollY: number): number {
|
|
30
|
+
return ((Math.floor((((centerY - (heightPx * 0.5)) - scrollY) + 0.5)) + Math.floor((snapBoxHeightPx(heightPx) * 0.5))) + 0.5)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function extendToMinWidthPx(x1: number, x2: number, minWidth: number): number {
|
|
34
|
+
let dx = (x2 - x1)
|
|
35
|
+
let _t0: number
|
|
36
|
+
if ((Math.abs(dx) < minWidth)) {
|
|
37
|
+
if ((dx < 0.0)) {
|
|
38
|
+
_t0 = -minWidth
|
|
39
|
+
} else {
|
|
40
|
+
_t0 = minWidth
|
|
41
|
+
}
|
|
42
|
+
_t0 = (x1 + _t0)
|
|
43
|
+
} else {
|
|
44
|
+
_t0 = x2
|
|
45
|
+
}
|
|
46
|
+
return _t0
|
|
47
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Shared high-precision genome-coordinate math. HP technique from genome-spy
|
|
2
|
+
// (MIT): https://github.com/genome-spy/genome-spy
|
|
3
|
+
//
|
|
4
|
+
// Imported via `import hpmath;` from shaders that need sub-pixel-accurate
|
|
5
|
+
// conversion of base-pair positions to clip space when zoomed far in.
|
|
6
|
+
//
|
|
7
|
+
// The `js-export` set is the px-space *decisions* the Canvas2D and SVG paths
|
|
8
|
+
// must make identically to the GPU — which pixel row a glyph centers on, how a
|
|
9
|
+
// sub-pixel span is widened. `pnpm gen:shaders` transliterates them into
|
|
10
|
+
// hpmath.js.generated.ts; nothing downstream re-derives them by hand.
|
|
11
|
+
//! js-export: snapBoxHeightPx, snapBoxCenterYPx, extendToMinWidthPx
|
|
12
|
+
//! js-skip: hpSplitUint — the hi/lo float32 precision split exists because a GPU has no float64; the Canvas2D path just uses a number
|
|
13
|
+
//! js-skip: quadLocal — maps a vertex id to a quad corner — Canvas2D has no vertices, it calls fillRect
|
|
14
|
+
//! js-skip: yPxToClipY — clip space is the GPU coordinate system; the px-space decision it wraps is what gets exported (snapBoxCenterYPx and friends)
|
|
15
|
+
//! js-skip: clipXToPx — the x half of the same clip-space conversion, and the reason a px decision can be written once — nothing outside a shader is in clip space
|
|
16
|
+
//! js-skip: pxToClipX — the inverse of clipXToPx, same reason
|
|
17
|
+
//! js-skip: pxToClipLen — the length half of the same conversion; a px dimension is already in px on the Canvas2D side, so there is nothing to convert
|
|
18
|
+
//! js-skip: clipLenToPx — the inverse of pxToClipLen, same reason
|
|
19
|
+
//! js-skip: extendToMinWidthX — clip-space wrapper over the exported extendToMinWidthPx, which is the decision
|
|
20
|
+
//! js-skip: expandToMinWidthPx — the midpoint rule has no Canvas2D counterpart: the pileup floors a cell width one-sidedly and adds a seam fudge the shader deliberately omits (rendererTypes.ts pileupCellWidth), and the variant matrix floors in physical px against a canvas the Canvas2D twin sizes itself. A different rule, not a twin
|
|
21
|
+
//! js-skip: expandToMinWidthX — clip-space wrapper over expandToMinWidthPx, same reason as extendToMinWidthX
|
|
22
|
+
//! js-skip: snapToPixelX — clip in, clip out; its px core is `floor(x + 0.5)`, which is Math.round and needs no twin
|
|
23
|
+
//! js-skip: snapBoxCenterY — clip-space wrapper over the exported snapBoxCenterYPx
|
|
24
|
+
module hpmath;
|
|
25
|
+
|
|
26
|
+
public static const uint HP_LOW_MASK = 0xFFFu;
|
|
27
|
+
|
|
28
|
+
public float2 hpSplitUint(uint value) {
|
|
29
|
+
uint lo = value & HP_LOW_MASK;
|
|
30
|
+
uint hi = value - lo;
|
|
31
|
+
return float2(float(hi), float(lo));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// `max(-inf)` and `dot()` exist to thwart the compiler — without them it
|
|
35
|
+
// collapses (hi_a+lo_a)-(hi_b+lo_b) into one large subtraction and the
|
|
36
|
+
// float32 precision the hi/lo split exists to preserve is gone. `hpZero` is
|
|
37
|
+
// a tiny CPU-supplied number; `-1/hpZero` is smaller than any real diff.
|
|
38
|
+
public float hpToClipX(float2 splitPos, float3 bpRange, float hpZero) {
|
|
39
|
+
float inf = 1.0 / hpZero;
|
|
40
|
+
float step = 2.0 / bpRange.z;
|
|
41
|
+
float hi = max(splitPos.x - bpRange.x, -inf);
|
|
42
|
+
float lo = max(splitPos.y - bpRange.y, -inf);
|
|
43
|
+
return dot(float3(-1.0, hi, lo), float3(1.0, step, step));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public float hpScaleLinear(float2 splitPos, float3 bpRange, float hpZero) {
|
|
47
|
+
float inf = 1.0 / hpZero;
|
|
48
|
+
float step = 1.0 / bpRange.z;
|
|
49
|
+
float hi = max(splitPos.x - bpRange.x, -inf);
|
|
50
|
+
float lo = max(splitPos.y - bpRange.y, -inf);
|
|
51
|
+
return dot(float2(hi, lo), float2(step, step));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Exact-as-possible bp offset from a split position to the viewport start
|
|
55
|
+
// stored in bpRange.xy. Subtracts the hi/lo components separately (same
|
|
56
|
+
// ordering as hpToClipX) so the result stays precise at high genomic
|
|
57
|
+
// coordinates — recombining bpStartHi+bpStartLo first would lose ~256bp near
|
|
58
|
+
// 3e9. Unlike hpToClipX this returns bp, not clip space.
|
|
59
|
+
public float hpViewportOffsetBp(float2 splitPos, float3 bpRange) {
|
|
60
|
+
return (bpRange.x - splitPos.x) + (bpRange.y - splitPos.y);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public float snapToPixelX(float clipX, float canvasWidth) {
|
|
64
|
+
float px = (clipX + 1.0) * 0.5 * canvasWidth;
|
|
65
|
+
return floor(px + 0.5) / canvasWidth * 2.0 - 1.0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public float yPxToClipY(float yPx, float canvasHeight) {
|
|
69
|
+
return 1.0 - (yPx / canvasHeight) * 2.0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Tallest box that gets the odd-height nudge below. Above this a half-pixel of
|
|
73
|
+
// asymmetry is a small fraction of the box and invisible; at 2px it is half of
|
|
74
|
+
// it, so the exons appear to float above their intron line.
|
|
75
|
+
public static const float THIN_BOX_PX = 4.0;
|
|
76
|
+
|
|
77
|
+
// The pixel height a box is drawn at: its rounded height, except that a THIN box
|
|
78
|
+
// with an EVEN height is nudged up to the next odd one. A box spanning an even
|
|
79
|
+
// number of rows has no middle ROW — its center falls on the seam between two —
|
|
80
|
+
// so the 1px glyphs that ride on it (intron line, chevrons, strand arrow) can
|
|
81
|
+
// only ever sit half a pixel off center, which at the 2px bodies fit mode
|
|
82
|
+
// squeezes down to reads as the exons sitting above the intron line. An odd
|
|
83
|
+
// height gives the box a true center row for them to land on. The extra pixel
|
|
84
|
+
// can't reach the row below: the packer quantizes row tops to a pitch of at
|
|
85
|
+
// least twice the body height at these sizes (see layout.ts pitchY).
|
|
86
|
+
// A height that rounds to 0 is left at 0: it draws nothing, and nothing is what
|
|
87
|
+
// a caller asking for a zero-height box wants.
|
|
88
|
+
public float snapBoxHeightPx(float heightPx) {
|
|
89
|
+
float hPx = floor(heightPx + 0.5);
|
|
90
|
+
bool even = hPx - 2.0 * floor(hPx * 0.5) == 0.0;
|
|
91
|
+
return even && hPx >= 2.0 && hPx <= THIN_BOX_PX ? hPx + 1.0 : hPx;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Center a thin glyph on the *drawn* middle row of the box it annotates, given
|
|
95
|
+
// the box's real center `centerY` and height `heightPx`. The box is rasterized
|
|
96
|
+
// with the rect shader's edge snapping (top = floor(top+0.5), height =
|
|
97
|
+
// snapBoxHeightPx(h)); reproducing that here lands the line/chevron exactly on
|
|
98
|
+
// the box's center row instead of the ~1px bias a plain pixel-center snap
|
|
99
|
+
// (`floor(centerY - scrollY + 0.5) + 0.5`) gives. Thin boxes are drawn at an odd
|
|
100
|
+
// height precisely so that center row exists; a taller even box still has none,
|
|
101
|
+
// and stays consistent (just below the seam).
|
|
102
|
+
//
|
|
103
|
+
// Split in two on purpose. The decision — which pixel row is the center — is
|
|
104
|
+
// resolution math the Canvas2D and SVG paths need in *screen px*; only the
|
|
105
|
+
// clip-space conversion is GPU-specific. `//! js-export` lifts the px half into
|
|
106
|
+
// TypeScript, so the two backends share the rule instead of the Canvas2D twin
|
|
107
|
+
// re-deriving it (see adr-051). Keep new shared decisions in this shape: pure
|
|
108
|
+
// px-in/px-out below, the `yPxToClipY` wrapper above it.
|
|
109
|
+
public float snapBoxCenterYPx(float centerY, float heightPx, float scrollY) {
|
|
110
|
+
float topPx = floor(centerY - heightPx * 0.5 - scrollY + 0.5);
|
|
111
|
+
return topPx + floor(snapBoxHeightPx(heightPx) * 0.5) + 0.5;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public float snapBoxCenterY(
|
|
115
|
+
float centerY, float heightPx, float scrollY, float canvasHeight
|
|
116
|
+
) {
|
|
117
|
+
return yPxToClipY(snapBoxCenterYPx(centerY, heightPx, scrollY), canvasHeight);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Extend the free edge `x2` away from the anchor `x1` so the span is at least
|
|
121
|
+
// `minWidthPx` CSS px wide, returning the new x2. Sub-pixel-narrow features (a
|
|
122
|
+
// 1bp rect, a zoomed-out coverage bin) still paint instead of vanishing.
|
|
123
|
+
//
|
|
124
|
+
// Orientation-preserving on purpose: reversal is baked into bpRangeX (negated
|
|
125
|
+
// length), so x2 < x1 on reversed blocks and the span must grow in the sign
|
|
126
|
+
// direction of dx. Widening toward +x would flip the quad on reversed regions.
|
|
127
|
+
// Contrast `expandToMinWidthPx` below, which moves both edges about the
|
|
128
|
+
// midpoint — that one is for spans with no anchored edge.
|
|
129
|
+
// Unit-agnostic core: `minWidth` is in whatever space x1/x2 are. Canvas2D calls
|
|
130
|
+
// this directly in screen px (paired with `spanLeft`, which owns the fill-x
|
|
131
|
+
// pivot); the clip-space wrapper below converts the px floor first. Same split
|
|
132
|
+
// as snapBoxCenterYPx — the rule is shared, the coordinate space is not.
|
|
133
|
+
public float extendToMinWidthPx(float x1, float x2, float minWidth) {
|
|
134
|
+
float dx = x2 - x1;
|
|
135
|
+
return abs(dx) < minWidth ? x1 + (dx < 0.0 ? -minWidth : minWidth) : x2;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public float extendToMinWidthX(
|
|
139
|
+
float x1, float x2, float minWidthPx, float canvasWidth
|
|
140
|
+
) {
|
|
141
|
+
return extendToMinWidthPx(x1, x2, minWidthPx * 2.0 / canvasWidth);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The other min-width rule: widen a [x1, x2] span about its MIDPOINT, so both
|
|
145
|
+
// edges move. For a span with no anchored edge — a coverage bin, a gap, an
|
|
146
|
+
// insertion bar, a variant-matrix column — where growing off one side would
|
|
147
|
+
// slide the mark away from the coordinate it reports.
|
|
148
|
+
//
|
|
149
|
+
// Not orientation-preserving, and deliberately not: the callers here hand it an
|
|
150
|
+
// already-ordered span. `x2 - x1 < minWidth` rather than `abs(...)` means a
|
|
151
|
+
// span given backwards is always widened and comes back forwards.
|
|
152
|
+
//
|
|
153
|
+
// Same px-core/clip-wrapper split as `extendToMinWidthPx` above, and here it is
|
|
154
|
+
// what lets the alignments passes (clip space, a 1 CSS px floor) and the variant
|
|
155
|
+
// matrix (physical px, its own MIN_COL_PX) run one rule instead of two spellings
|
|
156
|
+
// of it.
|
|
157
|
+
public float2 expandToMinWidthPx(float x1, float x2, float minWidth) {
|
|
158
|
+
if (x2 - x1 < minWidth) {
|
|
159
|
+
float mid = (x1 + x2) * 0.5;
|
|
160
|
+
return float2(mid - minWidth * 0.5, mid + minWidth * 0.5);
|
|
161
|
+
}
|
|
162
|
+
return float2(x1, x2);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public float2 expandToMinWidthX(
|
|
166
|
+
float x1, float x2, float minWidthPx, float canvasWidth
|
|
167
|
+
) {
|
|
168
|
+
return expandToMinWidthPx(x1, x2, minWidthPx * 2.0 / canvasWidth);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// vid%6: 0→(0,0) 1→(1,0) 2→(0,1) 3→(0,1) 4→(1,0) 5→(1,1)
|
|
172
|
+
public float2 quadLocal(uint vid) {
|
|
173
|
+
uint v = vid % 6u;
|
|
174
|
+
return float2(
|
|
175
|
+
(v == 0u || v == 2u || v == 3u) ? 0.0 : 1.0,
|
|
176
|
+
(v == 0u || v == 1u || v == 4u) ? 0.0 : 1.0
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Screen-space (CSS px, top-left origin) → clip space, with the Y flip. Shared
|
|
181
|
+
// by the synteny/dotplot/hic renderers, whose 2D marks all map a screen-px
|
|
182
|
+
// point onto the viewport this way; keeps the flip sign from drifting between
|
|
183
|
+
// the plugins that previously open-coded it. (Appended after quadLocal on
|
|
184
|
+
// purpose: inserting earlier reshuffles every importer's debug #line numbers.)
|
|
185
|
+
public float4 screenToClip(float2 screen, float2 resolution) {
|
|
186
|
+
float2 clip = screen / resolution * 2.0 - 1.0;
|
|
187
|
+
return float4(clip.x, -clip.y, 0.0, 1.0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// The x pair of `yPxToClipY`/its inverse: clip x ↔ screen px across the span
|
|
191
|
+
// clip [-1,1] covers. `canvasWidth` is whatever width that span means for the
|
|
192
|
+
// caller — for the LGV glyph passes it is the block's scissored width, not the
|
|
193
|
+
// canvas (see FeatureGlyphUniforms.canvasWidth).
|
|
194
|
+
//
|
|
195
|
+
// These exist so a horizontal decision can be written once in px and used from
|
|
196
|
+
// clip space, the way `snapBoxCenterYPx` already is vertically:
|
|
197
|
+
// `pxToClipX(decide(clipXToPx(a), clipXToPx(b)), w)`. `snapToPixelX` above is
|
|
198
|
+
// that sandwich with `floor(x + 0.5)` in the middle — the round trip is
|
|
199
|
+
// arithmetic it was already doing internally, so spelling a decision this way
|
|
200
|
+
// costs no precision. (Appended, per the note on screenToClip.)
|
|
201
|
+
public float clipXToPx(float clipX, float canvasWidth) {
|
|
202
|
+
return (clipX + 1.0) * 0.5 * canvasWidth;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public float pxToClipX(float xPx, float canvasWidth) {
|
|
206
|
+
return xPx / canvasWidth * 2.0 - 1.0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The LENGTH pair, as against the POSITION pair above: clip spans 2 units
|
|
210
|
+
// across `size`, so a px span is `px * 2 / size` with no origin term. `size` is
|
|
211
|
+
// the canvas width or height, or whatever span clip [-1,1] covers for the
|
|
212
|
+
// caller — the same argument `clipXToPx` and `yPxToClipY` take.
|
|
213
|
+
//
|
|
214
|
+
// Spell every px dimension through these. Writing `HALF_W_PX / u.canvasWidth`
|
|
215
|
+
// gives the same number but folds the /2 into the conversion, so the constant
|
|
216
|
+
// reads as a full width while meaning a half one; and `TRIANGLE_HW * 2.0 * 2.0
|
|
217
|
+
// / u.canvasW` (indicator.slang, before this existed) leaves a reader to work
|
|
218
|
+
// out which 2.0 doubles the half-width and which one is the clip span.
|
|
219
|
+
// (Appended, per the note on screenToClip.)
|
|
220
|
+
public float pxToClipLen(float px, float size) {
|
|
221
|
+
return px * 2.0 / size;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
public float clipLenToPx(float clipLen, float size) {
|
|
225
|
+
return clipLen * 0.5 * size;
|
|
226
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// AUTO-GENERATED by packages/shader-tools/src/shader-codegen from pointGlyph.slang.
|
|
2
|
+
// Do not edit. Run `pnpm gen:shaders` to regenerate.
|
|
3
|
+
|
|
4
|
+
// Scalar twins of pointGlyph.slang, transliterated from slangc's WGSL so
|
|
5
|
+
// the Canvas2D and SVG paths run the shader's own math. See adr-051.
|
|
6
|
+
|
|
7
|
+
export function crispSquareTopLeftPx(centerPx: number, diameterPx: number): number {
|
|
8
|
+
return Math.floor(((centerPx - (diameterPx * 0.5)) + 0.5))
|
|
9
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Shared scatter-point glyph math for the wiggle and Manhattan renderers, whose
|
|
2
|
+
// point mode draws the same antialiased disc. Factored here so the disc SDF and
|
|
3
|
+
// the "small point → crisp square" / AA-pad thresholds can't drift between the
|
|
4
|
+
// two shaders (they previously redeclared identical consts with "must match"
|
|
5
|
+
// comments pointing at each other).
|
|
6
|
+
//
|
|
7
|
+
// The CPU side keeps no copy of either the threshold or the snap:
|
|
8
|
+
// `export-consts` emits the constant and `js-export` emits the snap, both
|
|
9
|
+
// consumed by `pointMarker.ts` (adr-051).
|
|
10
|
+
//! export-consts: SMALL_POINT_MAX_DIAMETER
|
|
11
|
+
//! js-export: crispSquareTopLeftPx
|
|
12
|
+
//! js-skip: discExpand — expands a quad so the fragment AA ramp is not clipped; Canvas2D draws ctx.arc and has no quad to expand
|
|
13
|
+
module pointGlyph;
|
|
14
|
+
|
|
15
|
+
import antialias;
|
|
16
|
+
import hpmath;
|
|
17
|
+
|
|
18
|
+
// A disc's edge is ramped by `glyphEdgeAlpha`, whose width is one OUTPUT pixel
|
|
19
|
+
// measured in the SDF's own units — so in CSS px the ramp reaches `aaHalfPx`
|
|
20
|
+
// past the boundary and the quad has to grow by exactly that.
|
|
21
|
+
//
|
|
22
|
+
// This was a flat 1.0 under a comment calling it device px while both consumers
|
|
23
|
+
// added it in CSS px: 2x the reach at dpr 1 and 4x at dpr 2, every surplus
|
|
24
|
+
// fragment shading to alpha 0. A pad is the one place the two units cannot be
|
|
25
|
+
// swapped without anyone seeing.
|
|
26
|
+
|
|
27
|
+
// At/below this diameter an AA disc reads as a muddy blob, so callers draw a
|
|
28
|
+
// crisp filled square instead.
|
|
29
|
+
public static const float SMALL_POINT_MAX_DIAMETER = 3.0;
|
|
30
|
+
|
|
31
|
+
// Quad scale factor that grows the disc's bounding quad by the ramp's reach so
|
|
32
|
+
// its faded edge isn't clipped. localPos is scaled by the same factor so the
|
|
33
|
+
// disc boundary stays at distance 1 (see discAlpha). `max(radiusPx, 0.5)` keeps
|
|
34
|
+
// sub-pixel points from exploding the expansion.
|
|
35
|
+
public float discExpand(float radiusPx, float dpr) {
|
|
36
|
+
return (radiusPx + aaHalfPx(dpr)) / max(radiusPx, 0.5);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Pixel-grid-snapped top-left of the crisp small-point square, on one axis. The
|
|
40
|
+
// snap (round-half-up) is what makes the square's fill land on whole pixels
|
|
41
|
+
// instead of AA-blurring across columns/rows — it is SHAPE_SOLID with no
|
|
42
|
+
// fragment-side AA, so an unsnapped center reads as a fuzzy blob.
|
|
43
|
+
//
|
|
44
|
+
// Per-axis and scalar so `//! js-export` can lift it: Canvas2D's
|
|
45
|
+
// `appendPointMarker` calls this for x and y rather than re-deriving the snap
|
|
46
|
+
// (adr-051). The float2 wrapper below keeps the shader's call site unchanged.
|
|
47
|
+
public float crispSquareTopLeftPx(float centerPx, float diameterPx) {
|
|
48
|
+
return floor(centerPx - diameterPx * 0.5 + 0.5);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Screen-px position of quad corner `q` (each component 0 or 1) for the crisp
|
|
52
|
+
// small-point square, given the marker center and diameter in px.
|
|
53
|
+
public float2 crispSquareCornerPx(float2 centerPx, float diameterPx, float2 q) {
|
|
54
|
+
float2 topLeft = float2(
|
|
55
|
+
crispSquareTopLeftPx(centerPx.x, diameterPx),
|
|
56
|
+
crispSquareTopLeftPx(centerPx.y, diameterPx)
|
|
57
|
+
);
|
|
58
|
+
return topLeft + q * diameterPx;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The same corner in clip space, for a marker whose centre arrives as a clip x
|
|
62
|
+
// and a screen y — which is how both callers hold it, since x comes off the hp
|
|
63
|
+
// bp conversion and y off a score scale.
|
|
64
|
+
//
|
|
65
|
+
// The round trip is the point: the snap has to happen in px, so the caller
|
|
66
|
+
// converts out and back, and doing that in each of the two shaders is two
|
|
67
|
+
// chances to convert one of the four edges differently. `viewportWidth` and
|
|
68
|
+
// `canvasHeight` are whatever spans clip [-1,1] covers for the caller.
|
|
69
|
+
public float4 crispSquareCornerClip(
|
|
70
|
+
float centerClipX, float centerYPx, float diameterPx, float2 q,
|
|
71
|
+
float viewportWidth, float canvasHeight
|
|
72
|
+
) {
|
|
73
|
+
float2 centerPx = float2(clipXToPx(centerClipX, viewportWidth), centerYPx);
|
|
74
|
+
float2 vPx = crispSquareCornerPx(centerPx, diameterPx, q);
|
|
75
|
+
return float4(pxToClipX(vPx.x, viewportWidth),
|
|
76
|
+
yPxToClipY(vPx.y, canvasHeight), 0.0, 1.0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Disc SDF, positive inside. localPos is the expanded quad-local coord
|
|
80
|
+
// (-expand..expand); the disc boundary sits at distance 1, so the SDF is
|
|
81
|
+
// `1 - length`. Exposed separately for shaders that select between several
|
|
82
|
+
// glyph SDFs and take the AA derivative once afterwards — WGSL only allows
|
|
83
|
+
// derivatives in uniform control flow, so they can't call discAlpha inside a
|
|
84
|
+
// branch on a varying.
|
|
85
|
+
public float discCoverage(float2 localPos) {
|
|
86
|
+
return 1.0 - length(localPos);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Antialiased disc coverage → alpha, for callers that draw only a disc and can
|
|
90
|
+
// take the derivative inline. Only call from uniform control flow.
|
|
91
|
+
public float discAlpha(float2 localPos) {
|
|
92
|
+
return glyphEdgeAlpha(discCoverage(localPos));
|
|
93
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// AUTO-GENERATED by packages/shader-tools/src/shader-codegen from rowRect.slang.
|
|
2
|
+
// Do not edit. Run `pnpm gen:shaders` to regenerate.
|
|
3
|
+
|
|
4
|
+
// Scalar twins of rowRect.slang, transliterated from slangc's WGSL so
|
|
5
|
+
// the Canvas2D and SVG paths run the shader's own math. See adr-051.
|
|
6
|
+
|
|
7
|
+
function _max(a: number, b: number) {
|
|
8
|
+
return b > a || Number.isNaN(a) ? b : a
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function drawnRowHeightPx(rowHeight: number, rowProportion: number): number {
|
|
12
|
+
return _max((rowHeight * rowProportion), 1.0)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function rowBandOffsetPx(rowHeight: number, rowProportion: number): number {
|
|
16
|
+
return ((rowHeight - drawnRowHeightPx(rowHeight, rowProportion)) * 0.5)
|
|
17
|
+
}
|