@jbrowse/render-core 4.3.0 → 5.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/RenderCanvas.d.ts +2 -2
- package/esm/RenderLifecycleMixin.d.ts +52 -20
- package/esm/RenderLifecycleMixin.js +89 -23
- package/esm/canvas2dUtils.d.ts +90 -23
- package/esm/canvas2dUtils.js +119 -36
- package/esm/colorRampLut.d.ts +5 -5
- package/esm/colorRampLut.js +6 -6
- package/esm/computePipeline.d.ts +14 -0
- package/esm/computePipeline.js +62 -0
- package/esm/contractReports.d.ts +54 -0
- package/esm/contractReports.js +112 -0
- package/esm/coverageBand.d.ts +13 -8
- package/esm/coverageBand.js +15 -5
- package/esm/coverageBandBuffers.d.ts +2 -9
- package/esm/coverageBandBuffers.js +1 -13
- package/esm/createRenderingBackend.d.ts +14 -18
- package/esm/createRenderingBackend.js +4 -2
- package/esm/displayPhase.d.ts +83 -104
- package/esm/displayPhase.js +42 -82
- package/esm/encodeMemo.d.ts +14 -0
- package/esm/encodeMemo.js +48 -0
- package/esm/gpuContextLostError.d.ts +30 -0
- package/esm/gpuContextLostError.js +40 -0
- package/esm/gpuDevice.d.ts +29 -10
- package/esm/gpuDevice.js +35 -16
- package/esm/graphicsCapabilities.d.ts +27 -19
- package/esm/graphicsCapabilities.js +40 -18
- package/esm/hal/createHal.d.ts +4 -5
- package/esm/hal/createHal.js +14 -4
- package/esm/hal/deviceGpuCache.d.ts +25 -30
- package/esm/hal/deviceGpuCache.js +88 -97
- package/esm/hal/gpuHalBase.d.ts +5 -4
- package/esm/hal/gpuHalBase.js +25 -14
- package/esm/hal/index.d.ts +2 -2
- package/esm/hal/index.js +1 -1
- package/esm/hal/mockHal.d.ts +15 -10
- package/esm/hal/mockHal.js +31 -18
- package/esm/hal/oomReporter.d.ts +7 -0
- package/esm/hal/oomReporter.js +10 -0
- package/esm/hal/passIds.d.ts +8 -7
- package/esm/hal/passIds.js +11 -10
- package/esm/hal/regionRegistry.d.ts +0 -1
- package/esm/hal/regionRegistry.js +3 -15
- package/esm/hal/types.d.ts +55 -18
- package/esm/hal/webgl2Hal.d.ts +23 -6
- package/esm/hal/webgl2Hal.js +161 -155
- package/esm/hal/webgpuHal.d.ts +29 -13
- package/esm/hal/webgpuHal.js +221 -153
- package/esm/installUpload.d.ts +16 -3
- package/esm/installUpload.js +13 -41
- package/esm/instanceCache.d.ts +10 -7
- package/esm/instanceCache.js +14 -15
- package/esm/instancePass.d.ts +2 -1
- package/esm/mapUploadSync.d.ts +2 -10
- package/esm/mapUploadSync.js +2 -3
- package/esm/marks/barMark.d.ts +30 -0
- package/esm/marks/barMark.js +149 -0
- package/esm/marks/colorFill.d.ts +17 -0
- package/esm/marks/colorFill.js +26 -0
- package/esm/marks/drawAgainstHit.d.ts +113 -0
- package/esm/marks/drawAgainstHit.js +407 -0
- package/esm/marks/ellipseDistance.d.ts +10 -0
- package/esm/marks/ellipseDistance.js +136 -0
- package/esm/marks/glyphPaint.d.ts +36 -0
- package/esm/marks/glyphPaint.js +77 -0
- package/esm/marks/index.d.ts +33 -0
- package/esm/marks/index.js +20 -0
- package/esm/marks/lineMark.d.ts +37 -0
- package/esm/marks/lineMark.js +416 -0
- package/esm/marks/linkMark.d.ts +76 -0
- package/esm/marks/linkMark.js +585 -0
- package/esm/marks/markBackend.d.ts +118 -0
- package/esm/marks/markBackend.js +196 -0
- package/esm/marks/markHit.d.ts +32 -0
- package/esm/marks/markHit.js +61 -0
- package/esm/marks/markInk.d.ts +18 -0
- package/esm/marks/markInk.js +71 -0
- package/esm/marks/markPaint.d.ts +14 -0
- package/esm/marks/markPaint.js +32 -0
- package/esm/marks/markPlan.d.ts +25 -0
- package/esm/marks/markPlan.js +20 -0
- package/esm/marks/markRamp.d.ts +72 -0
- package/esm/marks/markRamp.js +197 -0
- package/esm/marks/nearestMarkHit.d.ts +55 -0
- package/esm/marks/nearestMarkHit.js +158 -0
- package/esm/marks/pathRecorder.d.ts +14 -0
- package/esm/marks/pathRecorder.js +80 -0
- package/esm/marks/pointMark.d.ts +42 -0
- package/esm/marks/pointMark.js +125 -0
- package/esm/marks/rowLane.d.ts +39 -0
- package/esm/marks/rowLane.js +41 -0
- package/esm/marks/rowSpanIndex.d.ts +29 -0
- package/esm/marks/rowSpanIndex.js +141 -0
- package/esm/marks/rowTable.d.ts +41 -0
- package/esm/marks/rowTable.js +75 -0
- package/esm/marks/ruleMark.d.ts +21 -0
- package/esm/marks/ruleMark.js +91 -0
- package/esm/marks/spanMark.d.ts +42 -0
- package/esm/marks/spanMark.js +81 -0
- package/esm/marks/types.d.ts +261 -0
- package/esm/marks/types.js +175 -0
- package/esm/marks/valueScale.d.ts +23 -0
- package/esm/marks/valueScale.js +11 -0
- package/esm/namedReactions.d.ts +3 -4
- package/esm/namedReactions.js +3 -4
- package/esm/perRegionRenderingBackend.d.ts +21 -3
- package/esm/perRegionRenderingBackend.js +37 -3
- package/esm/regionDataMap.d.ts +2 -2
- package/esm/regionDataMap.js +25 -23
- package/esm/renderBlock.d.ts +14 -0
- package/esm/renderBlock.js +25 -0
- package/esm/renderingBackendBase.d.ts +20 -14
- package/esm/renderingBackendBase.js +12 -7
- package/esm/scoreScale.d.ts +27 -0
- package/esm/scoreScale.js +88 -0
- package/esm/shaders/barMark.generated.d.ts +3 -0
- package/esm/shaders/barMark.generated.js +7 -0
- package/esm/shaders/barMark.glsl.generated.d.ts +2 -0
- package/esm/shaders/barMark.glsl.generated.js +4 -0
- package/esm/shaders/barMark.iface.generated.d.ts +90 -0
- package/esm/shaders/barMark.iface.generated.js +205 -0
- package/esm/shaders/barMark.wgsl.generated.d.ts +1 -0
- package/esm/shaders/barMark.wgsl.generated.js +3 -0
- package/esm/shaders/capsule.js.generated.d.ts +1 -0
- package/esm/shaders/capsule.js.generated.js +11 -0
- package/esm/shaders/clipStrip.generated.d.ts +2 -0
- package/esm/shaders/clipStrip.generated.js +4 -0
- package/esm/shaders/clipStrip.js.generated.d.ts +1 -0
- package/esm/shaders/clipStrip.js.generated.js +21 -0
- package/esm/shaders/colorRampLut.js.generated.d.ts +1 -0
- package/esm/shaders/colorRampLut.js.generated.js +17 -0
- package/esm/shaders/coverageBar.generated.d.ts +2 -3
- package/esm/shaders/coverageBar.generated.js +4 -3
- package/esm/shaders/coverageBar.glsl.generated.d.ts +2 -0
- package/esm/shaders/coverageBar.glsl.generated.js +4 -0
- package/esm/shaders/coverageBar.iface.generated.js +1 -1
- package/esm/shaders/coverageBar.wgsl.generated.d.ts +1 -0
- package/esm/shaders/coverageBar.wgsl.generated.js +3 -0
- package/esm/shaders/coverageClip.generated.d.ts +3 -0
- package/esm/shaders/coverageClip.generated.js +7 -0
- package/esm/shaders/coverageClip.glsl.generated.d.ts +2 -0
- package/esm/shaders/coverageClip.glsl.generated.js +4 -0
- package/esm/shaders/coverageClip.iface.generated.d.ts +85 -0
- package/esm/shaders/coverageClip.iface.generated.js +114 -0
- package/esm/shaders/coverageClip.wgsl.generated.d.ts +1 -0
- package/esm/shaders/coverageClip.wgsl.generated.js +3 -0
- package/esm/shaders/coverageIndicator.generated.d.ts +2 -3
- package/esm/shaders/coverageIndicator.generated.js +4 -3
- package/esm/shaders/coverageIndicator.glsl.generated.d.ts +2 -0
- package/esm/shaders/coverageIndicator.glsl.generated.js +4 -0
- package/esm/shaders/coverageIndicator.iface.generated.js +1 -1
- package/esm/shaders/coverageIndicator.wgsl.generated.d.ts +1 -0
- package/esm/shaders/coverageIndicator.wgsl.generated.js +3 -0
- package/esm/shaders/coverageInterbase.generated.d.ts +2 -3
- package/esm/shaders/coverageInterbase.generated.js +4 -3
- package/esm/shaders/coverageInterbase.glsl.generated.d.ts +2 -0
- package/esm/shaders/coverageInterbase.glsl.generated.js +4 -0
- package/esm/shaders/coverageInterbase.iface.generated.js +1 -1
- package/esm/shaders/coverageInterbase.wgsl.generated.d.ts +1 -0
- package/esm/shaders/coverageInterbase.wgsl.generated.js +3 -0
- package/esm/shaders/coverageMod.generated.d.ts +2 -3
- package/esm/shaders/coverageMod.generated.js +4 -3
- package/esm/shaders/coverageMod.glsl.generated.d.ts +2 -0
- package/esm/shaders/coverageMod.glsl.generated.js +4 -0
- package/esm/shaders/coverageMod.iface.generated.js +1 -1
- package/esm/shaders/coverageMod.wgsl.generated.d.ts +1 -0
- package/esm/shaders/coverageMod.wgsl.generated.js +3 -0
- package/esm/shaders/coverageSnp.generated.d.ts +2 -3
- package/esm/shaders/coverageSnp.generated.js +4 -3
- package/esm/shaders/coverageSnp.glsl.generated.d.ts +2 -0
- package/esm/shaders/coverageSnp.glsl.generated.js +4 -0
- package/esm/shaders/coverageSnp.iface.generated.js +1 -1
- package/esm/shaders/coverageSnp.wgsl.generated.d.ts +1 -0
- package/esm/shaders/coverageSnp.wgsl.generated.js +3 -0
- package/esm/shaders/curveDistance.js.generated.d.ts +5 -0
- package/esm/shaders/curveDistance.js.generated.js +87 -0
- package/esm/shaders/hpmath.js.generated.d.ts +2 -1
- package/esm/shaders/hpmath.js.generated.js +5 -2
- package/esm/shaders/lineCenterMark.generated.d.ts +3 -0
- package/esm/shaders/lineCenterMark.generated.js +7 -0
- package/esm/shaders/lineCenterMark.glsl.generated.d.ts +2 -0
- package/esm/shaders/lineCenterMark.glsl.generated.js +4 -0
- package/esm/shaders/lineCenterMark.iface.generated.d.ts +104 -0
- package/esm/shaders/lineCenterMark.iface.generated.js +237 -0
- package/esm/shaders/lineCenterMark.wgsl.generated.d.ts +1 -0
- package/esm/shaders/lineCenterMark.wgsl.generated.js +3 -0
- package/esm/shaders/lineCommon.generated.d.ts +2 -0
- package/esm/shaders/lineCommon.generated.js +4 -0
- package/esm/shaders/lineStepMark.generated.d.ts +3 -0
- package/esm/shaders/lineStepMark.generated.js +7 -0
- package/esm/shaders/lineStepMark.glsl.generated.d.ts +2 -0
- package/esm/shaders/lineStepMark.glsl.generated.js +4 -0
- package/esm/shaders/lineStepMark.iface.generated.d.ts +98 -0
- package/esm/shaders/lineStepMark.iface.generated.js +225 -0
- package/esm/shaders/lineStepMark.wgsl.generated.d.ts +1 -0
- package/esm/shaders/lineStepMark.wgsl.generated.js +3 -0
- package/esm/shaders/linkMark.consts.generated.d.ts +14 -0
- package/esm/shaders/linkMark.consts.generated.js +16 -0
- package/esm/shaders/linkMark.generated.d.ts +4 -0
- package/esm/shaders/linkMark.generated.js +8 -0
- package/esm/shaders/linkMark.glsl.generated.d.ts +2 -0
- package/esm/shaders/linkMark.glsl.generated.js +4 -0
- package/esm/shaders/linkMark.iface.generated.d.ts +142 -0
- package/esm/shaders/linkMark.iface.generated.js +1829 -0
- package/esm/shaders/linkMark.js.generated.d.ts +7 -0
- package/esm/shaders/linkMark.js.generated.js +159 -0
- package/esm/shaders/linkMark.wgsl.generated.d.ts +1 -0
- package/esm/shaders/linkMark.wgsl.generated.js +3 -0
- package/esm/shaders/markColor.generated.d.ts +9 -0
- package/esm/shaders/markColor.generated.js +11 -0
- package/esm/shaders/pointGlyph.js.generated.d.ts +2 -0
- package/esm/shaders/pointGlyph.js.generated.js +15 -0
- package/esm/shaders/pointMark.consts.generated.d.ts +7 -0
- package/esm/shaders/pointMark.consts.generated.js +9 -0
- package/esm/shaders/pointMark.generated.d.ts +4 -0
- package/esm/shaders/pointMark.generated.js +8 -0
- package/esm/shaders/pointMark.glsl.generated.d.ts +2 -0
- package/esm/shaders/pointMark.glsl.generated.js +4 -0
- package/esm/shaders/pointMark.iface.generated.d.ts +98 -0
- package/esm/shaders/pointMark.iface.generated.js +219 -0
- package/esm/shaders/pointMark.js.generated.d.ts +3 -0
- package/esm/shaders/pointMark.js.generated.js +93 -0
- package/esm/shaders/pointMark.wgsl.generated.d.ts +1 -0
- package/esm/shaders/pointMark.wgsl.generated.js +3 -0
- package/esm/shaders/rowRect.generated.d.ts +0 -1
- package/esm/shaders/rowRect.generated.js +0 -1
- package/esm/shaders/rowTable.generated.d.ts +1 -0
- package/esm/shaders/rowTable.generated.js +3 -0
- package/esm/shaders/rowTable.js.generated.d.ts +5 -0
- package/esm/shaders/rowTable.js.generated.js +29 -0
- package/esm/shaders/scoreScale.generated.d.ts +2 -0
- package/esm/shaders/scoreScale.generated.js +4 -0
- package/esm/shaders/scoreScale.js.generated.d.ts +1 -0
- package/esm/shaders/scoreScale.js.generated.js +50 -23
- package/esm/shaders/spanMark.generated.d.ts +3 -0
- package/esm/shaders/spanMark.generated.js +7 -0
- package/esm/shaders/spanMark.glsl.generated.d.ts +2 -0
- package/esm/shaders/spanMark.glsl.generated.js +4 -0
- package/esm/shaders/spanMark.iface.generated.d.ts +81 -0
- package/esm/shaders/spanMark.iface.generated.js +215 -0
- package/esm/shaders/spanMark.wgsl.generated.d.ts +1 -0
- package/esm/shaders/spanMark.wgsl.generated.js +3 -0
- package/esm/sharedBackendKey.d.ts +18 -0
- package/esm/{keyedRenderingBackend.js → sharedBackendKey.js} +5 -0
- package/esm/slangPass.d.ts +18 -11
- package/esm/slangPass.js +12 -4
- package/esm/useGraphicsCapabilities.d.ts +1 -1
- package/esm/useGraphicsCapabilities.js +1 -1
- package/esm/useRenderingBackend.d.ts +13 -13
- package/esm/useRenderingBackend.js +58 -20
- package/esm/webgpuUtils.d.ts +3 -11
- package/esm/webgpuUtils.js +12 -33
- package/package.json +32 -7
- package/src/shaders/antialias.slang +46 -0
- package/src/shaders/arcHull.test.ts +267 -0
- package/src/shaders/barMark.generated.ts +11 -0
- package/src/shaders/barMark.glsl.generated.ts +6 -0
- package/src/shaders/barMark.iface.generated.ts +285 -0
- package/src/shaders/barMark.slang +167 -0
- package/src/shaders/barMark.wgsl.generated.ts +4 -0
- package/src/shaders/capsule.js.generated.ts +14 -0
- package/src/shaders/capsule.slang +12 -4
- package/src/shaders/clipStrip.generated.ts +6 -0
- package/src/shaders/clipStrip.js.generated.ts +22 -0
- package/src/shaders/clipStrip.slang +34 -0
- package/src/shaders/clipStrip.test.ts +39 -0
- package/src/shaders/colorPack.slang +5 -1
- package/src/shaders/colorRampLut.js.generated.ts +22 -0
- package/src/shaders/colorRampLut.slang +15 -1
- package/src/shaders/coverageBand.slang +64 -32
- package/src/shaders/coverageBar.generated.ts +6 -5
- package/src/shaders/coverageBar.glsl.generated.ts +6 -0
- package/src/shaders/coverageBar.iface.generated.ts +1 -1
- package/src/shaders/coverageBar.slang +10 -10
- package/src/shaders/coverageBar.wgsl.generated.ts +4 -0
- package/src/shaders/coverageClip.generated.ts +11 -0
- package/src/shaders/coverageClip.glsl.generated.ts +6 -0
- package/src/shaders/coverageClip.iface.generated.ts +172 -0
- package/src/shaders/coverageClip.slang +38 -0
- package/src/shaders/coverageClip.wgsl.generated.ts +4 -0
- package/src/shaders/coverageIndicator.generated.ts +6 -5
- package/src/shaders/coverageIndicator.glsl.generated.ts +6 -0
- package/src/shaders/coverageIndicator.iface.generated.ts +1 -1
- package/src/shaders/coverageIndicator.wgsl.generated.ts +4 -0
- package/src/shaders/coverageInterbase.generated.ts +6 -5
- package/src/shaders/coverageInterbase.glsl.generated.ts +6 -0
- package/src/shaders/coverageInterbase.iface.generated.ts +1 -1
- package/src/shaders/coverageInterbase.wgsl.generated.ts +4 -0
- package/src/shaders/coverageMod.generated.ts +6 -5
- package/src/shaders/coverageMod.glsl.generated.ts +6 -0
- package/src/shaders/coverageMod.iface.generated.ts +1 -1
- package/src/shaders/coverageMod.wgsl.generated.ts +4 -0
- package/src/shaders/coverageSnp.generated.ts +6 -5
- package/src/shaders/coverageSnp.glsl.generated.ts +6 -0
- package/src/shaders/coverageSnp.iface.generated.ts +1 -1
- package/src/shaders/coverageSnp.wgsl.generated.ts +4 -0
- package/src/shaders/curveDistance.js.generated.ts +91 -0
- package/src/shaders/curveDistance.slang +265 -0
- package/src/shaders/hpClampFloor.test.ts +87 -0
- package/src/shaders/hpmath.js.generated.ts +6 -2
- package/src/shaders/hpmath.slang +100 -48
- package/src/shaders/insetValueScale.test.ts +67 -0
- package/src/shaders/lineCenterMark.generated.ts +11 -0
- package/src/shaders/lineCenterMark.glsl.generated.ts +6 -0
- package/src/shaders/lineCenterMark.iface.generated.ts +328 -0
- package/src/shaders/lineCenterMark.slang +127 -0
- package/src/shaders/lineCenterMark.wgsl.generated.ts +4 -0
- package/src/shaders/lineCommon.generated.ts +6 -0
- package/src/shaders/lineCommon.slang +102 -0
- package/src/shaders/lineStepMark.generated.ts +11 -0
- package/src/shaders/lineStepMark.glsl.generated.ts +6 -0
- package/src/shaders/lineStepMark.iface.generated.ts +311 -0
- package/src/shaders/lineStepMark.slang +125 -0
- package/src/shaders/lineStepMark.wgsl.generated.ts +4 -0
- package/src/shaders/linkMark.consts.generated.ts +30 -0
- package/src/shaders/linkMark.generated.ts +12 -0
- package/src/shaders/linkMark.glsl.generated.ts +6 -0
- package/src/shaders/linkMark.iface.generated.ts +1954 -0
- package/src/shaders/linkMark.js.generated.ts +167 -0
- package/src/shaders/linkMark.slang +439 -0
- package/src/shaders/linkMark.wgsl.generated.ts +4 -0
- package/src/shaders/markColor.generated.ts +20 -0
- package/src/shaders/markColor.slang +128 -0
- package/src/shaders/pointGlyph.js.generated.ts +20 -0
- package/src/shaders/pointGlyph.slang +23 -7
- package/src/shaders/pointGlyphPad.test.ts +242 -0
- package/src/shaders/pointMark.consts.generated.ts +16 -0
- package/src/shaders/pointMark.generated.ts +12 -0
- package/src/shaders/pointMark.glsl.generated.ts +6 -0
- package/src/shaders/pointMark.iface.generated.ts +304 -0
- package/src/shaders/pointMark.js.generated.ts +99 -0
- package/src/shaders/pointMark.slang +227 -0
- package/src/shaders/pointMark.wgsl.generated.ts +4 -0
- package/src/shaders/rowRect.generated.ts +0 -2
- package/src/shaders/rowRect.slang +39 -58
- package/src/shaders/rowTable.generated.ts +4 -0
- package/src/shaders/rowTable.js.generated.ts +37 -0
- package/src/shaders/rowTable.slang +98 -0
- package/src/shaders/scoreScale.generated.ts +6 -0
- package/src/shaders/scoreScale.js.generated.ts +48 -23
- package/src/shaders/scoreScale.slang +49 -55
- package/src/shaders/sdEllipse.test.ts +211 -0
- package/src/shaders/spanMark.generated.ts +11 -0
- package/src/shaders/spanMark.glsl.generated.ts +6 -0
- package/src/shaders/spanMark.iface.generated.ts +289 -0
- package/src/shaders/spanMark.slang +77 -0
- package/src/shaders/spanMark.wgsl.generated.ts +4 -0
- package/src/shaders/valueScale.slang +26 -0
- package/src/shaders/wideCircleLeg.test.ts +85 -0
- package/esm/globalRenderingBackend.d.ts +0 -90
- package/esm/globalRenderingBackend.js +0 -53
- package/esm/keyedRenderingBackend.d.ts +0 -41
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { inkOnRect } from './markHit.js';
|
|
2
|
+
const IDENTITY = [1, 0, 0, 1, 0, 0];
|
|
3
|
+
/**
|
|
4
|
+
* A `MarkContext2D` that records every rect a painter fills, in paint order and
|
|
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
|
|
7
|
+
* path traced with `moveTo`/`lineTo`/`bezierCurveTo` and filled records its
|
|
8
|
+
* bounding box, the extent a shape whose hit test answers a polygon's box
|
|
9
|
+
* (the variant inversion triangle) is checked against; a curve contributes its control
|
|
10
|
+
* points, whose hull contains it.
|
|
11
|
+
*
|
|
12
|
+
* A **stroked** path records one rect per `moveTo`/`lineTo` edge, each the
|
|
13
|
+
* edge's bounding box widened by half the current `lineWidth` — the extent a
|
|
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
|
|
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
|
|
18
|
+
* ink either way.
|
|
19
|
+
*
|
|
20
|
+
* `save`/`restore` bracket the transform and the styles the way a real context
|
|
21
|
+
* does, and every recorded point goes through the CTM. The half-widths — a
|
|
22
|
+
* stroke's and a `strokeRect`'s — are measured AFTER it, so they are exact under
|
|
23
|
+
* a translation and approximate under a scale; no shape in tree strokes under
|
|
24
|
+
* one, and a rotated CTM records the ink's axis-aligned box rather than its
|
|
25
|
+
* silhouette.
|
|
26
|
+
*/
|
|
27
|
+
const CURVE_SEGMENTS = 64;
|
|
28
|
+
export function recordingContext() {
|
|
29
|
+
const calls = [];
|
|
30
|
+
const points = [];
|
|
31
|
+
const edges = [];
|
|
32
|
+
let m = [...IDENTITY];
|
|
33
|
+
const stack = [];
|
|
34
|
+
const px = (x, y) => m[0] * x + m[2] * y + m[4];
|
|
35
|
+
const py = (x, y) => m[1] * x + m[3] * y + m[5];
|
|
36
|
+
const pushPoint = (x, y) => {
|
|
37
|
+
points.push([px(x, y), py(x, y)]);
|
|
38
|
+
};
|
|
39
|
+
// The transformed box of an axis-aligned rect. An axis-aligned CTM scales the
|
|
40
|
+
// extents directly rather than differencing two mapped corners, so an
|
|
41
|
+
// untranslated rect records the width the painter asked for and not a value
|
|
42
|
+
// one ulp off it; only a rotation falls back to the four-corner hull.
|
|
43
|
+
const boxOf = (x, y, w, h) => {
|
|
44
|
+
if (m[1] === 0 && m[2] === 0) {
|
|
45
|
+
const x0 = px(x, y);
|
|
46
|
+
const y0 = py(x, y);
|
|
47
|
+
const dx = m[0] * w;
|
|
48
|
+
const dy = m[3] * h;
|
|
49
|
+
return {
|
|
50
|
+
x: Math.min(x0, x0 + dx),
|
|
51
|
+
y: Math.min(y0, y0 + dy),
|
|
52
|
+
w: Math.abs(dx),
|
|
53
|
+
h: Math.abs(dy),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const xs = [px(x, y), px(x + w, y), px(x, y + h), px(x + w, y + h)];
|
|
57
|
+
const ys = [py(x, y), py(x + w, y), py(x, y + h), py(x + w, y + h)];
|
|
58
|
+
const left = Math.min(...xs);
|
|
59
|
+
const top = Math.min(...ys);
|
|
60
|
+
return {
|
|
61
|
+
x: left,
|
|
62
|
+
y: top,
|
|
63
|
+
w: Math.max(...xs) - left,
|
|
64
|
+
h: Math.max(...ys) - top,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
const ctx = {
|
|
68
|
+
fillStyle: '',
|
|
69
|
+
strokeStyle: '',
|
|
70
|
+
lineWidth: 1,
|
|
71
|
+
lineCap: 'butt',
|
|
72
|
+
lineJoin: 'miter',
|
|
73
|
+
save() {
|
|
74
|
+
const { fillStyle, strokeStyle, lineWidth, lineCap } = this;
|
|
75
|
+
stack.push({ m: [...m], fillStyle, strokeStyle, lineWidth, lineCap });
|
|
76
|
+
},
|
|
77
|
+
restore() {
|
|
78
|
+
const saved = stack.pop();
|
|
79
|
+
if (saved) {
|
|
80
|
+
m = saved.m;
|
|
81
|
+
this.fillStyle = saved.fillStyle;
|
|
82
|
+
this.strokeStyle = saved.strokeStyle;
|
|
83
|
+
this.lineWidth = saved.lineWidth;
|
|
84
|
+
this.lineCap = saved.lineCap;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
translate(x, y) {
|
|
88
|
+
m = [m[0], m[1], m[2], m[3], px(x, y), py(x, y)];
|
|
89
|
+
},
|
|
90
|
+
scale(x, y) {
|
|
91
|
+
m = [m[0] * x, m[1] * x, m[2] * y, m[3] * y, m[4], m[5]];
|
|
92
|
+
},
|
|
93
|
+
rotate(angle) {
|
|
94
|
+
const c = Math.cos(angle);
|
|
95
|
+
const s = Math.sin(angle);
|
|
96
|
+
m = [
|
|
97
|
+
m[0] * c + m[2] * s,
|
|
98
|
+
m[1] * c + m[3] * s,
|
|
99
|
+
m[2] * c - m[0] * s,
|
|
100
|
+
m[3] * c - m[1] * s,
|
|
101
|
+
m[4],
|
|
102
|
+
m[5],
|
|
103
|
+
];
|
|
104
|
+
},
|
|
105
|
+
beginPath() {
|
|
106
|
+
points.length = 0;
|
|
107
|
+
edges.length = 0;
|
|
108
|
+
},
|
|
109
|
+
clip() { },
|
|
110
|
+
setLineDash(_segments) { },
|
|
111
|
+
moveTo(x, y) {
|
|
112
|
+
pushPoint(x, y);
|
|
113
|
+
},
|
|
114
|
+
lineTo(x, y) {
|
|
115
|
+
const from = points.at(-1);
|
|
116
|
+
pushPoint(x, y);
|
|
117
|
+
const to = points.at(-1);
|
|
118
|
+
if (from) {
|
|
119
|
+
edges.push([from[0], from[1], to[0], to[1]]);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
|
|
123
|
+
pushPoint(cp1x, cp1y);
|
|
124
|
+
pushPoint(cp2x, cp2y);
|
|
125
|
+
pushPoint(x, y);
|
|
126
|
+
},
|
|
127
|
+
arc(x, y, radius, start = 0, end = 2 * Math.PI) {
|
|
128
|
+
this.ellipse(x, y, radius, radius, 0, start, end);
|
|
129
|
+
},
|
|
130
|
+
// Flattened into edges, so a stroked curve records the boxes its
|
|
131
|
+
// segments cover the way a polyline does; a filled one still records its
|
|
132
|
+
// point hull.
|
|
133
|
+
ellipse(x, y, radiusX, radiusY, _rotation = 0, start = 0, end = 2 * Math.PI) {
|
|
134
|
+
for (let k = 0; k <= CURVE_SEGMENTS; k++) {
|
|
135
|
+
const a = start + ((end - start) * k) / CURVE_SEGMENTS;
|
|
136
|
+
this.lineTo(x + radiusX * Math.cos(a), y + radiusY * Math.sin(a));
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
closePath() { },
|
|
140
|
+
fill() {
|
|
141
|
+
if (points.length > 0) {
|
|
142
|
+
const xs = points.map(p => p[0]);
|
|
143
|
+
const ys = points.map(p => p[1]);
|
|
144
|
+
const x = Math.min(...xs);
|
|
145
|
+
const y = Math.min(...ys);
|
|
146
|
+
calls.push({
|
|
147
|
+
x,
|
|
148
|
+
y,
|
|
149
|
+
w: Math.max(...xs) - x,
|
|
150
|
+
h: Math.max(...ys) - y,
|
|
151
|
+
fillStyle: this.fillStyle,
|
|
152
|
+
});
|
|
153
|
+
points.length = 0;
|
|
154
|
+
edges.length = 0;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
stroke() {
|
|
158
|
+
const half = this.lineWidth / 2;
|
|
159
|
+
for (const [ax, ay, bx, by] of edges) {
|
|
160
|
+
const x = Math.min(ax, bx) - half;
|
|
161
|
+
const y = Math.min(ay, by) - half;
|
|
162
|
+
calls.push({
|
|
163
|
+
x,
|
|
164
|
+
y,
|
|
165
|
+
w: Math.max(ax, bx) + half - x,
|
|
166
|
+
h: Math.max(ay, by) + half - y,
|
|
167
|
+
fillStyle: this.strokeStyle,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
points.length = 0;
|
|
171
|
+
edges.length = 0;
|
|
172
|
+
},
|
|
173
|
+
rect(x, y, w, h) {
|
|
174
|
+
calls.push({ ...boxOf(x, y, w, h), fillStyle: this.fillStyle });
|
|
175
|
+
},
|
|
176
|
+
fillRect(x, y, w, h) {
|
|
177
|
+
calls.push({ ...boxOf(x, y, w, h), fillStyle: this.fillStyle });
|
|
178
|
+
},
|
|
179
|
+
strokeRect(x, y, w, h) {
|
|
180
|
+
const half = this.lineWidth / 2;
|
|
181
|
+
const box = boxOf(x, y, w, h);
|
|
182
|
+
calls.push({
|
|
183
|
+
x: box.x - half,
|
|
184
|
+
y: box.y - half,
|
|
185
|
+
w: box.w + this.lineWidth,
|
|
186
|
+
h: box.h + this.lineWidth,
|
|
187
|
+
fillStyle: this.strokeStyle,
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
return { ctx, calls };
|
|
192
|
+
}
|
|
193
|
+
function contains(r, x, y, slack = 0) {
|
|
194
|
+
return (x >= r.x - slack &&
|
|
195
|
+
x <= r.x + r.w + slack &&
|
|
196
|
+
y >= r.y - slack &&
|
|
197
|
+
y <= r.y + r.h + slack);
|
|
198
|
+
}
|
|
199
|
+
function unionBox(rs) {
|
|
200
|
+
let box;
|
|
201
|
+
for (const r of rs) {
|
|
202
|
+
box = box
|
|
203
|
+
? {
|
|
204
|
+
x: Math.min(box.x, r.x),
|
|
205
|
+
y: Math.min(box.y, r.y),
|
|
206
|
+
w: Math.max(box.x + box.w, r.x + r.w) - Math.min(box.x, r.x),
|
|
207
|
+
h: Math.max(box.y + box.h, r.y + r.h) - Math.min(box.y, r.y),
|
|
208
|
+
}
|
|
209
|
+
: { x: r.x, y: r.y, w: r.w, h: r.h };
|
|
210
|
+
}
|
|
211
|
+
return box;
|
|
212
|
+
}
|
|
213
|
+
// The float slack the two distance comparisons take. Both sides are px
|
|
214
|
+
// arithmetic over the same projection, so a real disagreement is orders of
|
|
215
|
+
// magnitude above this and a rounding one is below it.
|
|
216
|
+
const EPS = 1e-9;
|
|
217
|
+
const MAX_VIOLATIONS = 10;
|
|
218
|
+
/**
|
|
219
|
+
* The ink clause of {@link sweepMarkAgainstHit} alone, for a mark with no
|
|
220
|
+
* `hitNearest`: `boxes[i]` is what the painter recorded for instance `i`.
|
|
221
|
+
*/
|
|
222
|
+
export function inkViolations(mark, channels, block, state, boxes, inkSlackPx) {
|
|
223
|
+
const violations = [];
|
|
224
|
+
if (!mark.ink) {
|
|
225
|
+
return violations;
|
|
226
|
+
}
|
|
227
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
228
|
+
const r = mark.ink(channels, block, state, i);
|
|
229
|
+
const b = boxes[i];
|
|
230
|
+
if (r && !b) {
|
|
231
|
+
violations.push(`${i} painted nothing but has ink`);
|
|
232
|
+
}
|
|
233
|
+
else if (b && !r) {
|
|
234
|
+
violations.push(`${i} painted but has no ink`);
|
|
235
|
+
}
|
|
236
|
+
else if (b && r) {
|
|
237
|
+
const gaps = [
|
|
238
|
+
b.x - r.left,
|
|
239
|
+
r.left + r.width - (b.x + b.w),
|
|
240
|
+
b.y - r.top,
|
|
241
|
+
r.top + r.height - (b.y + b.h),
|
|
242
|
+
];
|
|
243
|
+
if (gaps.some(g => g < -EPS)) {
|
|
244
|
+
violations.push(`${i} painted (${b.x}, ${b.y}, ${b.w}, ${b.h}) outside its ink (${r.left}, ${r.top}, ${r.width}, ${r.height})`);
|
|
245
|
+
}
|
|
246
|
+
else if (gaps.some(g => g > inkSlackPx + EPS)) {
|
|
247
|
+
violations.push(`${i}'s ink (${r.left}, ${r.top}, ${r.width}, ${r.height}) is more than ${inkSlackPx}px outside its painting (${b.x}, ${b.y}, ${b.w}, ${b.h})`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (violations.length >= MAX_VIOLATIONS) {
|
|
251
|
+
return violations;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return violations;
|
|
255
|
+
}
|
|
256
|
+
function hitViolations(hit, boxes, x, y) {
|
|
257
|
+
const violations = [];
|
|
258
|
+
const box = hit && boxes[hit.index];
|
|
259
|
+
if (hit && !box) {
|
|
260
|
+
violations.push(`(${x}, ${y}) answered ${hit.index}, which painted nothing`);
|
|
261
|
+
}
|
|
262
|
+
else if (hit && box) {
|
|
263
|
+
if (!contains(box, hit.x, hit.y, EPS)) {
|
|
264
|
+
violations.push(`(${x}, ${y}) answered ${hit.index} with ink at (${hit.x}, ${hit.y}), off its painting`);
|
|
265
|
+
}
|
|
266
|
+
const toBox = inkOnRect(x, y, box.x, box.y, box.w, box.h).distSq;
|
|
267
|
+
if (hit.distSq < toBox - EPS) {
|
|
268
|
+
violations.push(`(${x}, ${y}) answered ${hit.index} at ${hit.distSq}, nearer than its painting (${toBox})`);
|
|
269
|
+
}
|
|
270
|
+
const toPoint = (x - hit.x) ** 2 + (y - hit.y) ** 2;
|
|
271
|
+
if (Math.abs(hit.distSq - toPoint) > EPS * Math.max(1, toPoint)) {
|
|
272
|
+
violations.push(`(${x}, ${y}) answered ${hit.index} at ${hit.distSq}, which is not the distance to the ink it named (${toPoint})`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return violations;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The draw-against-hit gate: paint the block, then walk it in `step` px and
|
|
279
|
+
* hold `hitNearest` to what the painter actually put on the canvas.
|
|
280
|
+
*
|
|
281
|
+
* **What the painter drew for instance `i` has to be attributed to `i`,** and
|
|
282
|
+
* there are two ways to get that. A mark whose every instance is one `fillRect`
|
|
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
|
|
285
|
+
* instance the view has scrolled past (`cell`'s off-canvas rows), cannot be read
|
|
286
|
+
* that way at all — the batch has fewer rects than instances, or the wrong ones.
|
|
287
|
+
* `sliceOne` is the way in for those: hand back one instance's channels and the
|
|
288
|
+
* sweep paints it alone, so the attribution holds by construction and a culled
|
|
289
|
+
* instance is visibly an empty painting.
|
|
290
|
+
*
|
|
291
|
+
* Four claims, all of them one-directional and true of every mark:
|
|
292
|
+
*
|
|
293
|
+
* - an instance that painted nothing is never the answer,
|
|
294
|
+
* - `hit.x`/`hit.y` — where the mark says its ink is — lies inside the box the
|
|
295
|
+
* painter drew for the instance the hit names,
|
|
296
|
+
* - `hit.distSq` is no smaller than the distance to that box, since the box
|
|
297
|
+
* contains the ink and cannot be farther than it,
|
|
298
|
+
* - `hit.distSq` IS the distance to `hit.x`/`hit.y`, which is `MarkHit`'s own
|
|
299
|
+
* contract rather than a fact about the painting: a mark that answers a
|
|
300
|
+
* distance its own reported point does not support has two spellings of where
|
|
301
|
+
* its ink is, and the nearest-wins walk is resolved by the one the caller
|
|
302
|
+
* cannot see.
|
|
303
|
+
*
|
|
304
|
+
* The fifth is the containment rule: **a point the rule contains answers the
|
|
305
|
+
* nearest painted instance it contains**, the last-painted where tied, since the
|
|
306
|
+
* sweep hands candidates back to front. The default rule is the painted box, so
|
|
307
|
+
* a point on a box answers that box, and off every box a hit target wider than
|
|
308
|
+
* its ink may still answer. The default holds only where the box IS the hit
|
|
309
|
+
* target — every `fillRect` mark, and `cell`'s inversion triangle, which answers
|
|
310
|
+
* as its bounding box on purpose — and not for a glyph, whose box is a superset
|
|
311
|
+
* of its ink, so it runs exactly when the batch painted one rect per instance. A
|
|
312
|
+
* mark whose hit rule is not its box (the pileup's bp containment) passes
|
|
313
|
+
* `contains`, which is then the whole rule: the sweep covers the whole canvas,
|
|
314
|
+
* and a point the rule contains nowhere answers nothing.
|
|
315
|
+
*
|
|
316
|
+
* A mark declaring `ink` is held to one more clause, per instance: the box the
|
|
317
|
+
* painter recorded for `i` lies inside `ink(i)`, and each edge of `ink(i)` lies
|
|
318
|
+
* within `inkSlackPx` (default 1) of the painting's — so the rect is tight to
|
|
319
|
+
* the ink within a pixel on every side, which admits a seam overdraw or a pixel
|
|
320
|
+
* snap and rejects a rect that is the wrong row, the wrong width or the whole
|
|
321
|
+
* canvas. An instance that painted nothing must answer no ink, and one with ink
|
|
322
|
+
* must have painted.
|
|
323
|
+
*
|
|
324
|
+
* Returns the violations rather than asserting, so a test reads as
|
|
325
|
+
* `expect(sweep(...)).toEqual([])`. `maxDistSq` is the bound the hit test is
|
|
326
|
+
* asked with, `Infinity` unless the caller's differs.
|
|
327
|
+
*/
|
|
328
|
+
export function sweepMarkAgainstHit(mark, channels, block, state, { maxDistSq = Infinity, step = 0.5, sliceOne, inkSlackPx = 1, contains: rule, } = {}) {
|
|
329
|
+
const { count } = channels;
|
|
330
|
+
const paint = (c) => {
|
|
331
|
+
const { ctx, calls } = recordingContext();
|
|
332
|
+
mark.paintBlock(ctx, c, block, state);
|
|
333
|
+
return calls;
|
|
334
|
+
};
|
|
335
|
+
const rects = paint(channels);
|
|
336
|
+
const perRect = rects.length === count;
|
|
337
|
+
if (!perRect && !sliceOne) {
|
|
338
|
+
return [`painted ${rects.length} rects for ${count} instances`];
|
|
339
|
+
}
|
|
340
|
+
const boxes = sliceOne
|
|
341
|
+
? Array.from({ length: count }, (_, i) => unionBox(paint(sliceOne(channels, i))))
|
|
342
|
+
: rects.map(r => ({ x: r.x, y: r.y, w: r.w, h: r.h }));
|
|
343
|
+
let yMin = Infinity;
|
|
344
|
+
let yMax = -Infinity;
|
|
345
|
+
for (const b of boxes) {
|
|
346
|
+
if (b) {
|
|
347
|
+
yMin = Math.min(yMin, b.y);
|
|
348
|
+
yMax = Math.max(yMax, b.y + b.h);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (yMin === Infinity) {
|
|
352
|
+
return [`painted nothing for ${count} instances`];
|
|
353
|
+
}
|
|
354
|
+
const violations = inkViolations(mark, channels, block, state, boxes, inkSlackPx);
|
|
355
|
+
if (violations.length >= MAX_VIOLATIONS) {
|
|
356
|
+
return violations;
|
|
357
|
+
}
|
|
358
|
+
if (!mark.hitNearest) {
|
|
359
|
+
return ['no hit test'];
|
|
360
|
+
}
|
|
361
|
+
const claims = rule ??
|
|
362
|
+
(perRect
|
|
363
|
+
? (k, x, y) => contains(boxes[k], x, y)
|
|
364
|
+
: undefined);
|
|
365
|
+
const candidates = Array.from({ length: count }, (_, k) => count - 1 - k);
|
|
366
|
+
const x0 = Math.min(block.screenStartPx, block.screenEndPx) - 1;
|
|
367
|
+
const x1 = Math.max(block.screenStartPx, block.screenEndPx) + 1;
|
|
368
|
+
const y0 = rule ? -1 : Math.floor(yMin) - 1;
|
|
369
|
+
const y1 = rule ? state.canvasHeight + 1 : yMax + 1;
|
|
370
|
+
let answered = 0;
|
|
371
|
+
for (let y = y0; y <= y1; y += step) {
|
|
372
|
+
for (let x = x0; x <= x1; x += step) {
|
|
373
|
+
let expected = -1;
|
|
374
|
+
let expectedDistSq = maxDistSq;
|
|
375
|
+
if (claims) {
|
|
376
|
+
for (const k of candidates) {
|
|
377
|
+
const b = boxes[k];
|
|
378
|
+
if (b && claims(k, x, y)) {
|
|
379
|
+
const distSq = inkOnRect(x, y, b.x, b.y, b.w, b.h).distSq;
|
|
380
|
+
if (expected === -1
|
|
381
|
+
? distSq < maxDistSq
|
|
382
|
+
: distSq < expectedDistSq - EPS) {
|
|
383
|
+
expected = k;
|
|
384
|
+
expectedDistSq = distSq;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const hit = mark.hitNearest(channels, block, state, x, y, candidates, maxDistSq);
|
|
390
|
+
if (hit) {
|
|
391
|
+
answered++;
|
|
392
|
+
}
|
|
393
|
+
violations.push(...hitViolations(hit, boxes, x, y));
|
|
394
|
+
if (expected === -1
|
|
395
|
+
? rule !== undefined && hit !== undefined
|
|
396
|
+
: hit?.index !== expected) {
|
|
397
|
+
violations.push(`(${x}, ${y}) should answer ${expected === -1 ? 'nothing' : expected}, answered ${hit?.index ?? 'nothing'}`);
|
|
398
|
+
}
|
|
399
|
+
if (violations.length >= MAX_VIOLATIONS) {
|
|
400
|
+
return violations;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return answered === 0 && violations.length === 0
|
|
405
|
+
? [`no swept point answered any of ${count} instances`]
|
|
406
|
+
: violations;
|
|
407
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function ellipseDistance(px: number, py: number, rx: number, ry: number): number;
|
|
2
|
+
/**
|
|
3
|
+
* The point of the ellipse nearest (px, py), in the ellipse's own frame, and
|
|
4
|
+
* how far it is: what a hit test hands back as the ink it found.
|
|
5
|
+
*/
|
|
6
|
+
export declare function ellipseNearest(px: number, py: number, rx: number, ry: number): {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
dist: number;
|
|
10
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Exact distance from a point to the axis-aligned ellipse |p/ab| = 1 — Inigo
|
|
2
|
+
// Quilez's closed-form solve of the quartic, in float64.
|
|
3
|
+
//
|
|
4
|
+
// THIS IS A SECOND COPY of `sdEllipse` in curveDistance.slang, and the header
|
|
5
|
+
// there carries the argument for why it is not generated instead. The short
|
|
6
|
+
// version: making it liftable also enrolls it in `check-shader-oracle`, which
|
|
7
|
+
// referees the float64 twin against slangc's float32 C++ — and a quartic solve
|
|
8
|
+
// is exactly the shape that comparison cannot referee, because float32 loses
|
|
9
|
+
// the solver before the transliteration is in question.
|
|
10
|
+
//
|
|
11
|
+
// So the two copies are NOT held together by parity with each other. What has to
|
|
12
|
+
// agree is which ellipse, and that is the radii pair `linkMark.slang` lifts
|
|
13
|
+
// (`linkRadiiPx`), which both the shader and this file's callers read. Given the same two radii, this is geometry with one right answer, and
|
|
14
|
+
// `ellipseDistance.test.ts` pins it against a brute-force minimum over the
|
|
15
|
+
// parametric angle — a stronger check than agreeing with a float32 evaluation
|
|
16
|
+
// of the same formula would have been.
|
|
17
|
+
//
|
|
18
|
+
// The closed form rather than an iterative solve, for the reason the shader
|
|
19
|
+
// gives: the arc band produces very flat ellipses (a 25px band under a 1500px
|
|
20
|
+
// pair is 44:1, and it goes past 88:1), and a seeded Newton on the parametric
|
|
21
|
+
// angle runs away entirely there.
|
|
22
|
+
// The first-quadrant solve: `co` is the cosine of the parametric angle of the
|
|
23
|
+
// nearest point to (qx, qy), radii (ax, ay), with the point and radii already
|
|
24
|
+
// mirrored into the half where qx <= qy.
|
|
25
|
+
function nearestCos(qx, qy, ax, ay) {
|
|
26
|
+
const l = ay * ay - ax * ax;
|
|
27
|
+
const m = (ax * qx) / l;
|
|
28
|
+
const m2 = m * m;
|
|
29
|
+
const n = (ay * qy) / l;
|
|
30
|
+
const n2 = n * n;
|
|
31
|
+
const c = (m2 + n2 - 1) / 3;
|
|
32
|
+
const c3 = c * c * c;
|
|
33
|
+
const d = c3 + m2 * n2;
|
|
34
|
+
const g = m + m * n2;
|
|
35
|
+
let co;
|
|
36
|
+
if (d < 0) {
|
|
37
|
+
const h = Math.acos(clamp1((c3 + m2 * n2 * 2) / c3)) / 3;
|
|
38
|
+
const s = Math.cos(h);
|
|
39
|
+
const t = Math.sin(h) * Math.sqrt(3);
|
|
40
|
+
const ux = Math.sqrt(Math.max(-c * (s + t + 2) + m2, 0));
|
|
41
|
+
const uy = Math.sqrt(Math.max(-c * (s - t + 2) + m2, 0));
|
|
42
|
+
co =
|
|
43
|
+
(uy + Math.sign(l) * ux + Math.abs(g) / Math.max(ux * uy, 1e-20) - m) / 2;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const qq = c3 + m2 * n2 * 2;
|
|
47
|
+
const h = 2 * m * n * Math.sqrt(d);
|
|
48
|
+
const s = Math.sign(qq + h) * Math.cbrt(Math.abs(qq + h));
|
|
49
|
+
const t = Math.sign(qq - h) * Math.cbrt(Math.abs(qq - h));
|
|
50
|
+
const ux = -s - t - c * 4 + 2 * m2;
|
|
51
|
+
const uy = (s - t) * Math.sqrt(3);
|
|
52
|
+
const rm = Math.hypot(ux, uy);
|
|
53
|
+
co =
|
|
54
|
+
(uy / Math.sqrt(Math.max(rm - ux, 1e-20)) +
|
|
55
|
+
(2 * g) / Math.max(rm, 1e-20) -
|
|
56
|
+
m) /
|
|
57
|
+
2;
|
|
58
|
+
}
|
|
59
|
+
return clamp1(co);
|
|
60
|
+
}
|
|
61
|
+
export function ellipseDistance(px, py, rx, ry) {
|
|
62
|
+
let qx = Math.abs(px);
|
|
63
|
+
let qy = Math.abs(py);
|
|
64
|
+
let ax = rx;
|
|
65
|
+
let ay = ry;
|
|
66
|
+
// Solve in the half where q.x <= q.y, mirroring point and radii together.
|
|
67
|
+
if (qx > qy) {
|
|
68
|
+
;
|
|
69
|
+
[qx, qy] = [qy, qx];
|
|
70
|
+
[ax, ay] = [ay, ax];
|
|
71
|
+
}
|
|
72
|
+
// Degenerate to the point at the origin, which the circle test below cannot
|
|
73
|
+
// catch: its threshold is `ay * ay`, so it reads `abs(l) < 0` and lets a zero
|
|
74
|
+
// divisor through. Same guard as curveDistance.slang's.
|
|
75
|
+
// Degenerate to the point at the origin, which the circle test below cannot
|
|
76
|
+
// catch: its threshold is `ay * ay`, so it reads `abs(l) < 0` and lets a zero
|
|
77
|
+
// divisor through. Same guard as curveDistance.slang's.
|
|
78
|
+
if (ax * ax + ay * ay <= 0) {
|
|
79
|
+
return Math.hypot(qx, qy);
|
|
80
|
+
}
|
|
81
|
+
const l = ay * ay - ax * ax;
|
|
82
|
+
// Circle: `l` is the solver's divisor, so hand those back the exact answer
|
|
83
|
+
// rather than a division by ~0. Reachable — a clamped dome's ry is pinned to
|
|
84
|
+
// the band while rx tracks the pair's width, so they cross.
|
|
85
|
+
if (Math.abs(l) < 1e-6 * ay * ay) {
|
|
86
|
+
return Math.abs(Math.hypot(qx, qy) - ax);
|
|
87
|
+
}
|
|
88
|
+
const co = nearestCos(qx, qy, ax, ay);
|
|
89
|
+
return Math.hypot(ax * co - qx, ay * Math.sqrt(Math.max(1 - co * co, 0)) - qy);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The point of the ellipse nearest (px, py), in the ellipse's own frame, and
|
|
93
|
+
* how far it is: what a hit test hands back as the ink it found.
|
|
94
|
+
*/
|
|
95
|
+
export function ellipseNearest(px, py, rx, ry) {
|
|
96
|
+
let qx = Math.abs(px);
|
|
97
|
+
let qy = Math.abs(py);
|
|
98
|
+
let ax = rx;
|
|
99
|
+
let ay = ry;
|
|
100
|
+
const swapped = qx > qy;
|
|
101
|
+
if (swapped) {
|
|
102
|
+
;
|
|
103
|
+
[qx, qy] = [qy, qx];
|
|
104
|
+
[ax, ay] = [ay, ax];
|
|
105
|
+
}
|
|
106
|
+
const l = ay * ay - ax * ax;
|
|
107
|
+
let nx;
|
|
108
|
+
let ny;
|
|
109
|
+
if (ax * ax + ay * ay <= 0) {
|
|
110
|
+
nx = 0;
|
|
111
|
+
ny = 0;
|
|
112
|
+
}
|
|
113
|
+
else if (Math.abs(l) < 1e-6 * ay * ay) {
|
|
114
|
+
const len = Math.hypot(qx, qy);
|
|
115
|
+
nx = len > 0 ? (qx / len) * ax : ax;
|
|
116
|
+
ny = len > 0 ? (qy / len) * ax : 0;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const co = nearestCos(qx, qy, ax, ay);
|
|
120
|
+
nx = ax * co;
|
|
121
|
+
ny = ay * Math.sqrt(Math.max(1 - co * co, 0));
|
|
122
|
+
}
|
|
123
|
+
const dist = Math.hypot(nx - qx, ny - qy);
|
|
124
|
+
if (swapped) {
|
|
125
|
+
;
|
|
126
|
+
[nx, ny] = [ny, nx];
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
x: px < 0 ? -nx : nx,
|
|
130
|
+
y: py < 0 ? -ny : ny,
|
|
131
|
+
dist,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function clamp1(x) {
|
|
135
|
+
return x < -1 ? -1 : x > 1 ? 1 : x;
|
|
136
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { InkRect, MarkContext2D } from './types.ts';
|
|
2
|
+
/** The path calls a glyph is built from — what a recorder has to answer. */
|
|
3
|
+
export type GlyphPath = Pick<MarkContext2D, 'moveTo' | 'lineTo' | 'closePath' | 'rect' | 'arc'>;
|
|
4
|
+
/**
|
|
5
|
+
* Append one of the `point` shape's glyphs to the current path, centred on
|
|
6
|
+
* `(cx, y)`.
|
|
7
|
+
*
|
|
8
|
+
* The disc/crisp-square split and the square's snap are pointGlyph.slang's
|
|
9
|
+
* (adr-051), so the painter cannot drift from the shader about where the split
|
|
10
|
+
* is. The caller owns `beginPath`/`fill`, so a run of one colour can batch into
|
|
11
|
+
* a single fill.
|
|
12
|
+
*
|
|
13
|
+
* Its own module, not `pointMark.ts`'s: this is the half of the shape a
|
|
14
|
+
* consumer can want without the pass, and importing it through the shape would
|
|
15
|
+
* hand that consumer the shader's WGSL and GLSL strings. Wiggle's scatter mode
|
|
16
|
+
* is that consumer.
|
|
17
|
+
*/
|
|
18
|
+
export declare function appendGlyph(ctx: GlyphPath, glyph: number, cx: number, y: number, diameter: number): void;
|
|
19
|
+
/**
|
|
20
|
+
* Room a glyph needs at each end of a plot whose display wants its topmost and
|
|
21
|
+
* bottommost points drawn whole: a bar's datum is its top edge and never
|
|
22
|
+
* overshoots, a glyph's is its centre, so one at domain max loses its top half
|
|
23
|
+
* off the canvas. The diamond's scale, since that is the largest thing the
|
|
24
|
+
* shape draws and `glyphBox` measures it the same way.
|
|
25
|
+
*
|
|
26
|
+
* A display passes this as the point shape's `insetPx` and gives its axis the
|
|
27
|
+
* same number as an offset, so domain max goes on labelling the row the
|
|
28
|
+
* topmost glyph centres on.
|
|
29
|
+
*/
|
|
30
|
+
export declare function pointInsetPx(diameter: number): number;
|
|
31
|
+
/**
|
|
32
|
+
* The box `appendGlyph` paints inside, by the same split and snap: the
|
|
33
|
+
* diamond is scaled by `DIAMOND_GLYPH_SCALE` and the small square snaps to
|
|
34
|
+
* the pixel grid, so neither is the diameter about the centre.
|
|
35
|
+
*/
|
|
36
|
+
export declare function glyphBox(glyph: number, cx: number, y: number, diameter: number): InkRect;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { SMALL_POINT_MAX_DIAMETER } from '../shaders/pointGlyph.generated.js';
|
|
2
|
+
import { crispSquareTopLeftPx } from '../shaders/pointGlyph.js.generated.js';
|
|
3
|
+
import { DIAMOND_GLYPH_SCALE, GLYPH_DIAMOND, GLYPH_SQUARE, GLYPH_TRIANGLE, } from '../shaders/pointMark.consts.generated.js';
|
|
4
|
+
/**
|
|
5
|
+
* Append one of the `point` shape's glyphs to the current path, centred on
|
|
6
|
+
* `(cx, y)`.
|
|
7
|
+
*
|
|
8
|
+
* The disc/crisp-square split and the square's snap are pointGlyph.slang's
|
|
9
|
+
* (adr-051), so the painter cannot drift from the shader about where the split
|
|
10
|
+
* is. The caller owns `beginPath`/`fill`, so a run of one colour can batch into
|
|
11
|
+
* a single fill.
|
|
12
|
+
*
|
|
13
|
+
* Its own module, not `pointMark.ts`'s: this is the half of the shape a
|
|
14
|
+
* consumer can want without the pass, and importing it through the shape would
|
|
15
|
+
* hand that consumer the shader's WGSL and GLSL strings. Wiggle's scatter mode
|
|
16
|
+
* is that consumer.
|
|
17
|
+
*/
|
|
18
|
+
export function appendGlyph(ctx, glyph, cx, y, diameter) {
|
|
19
|
+
const r = diameter / 2;
|
|
20
|
+
if (glyph === GLYPH_TRIANGLE) {
|
|
21
|
+
ctx.moveTo(cx - r, y - r);
|
|
22
|
+
ctx.lineTo(cx + r, y - r);
|
|
23
|
+
ctx.lineTo(cx, y + r);
|
|
24
|
+
ctx.closePath();
|
|
25
|
+
}
|
|
26
|
+
else if (glyph === GLYPH_DIAMOND) {
|
|
27
|
+
const ri = r * DIAMOND_GLYPH_SCALE;
|
|
28
|
+
ctx.moveTo(cx, y - ri);
|
|
29
|
+
ctx.lineTo(cx + ri, y);
|
|
30
|
+
ctx.lineTo(cx, y + ri);
|
|
31
|
+
ctx.lineTo(cx - ri, y);
|
|
32
|
+
ctx.closePath();
|
|
33
|
+
}
|
|
34
|
+
else if (glyph === GLYPH_SQUARE || diameter <= SMALL_POINT_MAX_DIAMETER) {
|
|
35
|
+
ctx.rect(crispSquareTopLeftPx(cx, diameter), crispSquareTopLeftPx(y, diameter), diameter, diameter);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
ctx.moveTo(cx + r, y);
|
|
39
|
+
ctx.arc(cx, y, r, 0, Math.PI * 2);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Room a glyph needs at each end of a plot whose display wants its topmost and
|
|
44
|
+
* bottommost points drawn whole: a bar's datum is its top edge and never
|
|
45
|
+
* overshoots, a glyph's is its centre, so one at domain max loses its top half
|
|
46
|
+
* off the canvas. The diamond's scale, since that is the largest thing the
|
|
47
|
+
* shape draws and `glyphBox` measures it the same way.
|
|
48
|
+
*
|
|
49
|
+
* A display passes this as the point shape's `insetPx` and gives its axis the
|
|
50
|
+
* same number as an offset, so domain max goes on labelling the row the
|
|
51
|
+
* topmost glyph centres on.
|
|
52
|
+
*/
|
|
53
|
+
export function pointInsetPx(diameter) {
|
|
54
|
+
return (diameter / 2) * DIAMOND_GLYPH_SCALE;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The box `appendGlyph` paints inside, by the same split and snap: the
|
|
58
|
+
* diamond is scaled by `DIAMOND_GLYPH_SCALE` and the small square snaps to
|
|
59
|
+
* the pixel grid, so neither is the diameter about the centre.
|
|
60
|
+
*/
|
|
61
|
+
export function glyphBox(glyph, cx, y, diameter) {
|
|
62
|
+
const r = diameter / 2;
|
|
63
|
+
if (glyph === GLYPH_DIAMOND) {
|
|
64
|
+
const ri = r * DIAMOND_GLYPH_SCALE;
|
|
65
|
+
return { left: cx - ri, top: y - ri, width: 2 * ri, height: 2 * ri };
|
|
66
|
+
}
|
|
67
|
+
if (glyph === GLYPH_SQUARE ||
|
|
68
|
+
(glyph !== GLYPH_TRIANGLE && diameter <= SMALL_POINT_MAX_DIAMETER)) {
|
|
69
|
+
return {
|
|
70
|
+
left: crispSquareTopLeftPx(cx, diameter),
|
|
71
|
+
top: crispSquareTopLeftPx(y, diameter),
|
|
72
|
+
width: diameter,
|
|
73
|
+
height: diameter,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { left: cx - r, top: y - r, width: diameter, height: diameter };
|
|
77
|
+
}
|