supervision 0.2.8 → 0.2.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.
Files changed (97) hide show
  1. package/dist/{depth-preview-probe-DG7McQEN.js → depth-preview-probe-BG0k-33U.js} +3 -3
  2. package/dist/{depth-preview-probe-DG7McQEN.js.map → depth-preview-probe-BG0k-33U.js.map} +1 -1
  3. package/dist/{depth-preview-track-BvgwNEE9.js → depth-preview-track-BEag1ZWF.js} +2 -2
  4. package/dist/{depth-preview-track-BvgwNEE9.js.map → depth-preview-track-BEag1ZWF.js.map} +1 -1
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2261 -447
  8. package/dist/index.js.map +1 -1
  9. package/dist/mask-preparation.worker.js +166 -45
  10. package/dist/mask-preparation.worker.js.map +1 -1
  11. package/dist/media/display-pixel-ratio.d.ts +3 -8
  12. package/dist/media/display-pixel-ratio.d.ts.map +1 -1
  13. package/dist/media/video-engine-media-source.js +1 -1
  14. package/dist/render-preparation/mask-frame-artifact.d.ts +7 -1
  15. package/dist/render-preparation/mask-frame-artifact.d.ts.map +1 -1
  16. package/dist/render-preparation/mask-frame-compositor.d.ts +1 -1
  17. package/dist/render-preparation/mask-frame-compositor.d.ts.map +1 -1
  18. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts +3 -0
  19. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts.map +1 -1
  20. package/dist/render-preparation/prepared-render-window.d.ts +2 -0
  21. package/dist/render-preparation/prepared-render-window.d.ts.map +1 -1
  22. package/dist/renderers/annotation-fxaa.d.ts +37 -0
  23. package/dist/renderers/annotation-fxaa.d.ts.map +1 -0
  24. package/dist/renderers/id-mask-stroke-coverage.d.ts +2 -2
  25. package/dist/renderers/id-mask-stroke-coverage.d.ts.map +1 -1
  26. package/dist/renderers/mask-palette.d.ts +4 -8
  27. package/dist/renderers/mask-palette.d.ts.map +1 -1
  28. package/dist/renderers/media-clahe.d.ts +6 -0
  29. package/dist/renderers/media-clahe.d.ts.map +1 -0
  30. package/dist/renderers/media-renderer-core.d.ts.map +1 -1
  31. package/dist/renderers/media-renderer-scene.d.ts +1 -0
  32. package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
  33. package/dist/renderers/pixi-annotation-antialias-layer.d.ts +15 -0
  34. package/dist/renderers/pixi-annotation-antialias-layer.d.ts.map +1 -0
  35. package/dist/renderers/pixi-annotation-antialias.d.ts +14 -0
  36. package/dist/renderers/pixi-annotation-antialias.d.ts.map +1 -0
  37. package/dist/renderers/pixi-annotation-overlay-layer.d.ts +6 -1
  38. package/dist/renderers/pixi-annotation-overlay-layer.d.ts.map +1 -1
  39. package/dist/renderers/pixi-box-layer.d.ts +6 -1
  40. package/dist/renderers/pixi-box-layer.d.ts.map +1 -1
  41. package/dist/renderers/pixi-filter-bindings.d.ts +6 -0
  42. package/dist/renderers/pixi-filter-bindings.d.ts.map +1 -0
  43. package/dist/renderers/pixi-focus-heatmap-cutout.d.ts +17 -0
  44. package/dist/renderers/pixi-focus-heatmap-cutout.d.ts.map +1 -0
  45. package/dist/renderers/pixi-focus-layer.d.ts +18 -1
  46. package/dist/renderers/pixi-focus-layer.d.ts.map +1 -1
  47. package/dist/renderers/pixi-heatmap-layer.d.ts +4 -1
  48. package/dist/renderers/pixi-heatmap-layer.d.ts.map +1 -1
  49. package/dist/renderers/pixi-id-mask-shader.d.ts +2 -1
  50. package/dist/renderers/pixi-id-mask-shader.d.ts.map +1 -1
  51. package/dist/renderers/pixi-interaction-presentation-layer.d.ts +5 -1
  52. package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -1
  53. package/dist/renderers/pixi-label-layer.d.ts +8 -4
  54. package/dist/renderers/pixi-label-layer.d.ts.map +1 -1
  55. package/dist/renderers/pixi-mask-layer.d.ts +2 -1
  56. package/dist/renderers/pixi-mask-layer.d.ts.map +1 -1
  57. package/dist/renderers/pixi-mask-screen-stroke.d.ts +1 -1
  58. package/dist/renderers/pixi-mask-screen-stroke.d.ts.map +1 -1
  59. package/dist/renderers/pixi-mask-viewport.d.ts +21 -0
  60. package/dist/renderers/pixi-mask-viewport.d.ts.map +1 -0
  61. package/dist/renderers/pixi-media-inspection-filter.d.ts +8 -0
  62. package/dist/renderers/pixi-media-inspection-filter.d.ts.map +1 -0
  63. package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
  64. package/dist/renderers/pixi-polygon-layer.d.ts +3 -0
  65. package/dist/renderers/pixi-polygon-layer.d.ts.map +1 -1
  66. package/dist/renderers/pixi-region-coverage-mask.d.ts +14 -3
  67. package/dist/renderers/pixi-region-coverage-mask.d.ts.map +1 -1
  68. package/dist/renderers/pixi-region-layer.d.ts +7 -1
  69. package/dist/renderers/pixi-region-layer.d.ts.map +1 -1
  70. package/dist/sessions/media-session.d.ts.map +1 -1
  71. package/dist/tracking.worker.js.map +1 -1
  72. package/dist/types/media-renderer.d.ts +1 -1
  73. package/dist/types/media-renderer.d.ts.map +1 -1
  74. package/dist/{video-engine-media-source-DOFsuGS4.js → video-engine-media-source-MFthAkbe.js} +4 -9
  75. package/dist/video-engine-media-source-MFthAkbe.js.map +1 -0
  76. package/node_modules/supervision-js-core/dist/index.d.ts +2 -2
  77. package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
  78. package/node_modules/supervision-js-core/dist/index.js +57 -25
  79. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  80. package/node_modules/supervision-js-core/dist/styles/default-annotation-presentation.d.ts.map +1 -1
  81. package/node_modules/supervision-js-core/dist/styles/focus-style.d.ts +4 -3
  82. package/node_modules/supervision-js-core/dist/styles/focus-style.d.ts.map +1 -1
  83. package/node_modules/supervision-js-core/dist/styles/mask-style.d.ts.map +1 -1
  84. package/node_modules/supervision-js-core/dist/types/focus-style.d.ts +2 -1
  85. package/node_modules/supervision-js-core/dist/types/focus-style.d.ts.map +1 -1
  86. package/node_modules/supervision-js-core/dist/types/label-style.d.ts +4 -1
  87. package/node_modules/supervision-js-core/dist/types/label-style.d.ts.map +1 -1
  88. package/node_modules/supervision-js-core/dist/types/mask-style.d.ts +8 -1
  89. package/node_modules/supervision-js-core/dist/types/mask-style.d.ts.map +1 -1
  90. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +18 -0
  91. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
  92. package/node_modules/supervision-js-core/dist/utils/detection-conversions.d.ts +1 -0
  93. package/node_modules/supervision-js-core/dist/utils/detection-conversions.d.ts.map +1 -1
  94. package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts +21 -7
  95. package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts.map +1 -1
  96. package/package.json +1 -1
  97. package/dist/video-engine-media-source-DOFsuGS4.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { copySortedDetectionFrames, detectionFrameOverlapsRange, sortedDetectionFrames, projectDetectionFrameForTracking, createSortTracker, createOCSortTracker, createCBIoUTracker, createByteTrackTracker, MediaErrorKind, includeDefined, createDepthColormapLut, parseDepthManifest, validateDepthMap, resolveDepthFrameFile, PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS, decodeCompressedRleMask, decodeCompressedRleCounts, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, resolveIdMaskPaletteId, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createProjectedDetectionFrameSource, createArrayDetectionFrameSource, createBufferedDetectionTimeline, resolveAnnotationRendererPresentation, PlaybackGateReach, createIdleDetectionBufferState, MediaInteractionMode, MAX_ID_MASK_PALETTE_ENTRIES, StrokeAlignment, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, depthPreviewTopCode, resolveDepthColorMapping, resolveDepthQuantity, computeDepthPercentileRange, BaseFocusStyle, extractDetectionMaskRectRuns, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget, rebaseDetectionPickToFrame, pickDetectionAtPoint, createDetectionPickKey, followDetectionPickAcrossFrames, haveSameDetectionPickIdentities, pickAnnotationHandle, getAnnotationHandles, MAX_ID_MASK_STROKE_WIDTH, LabelPlacement, BasePolygonStyle, BasePolylineStyle, BaseKeypointStyle, ShapeInstructionKind, sampleEllipseArc, resolveEllipseSegmentCount, KeypointMarkerShape, MarkerSizeSpace, MarkerShape, resolveMarkerGeometry, BaseInteractionStyle, DetectionInteractionState, writeIdMaskPaletteEntry, resolveIdMaskStrokeTexels, DetectionBufferStatus, resolveMaskStyleOpacity, pickDetectionByMaskId, BoxStrokeAlignment, RegionRendererMediaEffectKind, RegionRendererSourceKind, RegionRendererCoverageKind, RegionRendererSizeSpace, resolveStyleValue, resolveAnnotationStyleState, lightenColor, resolveAnnotationRendererStyleFields, annotationRendererKinds, createViewportController, MediaSessionMode, DetectionFrameRetentionMode, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, createCompositeDetectionFrameSource, MediaSessionActivityStatus, MediaSessionActivityKind, MediaSessionStatus, createSourceAwarePresentation, projectDetectionFrames, DetectionFrameSelectionMode } from 'supervision-js-core';
2
- export { BaseBoxCornerStyle, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMarkerStyle, BaseMaskStyle, BaseOrientedBoxStyle, BasePercentageBarStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, BoxStrokeAlignment, DEFAULT_DETECTION_CLASS_STYLES, DEFAULT_DETECTION_COLOR_SEQUENCE, DepthColormap, DepthMapKind, DepthPreviewLevels, DepthQuantity, DepthSampling, DetectionBufferStatus, DetectionFrameRetentionMode, DetectionFrameSelectionMode, DetectionInteractionState, DetectionMaskEncoding, DetectionPickTarget, FocusTargetMode, KeypointMarkerShape, KeypointVisibility, LabelPlacement, LabelVisibilityMode, MarkerShape, MarkerSizeSpace, MaskRenderMode, MediaErrorKind, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, PercentageBarPlacement, PlaybackGateReach, RegionRendererComposeMode, RegionRendererCoverageKind, RegionRendererMediaEffectKind, RegionRendererRegionKind, RegionRendererSizeSpace, RegionRendererSourceKind, SUPERVISION_ROBOFLOW_COLOR, TrackingGeometry, annotationRendererKinds, annotationRenderers, computeDepthPercentileRange, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createByteTrackTracker, createCBIoUTracker, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createMemoryColdDetectionFrameStore, createOCSortTracker, createProjectedDetectionFrameSource, createSortTracker, createWritableDetectionFrameSource, depthColormapColors, detectionPostProcessors, normalizeDetectionClassName, parseDepthManifest, pickDetectionAtPoint, projectDetectionFrame, projectDetectionFrameForTracking, projectDetectionFrames, readDepthAt, resolveDetectionClassColorStyle } from 'supervision-js-core';
3
- import { c as createMediaFrameClock, M as MediaSourceError, t as toMediaSourceError, r as resolveDisplayPixelRatio } from './video-engine-media-source-DOFsuGS4.js';
4
- export { a as createWebVideoEngineMediaRendererSource, g as getMediaErrorKind, i as isMediaSourceError, o as openWebVideoEngineMediaSource } from './video-engine-media-source-DOFsuGS4.js';
1
+ import { copySortedDetectionFrames, detectionFrameOverlapsRange, sortedDetectionFrames, projectDetectionFrameForTracking, createSortTracker, createOCSortTracker, createCBIoUTracker, createByteTrackTracker, MediaErrorKind, includeDefined, createDepthColormapLut, parseDepthManifest, validateDepthMap, resolveDepthFrameFile, PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS, decodeCompressedRleMask, decodeCompressedRleCounts, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, resolveIdMaskStrokeTexels, resolveIdMaskPaletteId, StrokeAlignment, MAX_ID_MASK_STROKE_WIDTH, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createProjectedDetectionFrameSource, createArrayDetectionFrameSource, createBufferedDetectionTimeline, resolveAnnotationRendererPresentation, PlaybackGateReach, createIdleDetectionBufferState, MediaInteractionMode, MAX_ID_MASK_PALETTE_ENTRIES, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, depthPreviewTopCode, resolveDepthColorMapping, resolveDepthQuantity, computeDepthPercentileRange, BaseFocusStyle, extractDetectionMaskRectRuns, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget, rebaseDetectionPickToFrame, pickDetectionAtPoint, createDetectionPickKey, followDetectionPickAcrossFrames, haveSameDetectionPickIdentities, pickAnnotationHandle, getAnnotationHandles, LabelPlacement, BasePolygonStyle, BasePolylineStyle, BaseKeypointStyle, ShapeInstructionKind, sampleEllipseArc, resolveEllipseSegmentCount, KeypointMarkerShape, MarkerSizeSpace, MarkerShape, resolveMarkerGeometry, BaseInteractionStyle, DetectionInteractionState, writeIdMaskPaletteEntry, DetectionBufferStatus, resolveMaskStyleOpacity, pickDetectionByMaskId, BoxStrokeAlignment, RegionRendererMediaEffectKind, RegionRendererSourceKind, RegionRendererCoverageKind, RegionRendererSizeSpace, resolveStyleValue, resolveAnnotationStyleState, lightenColor, resolveAnnotationRendererStyleFields, annotationRendererKinds, createViewportController, MediaSessionMode, DetectionFrameRetentionMode, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, createCompositeDetectionFrameSource, MediaSessionActivityStatus, MediaSessionActivityKind, MediaSessionStatus, createSourceAwarePresentation, projectDetectionFrames, DetectionFrameSelectionMode } from 'supervision-js-core';
2
+ export { BaseBoxCornerStyle, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMarkerStyle, BaseMaskStyle, BaseOrientedBoxStyle, BasePercentageBarStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, BoxStrokeAlignment, DEFAULT_DETECTION_CLASS_STYLES, DEFAULT_DETECTION_COLOR_SEQUENCE, DepthColormap, DepthMapKind, DepthPreviewLevels, DepthQuantity, DepthSampling, DetectionBufferStatus, DetectionFrameRetentionMode, DetectionFrameSelectionMode, DetectionInteractionState, DetectionMaskEncoding, DetectionPickTarget, FocusTargetMode, KeypointMarkerShape, KeypointVisibility, LabelPlacement, LabelVisibilityMode, MarkerShape, MarkerSizeSpace, MaskRenderMode, MediaErrorKind, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, PercentageBarPlacement, PlaybackGateReach, RegionRendererComposeMode, RegionRendererCoverageKind, RegionRendererMediaEffectKind, RegionRendererRegionKind, RegionRendererSizeSpace, RegionRendererSourceKind, SUPERVISION_ROBOFLOW_COLOR, StrokeAlignment, TrackingGeometry, annotationRendererKinds, annotationRenderers, computeDepthPercentileRange, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createByteTrackTracker, createCBIoUTracker, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createMemoryColdDetectionFrameStore, createOCSortTracker, createProjectedDetectionFrameSource, createSortTracker, createWritableDetectionFrameSource, depthColormapColors, detectionPostProcessors, normalizeDetectionClassName, parseDepthManifest, pickDetectionAtPoint, projectDetectionFrame, projectDetectionFrameForTracking, projectDetectionFrames, readDepthAt, resolveDetectionClassColorStyle } from 'supervision-js-core';
3
+ import { c as createMediaFrameClock, M as MediaSourceError, t as toMediaSourceError, r as resolveDisplayPixelRatio } from './video-engine-media-source-MFthAkbe.js';
4
+ export { a as createWebVideoEngineMediaRendererSource, g as getMediaErrorKind, i as isMediaSourceError, o as openWebVideoEngineMediaSource } from './video-engine-media-source-MFthAkbe.js';
5
5
 
6
6
  const DEFAULT_DATABASE_NAME = "supervision-js-detection-frames";
7
7
  const DEFAULT_CHUNK_DURATION_SECONDS = 1;
@@ -3113,7 +3113,7 @@ function assertMediaAspect(map, media) {
3113
3113
  }
3114
3114
  }
3115
3115
 
3116
- const EMBEDDED_MASK_PREPARATION_WORKER_SOURCE = "(function () {\n 'use strict';\n\n /** Trace closed pixel boundaries, retaining holes and disconnected components. */\n function traceMaskOutlinePaths(raster, width, height, ids) {\n const edgesById = new Map();\n const stride = width + 1;\n const steps = [1, stride, -1, -stride];\n const add = (id, vertex, direction) => {\n let edges = edgesById.get(id);\n if (!edges)\n edgesById.set(id, (edges = new Map()));\n const outgoing = edges.get(vertex);\n if (outgoing)\n outgoing.push(direction);\n else\n edges.set(vertex, [direction]);\n };\n for (let y = 0; y < height; y += 1) {\n for (let x = 0; x < width; x += 1) {\n const offset = y * width + x;\n const id = raster[offset];\n if (!ids.has(id))\n continue;\n const vertex = y * stride + x;\n if (y === 0 || raster[offset - width] !== id)\n add(id, vertex, 0);\n if (x === width - 1 || raster[offset + 1] !== id)\n add(id, vertex + 1, 1);\n if (y === height - 1 || raster[offset + width] !== id)\n add(id, vertex + stride + 1, 2);\n if (x === 0 || raster[offset - 1] !== id)\n add(id, vertex + stride, 3);\n }\n }\n const result = new Map();\n for (const [id, edges] of edgesById) {\n const paths = [];\n while (edges.size > 0) {\n const start = edges.keys().next().value;\n let vertex = start;\n let previousDirection = -1;\n let firstDirection = -1;\n const points = [];\n do {\n const outgoing = edges.get(vertex);\n // Turn right at diagonal contacts so separate islands stay separate.\n const direction = previousDirection < 0\n ? outgoing[0]\n : [1, 0, 3, 2]\n .map((turn) => (previousDirection + turn) % 4)\n .find((candidate) => outgoing.includes(candidate));\n if (firstDirection < 0)\n firstDirection = direction;\n if (direction !== previousDirection)\n points.push(vertex % stride, Math.floor(vertex / stride));\n outgoing.splice(outgoing.indexOf(direction), 1);\n if (outgoing.length === 0)\n edges.delete(vertex);\n vertex += steps[direction];\n previousDirection = direction;\n } while (vertex !== start);\n if (previousDirection === firstDirection)\n points.splice(0, 2);\n paths.push(new Float32Array(points));\n }\n result.set(id, paths);\n }\n return result;\n }\n\n /** COCO-compatible keypoint visibility values. */\n var KeypointVisibility;\n (function (KeypointVisibility) {\n KeypointVisibility[KeypointVisibility[\"NotLabeled\"] = 0] = \"NotLabeled\";\n KeypointVisibility[KeypointVisibility[\"Occluded\"] = 1] = \"Occluded\";\n KeypointVisibility[KeypointVisibility[\"Visible\"] = 2] = \"Visible\";\n })(KeypointVisibility || (KeypointVisibility = {}));\n var DetectionMaskEncoding;\n (function (DetectionMaskEncoding) {\n DetectionMaskEncoding[\"CompressedRle\"] = \"compressedRle\";\n })(DetectionMaskEncoding || (DetectionMaskEncoding = {}));\n\n var DetectionBufferStatus;\n (function (DetectionBufferStatus) {\n DetectionBufferStatus[\"Idle\"] = \"idle\";\n DetectionBufferStatus[\"Loading\"] = \"loading\";\n /**\n * The playback gate is holding playback until the source produces detections\n * for the requested range. {@link DetectionBufferStatus.Loading} fetches\n * detections the source already has; this one waits on a producer.\n */\n DetectionBufferStatus[\"AwaitingCoverage\"] = \"awaitingCoverage\";\n DetectionBufferStatus[\"Ready\"] = \"ready\";\n DetectionBufferStatus[\"Error\"] = \"error\";\n DetectionBufferStatus[\"Destroyed\"] = \"destroyed\";\n })(DetectionBufferStatus || (DetectionBufferStatus = {}));\n /**\n * How the renderer selects the active detection frame for a media timestamp.\n */\n var DetectionFrameSelectionMode;\n (function (DetectionFrameSelectionMode) {\n /**\n * Select the frame whose `[mediaTime, endTime)` interval contains the media\n * time. This is the default for interval annotations and timestamped sources.\n *\n * Frame times must come from the media itself. Times reconstructed from a\n * nominal frame rate drift against a clip whose real rate differs, and once\n * that drift passes the half-millisecond selection tolerance a playhead\n * landing on a frame boundary selects the previous frame's detections.\n *\n * A frame that carries no `endTime` stays active until the next frame starts,\n * which bridges an index the source never produced.\n * {@link DetectionFrameSelectionMode.NearestFrameIndex} bounds each frame to\n * one grid step instead, so a missing index reads as no detections.\n */\n DetectionFrameSelectionMode[\"Interval\"] = \"interval\";\n /**\n * Select from a known inference frame grid using `frameIndex` and\n * `frameRate`. This is useful when inference was run on normalized frames and\n * playback should snap detections to that grid.\n *\n * A frame speaks for one grid step starting at its own media time, and an\n * index the source never produced selects nothing rather than a neighbour.\n *\n * A frame carrying no `frameIndex` speaks for its step on the same terms,\n * reached by the time it starts at rather than by an index that names it. Its\n * `endTime` does not widen it past that step, so a source that labels only\n * part of what it writes cannot bridge the indexes it never produced.\n */\n DetectionFrameSelectionMode[\"NearestFrameIndex\"] = \"nearestFrameIndex\";\n })(DetectionFrameSelectionMode || (DetectionFrameSelectionMode = {}));\n var DetectionFrameRetentionMode;\n (function (DetectionFrameRetentionMode) {\n /**\n * Keep writable detections only in an in-memory store. Useful for ephemeral\n * live streams where old predictions should disappear when evicted.\n */\n DetectionFrameRetentionMode[\"MemoryOnly\"] = \"memoryOnly\";\n /**\n * Persist every written detection frame. Useful for finite media where seek\n * and replay should not require recomputing detections.\n */\n DetectionFrameRetentionMode[\"PersistAll\"] = \"persistAll\";\n /**\n * Persist only the most recent retention window. Useful for long-running\n * streams where replay is bounded to a recent time horizon.\n */\n DetectionFrameRetentionMode[\"PersistWindow\"] = \"persistWindow\";\n })(DetectionFrameRetentionMode || (DetectionFrameRetentionMode = {}));\n function decodeCompressedRleMask(mask) {\n if (mask.encoding !== DetectionMaskEncoding.CompressedRle) {\n throw new Error(`Unsupported detection mask encoding: ${mask.encoding}`);\n }\n const data = new Uint8Array(mask.width * mask.height);\n const counts = decodeCompressedRleCounts(mask.counts);\n let offset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n const isForeground = index % 2 === 1;\n if (isForeground) {\n for (let runOffset = 0; runOffset < runLength; runOffset += 1) {\n const maskOffset = offset + runOffset;\n const x = Math.floor(maskOffset / mask.height);\n const y = maskOffset % mask.height;\n const rowMajorOffset = y * mask.width + x;\n if (rowMajorOffset < data.length) {\n data[rowMajorOffset] = 1;\n }\n }\n }\n offset += runLength;\n }\n return {\n data,\n height: mask.height,\n width: mask.width,\n };\n }\n function decodeCompressedRleCounts(counts) {\n const decoded = [];\n let index = 0;\n while (index < counts.length) {\n let value = 0;\n let shift = 0;\n let charCode;\n do {\n charCode = counts.charCodeAt(index) - 48;\n index += 1;\n value |= (charCode & 0x1f) << shift;\n shift += 5;\n } while (charCode & 0x20);\n if (charCode & 0x10) {\n value |= -1 << shift;\n }\n if (decoded.length > 2) {\n value += decoded[decoded.length - 2] ?? 0;\n }\n decoded.push(value);\n }\n return decoded;\n }\n function encodeCompressedRleCounts(counts) {\n return counts\n .map((count, index) => {\n let value = index > 2 ? count - counts[index - 2] : count;\n let encoded = \"\";\n let more = true;\n while (more) {\n let charCode = value & 0x1f;\n value >>= 5;\n more = !((value === 0 && (charCode & 0x10) === 0) ||\n (value === -1 && (charCode & 0x10) !== 0));\n if (more) {\n charCode |= 0x20;\n }\n encoded += String.fromCharCode(charCode + 48);\n }\n return encoded;\n })\n .join(\"\");\n }\n\n var AnnotationFrameMutationKind;\n (function (AnnotationFrameMutationKind) {\n AnnotationFrameMutationKind[\"Add\"] = \"add\";\n AnnotationFrameMutationKind[\"Remove\"] = \"remove\";\n AnnotationFrameMutationKind[\"Replace\"] = \"replace\";\n AnnotationFrameMutationKind[\"Transact\"] = \"transact\";\n AnnotationFrameMutationKind[\"Update\"] = \"update\";\n })(AnnotationFrameMutationKind || (AnnotationFrameMutationKind = {}));\n\n /** Geometry projected into a tracker. The original detection payload is retained. */\n var TrackingGeometry;\n (function (TrackingGeometry) {\n TrackingGeometry[\"Box\"] = \"box\";\n TrackingGeometry[\"Mask\"] = \"mask\";\n TrackingGeometry[\"Keypoints\"] = \"keypoints\";\n })(TrackingGeometry || (TrackingGeometry = {}));\n\n var DetectionMaskPayloadFormat;\n (function (DetectionMaskPayloadFormat) {\n DetectionMaskPayloadFormat[\"RawCocoRle\"] = \"rawCocoRle\";\n DetectionMaskPayloadFormat[\"DeflatedBase64\"] = \"deflatedBase64\";\n })(DetectionMaskPayloadFormat || (DetectionMaskPayloadFormat = {}));\n function encodeBinaryMask(data, width, height) {\n assertMaskDimensions(data, width, height);\n const runs = [];\n let currentValue = 0;\n let runLength = 0;\n for (let x = 0; x < width; x += 1) {\n for (let y = 0; y < height; y += 1) {\n const value = data[y * width + x] ? 1 : 0;\n if (value === currentValue) {\n runLength += 1;\n }\n else {\n runs.push(runLength);\n currentValue = value;\n runLength = 1;\n }\n }\n }\n runs.push(runLength);\n return {\n counts: encodeCompressedRleCounts(runs),\n encoding: DetectionMaskEncoding.CompressedRle,\n height,\n width,\n };\n }\n function assertMaskSize(width, height) {\n if (!Number.isInteger(width) || width <= 0) {\n throw new Error(\"Mask width must be a positive integer.\");\n }\n if (!Number.isInteger(height) || height <= 0) {\n throw new Error(\"Mask height must be a positive integer.\");\n }\n }\n function assertMaskDimensions(data, width, height) {\n assertMaskSize(width, height);\n if (data.length !== width * height) {\n throw new Error(\"Mask data length must equal width * height.\");\n }\n }\n\n function centerRectToTopLeftRect(rect) {\n return {\n height: rect.height,\n width: rect.width,\n x: rect.x - rect.width / 2,\n y: rect.y - rect.height / 2,\n };\n }\n function getPointsRect(points) {\n if (points.length === 0) {\n return undefined;\n }\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n for (const point of points) {\n minX = Math.min(minX, point.x);\n minY = Math.min(minY, point.y);\n maxX = Math.max(maxX, point.x);\n maxY = Math.max(maxY, point.y);\n }\n return {\n height: maxY - minY,\n width: maxX - minX,\n x: (minX + maxX) / 2,\n y: (minY + maxY) / 2,\n };\n }\n\n var DetectionPickTarget;\n (function (DetectionPickTarget) {\n DetectionPickTarget[\"Box\"] = \"box\";\n DetectionPickTarget[\"Edge\"] = \"edge\";\n DetectionPickTarget[\"Keypoint\"] = \"keypoint\";\n DetectionPickTarget[\"Label\"] = \"label\";\n DetectionPickTarget[\"Mask\"] = \"mask\";\n DetectionPickTarget[\"OrientedBox\"] = \"orientedBox\";\n DetectionPickTarget[\"Polygon\"] = \"polygon\";\n DetectionPickTarget[\"Polyline\"] = \"polyline\";\n })(DetectionPickTarget || (DetectionPickTarget = {}));\n var MediaInteractionMode;\n (function (MediaInteractionMode) {\n MediaInteractionMode[\"Always\"] = \"always\";\n MediaInteractionMode[\"Disabled\"] = \"disabled\";\n MediaInteractionMode[\"PausedOnly\"] = \"pausedOnly\";\n })(MediaInteractionMode || (MediaInteractionMode = {}));\n\n var AnnotationGeometryKind;\n (function (AnnotationGeometryKind) {\n AnnotationGeometryKind[\"Box\"] = \"box\";\n AnnotationGeometryKind[\"Polygon\"] = \"polygon\";\n AnnotationGeometryKind[\"Polyline\"] = \"polyline\";\n AnnotationGeometryKind[\"Keypoints\"] = \"keypoints\";\n AnnotationGeometryKind[\"Mask\"] = \"mask\";\n })(AnnotationGeometryKind || (AnnotationGeometryKind = {}));\n var AnnotationGestureStateKind;\n (function (AnnotationGestureStateKind) {\n AnnotationGestureStateKind[\"Idle\"] = \"idle\";\n AnnotationGestureStateKind[\"Creating\"] = \"creating\";\n AnnotationGestureStateKind[\"Moving\"] = \"moving\";\n AnnotationGestureStateKind[\"Resizing\"] = \"resizing\";\n AnnotationGestureStateKind[\"DragSelecting\"] = \"dragSelecting\";\n })(AnnotationGestureStateKind || (AnnotationGestureStateKind = {}));\n var AnnotationHandleKind;\n (function (AnnotationHandleKind) {\n AnnotationHandleKind[\"Resize\"] = \"resize\";\n AnnotationHandleKind[\"Vertex\"] = \"vertex\";\n AnnotationHandleKind[\"AddVertex\"] = \"addVertex\";\n AnnotationHandleKind[\"Keypoint\"] = \"keypoint\";\n })(AnnotationHandleKind || (AnnotationHandleKind = {}));\n\n /** Alignment for strokes applied to closed rendered geometry. */\n var StrokeAlignment;\n (function (StrokeAlignment) {\n StrokeAlignment[\"Inside\"] = \"inside\";\n StrokeAlignment[\"Center\"] = \"center\";\n StrokeAlignment[\"Outside\"] = \"outside\";\n })(StrokeAlignment || (StrokeAlignment = {}));\n\n var BoxShape;\n (function (BoxShape) {\n BoxShape[\"Rect\"] = \"rect\";\n BoxShape[\"RoundedRect\"] = \"roundedRect\";\n })(BoxShape || (BoxShape = {}));\n\n var FocusTargetMode;\n (function (FocusTargetMode) {\n FocusTargetMode[\"Hovered\"] = \"hovered\";\n FocusTargetMode[\"Selected\"] = \"selected\";\n FocusTargetMode[\"HoveredAndSelected\"] = \"hoveredAndSelected\";\n FocusTargetMode[\"Ambient\"] = \"ambient\";\n })(FocusTargetMode || (FocusTargetMode = {}));\n\n var DetectionInteractionState;\n (function (DetectionInteractionState) {\n DetectionInteractionState[\"Hovered\"] = \"hovered\";\n DetectionInteractionState[\"Selected\"] = \"selected\";\n })(DetectionInteractionState || (DetectionInteractionState = {}));\n\n var LabelPlacement;\n (function (LabelPlacement) {\n LabelPlacement[\"Top\"] = \"top\";\n LabelPlacement[\"Bottom\"] = \"bottom\";\n LabelPlacement[\"InsideTop\"] = \"insideTop\";\n LabelPlacement[\"InsideBottom\"] = \"insideBottom\";\n LabelPlacement[\"Center\"] = \"center\";\n })(LabelPlacement || (LabelPlacement = {}));\n var LabelVisibilityMode;\n (function (LabelVisibilityMode) {\n LabelVisibilityMode[\"Always\"] = \"always\";\n LabelVisibilityMode[\"HoveredOnly\"] = \"hoveredOnly\";\n })(LabelVisibilityMode || (LabelVisibilityMode = {}));\n\n var MaskRenderMode;\n (function (MaskRenderMode) {\n MaskRenderMode[\"FillAndStroke\"] = \"fillAndStroke\";\n MaskRenderMode[\"FillOnly\"] = \"fillOnly\";\n MaskRenderMode[\"StrokeOnly\"] = \"strokeOnly\";\n })(MaskRenderMode || (MaskRenderMode = {}));\n\n /**\n * Discriminator for renderer-neutral shape draw instructions.\n */\n var ShapeInstructionKind;\n (function (ShapeInstructionKind) {\n ShapeInstructionKind[\"Ellipse\"] = \"ellipse\";\n ShapeInstructionKind[\"Marker\"] = \"marker\";\n ShapeInstructionKind[\"Path\"] = \"path\";\n })(ShapeInstructionKind || (ShapeInstructionKind = {}));\n var MarkerShape;\n (function (MarkerShape) {\n MarkerShape[\"Circle\"] = \"circle\";\n MarkerShape[\"Cross\"] = \"cross\";\n MarkerShape[\"Square\"] = \"square\";\n MarkerShape[\"Triangle\"] = \"triangle\";\n })(MarkerShape || (MarkerShape = {}));\n /**\n * Coordinate space for marker sizing. Media sizes scale with the viewport;\n * screen sizes stay constant on screen, matching stroke-width semantics.\n */\n var MarkerSizeSpace;\n (function (MarkerSizeSpace) {\n MarkerSizeSpace[\"Media\"] = \"media\";\n MarkerSizeSpace[\"Screen\"] = \"screen\";\n })(MarkerSizeSpace || (MarkerSizeSpace = {}));\n\n var PercentageBarPlacement;\n (function (PercentageBarPlacement) {\n PercentageBarPlacement[\"Top\"] = \"top\";\n PercentageBarPlacement[\"Bottom\"] = \"bottom\";\n PercentageBarPlacement[\"InsideTop\"] = \"insideTop\";\n PercentageBarPlacement[\"InsideBottom\"] = \"insideBottom\";\n })(PercentageBarPlacement || (PercentageBarPlacement = {}));\n\n var KeypointMarkerShape;\n (function (KeypointMarkerShape) {\n KeypointMarkerShape[\"Circle\"] = \"circle\";\n KeypointMarkerShape[\"Cross\"] = \"cross\";\n })(KeypointMarkerShape || (KeypointMarkerShape = {}));\n\n /**\n * What the stored values of a depth map measure.\n *\n * Every kind stores 0 where there is no depth.\n */\n const DepthMapKind = {\n /** Stereo disparity in pixels of the map. Larger is nearer. */\n DisparityPx: \"disparity_px\",\n /** Metric depth along the optical axis in metres. Smaller is nearer. */\n DepthM: \"depth_m\",\n /**\n * Unitless inverse depth up to an unknown scale and shift, as monocular\n * models produce it. Larger is nearer; it has no metric depth.\n */\n RelativeInverse: \"relative_inverse\",\n };\n /**\n * The quantity a depth renderer colours.\n *\n * Disparity is inverse depth, so it spends colour on near detail the way\n * stereo measures it. Either way the nearest end of the range is the warm or\n * bright end of the colour table.\n */\n const DepthQuantity = {\n Disparity: \"disparity\",\n Depth: \"depth\",\n };\n const DepthColormap = {\n Turbo: \"turbo\",\n Viridis: \"viridis\",\n Cividis: \"cividis\",\n Inferno: \"inferno\",\n Magma: \"magma\",\n Grayscale: \"grayscale\",\n };\n /** How a depth map is sampled where one map pixel does not cover one media pixel. */\n const DepthSampling = {\n /** Nearest when the map is at least media size, edge-aware otherwise. */\n Auto: \"auto\",\n Nearest: \"nearest\",\n /**\n * Bilinear over valid neighbours only, falling back to the nearest one\n * across a depth edge, so objects never bleed into the background.\n */\n EdgeAware: \"edge-aware\",\n };\n /**\n * The luma range a depth preview video's codes are written in.\n *\n * TV range is what browsers' hardware decoders return as written; a decoder\n * may squeeze full range into TV range or move codes converting it to RGB.\n */\n const DepthPreviewLevels = {\n /** Every code from 0 to 255; the top code is 255. */\n Full: \"full\",\n /** Limited range: black is 16 and the top code is 235. */\n Tv: \"tv\",\n };\n new Set(Object.values(DepthColormap));\n new Set(Object.values(DepthQuantity));\n new Set(Object.values(DepthSampling));\n function rasterizePolygonToMask(points, dimensions) {\n const data = createEmptyMask(dimensions);\n if (points.length < 3) {\n return data;\n }\n const bounds = centerRectToTopLeftRect(getPointsRect(points));\n const startY = Math.max(0, Math.floor(bounds.y));\n const endY = Math.min(dimensions.height - 1, Math.ceil(bounds.y + bounds.height));\n for (let y = startY; y <= endY; y += 1) {\n const scanY = y + 0.5;\n const intersections = [];\n for (let index = 0; index < points.length; index += 1) {\n const current = points[index];\n const next = points[(index + 1) % points.length];\n if ((current.y <= scanY && next.y > scanY) ||\n (next.y <= scanY && current.y > scanY)) {\n const ratio = (scanY - current.y) / (next.y - current.y);\n intersections.push(current.x + ratio * (next.x - current.x));\n }\n }\n intersections.sort((left, right) => left - right);\n for (let index = 0; index < intersections.length - 1; index += 2) {\n const left = Math.max(0, Math.round(intersections[index]));\n const right = Math.min(dimensions.width, Math.round(intersections[index + 1]));\n for (let x = left; x < right; x += 1) {\n data[y * dimensions.width + x] = 1;\n }\n }\n }\n return data;\n }\n function createEmptyMask(dimensions) {\n if (!Number.isInteger(dimensions.width) ||\n dimensions.width <= 0 ||\n !Number.isInteger(dimensions.height) ||\n dimensions.height <= 0) {\n throw new Error(\"Media dimensions must be positive integers.\");\n }\n return new Uint8Array(dimensions.width * dimensions.height);\n }\n\n /**\n * Palette slots an id raster can name, one of them the background. A GLSL\n * fragment stage is guaranteed only 224 uniform vectors, and a palette entry\n * costs 2.25 of them, so this stays a multiple of four and well inside that\n * budget; past it a frame draws from the RGBA composite instead.\n */\n const MAX_ID_MASK_PALETTE_ENTRIES = 80;\n const MAX_ID_MASK_STROKE_WIDTH = 16;\n function createIdMaskFrame(instructions, options = {}) {\n if (instructions.length === 0) {\n return undefined;\n }\n /* Pixel-grid dimensions use integer parts throughout allocation and RLE\n traversal. */\n const maskWidth = Math.floor(Math.max(...instructions.map(({ mask }) => mask.width)));\n const maskHeight = Math.floor(Math.max(...instructions.map(({ mask }) => mask.height)));\n const width = resolveRasterWidth(maskWidth, options.maxWidth);\n const height = width === maskWidth\n ? maskHeight\n : Math.max(1, Math.round((width * maskHeight) / maskWidth));\n const scaledAxes = width === maskWidth\n ? undefined\n : createScaledMaskAxes({ height, maskHeight, maskWidth, width });\n const data = new Uint8Array(new ArrayBuffer(width * height));\n const fillPalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokePalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokeWidths = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4));\n let hasStroke = false;\n let maxStrokeWidth = 0;\n for (const instruction of instructions) {\n const detectionMaskId = resolveIdMaskPaletteId(instruction.detectionIndex);\n if (detectionMaskId === undefined) {\n return undefined;\n }\n writeIdMaskPaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);\n if (instruction.stroke && instruction.stroke.width > 0) {\n const strokeWidth = resolveIdMaskStrokeTexels(instruction.stroke.width, maskWidth, width);\n hasStroke = true;\n strokeWidths[detectionMaskId] = strokeWidth;\n maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth);\n writeIdMaskPaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);\n }\n if (scaledAxes) {\n writeScaledMaskRuns(data, scaledAxes, instruction.mask, detectionMaskId);\n }\n else {\n writeMaskRuns(data, width, instruction.mask, detectionMaskId);\n }\n }\n return {\n data,\n fillPalette,\n hasStroke,\n height,\n maxStrokeWidth,\n sourceWidth: maskWidth,\n strokePalette,\n strokeWidths,\n width,\n };\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function writeMaskRuns(data, frameWidth, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = Math.floor(mask.width);\n const maskHeight = Math.floor(mask.height);\n let maskOffset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let frameOffset = y * frameWidth + x;\n for (let step = 0; step < runLength; step += 1) {\n if (x >= maskWidth) {\n break;\n }\n data[frameOffset] = detectionMaskId;\n y += 1;\n frameOffset += frameWidth;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n frameOffset = x;\n }\n }\n maskOffset += runLength;\n }\n }\n /**\n * The scaled twin of writeMaskRuns. Every source pixel marks the texel it lands\n * in, so a mask keeps every part of itself the smaller raster can hold and\n * gains up to a texel at its edges.\n */\n function writeScaledMaskRuns(data, axes, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = Math.floor(mask.width);\n const maskHeight = Math.floor(mask.height);\n let maskOffset = 0;\n const stride = axes.stride;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let remaining = runLength;\n /* A run walks down one column and wraps into the next, so it splits into\n column-sized segments. Within a segment the destination offset only ever\n steps by the row pitch or stays put: createMaskAxisMap builds\n `rows[i] = min(height - 1, floor(i * scale)) * width` with scale <= 1, so\n consecutive entries differ by exactly 0 or exactly `width`. Walking the\n destination directly therefore touches the same bytes the per-pixel loop\n touched, and drops the bounds test, the wrap test and one typed-array\n load from every source pixel. Where the scale collapses several source\n rows onto one destination row, this writes that byte once instead of\n once per source row. */\n while (remaining > 0 && x < maskWidth) {\n const span = Math.min(remaining, maskHeight - y);\n const column = axes.columns[x];\n const first = axes.rows[y];\n const last = axes.rows[y + span - 1];\n for (let offset = first; offset <= last; offset += stride) {\n data[offset + column] = detectionMaskId;\n }\n remaining -= span;\n y += span;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n }\n }\n maskOffset += runLength;\n }\n }\n function resolveRasterWidth(maskWidth, maxWidth) {\n return maxWidth !== undefined && maxWidth > 0 && maxWidth < maskWidth\n ? Math.max(1, Math.floor(maxWidth))\n : maskWidth;\n }\n /**\n * Converts a source-grid stroke width into raster texels. Fractional widths\n * let coverage shaders draw a border within a texel. The ceiling bounds the\n * neighbourhood the shaders scan.\n */\n function resolveIdMaskStrokeTexels(strokeWidth, maskWidth, rasterWidth) {\n const scale = maskWidth > 0 ? rasterWidth / maskWidth : 1;\n return Math.min(strokeWidth * scale, MAX_ID_MASK_STROKE_WIDTH);\n }\n function createScaledMaskAxes(frame) {\n return {\n columns: createMaskAxisMap(frame.maskWidth, frame.width, 1),\n rows: createMaskAxisMap(frame.maskHeight, frame.height, frame.width),\n stride: frame.width,\n };\n }\n /** Rows hold their destination offset, so writing a run is two lookups. */\n function createMaskAxisMap(sourceLength, targetLength, stride) {\n const map = new Int32Array(sourceLength);\n const scale = targetLength / sourceLength;\n for (let index = 0; index < sourceLength; index += 1) {\n map[index] = Math.min(targetLength - 1, Math.floor(index * scale)) * stride;\n }\n return map;\n }\n /**\n * The palette slot a detection index names, or undefined when the palette has\n * no slot for it. Slot zero is the background, so the ids start at one and the\n * last detection the palette can name sits one index below the last slot.\n */\n function resolveIdMaskPaletteId(detectionIndex) {\n const detectionMaskId = detectionIndex + 1;\n return detectionMaskId > 0 && detectionMaskId < MAX_ID_MASK_PALETTE_ENTRIES\n ? detectionMaskId\n : undefined;\n }\n function writeIdMaskPaletteEntry(palette, detectionMaskId, color, alpha) {\n const offset = detectionMaskId * 4;\n palette[offset] = ((color >> 16) & 0xff) / 255;\n palette[offset + 1] = ((color >> 8) & 0xff) / 255;\n palette[offset + 2] = (color & 0xff) / 255;\n palette[offset + 3] = Math.max(0, Math.min(alpha, 1));\n }\n new Set(Object.values(DepthPreviewLevels));\n new Set(Object.values(DepthMapKind));\n\n var MediaRendererFit;\n (function (MediaRendererFit) {\n /**\n * Preserve media aspect ratio and fit the full frame inside the canvas.\n */\n MediaRendererFit[\"Contain\"] = \"contain\";\n /**\n * Preserve media aspect ratio and fill the canvas, cropping if necessary.\n */\n MediaRendererFit[\"Cover\"] = \"cover\";\n })(MediaRendererFit || (MediaRendererFit = {}));\n /**\n * Playback lifecycle state reported by a platform renderer.\n */\n var MediaRendererPlaybackState;\n (function (MediaRendererPlaybackState) {\n MediaRendererPlaybackState[\"Loading\"] = \"loading\";\n MediaRendererPlaybackState[\"Ready\"] = \"ready\";\n MediaRendererPlaybackState[\"Playing\"] = \"playing\";\n MediaRendererPlaybackState[\"Buffering\"] = \"buffering\";\n MediaRendererPlaybackState[\"Paused\"] = \"paused\";\n MediaRendererPlaybackState[\"Error\"] = \"error\";\n MediaRendererPlaybackState[\"Destroyed\"] = \"destroyed\";\n })(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));\n /**\n * Stable classification of a media failure.\n *\n * Applications branch on these kinds instead of parsing decoder, demuxer, or\n * container error text, and still own their localized user-facing copy. New\n * kinds may be added over time, so treat unrecognized values like `Unknown`.\n */\n var MediaErrorKind;\n (function (MediaErrorKind) {\n /** The media could not be opened or read, or was refused before any read. */\n MediaErrorKind[\"Unreadable\"] = \"unreadable\";\n /** The container or codec is not supported by this platform. */\n MediaErrorKind[\"UnsupportedFormat\"] = \"unsupportedFormat\";\n /** The media opened but carries no usable video track. */\n MediaErrorKind[\"NoVideoTrack\"] = \"noVideoTrack\";\n /** Decoding a sample failed after the source opened. */\n MediaErrorKind[\"Decode\"] = \"decode\";\n /** The media could not be fetched. */\n MediaErrorKind[\"Network\"] = \"network\";\n /** The host environment lacks the APIs this media source requires. */\n MediaErrorKind[\"EnvironmentUnsupported\"] = \"environmentUnsupported\";\n /** The failure could not be classified. */\n MediaErrorKind[\"Unknown\"] = \"unknown\";\n })(MediaErrorKind || (MediaErrorKind = {}));\n /**\n * Lower-level media source readiness.\n */\n var MediaSourceStatus;\n (function (MediaSourceStatus) {\n MediaSourceStatus[\"Loading\"] = \"loading\";\n MediaSourceStatus[\"Ready\"] = \"ready\";\n MediaSourceStatus[\"Error\"] = \"error\";\n MediaSourceStatus[\"Destroyed\"] = \"destroyed\";\n })(MediaSourceStatus || (MediaSourceStatus = {}));\n /**\n * Current renderer state.\n */\n /**\n * What a playback gate holds back.\n *\n * A source the renderer pulls samples from can be held between any two frames,\n * because the renderer decides when each one is drawn. A source that presents\n * its own frames owns the playhead, so holding it means stopping the producer\n * and starting it again, and a gate that stops one has to bound its own wait\n * or a producer nothing answers for never runs again.\n */\n var PlaybackGateReach;\n (function (PlaybackGateReach) {\n /** No gate: the picture moves and unprepared layers are absent from it. */\n PlaybackGateReach[\"Off\"] = \"off\";\n /**\n * Playback waits to begin, and stops again at any frame whose artifacts are\n * missing, until they arrive or the gate's own wait bound gives up on them.\n */\n PlaybackGateReach[\"EveryFrame\"] = \"everyFrame\";\n })(PlaybackGateReach || (PlaybackGateReach = {}));\n\n /**\n * Media-session operating mode.\n *\n * Core owns the names because file-like and stream-like lifecycle choices are\n * platform-neutral. Platform packages decide how these modes tune media,\n * storage, and renderer defaults.\n */\n var MediaSessionMode;\n (function (MediaSessionMode) {\n /**\n * Finite media. Defaults usually favor seek/replay and persistent detection\n * storage.\n */\n MediaSessionMode[\"File\"] = \"file\";\n /**\n * Live or append-only media. Defaults usually favor rolling windows and\n * bounded retention.\n */\n MediaSessionMode[\"Stream\"] = \"stream\";\n })(MediaSessionMode || (MediaSessionMode = {}));\n /**\n * Aggregate lifecycle state for a media session.\n */\n var MediaSessionStatus;\n (function (MediaSessionStatus) {\n MediaSessionStatus[\"Buffering\"] = \"buffering\";\n MediaSessionStatus[\"Destroyed\"] = \"destroyed\";\n MediaSessionStatus[\"Error\"] = \"error\";\n MediaSessionStatus[\"Loading\"] = \"loading\";\n MediaSessionStatus[\"Paused\"] = \"paused\";\n MediaSessionStatus[\"Playing\"] = \"playing\";\n MediaSessionStatus[\"Processing\"] = \"processing\";\n MediaSessionStatus[\"Ready\"] = \"ready\";\n })(MediaSessionStatus || (MediaSessionStatus = {}));\n /**\n * Subsystem currently affecting session readiness or presentation.\n */\n var MediaSessionActivityKind;\n (function (MediaSessionActivityKind) {\n /**\n * Playback is held waiting for a producer to emit detections for the frame\n * about to be shown, which is a wait on inference rather than on transfer or\n * decode. `DetectionsBuffering` is the transfer of detections that already\n * exist, and `PlaybackBuffering` is the wait for media bytes.\n */\n MediaSessionActivityKind[\"DetectionsAwaitingCoverage\"] = \"detectionsAwaitingCoverage\";\n MediaSessionActivityKind[\"DetectionsBuffering\"] = \"detectionsBuffering\";\n MediaSessionActivityKind[\"DetectionsLoading\"] = \"detectionsLoading\";\n MediaSessionActivityKind[\"Error\"] = \"error\";\n MediaSessionActivityKind[\"MediaNormalizing\"] = \"mediaNormalizing\";\n MediaSessionActivityKind[\"MediaOpening\"] = \"mediaOpening\";\n /**\n * The picture is waiting on media the source has not handed over yet, which\n * is the bytes and their decode rather than anything downstream of them.\n * `PlaybackBuffering` is what a transport reports once it has already\n * stopped; this is reported from the read itself, including the reads a seek\n * makes while the transport still reads as paused.\n */\n MediaSessionActivityKind[\"MediaSourceReading\"] = \"mediaSourceReading\";\n MediaSessionActivityKind[\"PlaybackBuffering\"] = \"playbackBuffering\";\n /**\n * The render-preparation gate held the picture for artifacts that never\n * arrived and let it go. Nothing is blocked: the frames reaching the screen\n * are the ones whose artifacts were given up on. What is waited on is\n * preparation finishing another frame, which is what lets the gate hold the\n * picture again; `RenderPreparing` covers the holds that are running.\n */\n MediaSessionActivityKind[\"RenderPreparationAbandoned\"] = \"renderPreparationAbandoned\";\n MediaSessionActivityKind[\"RenderPreparing\"] = \"renderPreparing\";\n })(MediaSessionActivityKind || (MediaSessionActivityKind = {}));\n /**\n * State of one session activity.\n */\n var MediaSessionActivityStatus;\n (function (MediaSessionActivityStatus) {\n MediaSessionActivityStatus[\"Error\"] = \"error\";\n MediaSessionActivityStatus[\"Running\"] = \"running\";\n MediaSessionActivityStatus[\"Waiting\"] = \"waiting\";\n })(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));\n\n /** Builds compact, independent alpha crops so overlapping masks stay exact. */\n function createRegionMaskCoverageFrame(instructions) {\n const entries = [];\n for (const instruction of instructions) {\n if (!instruction.regionCoverageMask) {\n continue;\n }\n const entry = cropCoverageMask(instruction.detectionIndex, decodeCompressedRleMask(instruction.regionCoverageMask));\n if (entry) {\n entries.push(entry);\n }\n }\n return entries.length > 0 ? { entries } : undefined;\n }\n function cropCoverageMask(detectionIndex, decodedMask) {\n let minX = decodedMask.width;\n let minY = decodedMask.height;\n let maxX = -1;\n let maxY = -1;\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n }\n if (maxX < minX || maxY < minY) {\n return undefined;\n }\n const width = maxX - minX + 1;\n const height = maxY - minY + 1;\n const data = new Uint8Array(new ArrayBuffer(width * height));\n for (let y = minY; y <= maxY; y += 1) {\n for (let x = minX; x <= maxX; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n data[(y - minY) * width + (x - minX)] = 255;\n }\n }\n return { data, detectionIndex, height, width, x: minX, y: minY };\n }\n function compositeMaskFrame(instructions) {\n const maskInstructions = materializeMaskInstructions(instructions);\n if (maskInstructions.length === 0) {\n return undefined;\n }\n const width = Math.max(...maskInstructions.map(({ mask }) => mask.width));\n const height = Math.max(...maskInstructions.map(({ mask }) => mask.height));\n const data = new Uint8ClampedArray(new ArrayBuffer(width * height * 4));\n for (const instruction of maskInstructions) {\n compositeInstruction(data, width, instruction);\n }\n return { data, height, width };\n }\n function createIdMaskRasterFrame(instructions, maxRasterWidth) {\n try {\n return createIdMaskFrame(materializeMaskInstructions(instructions).map((instruction) => instruction.stroke?.widthUnit === \"screen\"\n ? { ...instruction, stroke: undefined }\n : instruction), {\n maxWidth: maxRasterWidth,\n });\n }\n catch {\n // The id raster is the fast path, not the only one: answering with nothing\n // puts the caller on the RGBA composite, which draws the same picture.\n return undefined;\n }\n }\n /**\n * Whether the palette has a slot for every detection this frame masks. An id\n * names a detection by its index in the frame, so one masked detection past the\n * last slot leaves the whole frame without an id raster however few masks it\n * carries.\n */\n function canIdMaskPaletteNameFrame(instructions) {\n return instructions.every((instruction) => instruction.visible === false ||\n resolveIdMaskPaletteId(instruction.detectionIndex) !== undefined);\n }\n function createIdMaskPlane(instructions, maxRasterWidth) {\n // The plane carries the ids a failed cook could not. A frame the palette\n // cannot name has none to carry, and cooking it again reaches the same\n // refusal after rasterizing every mask a second time.\n if (!canIdMaskPaletteNameFrame(instructions)) {\n return undefined;\n }\n const frame = createIdMaskRasterFrame(instructions, maxRasterWidth);\n return frame\n ? { data: frame.data, height: frame.height, width: frame.width }\n : undefined;\n }\n function compositeInstruction(rgba, canvasWidth, instruction) {\n const fill = resolveRgbaColor(instruction.color, instruction.alpha);\n const bounds = compositeMaskFill(rgba, canvasWidth, instruction.mask, fill);\n if (instruction.stroke &&\n instruction.stroke.widthUnit !== \"screen\" &&\n bounds) {\n compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, instruction.stroke);\n }\n }\n function createMaskScreenStrokes(instructions, width, height, preparedRaster) {\n const outlined = instructions.filter(({ visible, stroke }) => visible !== false &&\n stroke?.widthUnit === \"screen\" &&\n stroke.width > 0 &&\n stroke.alpha > 0);\n if (outlined.length === 0)\n return undefined;\n const masks = materializeMaskInstructions(instructions);\n const raster = preparedRaster ?? new Uint32Array(width * height);\n if (!preparedRaster) {\n for (const { detectionIndex, mask } of masks) {\n const counts = decodeCompressedRleCounts(mask.counts);\n let offset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const length = counts[index];\n if (index % 2 === 1) {\n for (let pixel = offset; pixel < offset + length; pixel += 1) {\n const x = Math.floor(pixel / mask.height);\n const y = pixel % mask.height;\n raster[y * width + x] = detectionIndex + 1;\n }\n }\n offset += length;\n }\n }\n }\n const paths = traceMaskOutlinePaths(raster, width, height, new Set(outlined.map(({ detectionIndex }) => detectionIndex + 1)));\n return outlined.map(({ detectionIndex, stroke }) => ({\n detectionIndex,\n alpha: stroke.alpha,\n color: stroke.color,\n width: stroke.width,\n paths: paths.get(detectionIndex + 1) ?? [],\n }));\n }\n function materializeMaskInstructions(instructions) {\n return instructions\n .filter((instruction) => instruction.visible !== false)\n .map((instruction) => {\n if (instruction.mask) {\n return instruction;\n }\n const { height, points, width } = instruction.polygon;\n return {\n alpha: instruction.alpha,\n color: instruction.color,\n detectionIndex: instruction.detectionIndex,\n mask: encodeBinaryMask(rasterizePolygonToMask(points, { height, width }), width, height),\n stroke: instruction.stroke,\n };\n });\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function compositeMaskFill(rgba, canvasWidth, mask, fill) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = mask.width;\n const maskHeight = mask.height;\n const maskArea = maskWidth * maskHeight;\n let maskOffset = 0;\n let minX = maskWidth;\n let minY = maskHeight;\n let maxX = -1;\n let maxY = -1;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let columnX = Math.floor(maskOffset / maskHeight);\n let columnY = maskOffset - columnX * maskHeight;\n for (let step = 0; step < runLength; step += 1) {\n // A run can outlast the columns the mask has; the pixel it then names\n // is wherever that column-major offset lands when read back row-major.\n const rowMajorOffset = columnY * maskWidth + columnX;\n if (rowMajorOffset < maskArea) {\n const x = rowMajorOffset % maskWidth;\n const y = (rowMajorOffset - x) / maskWidth;\n writePixel(rgba, canvasWidth, x, y, fill);\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n columnY += 1;\n if (columnY === maskHeight) {\n columnY = 0;\n columnX += 1;\n }\n }\n maskOffset += runLength;\n }\n return maxX < minX || maxY < minY ? undefined : { maxX, maxY, minX, minY };\n }\n function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke) {\n const width = Math.round(stroke.width);\n if (width <= 0) {\n return;\n }\n const strokeColor = resolveRgbaColor(stroke.color, stroke.alpha);\n for (let y = bounds.minY; y <= bounds.maxY; y += 1) {\n for (let x = bounds.minX; x <= bounds.maxX; x += 1) {\n if (!isMaskPixel(decodedMask, x, y) ||\n !isBoundaryPixel(decodedMask, x, y)) {\n continue;\n }\n for (let offsetY = -width; offsetY <= width; offsetY += 1) {\n for (let offsetX = -width; offsetX <= width; offsetX += 1) {\n const strokeX = x + offsetX;\n const strokeY = y + offsetY;\n if (isOutsideMaskBounds(decodedMask, strokeX, strokeY) ||\n isMaskPixel(decodedMask, strokeX, strokeY)) {\n continue;\n }\n writePixel(rgba, canvasWidth, strokeX, strokeY, strokeColor);\n }\n }\n }\n }\n }\n function isBoundaryPixel(mask, x, y) {\n for (let offsetY = -1; offsetY <= 1; offsetY += 1) {\n for (let offsetX = -1; offsetX <= 1; offsetX += 1) {\n if (offsetX === 0 && offsetY === 0) {\n continue;\n }\n const neighborX = x + offsetX;\n const neighborY = y + offsetY;\n if (isOutsideMaskBounds(mask, neighborX, neighborY) ||\n !isMaskPixel(mask, neighborX, neighborY)) {\n return true;\n }\n }\n }\n return false;\n }\n function isMaskPixel(mask, x, y) {\n return mask.data[y * mask.width + x] === 1;\n }\n function isOutsideMaskBounds(mask, x, y) {\n return x < 0 || y < 0 || x >= mask.width || y >= mask.height;\n }\n function resolveRgbaColor(color, alpha) {\n return {\n alpha: Math.round(Math.max(0, Math.min(alpha, 1)) * 255),\n blue: color & 0xff,\n green: (color >> 8) & 0xff,\n red: (color >> 16) & 0xff,\n };\n }\n function writePixel(rgba, canvasWidth, x, y, color) {\n const rgbaOffset = (y * canvasWidth + x) * 4;\n rgba[rgbaOffset] = color.red;\n rgba[rgbaOffset + 1] = color.green;\n rgba[rgbaOffset + 2] = color.blue;\n rgba[rgbaOffset + 3] = color.alpha;\n }\n\n var PreparedMaskFrameKind;\n (function (PreparedMaskFrameKind) {\n PreparedMaskFrameKind[\"IdMask\"] = \"idMask\";\n PreparedMaskFrameKind[\"RgbaImage\"] = \"rgbaImage\";\n })(PreparedMaskFrameKind || (PreparedMaskFrameKind = {}));\n /**\n * How an id raster is laid out for the texture it is uploaded into. WebGPU\n * takes any bytesPerRow, so ids go up one byte per pixel. WebGL aligns every\n * uploaded row to four bytes and rejects a single-channel upload whose width\n * is not a multiple of four, which is what the four-channel layout is for.\n */\n var IdMaskTextureFormat;\n (function (IdMaskTextureFormat) {\n IdMaskTextureFormat[\"R8\"] = \"r8unorm\";\n IdMaskTextureFormat[\"Rgba8\"] = \"rgba8unorm\";\n })(IdMaskTextureFormat || (IdMaskTextureFormat = {}));\n\n const DEFAULT_STOPS = [\n { position: 0, color: 0xffd600 },\n { position: 0.35, color: 0xffd600 },\n { position: 0.7, color: 0xff7a00 },\n { position: 1, color: 0xdc1e1e },\n ];\n const MAX_HEATMAP_PIXELS = 16_777_216;\n const MAX_HEATMAP_SIDE = 8_192;\n /** Turns semantic scores into an RGBA raster; no palette enters cold data. */\n function colorizeHeatmap(map, renderer) {\n const { bounds, width, height, values } = map;\n if (!Number.isFinite(bounds?.x) ||\n !Number.isFinite(bounds?.y) ||\n !Number.isFinite(bounds?.width) ||\n !Number.isFinite(bounds?.height) ||\n bounds.width <= 0 ||\n bounds.height <= 0 ||\n !Number.isInteger(width) ||\n !Number.isInteger(height) ||\n width <= 0 ||\n height <= 0 ||\n width > MAX_HEATMAP_SIDE ||\n height > MAX_HEATMAP_SIDE ||\n width * height > MAX_HEATMAP_PIXELS ||\n !(Array.isArray(values) ||\n values instanceof Float32Array ||\n values instanceof Float64Array ||\n values instanceof Uint16Array) ||\n values.length !== width * height) {\n throw new RangeError(\"Heatmap bounds and dimensions must be valid, match row-major values, and fit the raster limit.\");\n }\n const scale = map.valueScale ?? 1;\n const threshold = (map.threshold ?? 0) * (renderer.thresholdScale ?? 1);\n const maximumScore = renderer.maximumScore ?? 1;\n const opacity = clamp01(renderer.opacity ?? 1);\n const minimumAlpha = clamp01(renderer.minimumAlpha ?? 0);\n const stops = renderer.colorStops ?? DEFAULT_STOPS;\n if (!Number.isFinite(scale) ||\n scale <= 0 ||\n !Number.isFinite(threshold) ||\n !Number.isFinite(maximumScore) ||\n maximumScore <= 0 ||\n !Number.isFinite(renderer.thresholdScale ?? 1) ||\n (renderer.thresholdScale ?? 1) < 0 ||\n !Number.isFinite(renderer.opacity ?? 1) ||\n !Number.isFinite(renderer.minimumAlpha ?? 0) ||\n stops.length === 0 ||\n stops.some((stop, index) => !Number.isFinite(stop.position) ||\n stop.position < 0 ||\n stop.position > 1 ||\n !Number.isInteger(stop.color) ||\n stop.color < 0 ||\n stop.color > 0xffffff ||\n (index > 0 && stop.position < stops[index - 1].position))) {\n throw new RangeError(\"Invalid heatmap scale, threshold, or color stops.\");\n }\n const rgba = new Uint8ClampedArray(new ArrayBuffer(values.length * 4));\n if (maximumScore <= threshold || opacity === 0)\n return rgba;\n for (let index = 0; index < values.length; index += 1) {\n const score = values[index] * scale;\n if (!Number.isFinite(score) || score <= threshold)\n continue;\n const color = interpolateColor(stops, clamp01(score / maximumScore));\n const alphaIntensity = clamp01((score - threshold) / (maximumScore - threshold));\n const offset = index * 4;\n rgba[offset] = color >> 16;\n rgba[offset + 1] = (color >> 8) & 0xff;\n rgba[offset + 2] = color & 0xff;\n rgba[offset + 3] = Math.round(255 * opacity * (minimumAlpha + (1 - minimumAlpha) * alphaIntensity));\n }\n return rgba;\n }\n function interpolateColor(stops, value) {\n const upperIndex = stops.findIndex((stop) => stop.position >= value);\n if (upperIndex === -1)\n return stops[stops.length - 1].color;\n if (upperIndex === 0)\n return stops[0].color;\n const left = stops[upperIndex - 1];\n const right = stops[upperIndex];\n const span = right.position - left.position;\n const ratio = span === 0 ? 1 : (value - left.position) / span;\n const channel = (shift) => Math.round(((left.color >> shift) & 0xff) * (1 - ratio) +\n ((right.color >> shift) & 0xff) * ratio);\n return (channel(16) << 16) | (channel(8) << 8) | channel(0);\n }\n function clamp01(value) {\n return Math.max(0, Math.min(1, value));\n }\n\n var HeatmapPreparationWorkerMessageType;\n (function (HeatmapPreparationWorkerMessageType) {\n HeatmapPreparationWorkerMessageType[\"Prepare\"] = \"heatmap-prepare\";\n HeatmapPreparationWorkerMessageType[\"Complete\"] = \"heatmap-complete\";\n HeatmapPreparationWorkerMessageType[\"Error\"] = \"heatmap-error\";\n })(HeatmapPreparationWorkerMessageType || (HeatmapPreparationWorkerMessageType = {}));\n\n var MaskPreparationWorkerMessageType;\n (function (MaskPreparationWorkerMessageType) {\n MaskPreparationWorkerMessageType[\"Complete\"] = \"complete\";\n MaskPreparationWorkerMessageType[\"Empty\"] = \"empty\";\n MaskPreparationWorkerMessageType[\"Error\"] = \"error\";\n MaskPreparationWorkerMessageType[\"Prepare\"] = \"prepare\";\n })(MaskPreparationWorkerMessageType || (MaskPreparationWorkerMessageType = {}));\n\n /**\n * Messages between the depth source and the render-preparation worker, which\n * decodes depth and confidence PNGs, and copies decoded preview frames' luma,\n * off the main thread. Internal: the worker script is a deployment asset, and\n * its protocol is not public.\n */\n var DepthPreparationWorkerMessageType;\n (function (DepthPreparationWorkerMessageType) {\n DepthPreparationWorkerMessageType[\"Decode\"] = \"depth-decode\";\n DepthPreparationWorkerMessageType[\"Complete\"] = \"depth-decode-complete\";\n DepthPreparationWorkerMessageType[\"Error\"] = \"depth-decode-error\";\n DepthPreparationWorkerMessageType[\"PreviewLuma\"] = \"depth-preview-luma\";\n DepthPreparationWorkerMessageType[\"PreviewLumaComplete\"] = \"depth-preview-luma-complete\";\n })(DepthPreparationWorkerMessageType || (DepthPreparationWorkerMessageType = {}));\n\n /*\n * Grayscale PNG decoding for depth maps and their confidence planes.\n *\n * Every image path a browser offers (<img>, createImageBitmap, ImageDecoder,\n * canvas) hands back 8 bits per channel, so a 16-bit depth PNG is decoded\n * here: the browser's own zlib inflates the IDAT stream, and the row filters\n * are undone in JavaScript.\n *\n * This module imports nothing, so the render-preparation worker, the main\n * thread and the depth benchmark all run the same code.\n */\n const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];\n const COLOR_TYPE_GRAY = 0;\n /** 16384 x 16384, beyond any GPU texture; a header claiming more is refused. */\n const MAX_PNG_PIXELS = 16_384 * 16_384;\n async function decodePng16(bytes, options = {}) {\n const header = readPngHeader(bytes, 16);\n const rowBytes = header.width * 2 + 1;\n const raw = await (options.inflate ?? inflateWithDecompressionStream)(header.idat, rowBytes * header.height);\n const values = new Uint16Array(header.width * header.height);\n checkInflatedLength(raw, rowBytes * header.height);\n unfilterGray16(raw, header.width, header.height, values);\n return {\n height: header.height,\n paddedUpload: options.padRowsForWebGl && header.width % 2 === 1\n ? padRows(values, header.width, header.height)\n : undefined,\n values,\n width: header.width,\n };\n }\n async function decodePng8Gray(bytes, options = {}) {\n const header = readPngHeader(bytes, 8);\n const rowBytes = header.width + 1;\n const raw = await (options.inflate ?? inflateWithDecompressionStream)(header.idat, rowBytes * header.height);\n const values = new Uint8Array(header.width * header.height);\n checkInflatedLength(raw, rowBytes * header.height);\n unfilterGray8(raw, header.width, header.height, values);\n return { height: header.height, values, width: header.width };\n }\n /**\n * Inflates with the platform's `DecompressionStream(\"deflate\")` (zlib\n * framing, which is what PNG uses) straight into one buffer of the expected\n * size, so no second copy joins the output chunks.\n */\n async function inflateWithDecompressionStream(chunks, byteLength) {\n const stream = new DecompressionStream(\"deflate\");\n const writer = stream.writable.getWriter();\n const reader = stream.readable.getReader();\n // Writers split IDAT into many small chunks (libpng's default is 8 KiB).\n // One write of the joined stream costs a copy of the compressed bytes and\n // saves a stream round trip per chunk.\n const zlib = chunks.length === 1 ? chunks[0] : joinChunks(chunks);\n const writing = (async () => {\n await writer.write(zlib);\n await writer.close();\n })();\n const output = new Uint8Array(byteLength);\n let offset = 0;\n // A corrupt stream rejects both sides; the reader's error is the one thrown.\n writing.catch(() => undefined);\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done)\n break;\n if (offset + value.byteLength > byteLength) {\n throw new RangeError(\"PNG image data is longer than its header says.\");\n }\n output.set(value, offset);\n offset += value.byteLength;\n }\n await writing;\n }\n catch (error) {\n await reader.cancel().catch(() => undefined);\n throw error instanceof RangeError\n ? error\n : new RangeError(`PNG image data does not inflate: ${error instanceof Error ? error.message : String(error)}`);\n }\n return offset === byteLength ? output : output.subarray(0, offset);\n }\n function joinChunks(chunks) {\n const joined = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0));\n let offset = 0;\n for (const chunk of chunks) {\n joined.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return joined;\n }\n function readPngHeader(input, bitDepth) {\n const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n if (bytes.byteLength < 8 ||\n PNG_SIGNATURE.some((value, index) => bytes[index] !== value)) {\n throw new RangeError(\"Not a PNG file.\");\n }\n let offset = 8;\n let width = 0;\n let height = 0;\n let sawHeader = false;\n let sawEnd = false;\n const idat = [];\n while (offset + 12 <= bytes.byteLength) {\n const length = view.getUint32(offset);\n const type = view.getUint32(offset + 4);\n const dataStart = offset + 8;\n const dataEnd = dataStart + length;\n if (dataEnd + 4 > bytes.byteLength) {\n throw new RangeError(\"PNG file is truncated.\");\n }\n if (type === 0x49484452) {\n // IHDR\n if (length !== 13)\n throw new RangeError(\"PNG header is malformed.\");\n width = view.getUint32(dataStart);\n height = view.getUint32(dataStart + 4);\n const depth = bytes[dataStart + 8];\n const colorType = bytes[dataStart + 9];\n if (bytes[dataStart + 10] !== 0 || bytes[dataStart + 11] !== 0) {\n throw new RangeError(\"PNG uses an unknown compression or filter.\");\n }\n if (bytes[dataStart + 12] !== 0) {\n throw new RangeError(\"Interlaced PNGs are not supported.\");\n }\n if (colorType !== COLOR_TYPE_GRAY || depth !== bitDepth) {\n throw new RangeError(`Expected a ${bitDepth}-bit grayscale PNG, got bit depth ${depth} with colour type ${colorType}.`);\n }\n sawHeader = true;\n }\n else if (type === 0x49444154) {\n // IDAT\n idat.push(bytes.subarray(dataStart, dataEnd));\n }\n else if (type === 0x49454e44) {\n // IEND\n sawEnd = true;\n break;\n }\n offset = dataEnd + 4;\n }\n if (!sawHeader)\n throw new RangeError(\"PNG has no header.\");\n if (!sawEnd)\n throw new RangeError(\"PNG file is truncated.\");\n if (width === 0 || height === 0 || width * height > MAX_PNG_PIXELS) {\n throw new RangeError(`PNG size ${width}x${height} is not supported.`);\n }\n if (idat.length === 0)\n throw new RangeError(\"PNG has no image data.\");\n return { height, idat, width };\n }\n function checkInflatedLength(raw, expected) {\n if (raw.byteLength !== expected) {\n throw new RangeError(\"PNG image data is shorter than its header says.\");\n }\n }\n /*\n * Undoes the row filters of a 16-bit grayscale image and writes host-order\n * samples, in one pass. Each filter runs on bytes, and the two bytes of a\n * sample never carry into each other, so the loops keep a sample's high and\n * low bytes apart and join them only for the store. The previous row is read\n * back from the output, which already holds it unfiltered; the first row reads\n * a row of zeros, which is what PNG defines above the image.\n *\n * Each filter is its own small function with one loop, called once per row,\n * so engines compile each loop on its own: SpiderMonkey slowed a Paeth loop\n * that shared a function with the other filters.\n */\n function unfilterGray16(raw, width, height, out) {\n const rowBytes = width * 2 + 1;\n const zeroRow = new Uint16Array(width);\n for (let y = 0; y < height; y += 1) {\n const filter = raw[y * rowBytes];\n const unfilterRow = ROW_FILTERS_16[filter];\n if (!unfilterRow) {\n throw new RangeError(`PNG row ${y} has unknown filter ${filter}.`);\n }\n const o = y * width;\n unfilterRow(raw, y * rowBytes + 1, out, o, o + width, y === 0 ? zeroRow : out, y === 0 ? 0 : o - width);\n }\n }\n function noneRow16(raw, s, out, o, end) {\n for (let i = o; i < end; i += 1, s += 2) {\n out[i] = (raw[s] << 8) | raw[s + 1];\n }\n }\n function subRow16(raw, s, out, o, end) {\n let hi = 0;\n let lo = 0;\n for (let i = o; i < end; i += 1, s += 2) {\n hi = (raw[s] + hi) & 0xff;\n lo = (raw[s + 1] + lo) & 0xff;\n out[i] = (hi << 8) | lo;\n }\n }\n function upRow16(raw, s, out, o, end, prev, p) {\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n out[i] = (((raw[s] + (up >> 8)) & 0xff) << 8) | ((raw[s + 1] + up) & 0xff);\n }\n }\n function averageRow16(raw, s, out, o, end, prev, p) {\n let hi = 0;\n let lo = 0;\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n hi = (raw[s] + ((hi + (up >> 8)) >> 1)) & 0xff;\n lo = (raw[s + 1] + ((lo + (up & 0xff)) >> 1)) & 0xff;\n out[i] = (hi << 8) | lo;\n }\n }\n /** Paeth: per byte, whichever of left, up and up-left is closest to left + up - up-left. */\n function paethRow16(raw, s, out, o, end, prev, p) {\n let hi = 0;\n let lo = 0;\n let upLeftHi = 0;\n let upLeftLo = 0;\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n const upHi = up >> 8;\n const upLo = up & 0xff;\n let pa = Math.abs(upHi - upLeftHi);\n let pb = Math.abs(hi - upLeftHi);\n let pc = Math.abs(hi + upHi - upLeftHi - upLeftHi);\n hi =\n (raw[s] + (pa <= pb && pa <= pc ? hi : pb <= pc ? upHi : upLeftHi)) &\n 0xff;\n pa = Math.abs(upLo - upLeftLo);\n pb = Math.abs(lo - upLeftLo);\n pc = Math.abs(lo + upLo - upLeftLo - upLeftLo);\n lo =\n (raw[s + 1] + (pa <= pb && pa <= pc ? lo : pb <= pc ? upLo : upLeftLo)) &\n 0xff;\n out[i] = (hi << 8) | lo;\n upLeftHi = upHi;\n upLeftLo = upLo;\n }\n }\n /** Indexed by the PNG filter type byte: None, Sub, Up, Average, Paeth. */\n const ROW_FILTERS_16 = [\n noneRow16,\n subRow16,\n upRow16,\n averageRow16,\n paethRow16,\n ];\n function unfilterGray8(raw, width, height, out) {\n const rowBytes = width + 1;\n const zeroRow = new Uint8Array(width);\n for (let y = 0; y < height; y += 1) {\n const filter = raw[y * rowBytes];\n const unfilterRow = ROW_FILTERS_8[filter];\n if (!unfilterRow) {\n throw new RangeError(`PNG row ${y} has unknown filter ${filter}.`);\n }\n const o = y * width;\n unfilterRow(raw, y * rowBytes + 1, out, o, o + width, y === 0 ? zeroRow : out, y === 0 ? 0 : o - width);\n }\n }\n function noneRow8(raw, s, out, o, end) {\n out.set(raw.subarray(s, s + end - o), o);\n }\n function subRow8(raw, s, out, o, end) {\n let left = 0;\n for (let i = o; i < end; i += 1, s += 1) {\n left = (raw[s] + left) & 0xff;\n out[i] = left;\n }\n }\n function upRow8(raw, s, out, o, end, prev, p) {\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n out[i] = (raw[s] + prev[p]) & 0xff;\n }\n }\n function averageRow8(raw, s, out, o, end, prev, p) {\n let left = 0;\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n left = (raw[s] + ((left + prev[p]) >> 1)) & 0xff;\n out[i] = left;\n }\n }\n function paethRow8(raw, s, out, o, end, prev, p) {\n let left = 0;\n let upLeft = 0;\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n const up = prev[p];\n const pa = Math.abs(up - upLeft);\n const pb = Math.abs(left - upLeft);\n const pc = Math.abs(left + up - upLeft - upLeft);\n left =\n (raw[s] + (pa <= pb && pa <= pc ? left : pb <= pc ? up : upLeft)) & 0xff;\n out[i] = left;\n upLeft = up;\n }\n }\n const ROW_FILTERS_8 = [\n noneRow8,\n subRow8,\n upRow8,\n averageRow8,\n paethRow8,\n ];\n /**\n * WebGL unpacks rows on four-byte boundaries, so an odd width of two-byte\n * texels needs one padding texel per row.\n */\n function padRows(values, width, height) {\n const textureWidth = width + 1;\n const padded = new Uint16Array(textureWidth * height);\n for (let y = 0; y < height; y += 1) {\n padded.set(values.subarray(y * width, (y + 1) * width), y * textureWidth);\n }\n return { bytes: new Uint8Array(padded.buffer), textureWidth };\n }\n\n /**\n * Answers one decode request, in the worker or on the main thread: the reply\n * and the buffers it can transfer. A file that does not decode is an error\n * reply, never a thrown exception, so the caller can tell a bad file from a\n * broken worker.\n */\n async function decodeDepthPreparationRequest(request) {\n const requestId = request.requestId ?? 0;\n try {\n if (request.bitDepth === 8) {\n const decoded = await decodePng8Gray(request.bytes);\n return {\n response: {\n bitDepth: 8,\n height: decoded.height,\n requestId,\n type: DepthPreparationWorkerMessageType.Complete,\n values: decoded.values.buffer,\n width: decoded.width,\n },\n transfer: [decoded.values.buffer],\n };\n }\n const decoded = await decodePng16(request.bytes, {\n padRowsForWebGl: request.padRowsForWebGl,\n });\n const padded = decoded.paddedUpload;\n const decimated = request.decimateBy !== undefined && request.decimateBy >= 2\n ? decimateDepthUpload(decoded.values, decoded.width, decoded.height, Math.floor(request.decimateBy), request.padRowsForWebGl === true)\n : null;\n const transfer = [decoded.values.buffer];\n if (padded)\n transfer.push(padded.bytes.buffer);\n if (decimated)\n transfer.push(decimated.bytes.buffer);\n return {\n response: {\n bitDepth: 16,\n decimatedUpload: decimated\n ? { ...decimated, bytes: decimated.bytes.buffer }\n : undefined,\n height: decoded.height,\n paddedUpload: padded\n ? {\n bytes: padded.bytes.buffer,\n textureWidth: padded.textureWidth,\n }\n : undefined,\n requestId,\n type: DepthPreparationWorkerMessageType.Complete,\n values: decoded.values.buffer,\n width: decoded.width,\n },\n transfer,\n };\n }\n catch (error) {\n return {\n response: {\n error: error instanceof Error ? error.message : \"Unable to decode depth.\",\n requestId,\n type: DepthPreparationWorkerMessageType.Error,\n },\n transfer: [],\n };\n }\n }\n /**\n * Depth for a texture `factor` times smaller on each side, each texel the\n * sample at its block's centre, as the renderer decimates a map larger than\n * the GPU allows.\n */\n function decimateDepthUpload(values, width, height, factor, padRowsForWebGl) {\n if (factor < 2)\n return null;\n const targetWidth = Math.ceil(width / factor);\n const targetHeight = Math.ceil(height / factor);\n // WebGL reads two-byte texels in rows of four bytes.\n const textureWidth = padRowsForWebGl\n ? targetWidth + (targetWidth % 2)\n : targetWidth;\n const texels = new Uint16Array(textureWidth * targetHeight);\n const columns = new Uint32Array(targetWidth);\n for (let x = 0; x < targetWidth; x += 1) {\n columns[x] = Math.min(width - 1, Math.floor(((x + 0.5) * width) / targetWidth));\n }\n for (let y = 0; y < targetHeight; y += 1) {\n const row = Math.min(height - 1, Math.floor(((y + 0.5) * height) / targetHeight)) *\n width;\n const target = y * textureWidth;\n for (let x = 0; x < targetWidth; x += 1) {\n texels[target + x] = values[row + columns[x]];\n }\n }\n return {\n bytes: new Uint8Array(texels.buffer),\n height: targetHeight,\n textureWidth,\n width: targetWidth,\n };\n }\n\n /** Formats whose first plane is 8-bit luma, which is the code as written. */\n const PLANAR_8_BIT_FORMATS = new Set([\n \"I420\",\n \"I420A\",\n \"I422\",\n \"I422A\",\n \"I444\",\n \"I444A\",\n \"NV12\",\n ]);\n const RGB_FORMATS = new Set([\"RGBA\", \"RGBX\", \"BGRA\", \"BGRX\"]);\n /**\n * Copies a decoded preview frame's luma codes out of it, mapped through\n * `correction` when one is given. The frame stays open; its owner closes\n * it.\n */\n async function readVideoFrameLuma(frame, scratch, correction = null) {\n const rect = frame.visibleRect ?? {\n height: frame.codedHeight,\n width: frame.codedWidth,\n x: 0,\n y: 0,\n };\n const { width, height } = rect;\n const format = frame.format;\n const planar = format !== null && PLANAR_8_BIT_FORMATS.has(format);\n let bytes;\n let plane;\n if (planar || (format !== null && RGB_FORMATS.has(format))) {\n const size = frame.allocationSize({ rect });\n if (scratch.buffer.byteLength < size)\n scratch.buffer = new ArrayBuffer(size);\n [plane] = await frame.copyTo(scratch.buffer, { rect });\n bytes = new Uint8Array(scratch.buffer);\n }\n else {\n // A frame in a layout no copy can read is drawn to a canvas, which\n // converts it to RGB as the browser sees fit.\n bytes = drawFrameRgba(frame, width, height);\n }\n const luma = new Uint8Array(width * height);\n const stride = plane?.stride ?? (planar ? width : width * 4);\n const offset = plane?.offset ?? 0;\n if (!planar) {\n // Every RGB layout keeps green second, and luma with neutral chroma\n // converts to three equal channels.\n for (let y = 0; y < height; y += 1) {\n const row = offset + y * stride + 1;\n const target = y * width;\n for (let x = 0; x < width; x += 1) {\n luma[target + x] = bytes[row + x * 4];\n }\n }\n }\n else if (stride === width) {\n luma.set(bytes.subarray(offset, offset + width * height));\n }\n else {\n for (let y = 0; y < height; y += 1) {\n luma.set(bytes.subarray(offset + y * stride, offset + y * stride + width), y * width);\n }\n }\n if (correction) {\n for (let index = 0; index < luma.length; index += 1) {\n luma[index] = correction[luma[index]];\n }\n }\n return { height, luma, path: planar ? \"plane\" : \"rgb\", width };\n }\n function drawFrameRgba(frame, width, height) {\n const canvas = new OffscreenCanvas(width, height);\n const context = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!context) {\n throw new Error(\"The depth preview needs a 2D canvas to read this frame.\");\n }\n context.drawImage(frame, 0, 0, width, height);\n const { data } = context.getImageData(0, 0, width, height);\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n\n const workerScope = globalThis;\n workerScope.addEventListener(\"message\", (event) => {\n const message = event.data;\n if (message.type === HeatmapPreparationWorkerMessageType.Prepare) {\n prepareHeatmap(message);\n return;\n }\n if (message.type === DepthPreparationWorkerMessageType.Decode) {\n void decodeDepthFrame(message);\n return;\n }\n if (message.type === DepthPreparationWorkerMessageType.PreviewLuma) {\n void copyPreviewLuma(message);\n return;\n }\n if (message.type !== MaskPreparationWorkerMessageType.Prepare) {\n return;\n }\n prepareMaskFrame(message);\n });\n function prepareHeatmap(message) {\n try {\n const data = colorizeHeatmap(message.map, message.renderer);\n const imageData = new ImageData(data, message.map.width, message.map.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n if (imageBitmap) {\n workerScope.postMessage({\n imageBitmap,\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Complete,\n }, [imageBitmap]);\n return;\n }\n workerScope.postMessage({\n imageData,\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Complete,\n }, [imageData.data.buffer]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error ? error.message : \"Unable to prepare heatmap.\",\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Error,\n });\n }\n }\n /** Decoding waits on the platform's inflate, so replies arrive out of order. */\n async function decodeDepthFrame(message) {\n const { response, transfer } = await decodeDepthPreparationRequest(message);\n workerScope.postMessage(response, transfer);\n }\n const previewLumaScratch = {\n buffer: new ArrayBuffer(0),\n };\n /** Frames arrive in order and are copied in order, through one scratch. */\n let previewLumaQueue = Promise.resolve();\n function copyPreviewLuma(message) {\n previewLumaQueue = previewLumaQueue.then(async () => {\n try {\n const copied = await readVideoFrameLuma(message.frame, previewLumaScratch, message.correction);\n workerScope.postMessage({\n height: copied.height,\n luma: copied.luma.buffer,\n path: copied.path,\n requestId: message.requestId,\n type: DepthPreparationWorkerMessageType.PreviewLumaComplete,\n width: copied.width,\n }, [copied.luma.buffer]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to copy the depth preview frame.\",\n requestId: message.requestId,\n type: DepthPreparationWorkerMessageType.Error,\n });\n }\n finally {\n message.frame.close();\n }\n });\n return previewLumaQueue;\n }\n function prepareMaskFrame(message) {\n try {\n const regionMaskCoverage = createRegionMaskCoverageFrame(message.job.instructions);\n const coverageTransfers = getRegionMaskCoverageTransfers(regionMaskCoverage);\n const idMaskFrame = createIdMaskRasterFrame(message.job.instructions, message.job.maxRasterWidth);\n if (idMaskFrame) {\n const screenStrokes = createMaskScreenStrokes(message.job.instructions, idMaskFrame.width, idMaskFrame.height, idMaskFrame.data);\n const strokeTransfers = screenStrokes?.flatMap(({ paths }) => paths.map((path) => path.buffer)) ?? [];\n workerScope.postMessage({\n artifactKind: PreparedMaskFrameKind.IdMask,\n fillPalette: idMaskFrame.fillPalette,\n hasStroke: idMaskFrame.hasStroke,\n height: idMaskFrame.height,\n key: message.job.key,\n maxStrokeWidth: idMaskFrame.maxStrokeWidth,\n raster: idMaskFrame.data,\n screenStrokes,\n regionMaskCoverage,\n requestId: message.requestId,\n sourceWidth: idMaskFrame.sourceWidth,\n strokePalette: idMaskFrame.strokePalette,\n strokeWidths: idMaskFrame.strokeWidths,\n type: MaskPreparationWorkerMessageType.Complete,\n width: idMaskFrame.width,\n }, [\n idMaskFrame.data.buffer,\n idMaskFrame.fillPalette.buffer,\n idMaskFrame.strokePalette.buffer,\n idMaskFrame.strokeWidths.buffer,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n return;\n }\n const compositedFrame = compositeMaskFrame(message.job.instructions);\n if (!compositedFrame && !regionMaskCoverage) {\n workerScope.postMessage({\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Empty,\n });\n return;\n }\n const preparedPixels = compositedFrame ?? createTransparentCoverageCarrier();\n const imageData = new ImageData(preparedPixels.data, preparedPixels.width, preparedPixels.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n const idMaskPlane = createIdMaskPlane(message.job.instructions, message.job.maxRasterWidth);\n const idMaskTransfers = idMaskPlane ? [idMaskPlane.data.buffer] : [];\n const screenStrokes = createMaskScreenStrokes(message.job.instructions, preparedPixels.width, preparedPixels.height);\n const strokeTransfers = screenStrokes?.flatMap(({ paths }) => paths.map((path) => path.buffer)) ??\n [];\n if (imageBitmap) {\n workerScope.postMessage({\n idMaskPlane,\n imageBitmap,\n screenStrokes,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [\n imageBitmap,\n ...idMaskTransfers,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n return;\n }\n workerScope.postMessage({\n idMaskPlane,\n imageData,\n screenStrokes,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [\n imageData.data.buffer,\n ...idMaskTransfers,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to prepare mask frame.\",\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Error,\n });\n }\n }\n /**\n * A frame that carries only region coverage has nothing to composite, and the\n * RGBA branch still has to produce an artifact for the coverage to ride on.\n */\n function createTransparentCoverageCarrier() {\n return {\n data: new Uint8ClampedArray(new ArrayBuffer(4)),\n height: 1,\n width: 1,\n };\n }\n function getRegionMaskCoverageTransfers(coverage) {\n return coverage?.entries.map(({ data }) => data.buffer) ?? [];\n }\n function createImageBitmapFromImageData(imageData) {\n if (typeof OffscreenCanvas === \"undefined\") {\n return null;\n }\n const canvas = new OffscreenCanvas(imageData.width, imageData.height);\n const context = canvas.getContext(\"2d\");\n if (!context) {\n return null;\n }\n context.putImageData(imageData, 0, 0);\n return canvas.transferToImageBitmap();\n }\n\n})();";
3116
+ const EMBEDDED_MASK_PREPARATION_WORKER_SOURCE = "(function () {\n 'use strict';\n\n /** Trace closed pixel boundaries, retaining holes and disconnected components. */\n function traceMaskOutlinePaths(raster, width, height, ids) {\n const edgesById = new Map();\n const stride = width + 1;\n const steps = [1, stride, -1, -stride];\n const add = (id, vertex, direction) => {\n let edges = edgesById.get(id);\n if (!edges)\n edgesById.set(id, (edges = new Map()));\n const outgoing = edges.get(vertex);\n if (outgoing)\n outgoing.push(direction);\n else\n edges.set(vertex, [direction]);\n };\n for (let y = 0; y < height; y += 1) {\n for (let x = 0; x < width; x += 1) {\n const offset = y * width + x;\n const id = raster[offset];\n if (!ids.has(id))\n continue;\n const vertex = y * stride + x;\n if (y === 0 || raster[offset - width] !== id)\n add(id, vertex, 0);\n if (x === width - 1 || raster[offset + 1] !== id)\n add(id, vertex + 1, 1);\n if (y === height - 1 || raster[offset + width] !== id)\n add(id, vertex + stride + 1, 2);\n if (x === 0 || raster[offset - 1] !== id)\n add(id, vertex + stride, 3);\n }\n }\n const result = new Map();\n for (const [id, edges] of edgesById) {\n const paths = [];\n while (edges.size > 0) {\n const start = edges.keys().next().value;\n let vertex = start;\n let previousDirection = -1;\n let firstDirection = -1;\n const points = [];\n do {\n const outgoing = edges.get(vertex);\n // Turn right at diagonal contacts so separate islands stay separate.\n const direction = previousDirection < 0\n ? outgoing[0]\n : [1, 0, 3, 2]\n .map((turn) => (previousDirection + turn) % 4)\n .find((candidate) => outgoing.includes(candidate));\n if (firstDirection < 0)\n firstDirection = direction;\n if (direction !== previousDirection)\n points.push(vertex % stride, Math.floor(vertex / stride));\n outgoing.splice(outgoing.indexOf(direction), 1);\n if (outgoing.length === 0)\n edges.delete(vertex);\n vertex += steps[direction];\n previousDirection = direction;\n } while (vertex !== start);\n if (previousDirection === firstDirection)\n points.splice(0, 2);\n paths.push(new Float32Array(points));\n }\n result.set(id, paths);\n }\n return result;\n }\n\n /** COCO-compatible keypoint visibility values. */\n var KeypointVisibility;\n (function (KeypointVisibility) {\n KeypointVisibility[KeypointVisibility[\"NotLabeled\"] = 0] = \"NotLabeled\";\n KeypointVisibility[KeypointVisibility[\"Occluded\"] = 1] = \"Occluded\";\n KeypointVisibility[KeypointVisibility[\"Visible\"] = 2] = \"Visible\";\n })(KeypointVisibility || (KeypointVisibility = {}));\n var DetectionMaskEncoding;\n (function (DetectionMaskEncoding) {\n DetectionMaskEncoding[\"CompressedRle\"] = \"compressedRle\";\n })(DetectionMaskEncoding || (DetectionMaskEncoding = {}));\n\n var DetectionBufferStatus;\n (function (DetectionBufferStatus) {\n DetectionBufferStatus[\"Idle\"] = \"idle\";\n DetectionBufferStatus[\"Loading\"] = \"loading\";\n /**\n * The playback gate is holding playback until the source produces detections\n * for the requested range. {@link DetectionBufferStatus.Loading} fetches\n * detections the source already has; this one waits on a producer.\n */\n DetectionBufferStatus[\"AwaitingCoverage\"] = \"awaitingCoverage\";\n DetectionBufferStatus[\"Ready\"] = \"ready\";\n DetectionBufferStatus[\"Error\"] = \"error\";\n DetectionBufferStatus[\"Destroyed\"] = \"destroyed\";\n })(DetectionBufferStatus || (DetectionBufferStatus = {}));\n /**\n * How the renderer selects the active detection frame for a media timestamp.\n */\n var DetectionFrameSelectionMode;\n (function (DetectionFrameSelectionMode) {\n /**\n * Select the frame whose `[mediaTime, endTime)` interval contains the media\n * time. This is the default for interval annotations and timestamped sources.\n *\n * Frame times must come from the media itself. Times reconstructed from a\n * nominal frame rate drift against a clip whose real rate differs, and once\n * that drift passes the half-millisecond selection tolerance a playhead\n * landing on a frame boundary selects the previous frame's detections.\n *\n * A frame that carries no `endTime` stays active until the next frame starts,\n * which bridges an index the source never produced.\n * {@link DetectionFrameSelectionMode.NearestFrameIndex} bounds each frame to\n * one grid step instead, so a missing index reads as no detections.\n */\n DetectionFrameSelectionMode[\"Interval\"] = \"interval\";\n /**\n * Select from a known inference frame grid using `frameIndex` and\n * `frameRate`. This is useful when inference was run on normalized frames and\n * playback should snap detections to that grid.\n *\n * A frame speaks for one grid step starting at its own media time, and an\n * index the source never produced selects nothing rather than a neighbour.\n *\n * A frame carrying no `frameIndex` speaks for its step on the same terms,\n * reached by the time it starts at rather than by an index that names it. Its\n * `endTime` does not widen it past that step, so a source that labels only\n * part of what it writes cannot bridge the indexes it never produced.\n */\n DetectionFrameSelectionMode[\"NearestFrameIndex\"] = \"nearestFrameIndex\";\n })(DetectionFrameSelectionMode || (DetectionFrameSelectionMode = {}));\n var DetectionFrameRetentionMode;\n (function (DetectionFrameRetentionMode) {\n /**\n * Keep writable detections only in an in-memory store. Useful for ephemeral\n * live streams where old predictions should disappear when evicted.\n */\n DetectionFrameRetentionMode[\"MemoryOnly\"] = \"memoryOnly\";\n /**\n * Persist every written detection frame. Useful for finite media where seek\n * and replay should not require recomputing detections.\n */\n DetectionFrameRetentionMode[\"PersistAll\"] = \"persistAll\";\n /**\n * Persist only the most recent retention window. Useful for long-running\n * streams where replay is bounded to a recent time horizon.\n */\n DetectionFrameRetentionMode[\"PersistWindow\"] = \"persistWindow\";\n })(DetectionFrameRetentionMode || (DetectionFrameRetentionMode = {}));\n function decodeCompressedRleMask(mask) {\n if (mask.encoding !== DetectionMaskEncoding.CompressedRle) {\n throw new Error(`Unsupported detection mask encoding: ${mask.encoding}`);\n }\n const data = new Uint8Array(mask.width * mask.height);\n const counts = decodeCompressedRleCounts(mask.counts);\n let offset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n const isForeground = index % 2 === 1;\n if (isForeground) {\n for (let runOffset = 0; runOffset < runLength; runOffset += 1) {\n const maskOffset = offset + runOffset;\n const x = Math.floor(maskOffset / mask.height);\n const y = maskOffset % mask.height;\n const rowMajorOffset = y * mask.width + x;\n if (rowMajorOffset < data.length) {\n data[rowMajorOffset] = 1;\n }\n }\n }\n offset += runLength;\n }\n return {\n data,\n height: mask.height,\n width: mask.width,\n };\n }\n function decodeCompressedRleCounts(counts) {\n const decoded = [];\n let index = 0;\n while (index < counts.length) {\n let value = 0;\n let shift = 0;\n let charCode;\n do {\n charCode = counts.charCodeAt(index) - 48;\n index += 1;\n value |= (charCode & 0x1f) << shift;\n shift += 5;\n } while (charCode & 0x20);\n if (charCode & 0x10) {\n value |= -1 << shift;\n }\n if (decoded.length > 2) {\n value += decoded[decoded.length - 2] ?? 0;\n }\n decoded.push(value);\n }\n return decoded;\n }\n function encodeCompressedRleCounts(counts) {\n return counts\n .map((count, index) => {\n let value = index > 2 ? count - counts[index - 2] : count;\n let encoded = \"\";\n let more = true;\n while (more) {\n let charCode = value & 0x1f;\n value >>= 5;\n more = !((value === 0 && (charCode & 0x10) === 0) ||\n (value === -1 && (charCode & 0x10) !== 0));\n if (more) {\n charCode |= 0x20;\n }\n encoded += String.fromCharCode(charCode + 48);\n }\n return encoded;\n })\n .join(\"\");\n }\n\n var AnnotationFrameMutationKind;\n (function (AnnotationFrameMutationKind) {\n AnnotationFrameMutationKind[\"Add\"] = \"add\";\n AnnotationFrameMutationKind[\"Remove\"] = \"remove\";\n AnnotationFrameMutationKind[\"Replace\"] = \"replace\";\n AnnotationFrameMutationKind[\"Transact\"] = \"transact\";\n AnnotationFrameMutationKind[\"Update\"] = \"update\";\n })(AnnotationFrameMutationKind || (AnnotationFrameMutationKind = {}));\n\n /** Geometry projected into a tracker. The original detection payload is retained. */\n var TrackingGeometry;\n (function (TrackingGeometry) {\n TrackingGeometry[\"Box\"] = \"box\";\n TrackingGeometry[\"Mask\"] = \"mask\";\n TrackingGeometry[\"Keypoints\"] = \"keypoints\";\n })(TrackingGeometry || (TrackingGeometry = {}));\n\n var DetectionMaskPayloadFormat;\n (function (DetectionMaskPayloadFormat) {\n DetectionMaskPayloadFormat[\"RawCocoRle\"] = \"rawCocoRle\";\n DetectionMaskPayloadFormat[\"DeflatedBase64\"] = \"deflatedBase64\";\n })(DetectionMaskPayloadFormat || (DetectionMaskPayloadFormat = {}));\n function encodeBinaryMask(data, width, height) {\n assertMaskDimensions(data, width, height);\n const runs = [];\n let currentValue = 0;\n let runLength = 0;\n for (let x = 0; x < width; x += 1) {\n for (let y = 0; y < height; y += 1) {\n const value = data[y * width + x] ? 1 : 0;\n if (value === currentValue) {\n runLength += 1;\n }\n else {\n runs.push(runLength);\n currentValue = value;\n runLength = 1;\n }\n }\n }\n runs.push(runLength);\n return {\n counts: encodeCompressedRleCounts(runs),\n encoding: DetectionMaskEncoding.CompressedRle,\n height,\n width,\n };\n }\n function assertMaskSize(width, height) {\n if (!Number.isInteger(width) || width <= 0) {\n throw new Error(\"Mask width must be a positive integer.\");\n }\n if (!Number.isInteger(height) || height <= 0) {\n throw new Error(\"Mask height must be a positive integer.\");\n }\n }\n function assertMaskDimensions(data, width, height) {\n assertMaskSize(width, height);\n if (data.length !== width * height) {\n throw new Error(\"Mask data length must equal width * height.\");\n }\n }\n\n function centerRectToTopLeftRect(rect) {\n return {\n height: rect.height,\n width: rect.width,\n x: rect.x - rect.width / 2,\n y: rect.y - rect.height / 2,\n };\n }\n function getPointsRect(points) {\n if (points.length === 0) {\n return undefined;\n }\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n for (const point of points) {\n minX = Math.min(minX, point.x);\n minY = Math.min(minY, point.y);\n maxX = Math.max(maxX, point.x);\n maxY = Math.max(maxY, point.y);\n }\n return {\n height: maxY - minY,\n width: maxX - minX,\n x: (minX + maxX) / 2,\n y: (minY + maxY) / 2,\n };\n }\n\n var DetectionPickTarget;\n (function (DetectionPickTarget) {\n DetectionPickTarget[\"Box\"] = \"box\";\n DetectionPickTarget[\"Edge\"] = \"edge\";\n DetectionPickTarget[\"Keypoint\"] = \"keypoint\";\n DetectionPickTarget[\"Label\"] = \"label\";\n DetectionPickTarget[\"Mask\"] = \"mask\";\n DetectionPickTarget[\"OrientedBox\"] = \"orientedBox\";\n DetectionPickTarget[\"Polygon\"] = \"polygon\";\n DetectionPickTarget[\"Polyline\"] = \"polyline\";\n })(DetectionPickTarget || (DetectionPickTarget = {}));\n var MediaInteractionMode;\n (function (MediaInteractionMode) {\n MediaInteractionMode[\"Always\"] = \"always\";\n MediaInteractionMode[\"Disabled\"] = \"disabled\";\n MediaInteractionMode[\"PausedOnly\"] = \"pausedOnly\";\n })(MediaInteractionMode || (MediaInteractionMode = {}));\n\n var AnnotationGeometryKind;\n (function (AnnotationGeometryKind) {\n AnnotationGeometryKind[\"Box\"] = \"box\";\n AnnotationGeometryKind[\"Polygon\"] = \"polygon\";\n AnnotationGeometryKind[\"Polyline\"] = \"polyline\";\n AnnotationGeometryKind[\"Keypoints\"] = \"keypoints\";\n AnnotationGeometryKind[\"Mask\"] = \"mask\";\n })(AnnotationGeometryKind || (AnnotationGeometryKind = {}));\n var AnnotationGestureStateKind;\n (function (AnnotationGestureStateKind) {\n AnnotationGestureStateKind[\"Idle\"] = \"idle\";\n AnnotationGestureStateKind[\"Creating\"] = \"creating\";\n AnnotationGestureStateKind[\"Moving\"] = \"moving\";\n AnnotationGestureStateKind[\"Resizing\"] = \"resizing\";\n AnnotationGestureStateKind[\"DragSelecting\"] = \"dragSelecting\";\n })(AnnotationGestureStateKind || (AnnotationGestureStateKind = {}));\n var AnnotationHandleKind;\n (function (AnnotationHandleKind) {\n AnnotationHandleKind[\"Resize\"] = \"resize\";\n AnnotationHandleKind[\"Vertex\"] = \"vertex\";\n AnnotationHandleKind[\"AddVertex\"] = \"addVertex\";\n AnnotationHandleKind[\"Keypoint\"] = \"keypoint\";\n })(AnnotationHandleKind || (AnnotationHandleKind = {}));\n\n /** Alignment for strokes applied to closed rendered geometry. */\n var StrokeAlignment;\n (function (StrokeAlignment) {\n StrokeAlignment[\"Inside\"] = \"inside\";\n StrokeAlignment[\"Center\"] = \"center\";\n StrokeAlignment[\"Outside\"] = \"outside\";\n })(StrokeAlignment || (StrokeAlignment = {}));\n\n var BoxShape;\n (function (BoxShape) {\n BoxShape[\"Rect\"] = \"rect\";\n BoxShape[\"RoundedRect\"] = \"roundedRect\";\n })(BoxShape || (BoxShape = {}));\n\n var FocusTargetMode;\n (function (FocusTargetMode) {\n FocusTargetMode[\"Hovered\"] = \"hovered\";\n FocusTargetMode[\"Selected\"] = \"selected\";\n FocusTargetMode[\"HoveredAndSelected\"] = \"hoveredAndSelected\";\n FocusTargetMode[\"Ambient\"] = \"ambient\";\n })(FocusTargetMode || (FocusTargetMode = {}));\n\n var DetectionInteractionState;\n (function (DetectionInteractionState) {\n DetectionInteractionState[\"Hovered\"] = \"hovered\";\n DetectionInteractionState[\"Selected\"] = \"selected\";\n })(DetectionInteractionState || (DetectionInteractionState = {}));\n\n var LabelPlacement;\n (function (LabelPlacement) {\n LabelPlacement[\"Top\"] = \"top\";\n LabelPlacement[\"Bottom\"] = \"bottom\";\n LabelPlacement[\"InsideTop\"] = \"insideTop\";\n LabelPlacement[\"InsideBottom\"] = \"insideBottom\";\n LabelPlacement[\"Center\"] = \"center\";\n })(LabelPlacement || (LabelPlacement = {}));\n var LabelVisibilityMode;\n (function (LabelVisibilityMode) {\n LabelVisibilityMode[\"Always\"] = \"always\";\n LabelVisibilityMode[\"HoveredOnly\"] = \"hoveredOnly\";\n })(LabelVisibilityMode || (LabelVisibilityMode = {}));\n\n var MaskRenderMode;\n (function (MaskRenderMode) {\n MaskRenderMode[\"FillAndStroke\"] = \"fillAndStroke\";\n MaskRenderMode[\"FillOnly\"] = \"fillOnly\";\n MaskRenderMode[\"StrokeOnly\"] = \"strokeOnly\";\n })(MaskRenderMode || (MaskRenderMode = {}));\n\n /**\n * Discriminator for renderer-neutral shape draw instructions.\n */\n var ShapeInstructionKind;\n (function (ShapeInstructionKind) {\n ShapeInstructionKind[\"Ellipse\"] = \"ellipse\";\n ShapeInstructionKind[\"Marker\"] = \"marker\";\n ShapeInstructionKind[\"Path\"] = \"path\";\n })(ShapeInstructionKind || (ShapeInstructionKind = {}));\n var MarkerShape;\n (function (MarkerShape) {\n MarkerShape[\"Circle\"] = \"circle\";\n MarkerShape[\"Cross\"] = \"cross\";\n MarkerShape[\"Square\"] = \"square\";\n MarkerShape[\"Triangle\"] = \"triangle\";\n })(MarkerShape || (MarkerShape = {}));\n /**\n * Coordinate space for marker sizing. Media sizes scale with the viewport;\n * screen sizes stay constant on screen, matching stroke-width semantics.\n */\n var MarkerSizeSpace;\n (function (MarkerSizeSpace) {\n MarkerSizeSpace[\"Media\"] = \"media\";\n MarkerSizeSpace[\"Screen\"] = \"screen\";\n })(MarkerSizeSpace || (MarkerSizeSpace = {}));\n\n var PercentageBarPlacement;\n (function (PercentageBarPlacement) {\n PercentageBarPlacement[\"Top\"] = \"top\";\n PercentageBarPlacement[\"Bottom\"] = \"bottom\";\n PercentageBarPlacement[\"InsideTop\"] = \"insideTop\";\n PercentageBarPlacement[\"InsideBottom\"] = \"insideBottom\";\n })(PercentageBarPlacement || (PercentageBarPlacement = {}));\n\n var KeypointMarkerShape;\n (function (KeypointMarkerShape) {\n KeypointMarkerShape[\"Circle\"] = \"circle\";\n KeypointMarkerShape[\"Cross\"] = \"cross\";\n })(KeypointMarkerShape || (KeypointMarkerShape = {}));\n\n /**\n * What the stored values of a depth map measure.\n *\n * Every kind stores 0 where there is no depth.\n */\n const DepthMapKind = {\n /** Stereo disparity in pixels of the map. Larger is nearer. */\n DisparityPx: \"disparity_px\",\n /** Metric depth along the optical axis in metres. Smaller is nearer. */\n DepthM: \"depth_m\",\n /**\n * Unitless inverse depth up to an unknown scale and shift, as monocular\n * models produce it. Larger is nearer; it has no metric depth.\n */\n RelativeInverse: \"relative_inverse\",\n };\n /**\n * The quantity a depth renderer colours.\n *\n * Disparity is inverse depth, so it spends colour on near detail the way\n * stereo measures it. Either way the nearest end of the range is the warm or\n * bright end of the colour table.\n */\n const DepthQuantity = {\n Disparity: \"disparity\",\n Depth: \"depth\",\n };\n const DepthColormap = {\n Turbo: \"turbo\",\n Viridis: \"viridis\",\n Cividis: \"cividis\",\n Inferno: \"inferno\",\n Magma: \"magma\",\n Grayscale: \"grayscale\",\n };\n /** How a depth map is sampled where one map pixel does not cover one media pixel. */\n const DepthSampling = {\n /** Nearest when the map is at least media size, edge-aware otherwise. */\n Auto: \"auto\",\n Nearest: \"nearest\",\n /**\n * Bilinear over valid neighbours only, falling back to the nearest one\n * across a depth edge, so objects never bleed into the background.\n */\n EdgeAware: \"edge-aware\",\n };\n /**\n * The luma range a depth preview video's codes are written in.\n *\n * TV range is what browsers' hardware decoders return as written; a decoder\n * may squeeze full range into TV range or move codes converting it to RGB.\n */\n const DepthPreviewLevels = {\n /** Every code from 0 to 255; the top code is 255. */\n Full: \"full\",\n /** Limited range: black is 16 and the top code is 235. */\n Tv: \"tv\",\n };\n new Set(Object.values(DepthColormap));\n new Set(Object.values(DepthQuantity));\n new Set(Object.values(DepthSampling));\n function rasterizePolygonToMask(points, dimensions) {\n const data = createEmptyMask(dimensions);\n forEachRasterPolygonSpan(points, dimensions, (y, left, right) => {\n data.fill(1, y * dimensions.width + left, y * dimensions.width + right);\n });\n return data;\n }\n function forEachRasterPolygonSpan(points, dimensions, writeSpan) {\n assertMediaDimensions(dimensions);\n if (points.length < 3) {\n return;\n }\n const bounds = centerRectToTopLeftRect(getPointsRect(points));\n const startY = Math.max(0, Math.floor(bounds.y));\n const endY = Math.min(dimensions.height - 1, Math.ceil(bounds.y + bounds.height));\n for (let y = startY; y <= endY; y += 1) {\n const scanY = y + 0.5;\n const intersections = [];\n for (let index = 0; index < points.length; index += 1) {\n const current = points[index];\n const next = points[(index + 1) % points.length];\n if ((current.y <= scanY && next.y > scanY) ||\n (next.y <= scanY && current.y > scanY)) {\n const ratio = (scanY - current.y) / (next.y - current.y);\n intersections.push(current.x + ratio * (next.x - current.x));\n }\n }\n intersections.sort((left, right) => left - right);\n for (let index = 0; index < intersections.length - 1; index += 2) {\n const left = Math.max(0, Math.round(intersections[index]));\n const right = Math.min(dimensions.width, Math.round(intersections[index + 1]));\n if (left < right) {\n writeSpan(y, left, right);\n }\n }\n }\n }\n function createEmptyMask(dimensions) {\n assertMediaDimensions(dimensions);\n return new Uint8Array(dimensions.width * dimensions.height);\n }\n function assertMediaDimensions(dimensions) {\n if (!Number.isInteger(dimensions.width) ||\n dimensions.width <= 0 ||\n !Number.isInteger(dimensions.height) ||\n dimensions.height <= 0) {\n throw new Error(\"Media dimensions must be positive integers.\");\n }\n }\n\n /**\n * Palette slots an id raster can name, one of them the background. A GLSL\n * fragment stage is guaranteed only 224 uniform vectors, and a palette entry\n * costs 2.5 of them, so this stays a multiple of four and well inside that\n * budget; past it a frame draws from the RGBA composite instead.\n */\n const MAX_ID_MASK_PALETTE_ENTRIES = 80;\n const MAX_ID_MASK_STROKE_WIDTH = 16;\n function createIdMaskFrame(instructions, options = {}) {\n if (instructions.length === 0) {\n return undefined;\n }\n /* Pixel-grid dimensions use integer parts throughout allocation and RLE\n traversal. */\n const maskWidth = Math.floor(Math.max(...instructions.map((instruction) => (instruction.mask ?? instruction.polygon).width)));\n const maskHeight = Math.floor(Math.max(...instructions.map((instruction) => (instruction.mask ?? instruction.polygon).height)));\n const width = resolveRasterWidth(maskWidth, options.maxWidth);\n const height = width === maskWidth\n ? maskHeight\n : Math.max(1, Math.round((width * maskHeight) / maskWidth));\n const scaledAxes = width === maskWidth\n ? undefined\n : createScaledMaskAxes({ height, maskHeight, maskWidth, width });\n const data = new Uint8Array(new ArrayBuffer(width * height));\n const fillPalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokePalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokeWidths = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4));\n const strokeAlignments = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4));\n let hasStroke = false;\n let maxStrokeWidth = 0;\n for (const instruction of instructions) {\n const detectionMaskId = resolveIdMaskPaletteId(instruction.detectionIndex);\n if (detectionMaskId === undefined) {\n return undefined;\n }\n writeIdMaskPaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);\n if (instruction.stroke && instruction.stroke.width > 0) {\n const strokeWidth = instruction.stroke.width;\n const alignment = instruction.stroke.alignment === StrokeAlignment.Inside\n ? 1\n : instruction.stroke.alignment === StrokeAlignment.Center\n ? 0.5\n : 0;\n hasStroke = true;\n strokeWidths[detectionMaskId] = strokeWidth;\n strokeAlignments[detectionMaskId] = alignment;\n maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth * (1 - alignment));\n writeIdMaskPaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);\n }\n if (instruction.polygon) {\n writePolygonSpans(data, width, instruction.polygon, detectionMaskId, scaledAxes);\n }\n else if (scaledAxes) {\n writeScaledMaskRuns(data, scaledAxes, instruction.mask, detectionMaskId);\n }\n else {\n writeMaskRuns(data, width, instruction.mask, detectionMaskId);\n }\n }\n return {\n data,\n fillPalette,\n hasStroke,\n height,\n maxStrokeWidth,\n sourceWidth: maskWidth,\n strokePalette,\n strokeAlignments,\n strokeWidths,\n width,\n };\n }\n function writePolygonSpans(data, frameWidth, polygon, detectionMaskId, axes) {\n forEachRasterPolygonSpan(polygon.points, polygon, (y, left, right) => {\n const row = axes ? axes.rows[y] : y * frameWidth;\n const first = axes ? axes.columns[left] : left;\n const last = axes ? axes.columns[right - 1] + 1 : right;\n data.fill(detectionMaskId, row + first, row + last);\n });\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function writeMaskRuns(data, frameWidth, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = Math.floor(mask.width);\n const maskHeight = Math.floor(mask.height);\n let maskOffset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let frameOffset = y * frameWidth + x;\n for (let step = 0; step < runLength; step += 1) {\n if (x >= maskWidth) {\n break;\n }\n data[frameOffset] = detectionMaskId;\n y += 1;\n frameOffset += frameWidth;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n frameOffset = x;\n }\n }\n maskOffset += runLength;\n }\n }\n /**\n * The scaled twin of writeMaskRuns. Every source pixel marks the texel it lands\n * in, so a mask keeps every part of itself the smaller raster can hold and\n * gains up to a texel at its edges.\n */\n function writeScaledMaskRuns(data, axes, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = Math.floor(mask.width);\n const maskHeight = Math.floor(mask.height);\n let maskOffset = 0;\n const stride = axes.stride;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let remaining = runLength;\n /* A run walks down one column and wraps into the next, so it splits into\n column-sized segments. Within a segment the destination offset only ever\n steps by the row pitch or stays put: createMaskAxisMap builds\n `rows[i] = min(height - 1, floor(i * scale)) * width` with scale <= 1, so\n consecutive entries differ by exactly 0 or exactly `width`. Walking the\n destination directly therefore touches the same bytes the per-pixel loop\n touched, and drops the bounds test, the wrap test and one typed-array\n load from every source pixel. Where the scale collapses several source\n rows onto one destination row, this writes that byte once instead of\n once per source row. */\n while (remaining > 0 && x < maskWidth) {\n const span = Math.min(remaining, maskHeight - y);\n const column = axes.columns[x];\n const first = axes.rows[y];\n const last = axes.rows[y + span - 1];\n for (let offset = first; offset <= last; offset += stride) {\n data[offset + column] = detectionMaskId;\n }\n remaining -= span;\n y += span;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n }\n }\n maskOffset += runLength;\n }\n }\n function resolveRasterWidth(maskWidth, maxWidth) {\n return maxWidth !== undefined && maxWidth > 0 && maxWidth < maskWidth\n ? Math.max(1, Math.floor(maxWidth))\n : maskWidth;\n }\n /**\n * Converts total CSS outline width into raster texels. Each side is bounded\n * independently so centered outlines can use the full supported radius.\n */\n function resolveIdMaskStrokeTexels(strokeWidth, displayWidth, rasterWidth, alignment = StrokeAlignment.Outside) {\n const scale = displayWidth > 0 ? rasterWidth / displayWidth : 1;\n const sideFraction = alignment === StrokeAlignment.Center ? 0.5 : 1;\n return Math.min(strokeWidth * scale, MAX_ID_MASK_STROKE_WIDTH / sideFraction);\n }\n function createScaledMaskAxes(frame) {\n return {\n columns: createMaskAxisMap(frame.maskWidth, frame.width, 1),\n rows: createMaskAxisMap(frame.maskHeight, frame.height, frame.width),\n stride: frame.width,\n };\n }\n /** Rows hold their destination offset, so writing a run is two lookups. */\n function createMaskAxisMap(sourceLength, targetLength, stride) {\n const map = new Int32Array(sourceLength);\n const scale = targetLength / sourceLength;\n for (let index = 0; index < sourceLength; index += 1) {\n map[index] = Math.min(targetLength - 1, Math.floor(index * scale)) * stride;\n }\n return map;\n }\n /**\n * The palette slot a detection index names, or undefined when the palette has\n * no slot for it. Slot zero is the background, so the ids start at one and the\n * last detection the palette can name sits one index below the last slot.\n */\n function resolveIdMaskPaletteId(detectionIndex) {\n const detectionMaskId = detectionIndex + 1;\n return detectionMaskId > 0 && detectionMaskId < MAX_ID_MASK_PALETTE_ENTRIES\n ? detectionMaskId\n : undefined;\n }\n function writeIdMaskPaletteEntry(palette, detectionMaskId, color, alpha) {\n const offset = detectionMaskId * 4;\n palette[offset] = ((color >> 16) & 0xff) / 255;\n palette[offset + 1] = ((color >> 8) & 0xff) / 255;\n palette[offset + 2] = (color & 0xff) / 255;\n palette[offset + 3] = Math.max(0, Math.min(alpha, 1));\n }\n new Set(Object.values(DepthPreviewLevels));\n new Set(Object.values(DepthMapKind));\n\n var MediaRendererFit;\n (function (MediaRendererFit) {\n /**\n * Preserve media aspect ratio and fit the full frame inside the canvas.\n */\n MediaRendererFit[\"Contain\"] = \"contain\";\n /**\n * Preserve media aspect ratio and fill the canvas, cropping if necessary.\n */\n MediaRendererFit[\"Cover\"] = \"cover\";\n })(MediaRendererFit || (MediaRendererFit = {}));\n /**\n * Playback lifecycle state reported by a platform renderer.\n */\n var MediaRendererPlaybackState;\n (function (MediaRendererPlaybackState) {\n MediaRendererPlaybackState[\"Loading\"] = \"loading\";\n MediaRendererPlaybackState[\"Ready\"] = \"ready\";\n MediaRendererPlaybackState[\"Playing\"] = \"playing\";\n MediaRendererPlaybackState[\"Buffering\"] = \"buffering\";\n MediaRendererPlaybackState[\"Paused\"] = \"paused\";\n MediaRendererPlaybackState[\"Error\"] = \"error\";\n MediaRendererPlaybackState[\"Destroyed\"] = \"destroyed\";\n })(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));\n /**\n * Stable classification of a media failure.\n *\n * Applications branch on these kinds instead of parsing decoder, demuxer, or\n * container error text, and still own their localized user-facing copy. New\n * kinds may be added over time, so treat unrecognized values like `Unknown`.\n */\n var MediaErrorKind;\n (function (MediaErrorKind) {\n /** The media could not be opened or read, or was refused before any read. */\n MediaErrorKind[\"Unreadable\"] = \"unreadable\";\n /** The container or codec is not supported by this platform. */\n MediaErrorKind[\"UnsupportedFormat\"] = \"unsupportedFormat\";\n /** The media opened but carries no usable video track. */\n MediaErrorKind[\"NoVideoTrack\"] = \"noVideoTrack\";\n /** Decoding a sample failed after the source opened. */\n MediaErrorKind[\"Decode\"] = \"decode\";\n /** The media could not be fetched. */\n MediaErrorKind[\"Network\"] = \"network\";\n /** The host environment lacks the APIs this media source requires. */\n MediaErrorKind[\"EnvironmentUnsupported\"] = \"environmentUnsupported\";\n /** The failure could not be classified. */\n MediaErrorKind[\"Unknown\"] = \"unknown\";\n })(MediaErrorKind || (MediaErrorKind = {}));\n /**\n * Lower-level media source readiness.\n */\n var MediaSourceStatus;\n (function (MediaSourceStatus) {\n MediaSourceStatus[\"Loading\"] = \"loading\";\n MediaSourceStatus[\"Ready\"] = \"ready\";\n MediaSourceStatus[\"Error\"] = \"error\";\n MediaSourceStatus[\"Destroyed\"] = \"destroyed\";\n })(MediaSourceStatus || (MediaSourceStatus = {}));\n /**\n * Current renderer state.\n */\n /**\n * What a playback gate holds back.\n *\n * A source the renderer pulls samples from can be held between any two frames,\n * because the renderer decides when each one is drawn. A source that presents\n * its own frames owns the playhead, so holding it means stopping the producer\n * and starting it again, and a gate that stops one has to bound its own wait\n * or a producer nothing answers for never runs again.\n */\n var PlaybackGateReach;\n (function (PlaybackGateReach) {\n /** No gate: the picture moves and unprepared layers are absent from it. */\n PlaybackGateReach[\"Off\"] = \"off\";\n /**\n * Playback waits to begin, and stops again at any frame whose artifacts are\n * missing, until they arrive or the gate's own wait bound gives up on them.\n */\n PlaybackGateReach[\"EveryFrame\"] = \"everyFrame\";\n })(PlaybackGateReach || (PlaybackGateReach = {}));\n\n /**\n * Media-session operating mode.\n *\n * Core owns the names because file-like and stream-like lifecycle choices are\n * platform-neutral. Platform packages decide how these modes tune media,\n * storage, and renderer defaults.\n */\n var MediaSessionMode;\n (function (MediaSessionMode) {\n /**\n * Finite media. Defaults usually favor seek/replay and persistent detection\n * storage.\n */\n MediaSessionMode[\"File\"] = \"file\";\n /**\n * Live or append-only media. Defaults usually favor rolling windows and\n * bounded retention.\n */\n MediaSessionMode[\"Stream\"] = \"stream\";\n })(MediaSessionMode || (MediaSessionMode = {}));\n /**\n * Aggregate lifecycle state for a media session.\n */\n var MediaSessionStatus;\n (function (MediaSessionStatus) {\n MediaSessionStatus[\"Buffering\"] = \"buffering\";\n MediaSessionStatus[\"Destroyed\"] = \"destroyed\";\n MediaSessionStatus[\"Error\"] = \"error\";\n MediaSessionStatus[\"Loading\"] = \"loading\";\n MediaSessionStatus[\"Paused\"] = \"paused\";\n MediaSessionStatus[\"Playing\"] = \"playing\";\n MediaSessionStatus[\"Processing\"] = \"processing\";\n MediaSessionStatus[\"Ready\"] = \"ready\";\n })(MediaSessionStatus || (MediaSessionStatus = {}));\n /**\n * Subsystem currently affecting session readiness or presentation.\n */\n var MediaSessionActivityKind;\n (function (MediaSessionActivityKind) {\n /**\n * Playback is held waiting for a producer to emit detections for the frame\n * about to be shown, which is a wait on inference rather than on transfer or\n * decode. `DetectionsBuffering` is the transfer of detections that already\n * exist, and `PlaybackBuffering` is the wait for media bytes.\n */\n MediaSessionActivityKind[\"DetectionsAwaitingCoverage\"] = \"detectionsAwaitingCoverage\";\n MediaSessionActivityKind[\"DetectionsBuffering\"] = \"detectionsBuffering\";\n MediaSessionActivityKind[\"DetectionsLoading\"] = \"detectionsLoading\";\n MediaSessionActivityKind[\"Error\"] = \"error\";\n MediaSessionActivityKind[\"MediaNormalizing\"] = \"mediaNormalizing\";\n MediaSessionActivityKind[\"MediaOpening\"] = \"mediaOpening\";\n /**\n * The picture is waiting on media the source has not handed over yet, which\n * is the bytes and their decode rather than anything downstream of them.\n * `PlaybackBuffering` is what a transport reports once it has already\n * stopped; this is reported from the read itself, including the reads a seek\n * makes while the transport still reads as paused.\n */\n MediaSessionActivityKind[\"MediaSourceReading\"] = \"mediaSourceReading\";\n MediaSessionActivityKind[\"PlaybackBuffering\"] = \"playbackBuffering\";\n /**\n * The render-preparation gate held the picture for artifacts that never\n * arrived and let it go. Nothing is blocked: the frames reaching the screen\n * are the ones whose artifacts were given up on. What is waited on is\n * preparation finishing another frame, which is what lets the gate hold the\n * picture again; `RenderPreparing` covers the holds that are running.\n */\n MediaSessionActivityKind[\"RenderPreparationAbandoned\"] = \"renderPreparationAbandoned\";\n MediaSessionActivityKind[\"RenderPreparing\"] = \"renderPreparing\";\n })(MediaSessionActivityKind || (MediaSessionActivityKind = {}));\n /**\n * State of one session activity.\n */\n var MediaSessionActivityStatus;\n (function (MediaSessionActivityStatus) {\n MediaSessionActivityStatus[\"Error\"] = \"error\";\n MediaSessionActivityStatus[\"Running\"] = \"running\";\n MediaSessionActivityStatus[\"Waiting\"] = \"waiting\";\n })(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));\n\n /** Builds compact, independent alpha crops so overlapping masks stay exact. */\n function createRegionMaskCoverageFrame(instructions) {\n const entries = [];\n for (const instruction of instructions) {\n if (!instruction.regionCoverageMask) {\n continue;\n }\n const entry = cropCoverageMask(instruction.detectionIndex, decodeCompressedRleMask(instruction.regionCoverageMask));\n if (entry) {\n entries.push(entry);\n }\n }\n return entries.length > 0 ? { entries } : undefined;\n }\n function cropCoverageMask(detectionIndex, decodedMask) {\n let minX = decodedMask.width;\n let minY = decodedMask.height;\n let maxX = -1;\n let maxY = -1;\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n }\n if (maxX < minX || maxY < minY) {\n return undefined;\n }\n const width = maxX - minX + 1;\n const height = maxY - minY + 1;\n const data = new Uint8Array(new ArrayBuffer(width * height));\n for (let y = minY; y <= maxY; y += 1) {\n for (let x = minX; x <= maxX; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n data[(y - minY) * width + (x - minX)] = 255;\n }\n }\n return { data, detectionIndex, height, width, x: minX, y: minY };\n }\n function compositeMaskFrame(instructions, displayWidth) {\n const maskInstructions = materializeMaskInstructions(instructions);\n if (maskInstructions.length === 0) {\n return undefined;\n }\n const width = Math.max(...maskInstructions.map(({ mask }) => mask.width));\n const height = Math.max(...maskInstructions.map(({ mask }) => mask.height));\n const data = new Uint8ClampedArray(new ArrayBuffer(width * height * 4));\n const strokeCoverage = maskInstructions.some(({ stroke }) => stroke && stroke.width > 0 && stroke.alpha > 0)\n ? new Uint8Array(width * height)\n : undefined;\n const strokeKernels = new Map();\n for (const instruction of maskInstructions) {\n compositeInstruction(data, width, instruction, strokeCoverage, strokeKernels, displayWidth);\n }\n return { data, height, width };\n }\n function createIdMaskRasterFrame(instructions, maxRasterWidth) {\n try {\n return createIdMaskFrame(instructions\n .filter((instruction) => instruction.visible !== false)\n .map((instruction) => instruction.stroke?.widthUnit\n ? { ...instruction, stroke: undefined }\n : instruction), { maxWidth: maxRasterWidth });\n }\n catch {\n // The id raster is the fast path, not the only one: answering with nothing\n // puts the caller on the RGBA composite, which draws the same picture.\n return undefined;\n }\n }\n /**\n * Whether the palette has a slot for every detection this frame masks. An id\n * names a detection by its index in the frame, so one masked detection past the\n * last slot leaves the whole frame without an id raster however few masks it\n * carries.\n */\n function canIdMaskPaletteNameFrame(instructions) {\n return instructions.every((instruction) => instruction.visible === false ||\n resolveIdMaskPaletteId(instruction.detectionIndex) !== undefined);\n }\n function createIdMaskPlane(instructions, maxRasterWidth) {\n // The plane carries the ids a failed cook could not. A frame the palette\n // cannot name has none to carry, and cooking it again reaches the same\n // refusal after rasterizing every mask a second time.\n if (!canIdMaskPaletteNameFrame(instructions)) {\n return undefined;\n }\n const frame = createIdMaskRasterFrame(instructions, maxRasterWidth);\n return frame\n ? { data: frame.data, height: frame.height, width: frame.width }\n : undefined;\n }\n function compositeInstruction(rgba, canvasWidth, instruction, strokeCoverage, strokeKernels, displayWidth) {\n const fill = resolveRgbaColor(instruction.color, instruction.alpha);\n const bounds = compositeMaskFill(rgba, canvasWidth, instruction.mask, fill);\n if (instruction.stroke &&\n instruction.stroke.widthUnit === undefined &&\n bounds &&\n strokeCoverage) {\n compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, {\n ...instruction.stroke,\n width: resolveIdMaskStrokeTexels(instruction.stroke.width, displayWidth ?? canvasWidth, canvasWidth, instruction.stroke.alignment),\n }, strokeCoverage, strokeKernels);\n }\n }\n function createMaskScreenStrokes(instructions, width, height, preparedRaster) {\n const outlined = instructions.filter(({ visible, stroke }) => visible !== false &&\n stroke?.widthUnit !== undefined &&\n stroke.width > 0 &&\n stroke.alpha > 0);\n if (outlined.length === 0)\n return undefined;\n const raster = preparedRaster ?? new Uint32Array(width * height);\n if (!preparedRaster) {\n const masks = materializeMaskInstructions(instructions);\n for (const { detectionIndex, mask } of masks) {\n const counts = decodeCompressedRleCounts(mask.counts);\n let offset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const length = counts[index];\n if (index % 2 === 1) {\n for (let pixel = offset; pixel < offset + length; pixel += 1) {\n const x = Math.floor(pixel / mask.height);\n const y = pixel % mask.height;\n raster[y * width + x] = detectionIndex + 1;\n }\n }\n offset += length;\n }\n }\n }\n const paths = traceMaskOutlinePaths(raster, width, height, new Set(outlined.map(({ detectionIndex }) => detectionIndex + 1)));\n return outlined.map(({ detectionIndex, stroke }) => ({\n detectionIndex,\n alpha: stroke.alpha,\n color: stroke.color,\n width: stroke.width,\n widthUnit: stroke.widthUnit,\n alignment: stroke.alignment,\n paths: paths.get(detectionIndex + 1) ?? [],\n }));\n }\n function materializeMaskInstructions(instructions) {\n return instructions\n .filter((instruction) => instruction.visible !== false)\n .map((instruction) => {\n if (instruction.mask) {\n return instruction;\n }\n const { height, points, width } = instruction.polygon;\n return {\n alpha: instruction.alpha,\n color: instruction.color,\n detectionIndex: instruction.detectionIndex,\n mask: encodeBinaryMask(rasterizePolygonToMask(points, { height, width }), width, height),\n stroke: instruction.stroke,\n };\n });\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function compositeMaskFill(rgba, canvasWidth, mask, fill) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = mask.width;\n const maskHeight = mask.height;\n const maskArea = maskWidth * maskHeight;\n let maskOffset = 0;\n let minX = maskWidth;\n let minY = maskHeight;\n let maxX = -1;\n let maxY = -1;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let columnX = Math.floor(maskOffset / maskHeight);\n let columnY = maskOffset - columnX * maskHeight;\n for (let step = 0; step < runLength; step += 1) {\n // A run can outlast the columns the mask has; the pixel it then names\n // is wherever that column-major offset lands when read back row-major.\n const rowMajorOffset = columnY * maskWidth + columnX;\n if (rowMajorOffset < maskArea) {\n const x = rowMajorOffset % maskWidth;\n const y = (rowMajorOffset - x) / maskWidth;\n writePixel(rgba, canvasWidth, x, y, fill);\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n columnY += 1;\n if (columnY === maskHeight) {\n columnY = 0;\n columnX += 1;\n }\n }\n maskOffset += runLength;\n }\n return maxX < minX || maxY < minY ? undefined : { maxX, maxY, minX, minY };\n }\n function createStrokeKernel(width, alignment) {\n const innerFraction = alignment === StrokeAlignment.Inside\n ? 1\n : alignment === StrokeAlignment.Center\n ? 0.5\n : 0;\n const inside = [];\n const outside = [];\n const radius = Math.min(MAX_ID_MASK_STROKE_WIDTH + 1, Math.ceil(width * Math.max(innerFraction, 1 - innerFraction) + 0.5));\n const clamp = (value) => Math.max(0, Math.min(1, value));\n const coverage = (signed) => Math.round(255 *\n (clamp(0.5 + width * (1 - innerFraction) - signed) -\n clamp(0.5 - width * innerFraction - signed)));\n for (let y = -radius; y <= radius; y += 1) {\n for (let x = -radius; x <= radius; x += 1) {\n if (x === 0 && y === 0)\n continue;\n const distance = Math.max(Math.abs(x) - 0.5, Math.abs(y) - 0.5, 0);\n const innerCoverage = coverage(-distance);\n const outerCoverage = coverage(distance);\n if (innerCoverage > 0)\n inside.push([x, y, innerCoverage]);\n if (outerCoverage > 0)\n outside.push([x, y, outerCoverage]);\n }\n }\n return { inside, outside, radius };\n }\n function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke, coverage, kernels) {\n if (stroke.width <= 0 || stroke.alpha <= 0)\n return;\n const key = `${stroke.width}:${stroke.alignment}`;\n let kernel = kernels.get(key);\n if (!kernel) {\n kernel = createStrokeKernel(stroke.width, stroke.alignment);\n kernels.set(key, kernel);\n }\n const visitedAir = kernel.inside.length > 0 ? new Set() : undefined;\n const mark = (sourceX, sourceY, offsets, foreground) => {\n for (const [dx, dy, value] of offsets) {\n const x = sourceX + dx;\n const y = sourceY + dy;\n if (isOutsideMaskBounds(decodedMask, x, y) ||\n isMaskPixel(decodedMask, x, y) !== foreground)\n continue;\n const index = y * canvasWidth + x;\n coverage[index] = Math.max(coverage[index], value);\n }\n };\n for (let y = bounds.minY; y <= bounds.maxY; y += 1) {\n for (let x = bounds.minX; x <= bounds.maxX; x += 1) {\n if (!isMaskPixel(decodedMask, x, y) ||\n !isBoundaryPixel(decodedMask, x, y))\n continue;\n mark(x, y, kernel.outside, false);\n if (!visitedAir)\n continue;\n for (let dy = -1; dy <= 1; dy += 1) {\n for (let dx = -1; dx <= 1; dx += 1) {\n const airX = x + dx;\n const airY = y + dy;\n if (isOutsideMaskBounds(decodedMask, airX, airY) ||\n isMaskPixel(decodedMask, airX, airY))\n continue;\n const airIndex = airY * decodedMask.width + airX;\n if (visitedAir.has(airIndex))\n continue;\n visitedAir.add(airIndex);\n mark(airX, airY, kernel.inside, true);\n }\n }\n }\n }\n const color = resolveRgbaColor(stroke.color, stroke.alpha);\n for (let y = Math.max(0, bounds.minY - kernel.radius); y <= Math.min(decodedMask.height - 1, bounds.maxY + kernel.radius); y += 1) {\n for (let x = Math.max(0, bounds.minX - kernel.radius); x <= Math.min(decodedMask.width - 1, bounds.maxX + kernel.radius); x += 1) {\n const index = y * canvasWidth + x;\n const amount = coverage[index] / 255;\n if (amount <= 0)\n continue;\n coverage[index] = 0;\n const offset = index * 4;\n const alpha = rgba[offset + 3] * (1 - amount) + color.alpha * amount;\n rgba[offset] =\n alpha > 0\n ? (rgba[offset] * rgba[offset + 3] * (1 - amount) +\n color.red * color.alpha * amount) /\n alpha\n : 0;\n rgba[offset + 1] =\n alpha > 0\n ? (rgba[offset + 1] * rgba[offset + 3] * (1 - amount) +\n color.green * color.alpha * amount) /\n alpha\n : 0;\n rgba[offset + 2] =\n alpha > 0\n ? (rgba[offset + 2] * rgba[offset + 3] * (1 - amount) +\n color.blue * color.alpha * amount) /\n alpha\n : 0;\n rgba[offset + 3] = alpha;\n }\n }\n }\n function isBoundaryPixel(mask, x, y) {\n for (let offsetY = -1; offsetY <= 1; offsetY += 1) {\n for (let offsetX = -1; offsetX <= 1; offsetX += 1) {\n if (offsetX === 0 && offsetY === 0) {\n continue;\n }\n const neighborX = x + offsetX;\n const neighborY = y + offsetY;\n if (isOutsideMaskBounds(mask, neighborX, neighborY) ||\n !isMaskPixel(mask, neighborX, neighborY)) {\n return true;\n }\n }\n }\n return false;\n }\n function isMaskPixel(mask, x, y) {\n return mask.data[y * mask.width + x] === 1;\n }\n function isOutsideMaskBounds(mask, x, y) {\n return x < 0 || y < 0 || x >= mask.width || y >= mask.height;\n }\n function resolveRgbaColor(color, alpha) {\n return {\n alpha: Math.round(Math.max(0, Math.min(alpha, 1)) * 255),\n blue: color & 0xff,\n green: (color >> 8) & 0xff,\n red: (color >> 16) & 0xff,\n };\n }\n function writePixel(rgba, canvasWidth, x, y, color) {\n const rgbaOffset = (y * canvasWidth + x) * 4;\n rgba[rgbaOffset] = color.red;\n rgba[rgbaOffset + 1] = color.green;\n rgba[rgbaOffset + 2] = color.blue;\n rgba[rgbaOffset + 3] = color.alpha;\n }\n\n var PreparedMaskFrameKind;\n (function (PreparedMaskFrameKind) {\n PreparedMaskFrameKind[\"IdMask\"] = \"idMask\";\n PreparedMaskFrameKind[\"RgbaImage\"] = \"rgbaImage\";\n })(PreparedMaskFrameKind || (PreparedMaskFrameKind = {}));\n /**\n * How an id raster is laid out for the texture it is uploaded into. WebGPU\n * takes any bytesPerRow, so ids go up one byte per pixel. WebGL aligns every\n * uploaded row to four bytes and rejects a single-channel upload whose width\n * is not a multiple of four, which is what the four-channel layout is for.\n */\n var IdMaskTextureFormat;\n (function (IdMaskTextureFormat) {\n IdMaskTextureFormat[\"R8\"] = \"r8unorm\";\n IdMaskTextureFormat[\"Rgba8\"] = \"rgba8unorm\";\n })(IdMaskTextureFormat || (IdMaskTextureFormat = {}));\n\n const DEFAULT_STOPS = [\n { position: 0, color: 0xffd600 },\n { position: 0.35, color: 0xffd600 },\n { position: 0.7, color: 0xff7a00 },\n { position: 1, color: 0xdc1e1e },\n ];\n const MAX_HEATMAP_PIXELS = 16_777_216;\n const MAX_HEATMAP_SIDE = 8_192;\n /** Turns semantic scores into an RGBA raster; no palette enters cold data. */\n function colorizeHeatmap(map, renderer) {\n const { bounds, width, height, values } = map;\n if (!Number.isFinite(bounds?.x) ||\n !Number.isFinite(bounds?.y) ||\n !Number.isFinite(bounds?.width) ||\n !Number.isFinite(bounds?.height) ||\n bounds.width <= 0 ||\n bounds.height <= 0 ||\n !Number.isInteger(width) ||\n !Number.isInteger(height) ||\n width <= 0 ||\n height <= 0 ||\n width > MAX_HEATMAP_SIDE ||\n height > MAX_HEATMAP_SIDE ||\n width * height > MAX_HEATMAP_PIXELS ||\n !(Array.isArray(values) ||\n values instanceof Float32Array ||\n values instanceof Float64Array ||\n values instanceof Uint16Array) ||\n values.length !== width * height) {\n throw new RangeError(\"Heatmap bounds and dimensions must be valid, match row-major values, and fit the raster limit.\");\n }\n const scale = map.valueScale ?? 1;\n const threshold = (map.threshold ?? 0) * (renderer.thresholdScale ?? 1);\n const maximumScore = renderer.maximumScore ?? 1;\n const opacity = clamp01(renderer.opacity ?? 1);\n const minimumAlpha = clamp01(renderer.minimumAlpha ?? 0);\n const stops = renderer.colorStops ?? DEFAULT_STOPS;\n if (!Number.isFinite(scale) ||\n scale <= 0 ||\n !Number.isFinite(threshold) ||\n !Number.isFinite(maximumScore) ||\n maximumScore <= 0 ||\n !Number.isFinite(renderer.thresholdScale ?? 1) ||\n (renderer.thresholdScale ?? 1) < 0 ||\n !Number.isFinite(renderer.opacity ?? 1) ||\n !Number.isFinite(renderer.minimumAlpha ?? 0) ||\n stops.length === 0 ||\n stops.some((stop, index) => !Number.isFinite(stop.position) ||\n stop.position < 0 ||\n stop.position > 1 ||\n !Number.isInteger(stop.color) ||\n stop.color < 0 ||\n stop.color > 0xffffff ||\n (index > 0 && stop.position < stops[index - 1].position))) {\n throw new RangeError(\"Invalid heatmap scale, threshold, or color stops.\");\n }\n const rgba = new Uint8ClampedArray(new ArrayBuffer(values.length * 4));\n if (maximumScore <= threshold || opacity === 0)\n return rgba;\n for (let index = 0; index < values.length; index += 1) {\n const score = values[index] * scale;\n if (!Number.isFinite(score) || score <= threshold)\n continue;\n const color = interpolateColor(stops, clamp01(score / maximumScore));\n const alphaIntensity = clamp01((score - threshold) / (maximumScore - threshold));\n const offset = index * 4;\n rgba[offset] = color >> 16;\n rgba[offset + 1] = (color >> 8) & 0xff;\n rgba[offset + 2] = color & 0xff;\n rgba[offset + 3] = Math.round(255 * opacity * (minimumAlpha + (1 - minimumAlpha) * alphaIntensity));\n }\n return rgba;\n }\n function interpolateColor(stops, value) {\n const upperIndex = stops.findIndex((stop) => stop.position >= value);\n if (upperIndex === -1)\n return stops[stops.length - 1].color;\n if (upperIndex === 0)\n return stops[0].color;\n const left = stops[upperIndex - 1];\n const right = stops[upperIndex];\n const span = right.position - left.position;\n const ratio = span === 0 ? 1 : (value - left.position) / span;\n const channel = (shift) => Math.round(((left.color >> shift) & 0xff) * (1 - ratio) +\n ((right.color >> shift) & 0xff) * ratio);\n return (channel(16) << 16) | (channel(8) << 8) | channel(0);\n }\n function clamp01(value) {\n return Math.max(0, Math.min(1, value));\n }\n\n var HeatmapPreparationWorkerMessageType;\n (function (HeatmapPreparationWorkerMessageType) {\n HeatmapPreparationWorkerMessageType[\"Prepare\"] = \"heatmap-prepare\";\n HeatmapPreparationWorkerMessageType[\"Complete\"] = \"heatmap-complete\";\n HeatmapPreparationWorkerMessageType[\"Error\"] = \"heatmap-error\";\n })(HeatmapPreparationWorkerMessageType || (HeatmapPreparationWorkerMessageType = {}));\n\n var MaskPreparationWorkerMessageType;\n (function (MaskPreparationWorkerMessageType) {\n MaskPreparationWorkerMessageType[\"Complete\"] = \"complete\";\n MaskPreparationWorkerMessageType[\"Empty\"] = \"empty\";\n MaskPreparationWorkerMessageType[\"Error\"] = \"error\";\n MaskPreparationWorkerMessageType[\"Prepare\"] = \"prepare\";\n })(MaskPreparationWorkerMessageType || (MaskPreparationWorkerMessageType = {}));\n\n /**\n * Messages between the depth source and the render-preparation worker, which\n * decodes depth and confidence PNGs, and copies decoded preview frames' luma,\n * off the main thread. Internal: the worker script is a deployment asset, and\n * its protocol is not public.\n */\n var DepthPreparationWorkerMessageType;\n (function (DepthPreparationWorkerMessageType) {\n DepthPreparationWorkerMessageType[\"Decode\"] = \"depth-decode\";\n DepthPreparationWorkerMessageType[\"Complete\"] = \"depth-decode-complete\";\n DepthPreparationWorkerMessageType[\"Error\"] = \"depth-decode-error\";\n DepthPreparationWorkerMessageType[\"PreviewLuma\"] = \"depth-preview-luma\";\n DepthPreparationWorkerMessageType[\"PreviewLumaComplete\"] = \"depth-preview-luma-complete\";\n })(DepthPreparationWorkerMessageType || (DepthPreparationWorkerMessageType = {}));\n\n /*\n * Grayscale PNG decoding for depth maps and their confidence planes.\n *\n * Every image path a browser offers (<img>, createImageBitmap, ImageDecoder,\n * canvas) hands back 8 bits per channel, so a 16-bit depth PNG is decoded\n * here: the browser's own zlib inflates the IDAT stream, and the row filters\n * are undone in JavaScript.\n *\n * This module imports nothing, so the render-preparation worker, the main\n * thread and the depth benchmark all run the same code.\n */\n const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];\n const COLOR_TYPE_GRAY = 0;\n /** 16384 x 16384, beyond any GPU texture; a header claiming more is refused. */\n const MAX_PNG_PIXELS = 16_384 * 16_384;\n async function decodePng16(bytes, options = {}) {\n const header = readPngHeader(bytes, 16);\n const rowBytes = header.width * 2 + 1;\n const raw = await (options.inflate ?? inflateWithDecompressionStream)(header.idat, rowBytes * header.height);\n const values = new Uint16Array(header.width * header.height);\n checkInflatedLength(raw, rowBytes * header.height);\n unfilterGray16(raw, header.width, header.height, values);\n return {\n height: header.height,\n paddedUpload: options.padRowsForWebGl && header.width % 2 === 1\n ? padRows(values, header.width, header.height)\n : undefined,\n values,\n width: header.width,\n };\n }\n async function decodePng8Gray(bytes, options = {}) {\n const header = readPngHeader(bytes, 8);\n const rowBytes = header.width + 1;\n const raw = await (options.inflate ?? inflateWithDecompressionStream)(header.idat, rowBytes * header.height);\n const values = new Uint8Array(header.width * header.height);\n checkInflatedLength(raw, rowBytes * header.height);\n unfilterGray8(raw, header.width, header.height, values);\n return { height: header.height, values, width: header.width };\n }\n /**\n * Inflates with the platform's `DecompressionStream(\"deflate\")` (zlib\n * framing, which is what PNG uses) straight into one buffer of the expected\n * size, so no second copy joins the output chunks.\n */\n async function inflateWithDecompressionStream(chunks, byteLength) {\n const stream = new DecompressionStream(\"deflate\");\n const writer = stream.writable.getWriter();\n const reader = stream.readable.getReader();\n // Writers split IDAT into many small chunks (libpng's default is 8 KiB).\n // One write of the joined stream costs a copy of the compressed bytes and\n // saves a stream round trip per chunk.\n const zlib = chunks.length === 1 ? chunks[0] : joinChunks(chunks);\n const writing = (async () => {\n await writer.write(zlib);\n await writer.close();\n })();\n const output = new Uint8Array(byteLength);\n let offset = 0;\n // A corrupt stream rejects both sides; the reader's error is the one thrown.\n writing.catch(() => undefined);\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done)\n break;\n if (offset + value.byteLength > byteLength) {\n throw new RangeError(\"PNG image data is longer than its header says.\");\n }\n output.set(value, offset);\n offset += value.byteLength;\n }\n await writing;\n }\n catch (error) {\n await reader.cancel().catch(() => undefined);\n throw error instanceof RangeError\n ? error\n : new RangeError(`PNG image data does not inflate: ${error instanceof Error ? error.message : String(error)}`);\n }\n return offset === byteLength ? output : output.subarray(0, offset);\n }\n function joinChunks(chunks) {\n const joined = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0));\n let offset = 0;\n for (const chunk of chunks) {\n joined.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return joined;\n }\n function readPngHeader(input, bitDepth) {\n const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n if (bytes.byteLength < 8 ||\n PNG_SIGNATURE.some((value, index) => bytes[index] !== value)) {\n throw new RangeError(\"Not a PNG file.\");\n }\n let offset = 8;\n let width = 0;\n let height = 0;\n let sawHeader = false;\n let sawEnd = false;\n const idat = [];\n while (offset + 12 <= bytes.byteLength) {\n const length = view.getUint32(offset);\n const type = view.getUint32(offset + 4);\n const dataStart = offset + 8;\n const dataEnd = dataStart + length;\n if (dataEnd + 4 > bytes.byteLength) {\n throw new RangeError(\"PNG file is truncated.\");\n }\n if (type === 0x49484452) {\n // IHDR\n if (length !== 13)\n throw new RangeError(\"PNG header is malformed.\");\n width = view.getUint32(dataStart);\n height = view.getUint32(dataStart + 4);\n const depth = bytes[dataStart + 8];\n const colorType = bytes[dataStart + 9];\n if (bytes[dataStart + 10] !== 0 || bytes[dataStart + 11] !== 0) {\n throw new RangeError(\"PNG uses an unknown compression or filter.\");\n }\n if (bytes[dataStart + 12] !== 0) {\n throw new RangeError(\"Interlaced PNGs are not supported.\");\n }\n if (colorType !== COLOR_TYPE_GRAY || depth !== bitDepth) {\n throw new RangeError(`Expected a ${bitDepth}-bit grayscale PNG, got bit depth ${depth} with colour type ${colorType}.`);\n }\n sawHeader = true;\n }\n else if (type === 0x49444154) {\n // IDAT\n idat.push(bytes.subarray(dataStart, dataEnd));\n }\n else if (type === 0x49454e44) {\n // IEND\n sawEnd = true;\n break;\n }\n offset = dataEnd + 4;\n }\n if (!sawHeader)\n throw new RangeError(\"PNG has no header.\");\n if (!sawEnd)\n throw new RangeError(\"PNG file is truncated.\");\n if (width === 0 || height === 0 || width * height > MAX_PNG_PIXELS) {\n throw new RangeError(`PNG size ${width}x${height} is not supported.`);\n }\n if (idat.length === 0)\n throw new RangeError(\"PNG has no image data.\");\n return { height, idat, width };\n }\n function checkInflatedLength(raw, expected) {\n if (raw.byteLength !== expected) {\n throw new RangeError(\"PNG image data is shorter than its header says.\");\n }\n }\n /*\n * Undoes the row filters of a 16-bit grayscale image and writes host-order\n * samples, in one pass. Each filter runs on bytes, and the two bytes of a\n * sample never carry into each other, so the loops keep a sample's high and\n * low bytes apart and join them only for the store. The previous row is read\n * back from the output, which already holds it unfiltered; the first row reads\n * a row of zeros, which is what PNG defines above the image.\n *\n * Each filter is its own small function with one loop, called once per row,\n * so engines compile each loop on its own: SpiderMonkey slowed a Paeth loop\n * that shared a function with the other filters.\n */\n function unfilterGray16(raw, width, height, out) {\n const rowBytes = width * 2 + 1;\n const zeroRow = new Uint16Array(width);\n for (let y = 0; y < height; y += 1) {\n const filter = raw[y * rowBytes];\n const unfilterRow = ROW_FILTERS_16[filter];\n if (!unfilterRow) {\n throw new RangeError(`PNG row ${y} has unknown filter ${filter}.`);\n }\n const o = y * width;\n unfilterRow(raw, y * rowBytes + 1, out, o, o + width, y === 0 ? zeroRow : out, y === 0 ? 0 : o - width);\n }\n }\n function noneRow16(raw, s, out, o, end) {\n for (let i = o; i < end; i += 1, s += 2) {\n out[i] = (raw[s] << 8) | raw[s + 1];\n }\n }\n function subRow16(raw, s, out, o, end) {\n let hi = 0;\n let lo = 0;\n for (let i = o; i < end; i += 1, s += 2) {\n hi = (raw[s] + hi) & 0xff;\n lo = (raw[s + 1] + lo) & 0xff;\n out[i] = (hi << 8) | lo;\n }\n }\n function upRow16(raw, s, out, o, end, prev, p) {\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n out[i] = (((raw[s] + (up >> 8)) & 0xff) << 8) | ((raw[s + 1] + up) & 0xff);\n }\n }\n function averageRow16(raw, s, out, o, end, prev, p) {\n let hi = 0;\n let lo = 0;\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n hi = (raw[s] + ((hi + (up >> 8)) >> 1)) & 0xff;\n lo = (raw[s + 1] + ((lo + (up & 0xff)) >> 1)) & 0xff;\n out[i] = (hi << 8) | lo;\n }\n }\n /** Paeth: per byte, whichever of left, up and up-left is closest to left + up - up-left. */\n function paethRow16(raw, s, out, o, end, prev, p) {\n let hi = 0;\n let lo = 0;\n let upLeftHi = 0;\n let upLeftLo = 0;\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n const upHi = up >> 8;\n const upLo = up & 0xff;\n let pa = Math.abs(upHi - upLeftHi);\n let pb = Math.abs(hi - upLeftHi);\n let pc = Math.abs(hi + upHi - upLeftHi - upLeftHi);\n hi =\n (raw[s] + (pa <= pb && pa <= pc ? hi : pb <= pc ? upHi : upLeftHi)) &\n 0xff;\n pa = Math.abs(upLo - upLeftLo);\n pb = Math.abs(lo - upLeftLo);\n pc = Math.abs(lo + upLo - upLeftLo - upLeftLo);\n lo =\n (raw[s + 1] + (pa <= pb && pa <= pc ? lo : pb <= pc ? upLo : upLeftLo)) &\n 0xff;\n out[i] = (hi << 8) | lo;\n upLeftHi = upHi;\n upLeftLo = upLo;\n }\n }\n /** Indexed by the PNG filter type byte: None, Sub, Up, Average, Paeth. */\n const ROW_FILTERS_16 = [\n noneRow16,\n subRow16,\n upRow16,\n averageRow16,\n paethRow16,\n ];\n function unfilterGray8(raw, width, height, out) {\n const rowBytes = width + 1;\n const zeroRow = new Uint8Array(width);\n for (let y = 0; y < height; y += 1) {\n const filter = raw[y * rowBytes];\n const unfilterRow = ROW_FILTERS_8[filter];\n if (!unfilterRow) {\n throw new RangeError(`PNG row ${y} has unknown filter ${filter}.`);\n }\n const o = y * width;\n unfilterRow(raw, y * rowBytes + 1, out, o, o + width, y === 0 ? zeroRow : out, y === 0 ? 0 : o - width);\n }\n }\n function noneRow8(raw, s, out, o, end) {\n out.set(raw.subarray(s, s + end - o), o);\n }\n function subRow8(raw, s, out, o, end) {\n let left = 0;\n for (let i = o; i < end; i += 1, s += 1) {\n left = (raw[s] + left) & 0xff;\n out[i] = left;\n }\n }\n function upRow8(raw, s, out, o, end, prev, p) {\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n out[i] = (raw[s] + prev[p]) & 0xff;\n }\n }\n function averageRow8(raw, s, out, o, end, prev, p) {\n let left = 0;\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n left = (raw[s] + ((left + prev[p]) >> 1)) & 0xff;\n out[i] = left;\n }\n }\n function paethRow8(raw, s, out, o, end, prev, p) {\n let left = 0;\n let upLeft = 0;\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n const up = prev[p];\n const pa = Math.abs(up - upLeft);\n const pb = Math.abs(left - upLeft);\n const pc = Math.abs(left + up - upLeft - upLeft);\n left =\n (raw[s] + (pa <= pb && pa <= pc ? left : pb <= pc ? up : upLeft)) & 0xff;\n out[i] = left;\n upLeft = up;\n }\n }\n const ROW_FILTERS_8 = [\n noneRow8,\n subRow8,\n upRow8,\n averageRow8,\n paethRow8,\n ];\n /**\n * WebGL unpacks rows on four-byte boundaries, so an odd width of two-byte\n * texels needs one padding texel per row.\n */\n function padRows(values, width, height) {\n const textureWidth = width + 1;\n const padded = new Uint16Array(textureWidth * height);\n for (let y = 0; y < height; y += 1) {\n padded.set(values.subarray(y * width, (y + 1) * width), y * textureWidth);\n }\n return { bytes: new Uint8Array(padded.buffer), textureWidth };\n }\n\n /**\n * Answers one decode request, in the worker or on the main thread: the reply\n * and the buffers it can transfer. A file that does not decode is an error\n * reply, never a thrown exception, so the caller can tell a bad file from a\n * broken worker.\n */\n async function decodeDepthPreparationRequest(request) {\n const requestId = request.requestId ?? 0;\n try {\n if (request.bitDepth === 8) {\n const decoded = await decodePng8Gray(request.bytes);\n return {\n response: {\n bitDepth: 8,\n height: decoded.height,\n requestId,\n type: DepthPreparationWorkerMessageType.Complete,\n values: decoded.values.buffer,\n width: decoded.width,\n },\n transfer: [decoded.values.buffer],\n };\n }\n const decoded = await decodePng16(request.bytes, {\n padRowsForWebGl: request.padRowsForWebGl,\n });\n const padded = decoded.paddedUpload;\n const decimated = request.decimateBy !== undefined && request.decimateBy >= 2\n ? decimateDepthUpload(decoded.values, decoded.width, decoded.height, Math.floor(request.decimateBy), request.padRowsForWebGl === true)\n : null;\n const transfer = [decoded.values.buffer];\n if (padded)\n transfer.push(padded.bytes.buffer);\n if (decimated)\n transfer.push(decimated.bytes.buffer);\n return {\n response: {\n bitDepth: 16,\n decimatedUpload: decimated\n ? { ...decimated, bytes: decimated.bytes.buffer }\n : undefined,\n height: decoded.height,\n paddedUpload: padded\n ? {\n bytes: padded.bytes.buffer,\n textureWidth: padded.textureWidth,\n }\n : undefined,\n requestId,\n type: DepthPreparationWorkerMessageType.Complete,\n values: decoded.values.buffer,\n width: decoded.width,\n },\n transfer,\n };\n }\n catch (error) {\n return {\n response: {\n error: error instanceof Error ? error.message : \"Unable to decode depth.\",\n requestId,\n type: DepthPreparationWorkerMessageType.Error,\n },\n transfer: [],\n };\n }\n }\n /**\n * Depth for a texture `factor` times smaller on each side, each texel the\n * sample at its block's centre, as the renderer decimates a map larger than\n * the GPU allows.\n */\n function decimateDepthUpload(values, width, height, factor, padRowsForWebGl) {\n if (factor < 2)\n return null;\n const targetWidth = Math.ceil(width / factor);\n const targetHeight = Math.ceil(height / factor);\n // WebGL reads two-byte texels in rows of four bytes.\n const textureWidth = padRowsForWebGl\n ? targetWidth + (targetWidth % 2)\n : targetWidth;\n const texels = new Uint16Array(textureWidth * targetHeight);\n const columns = new Uint32Array(targetWidth);\n for (let x = 0; x < targetWidth; x += 1) {\n columns[x] = Math.min(width - 1, Math.floor(((x + 0.5) * width) / targetWidth));\n }\n for (let y = 0; y < targetHeight; y += 1) {\n const row = Math.min(height - 1, Math.floor(((y + 0.5) * height) / targetHeight)) *\n width;\n const target = y * textureWidth;\n for (let x = 0; x < targetWidth; x += 1) {\n texels[target + x] = values[row + columns[x]];\n }\n }\n return {\n bytes: new Uint8Array(texels.buffer),\n height: targetHeight,\n textureWidth,\n width: targetWidth,\n };\n }\n\n /** Formats whose first plane is 8-bit luma, which is the code as written. */\n const PLANAR_8_BIT_FORMATS = new Set([\n \"I420\",\n \"I420A\",\n \"I422\",\n \"I422A\",\n \"I444\",\n \"I444A\",\n \"NV12\",\n ]);\n const RGB_FORMATS = new Set([\"RGBA\", \"RGBX\", \"BGRA\", \"BGRX\"]);\n /**\n * Copies a decoded preview frame's luma codes out of it, mapped through\n * `correction` when one is given. The frame stays open; its owner closes\n * it.\n */\n async function readVideoFrameLuma(frame, scratch, correction = null) {\n const rect = frame.visibleRect ?? {\n height: frame.codedHeight,\n width: frame.codedWidth,\n x: 0,\n y: 0,\n };\n const { width, height } = rect;\n const format = frame.format;\n const planar = format !== null && PLANAR_8_BIT_FORMATS.has(format);\n let bytes;\n let plane;\n if (planar || (format !== null && RGB_FORMATS.has(format))) {\n const size = frame.allocationSize({ rect });\n if (scratch.buffer.byteLength < size)\n scratch.buffer = new ArrayBuffer(size);\n [plane] = await frame.copyTo(scratch.buffer, { rect });\n bytes = new Uint8Array(scratch.buffer);\n }\n else {\n // A frame in a layout no copy can read is drawn to a canvas, which\n // converts it to RGB as the browser sees fit.\n bytes = drawFrameRgba(frame, width, height);\n }\n const luma = new Uint8Array(width * height);\n const stride = plane?.stride ?? (planar ? width : width * 4);\n const offset = plane?.offset ?? 0;\n if (!planar) {\n // Every RGB layout keeps green second, and luma with neutral chroma\n // converts to three equal channels.\n for (let y = 0; y < height; y += 1) {\n const row = offset + y * stride + 1;\n const target = y * width;\n for (let x = 0; x < width; x += 1) {\n luma[target + x] = bytes[row + x * 4];\n }\n }\n }\n else if (stride === width) {\n luma.set(bytes.subarray(offset, offset + width * height));\n }\n else {\n for (let y = 0; y < height; y += 1) {\n luma.set(bytes.subarray(offset + y * stride, offset + y * stride + width), y * width);\n }\n }\n if (correction) {\n for (let index = 0; index < luma.length; index += 1) {\n luma[index] = correction[luma[index]];\n }\n }\n return { height, luma, path: planar ? \"plane\" : \"rgb\", width };\n }\n function drawFrameRgba(frame, width, height) {\n const canvas = new OffscreenCanvas(width, height);\n const context = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!context) {\n throw new Error(\"The depth preview needs a 2D canvas to read this frame.\");\n }\n context.drawImage(frame, 0, 0, width, height);\n const { data } = context.getImageData(0, 0, width, height);\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n\n const workerScope = globalThis;\n workerScope.addEventListener(\"message\", (event) => {\n const message = event.data;\n if (message.type === HeatmapPreparationWorkerMessageType.Prepare) {\n prepareHeatmap(message);\n return;\n }\n if (message.type === DepthPreparationWorkerMessageType.Decode) {\n void decodeDepthFrame(message);\n return;\n }\n if (message.type === DepthPreparationWorkerMessageType.PreviewLuma) {\n void copyPreviewLuma(message);\n return;\n }\n if (message.type !== MaskPreparationWorkerMessageType.Prepare) {\n return;\n }\n prepareMaskFrame(message);\n });\n function prepareHeatmap(message) {\n try {\n const data = colorizeHeatmap(message.map, message.renderer);\n const imageData = new ImageData(data, message.map.width, message.map.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n if (imageBitmap) {\n workerScope.postMessage({\n imageBitmap,\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Complete,\n }, [imageBitmap]);\n return;\n }\n workerScope.postMessage({\n imageData,\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Complete,\n }, [imageData.data.buffer]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error ? error.message : \"Unable to prepare heatmap.\",\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Error,\n });\n }\n }\n /** Decoding waits on the platform's inflate, so replies arrive out of order. */\n async function decodeDepthFrame(message) {\n const { response, transfer } = await decodeDepthPreparationRequest(message);\n workerScope.postMessage(response, transfer);\n }\n const previewLumaScratch = {\n buffer: new ArrayBuffer(0),\n };\n /** Frames arrive in order and are copied in order, through one scratch. */\n let previewLumaQueue = Promise.resolve();\n function copyPreviewLuma(message) {\n previewLumaQueue = previewLumaQueue.then(async () => {\n try {\n const copied = await readVideoFrameLuma(message.frame, previewLumaScratch, message.correction);\n workerScope.postMessage({\n height: copied.height,\n luma: copied.luma.buffer,\n path: copied.path,\n requestId: message.requestId,\n type: DepthPreparationWorkerMessageType.PreviewLumaComplete,\n width: copied.width,\n }, [copied.luma.buffer]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to copy the depth preview frame.\",\n requestId: message.requestId,\n type: DepthPreparationWorkerMessageType.Error,\n });\n }\n finally {\n message.frame.close();\n }\n });\n return previewLumaQueue;\n }\n function prepareMaskFrame(message) {\n try {\n const regionMaskCoverage = createRegionMaskCoverageFrame(message.job.instructions);\n const coverageTransfers = getRegionMaskCoverageTransfers(regionMaskCoverage);\n const idMaskFrame = createIdMaskRasterFrame(message.job.instructions, message.job.maxRasterWidth);\n if (idMaskFrame) {\n const screenStrokes = createMaskScreenStrokes(message.job.instructions, idMaskFrame.width, idMaskFrame.height, idMaskFrame.data);\n const strokeTransfers = screenStrokes?.flatMap(({ paths }) => paths.map((path) => path.buffer)) ?? [];\n workerScope.postMessage({\n artifactKind: PreparedMaskFrameKind.IdMask,\n fillPalette: idMaskFrame.fillPalette,\n hasStroke: idMaskFrame.hasStroke,\n height: idMaskFrame.height,\n key: message.job.key,\n maxStrokeWidth: idMaskFrame.maxStrokeWidth,\n raster: idMaskFrame.data,\n screenStrokes,\n regionMaskCoverage,\n requestId: message.requestId,\n sourceWidth: idMaskFrame.sourceWidth,\n strokePalette: idMaskFrame.strokePalette,\n strokeAlignments: idMaskFrame.strokeAlignments,\n strokeWidths: idMaskFrame.strokeWidths,\n type: MaskPreparationWorkerMessageType.Complete,\n width: idMaskFrame.width,\n }, [\n idMaskFrame.data.buffer,\n idMaskFrame.fillPalette.buffer,\n idMaskFrame.strokePalette.buffer,\n ...(idMaskFrame.strokeAlignments\n ? [idMaskFrame.strokeAlignments.buffer]\n : []),\n idMaskFrame.strokeWidths.buffer,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n return;\n }\n const compositedFrame = compositeMaskFrame(message.job.instructions, message.job.displayWidth);\n if (!compositedFrame && !regionMaskCoverage) {\n workerScope.postMessage({\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Empty,\n });\n return;\n }\n const preparedPixels = compositedFrame ?? createTransparentCoverageCarrier();\n const imageData = new ImageData(preparedPixels.data, preparedPixels.width, preparedPixels.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n const idMaskPlane = createIdMaskPlane(message.job.instructions, message.job.maxRasterWidth);\n const idMaskTransfers = idMaskPlane ? [idMaskPlane.data.buffer] : [];\n const screenStrokes = createMaskScreenStrokes(message.job.instructions, preparedPixels.width, preparedPixels.height);\n const strokeTransfers = screenStrokes?.flatMap(({ paths }) => paths.map((path) => path.buffer)) ??\n [];\n if (imageBitmap) {\n workerScope.postMessage({\n idMaskPlane,\n imageBitmap,\n screenStrokes,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [\n imageBitmap,\n ...idMaskTransfers,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n return;\n }\n workerScope.postMessage({\n idMaskPlane,\n imageData,\n screenStrokes,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [\n imageData.data.buffer,\n ...idMaskTransfers,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to prepare mask frame.\",\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Error,\n });\n }\n }\n /**\n * A frame that carries only region coverage has nothing to composite, and the\n * RGBA branch still has to produce an artifact for the coverage to ride on.\n */\n function createTransparentCoverageCarrier() {\n return {\n data: new Uint8ClampedArray(new ArrayBuffer(4)),\n height: 1,\n width: 1,\n };\n }\n function getRegionMaskCoverageTransfers(coverage) {\n return coverage?.entries.map(({ data }) => data.buffer) ?? [];\n }\n function createImageBitmapFromImageData(imageData) {\n if (typeof OffscreenCanvas === \"undefined\") {\n return null;\n }\n const canvas = new OffscreenCanvas(imageData.width, imageData.height);\n const context = canvas.getContext(\"2d\");\n if (!context) {\n return null;\n }\n context.putImageData(imageData, 0, 0);\n return canvas.transferToImageBitmap();\n }\n\n})();";
3117
3117
 
3118
3118
  const DEFAULT_WORKER_NAME = "supervision-js-render-preparation";
3119
3119
  let defaultWorkerUrl;
@@ -3890,11 +3890,11 @@ function describePreviewDecoding(decoding) {
3890
3890
  return `This browser's ${decoding.hardwareAcceleration} decoder changes depth preview codes: ${probe.mismatchedCodes} of ${probe.judgedCodes} come back different, by up to ${probe.maxError} (${probe.lumaPath ?? "unknown"} path)${corrected}. Preview depth during playback is off by up to ${decoding.correction ? decoding.residualError : probe.maxError} preview steps; exact depth at rest is not affected.`;
3891
3891
  }
3892
3892
  async function openDefaultPreviewTrack(url, options) {
3893
- const { openDepthPreviewTrack } = await import('./depth-preview-track-BvgwNEE9.js').then(function (n) { return n.d; });
3893
+ const { openDepthPreviewTrack } = await import('./depth-preview-track-BEag1ZWF.js').then(function (n) { return n.d; });
3894
3894
  return openDepthPreviewTrack(url, options);
3895
3895
  }
3896
3896
  async function chooseDefaultPreviewDecoding(levels) {
3897
- const { chooseDepthPreviewDecoding } = await import('./depth-preview-probe-DG7McQEN.js');
3897
+ const { chooseDepthPreviewDecoding } = await import('./depth-preview-probe-BG0k-33U.js');
3898
3898
  return chooseDepthPreviewDecoding(levels);
3899
3899
  }
3900
3900
  /**
@@ -5252,7 +5252,7 @@ function cropCoverageMask(detectionIndex, decodedMask) {
5252
5252
  }
5253
5253
  return { data, detectionIndex, height, width, x: minX, y: minY };
5254
5254
  }
5255
- function compositeMaskFrame(instructions) {
5255
+ function compositeMaskFrame(instructions, displayWidth) {
5256
5256
  const maskInstructions = materializeMaskInstructions(instructions);
5257
5257
  if (maskInstructions.length === 0) {
5258
5258
  return undefined;
@@ -5260,18 +5260,22 @@ function compositeMaskFrame(instructions) {
5260
5260
  const width = Math.max(...maskInstructions.map(({ mask }) => mask.width));
5261
5261
  const height = Math.max(...maskInstructions.map(({ mask }) => mask.height));
5262
5262
  const data = new Uint8ClampedArray(new ArrayBuffer(width * height * 4));
5263
+ const strokeCoverage = maskInstructions.some(({ stroke }) => stroke && stroke.width > 0 && stroke.alpha > 0)
5264
+ ? new Uint8Array(width * height)
5265
+ : undefined;
5266
+ const strokeKernels = new Map();
5263
5267
  for (const instruction of maskInstructions) {
5264
- compositeInstruction(data, width, instruction);
5268
+ compositeInstruction(data, width, instruction, strokeCoverage, strokeKernels, displayWidth);
5265
5269
  }
5266
5270
  return { data, height, width };
5267
5271
  }
5268
5272
  function createIdMaskRasterFrame(instructions, maxRasterWidth) {
5269
5273
  try {
5270
- return createIdMaskFrame(materializeMaskInstructions(instructions).map((instruction) => instruction.stroke?.widthUnit === "screen"
5274
+ return createIdMaskFrame(instructions
5275
+ .filter((instruction) => instruction.visible !== false)
5276
+ .map((instruction) => instruction.stroke?.widthUnit
5271
5277
  ? { ...instruction, stroke: undefined }
5272
- : instruction), {
5273
- maxWidth: maxRasterWidth,
5274
- });
5278
+ : instruction), { maxWidth: maxRasterWidth });
5275
5279
  }
5276
5280
  catch {
5277
5281
  // The id raster is the fast path, not the only one: answering with nothing
@@ -5301,25 +5305,29 @@ function createIdMaskPlane(instructions, maxRasterWidth) {
5301
5305
  ? { data: frame.data, height: frame.height, width: frame.width }
5302
5306
  : undefined;
5303
5307
  }
5304
- function compositeInstruction(rgba, canvasWidth, instruction) {
5308
+ function compositeInstruction(rgba, canvasWidth, instruction, strokeCoverage, strokeKernels, displayWidth) {
5305
5309
  const fill = resolveRgbaColor(instruction.color, instruction.alpha);
5306
5310
  const bounds = compositeMaskFill(rgba, canvasWidth, instruction.mask, fill);
5307
5311
  if (instruction.stroke &&
5308
- instruction.stroke.widthUnit !== "screen" &&
5309
- bounds) {
5310
- compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, instruction.stroke);
5312
+ instruction.stroke.widthUnit === undefined &&
5313
+ bounds &&
5314
+ strokeCoverage) {
5315
+ compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, {
5316
+ ...instruction.stroke,
5317
+ width: resolveIdMaskStrokeTexels(instruction.stroke.width, displayWidth ?? canvasWidth, canvasWidth, instruction.stroke.alignment),
5318
+ }, strokeCoverage, strokeKernels);
5311
5319
  }
5312
5320
  }
5313
5321
  function createMaskScreenStrokes(instructions, width, height, preparedRaster) {
5314
5322
  const outlined = instructions.filter(({ visible, stroke }) => visible !== false &&
5315
- stroke?.widthUnit === "screen" &&
5323
+ stroke?.widthUnit !== undefined &&
5316
5324
  stroke.width > 0 &&
5317
5325
  stroke.alpha > 0);
5318
5326
  if (outlined.length === 0)
5319
5327
  return undefined;
5320
- const masks = materializeMaskInstructions(instructions);
5321
5328
  const raster = preparedRaster ?? new Uint32Array(width * height);
5322
5329
  if (!preparedRaster) {
5330
+ const masks = materializeMaskInstructions(instructions);
5323
5331
  for (const { detectionIndex, mask } of masks) {
5324
5332
  const counts = decodeCompressedRleCounts(mask.counts);
5325
5333
  let offset = 0;
@@ -5342,6 +5350,8 @@ function createMaskScreenStrokes(instructions, width, height, preparedRaster) {
5342
5350
  alpha: stroke.alpha,
5343
5351
  color: stroke.color,
5344
5352
  width: stroke.width,
5353
+ widthUnit: stroke.widthUnit,
5354
+ alignment: stroke.alignment,
5345
5355
  paths: paths.get(detectionIndex + 1) ?? [],
5346
5356
  }));
5347
5357
  }
@@ -5407,31 +5417,110 @@ function compositeMaskFill(rgba, canvasWidth, mask, fill) {
5407
5417
  }
5408
5418
  return maxX < minX || maxY < minY ? undefined : { maxX, maxY, minX, minY };
5409
5419
  }
5410
- function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke) {
5411
- const width = Math.round(stroke.width);
5412
- if (width <= 0) {
5413
- return;
5420
+ function createStrokeKernel(width, alignment) {
5421
+ const innerFraction = alignment === StrokeAlignment.Inside
5422
+ ? 1
5423
+ : alignment === StrokeAlignment.Center
5424
+ ? 0.5
5425
+ : 0;
5426
+ const inside = [];
5427
+ const outside = [];
5428
+ const radius = Math.min(MAX_ID_MASK_STROKE_WIDTH + 1, Math.ceil(width * Math.max(innerFraction, 1 - innerFraction) + 0.5));
5429
+ const clamp = (value) => Math.max(0, Math.min(1, value));
5430
+ const coverage = (signed) => Math.round(255 *
5431
+ (clamp(0.5 + width * (1 - innerFraction) - signed) -
5432
+ clamp(0.5 - width * innerFraction - signed)));
5433
+ for (let y = -radius; y <= radius; y += 1) {
5434
+ for (let x = -radius; x <= radius; x += 1) {
5435
+ if (x === 0 && y === 0)
5436
+ continue;
5437
+ const distance = Math.max(Math.abs(x) - 0.5, Math.abs(y) - 0.5, 0);
5438
+ const innerCoverage = coverage(-distance);
5439
+ const outerCoverage = coverage(distance);
5440
+ if (innerCoverage > 0)
5441
+ inside.push([x, y, innerCoverage]);
5442
+ if (outerCoverage > 0)
5443
+ outside.push([x, y, outerCoverage]);
5444
+ }
5414
5445
  }
5415
- const strokeColor = resolveRgbaColor(stroke.color, stroke.alpha);
5446
+ return { inside, outside, radius };
5447
+ }
5448
+ function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke, coverage, kernels) {
5449
+ if (stroke.width <= 0 || stroke.alpha <= 0)
5450
+ return;
5451
+ const key = `${stroke.width}:${stroke.alignment}`;
5452
+ let kernel = kernels.get(key);
5453
+ if (!kernel) {
5454
+ kernel = createStrokeKernel(stroke.width, stroke.alignment);
5455
+ kernels.set(key, kernel);
5456
+ }
5457
+ const visitedAir = kernel.inside.length > 0 ? new Set() : undefined;
5458
+ const mark = (sourceX, sourceY, offsets, foreground) => {
5459
+ for (const [dx, dy, value] of offsets) {
5460
+ const x = sourceX + dx;
5461
+ const y = sourceY + dy;
5462
+ if (isOutsideMaskBounds(decodedMask, x, y) ||
5463
+ isMaskPixel(decodedMask, x, y) !== foreground)
5464
+ continue;
5465
+ const index = y * canvasWidth + x;
5466
+ coverage[index] = Math.max(coverage[index], value);
5467
+ }
5468
+ };
5416
5469
  for (let y = bounds.minY; y <= bounds.maxY; y += 1) {
5417
5470
  for (let x = bounds.minX; x <= bounds.maxX; x += 1) {
5418
5471
  if (!isMaskPixel(decodedMask, x, y) ||
5419
- !isBoundaryPixel(decodedMask, x, y)) {
5472
+ !isBoundaryPixel(decodedMask, x, y))
5420
5473
  continue;
5421
- }
5422
- for (let offsetY = -width; offsetY <= width; offsetY += 1) {
5423
- for (let offsetX = -width; offsetX <= width; offsetX += 1) {
5424
- const strokeX = x + offsetX;
5425
- const strokeY = y + offsetY;
5426
- if (isOutsideMaskBounds(decodedMask, strokeX, strokeY) ||
5427
- isMaskPixel(decodedMask, strokeX, strokeY)) {
5474
+ mark(x, y, kernel.outside, false);
5475
+ if (!visitedAir)
5476
+ continue;
5477
+ for (let dy = -1; dy <= 1; dy += 1) {
5478
+ for (let dx = -1; dx <= 1; dx += 1) {
5479
+ const airX = x + dx;
5480
+ const airY = y + dy;
5481
+ if (isOutsideMaskBounds(decodedMask, airX, airY) ||
5482
+ isMaskPixel(decodedMask, airX, airY))
5428
5483
  continue;
5429
- }
5430
- writePixel(rgba, canvasWidth, strokeX, strokeY, strokeColor);
5484
+ const airIndex = airY * decodedMask.width + airX;
5485
+ if (visitedAir.has(airIndex))
5486
+ continue;
5487
+ visitedAir.add(airIndex);
5488
+ mark(airX, airY, kernel.inside, true);
5431
5489
  }
5432
5490
  }
5433
5491
  }
5434
5492
  }
5493
+ const color = resolveRgbaColor(stroke.color, stroke.alpha);
5494
+ for (let y = Math.max(0, bounds.minY - kernel.radius); y <= Math.min(decodedMask.height - 1, bounds.maxY + kernel.radius); y += 1) {
5495
+ for (let x = Math.max(0, bounds.minX - kernel.radius); x <= Math.min(decodedMask.width - 1, bounds.maxX + kernel.radius); x += 1) {
5496
+ const index = y * canvasWidth + x;
5497
+ const amount = coverage[index] / 255;
5498
+ if (amount <= 0)
5499
+ continue;
5500
+ coverage[index] = 0;
5501
+ const offset = index * 4;
5502
+ const alpha = rgba[offset + 3] * (1 - amount) + color.alpha * amount;
5503
+ rgba[offset] =
5504
+ alpha > 0
5505
+ ? (rgba[offset] * rgba[offset + 3] * (1 - amount) +
5506
+ color.red * color.alpha * amount) /
5507
+ alpha
5508
+ : 0;
5509
+ rgba[offset + 1] =
5510
+ alpha > 0
5511
+ ? (rgba[offset + 1] * rgba[offset + 3] * (1 - amount) +
5512
+ color.green * color.alpha * amount) /
5513
+ alpha
5514
+ : 0;
5515
+ rgba[offset + 2] =
5516
+ alpha > 0
5517
+ ? (rgba[offset + 2] * rgba[offset + 3] * (1 - amount) +
5518
+ color.blue * color.alpha * amount) /
5519
+ alpha
5520
+ : 0;
5521
+ rgba[offset + 3] = alpha;
5522
+ }
5523
+ }
5435
5524
  }
5436
5525
  function isBoundaryPixel(mask, x, y) {
5437
5526
  for (let offsetY = -1; offsetY <= 1; offsetY += 1) {
@@ -5615,7 +5704,7 @@ function createMainThreadMaskFramePreparer(workerStatus, message = null) {
5615
5704
  if (idMaskFrame) {
5616
5705
  return idMaskFrame;
5617
5706
  }
5618
- const compositedFrame = compositeMaskFrame(job.instructions);
5707
+ const compositedFrame = compositeMaskFrame(job.instructions, job.displayWidth);
5619
5708
  if (!compositedFrame && !regionMaskCoverage) {
5620
5709
  return undefined;
5621
5710
  }
@@ -5678,6 +5767,7 @@ function createPreparedIdMaskFrame(job, regionMaskCoverage) {
5678
5767
  regionMaskCoverage,
5679
5768
  sourceWidth: frame.sourceWidth,
5680
5769
  strokePalette: frame.strokePalette,
5770
+ strokeAlignments: frame.strokeAlignments,
5681
5771
  strokeWidths: frame.strokeWidths,
5682
5772
  width: frame.width,
5683
5773
  };
@@ -5803,6 +5893,7 @@ function createPreparedFrameFromWorkerResponse(message) {
5803
5893
  screenStrokes: message.screenStrokes,
5804
5894
  sourceWidth: message.sourceWidth,
5805
5895
  strokePalette: message.strokePalette,
5896
+ strokeAlignments: message.strokeAlignments,
5806
5897
  strokeWidths: message.strokeWidths,
5807
5898
  width: message.width,
5808
5899
  };
@@ -6013,7 +6104,8 @@ function createPreparedRenderWindow(options) {
6013
6104
  ? maskFrame.raster.byteLength +
6014
6105
  maskFrame.fillPalette.byteLength +
6015
6106
  maskFrame.strokePalette.byteLength +
6016
- maskFrame.strokeWidths.byteLength
6107
+ maskFrame.strokeWidths.byteLength +
6108
+ (maskFrame.strokeAlignments?.byteLength ?? 0)
6017
6109
  : pixels * 4 + (maskFrame.idMaskPlane?.data.byteLength ?? 0);
6018
6110
  const coverageBytes = maskFrame.regionMaskCoverage?.entries.reduce((total, entry) => total + entry.data.byteLength, 0) ?? 0;
6019
6111
  const backingBytes = options.resolveMaskFrameBackingBytes
@@ -6071,7 +6163,7 @@ function createPreparedRenderWindow(options) {
6071
6163
  let generation = 0;
6072
6164
  let preparationProgress = 0;
6073
6165
  let nextArtifactRevision = 0;
6074
- const artifactRevisions = new WeakMap();
6166
+ const artifactMetadata = new WeakMap();
6075
6167
  const largestMaskFrameBytesByTier = new Map();
6076
6168
  const preparedMaskFrames = new Map();
6077
6169
  /** Keys whose prepared raster is the coarse tier and owed a fine cook once settled. */
@@ -6095,15 +6187,23 @@ function createPreparedRenderWindow(options) {
6095
6187
  return false;
6096
6188
  }
6097
6189
  observeMaskFrame(frame, key);
6098
- const tier = scheduleOptions.tier ?? getRequestedRasterTier();
6190
+ let tier = scheduleOptions.tier ?? getRequestedRasterTier();
6099
6191
  if (emptyMaskFrameKeys.has(key)) {
6100
6192
  return false;
6101
6193
  }
6102
- if (preparedMaskFrames.has(key)) {
6194
+ const prepared = preparedMaskFrames.get(key);
6195
+ if (prepared) {
6196
+ const metadata = artifactMetadata.get(prepared);
6197
+ const owedDisplayWidth = prepared.kind === PreparedMaskFrameKind.RgbaImage &&
6198
+ metadata?.hasStroke === true &&
6199
+ metadata.displayWidth !== options.resolveMaskDisplayWidth?.();
6103
6200
  const owedFine = coarseMaskFrameKeys.has(key) && tier === PreparedRasterTier.Fine;
6104
- if (!owedFine) {
6201
+ if (!owedFine && !owedDisplayWidth) {
6105
6202
  return false;
6106
6203
  }
6204
+ if (!coarseMaskFrameKeys.has(key)) {
6205
+ tier = PreparedRasterTier.Fine;
6206
+ }
6107
6207
  }
6108
6208
  const pending = pendingMaskFrames.get(key);
6109
6209
  if (pending) {
@@ -6200,11 +6300,22 @@ function createPreparedRenderWindow(options) {
6200
6300
  emitDiagnostics();
6201
6301
  continue;
6202
6302
  }
6303
+ const hasStroke = instructions.some((instruction) => instruction.visible !== false &&
6304
+ instruction.stroke !== undefined &&
6305
+ instruction.stroke.widthUnit === undefined &&
6306
+ instruction.stroke.width > 0 &&
6307
+ instruction.stroke.alpha > 0);
6308
+ const displayWidth = options.resolveMaskDisplayWidth?.();
6203
6309
  void maskFramePreparer
6204
6310
  .prepare({
6311
+ displayWidth,
6205
6312
  instructions,
6206
6313
  key,
6207
- maxRasterWidth: resolveRasterWidthFor(job.tier),
6314
+ maxRasterWidth: resolveRasterWidthFor(job.tier, instructions.reduce((width, instruction) => instruction.visible === false
6315
+ ? width
6316
+ : Math.max(width, instruction.mask !== undefined
6317
+ ? instruction.mask.width
6318
+ : instruction.polygon.width), 0)),
6208
6319
  })
6209
6320
  .then((maskFrame) => {
6210
6321
  inFlightMaskFrames.delete(job);
@@ -6213,10 +6324,25 @@ function createPreparedRenderWindow(options) {
6213
6324
  if (pendingJob === job) {
6214
6325
  pendingMaskFrames.delete(key);
6215
6326
  }
6327
+ const staleDisplayWidth = maskFrame?.kind === PreparedMaskFrameKind.RgbaImage &&
6328
+ hasStroke &&
6329
+ displayWidth !== options.resolveMaskDisplayWidth?.();
6216
6330
  if (isDestroyed ||
6217
6331
  job.generation !== generation ||
6218
- pendingJob !== job) {
6332
+ pendingJob !== job ||
6333
+ staleDisplayWidth) {
6219
6334
  maskFrame?.close();
6335
+ if (staleDisplayWidth &&
6336
+ !isDestroyed &&
6337
+ job.generation === generation &&
6338
+ pendingJob === job) {
6339
+ scheduleMaskFrame(job.frame, job.mediaTime, {
6340
+ tier: upgradeToFine ? PreparedRasterTier.Fine : job.tier,
6341
+ priority: key === activeMaskFrame?.key
6342
+ ? PreparedRenderSchedulePriority.Active
6343
+ : PreparedRenderSchedulePriority.Background,
6344
+ });
6345
+ }
6220
6346
  schedulePreparedTargetBatch();
6221
6347
  emitDiagnostics();
6222
6348
  pumpMaskFrameQueue();
@@ -6232,13 +6358,16 @@ function createPreparedRenderWindow(options) {
6232
6358
  }
6233
6359
  const previous = preparedMaskFrames.get(key);
6234
6360
  if (previous) {
6235
- // an upgrade: the coarse raster gives way to the fine one
6236
6361
  releaseMaskFrame(key);
6237
6362
  options.onMaskFrameEvicted?.(key);
6238
6363
  previous.close();
6239
6364
  }
6240
6365
  preparedMaskFrames.set(key, maskFrame);
6241
- artifactRevisions.set(maskFrame, ++nextArtifactRevision);
6366
+ artifactMetadata.set(maskFrame, {
6367
+ revision: ++nextArtifactRevision,
6368
+ displayWidth,
6369
+ hasStroke,
6370
+ });
6242
6371
  const bytes = chargeMaskFrame(key, maskFrame, job.tier);
6243
6372
  if (job.tier === PreparedRasterTier.Coarse &&
6244
6373
  maskFrameNeedsRasterSizeChange(key, maskFrame, PreparedRasterTier.Fine)) {
@@ -6523,7 +6652,7 @@ function createPreparedRenderWindow(options) {
6523
6652
  getArtifactRevision(mediaTime) {
6524
6653
  const frame = options.detectionTimeline.selectFrame(mediaTime);
6525
6654
  const artifact = frame && preparedMaskFrames.get(getFrameKey(frame));
6526
- return artifact ? (artifactRevisions.get(artifact) ?? 0) : 0;
6655
+ return artifact ? (artifactMetadata.get(artifact)?.revision ?? 0) : 0;
6527
6656
  },
6528
6657
  invalidateRasterSize() {
6529
6658
  if (isDestroyed)
@@ -6557,6 +6686,13 @@ function createPreparedRenderWindow(options) {
6557
6686
  }
6558
6687
  }
6559
6688
  },
6689
+ invalidateMaskDisplayWidth() {
6690
+ if (isDestroyed)
6691
+ return;
6692
+ if (activeMaskFrame) {
6693
+ getFrame(activeMaskFrame.mediaTime, { forcePreparedWindow: true });
6694
+ }
6695
+ },
6560
6696
  getPreparationProgress() {
6561
6697
  return preparationProgress;
6562
6698
  },
@@ -6663,6 +6799,13 @@ function createPreparedRenderWindow(options) {
6663
6799
  return;
6664
6800
  }
6665
6801
  clearPreparedMaskFrames();
6802
+ if (!isDestroyed &&
6803
+ maskStyle &&
6804
+ activeMaskFrame &&
6805
+ isPlayheadFast &&
6806
+ settleTimer === undefined) {
6807
+ armSettleTimer();
6808
+ }
6666
6809
  },
6667
6810
  destroy() {
6668
6811
  if (isDestroyed) {
@@ -6751,8 +6894,11 @@ function createPreparedRenderWindow(options) {
6751
6894
  notifyReadinessWaiters();
6752
6895
  options.onPreparedWindowChange?.();
6753
6896
  }
6754
- function resolveRasterWidthFor(tier) {
6755
- const fine = options.resolveMaxRasterWidth?.();
6897
+ function resolveRasterWidthFor(tier, sourceWidth) {
6898
+ const cap = options.resolveMaxRasterWidth?.();
6899
+ const fine = cap !== undefined && sourceWidth !== undefined && sourceWidth > 0
6900
+ ? Math.min(cap, sourceWidth)
6901
+ : cap;
6756
6902
  if (tier === PreparedRasterTier.Fine || fine === undefined) {
6757
6903
  return fine;
6758
6904
  }
@@ -6772,10 +6918,10 @@ function createPreparedRenderWindow(options) {
6772
6918
  function maskFrameNeedsRasterSizeChange(key, frame, tier = coarseMaskFrameKeys.has(key)
6773
6919
  ? PreparedRasterTier.Coarse
6774
6920
  : PreparedRasterTier.Fine) {
6775
- const maxWidth = resolveRasterWidthFor(tier);
6776
6921
  const sourceWidth = frame.kind === PreparedMaskFrameKind.IdMask
6777
6922
  ? frame.sourceWidth
6778
6923
  : frame.width;
6924
+ const maxWidth = resolveRasterWidthFor(tier, sourceWidth);
6779
6925
  const width = maxWidth !== undefined && maxWidth > 0
6780
6926
  ? Math.min(sourceWidth, Math.max(1, Math.floor(maxWidth)))
6781
6927
  : sourceWidth;
@@ -9215,6 +9361,7 @@ async function createMediaRendererCore(options, providers) {
9215
9361
  ...presentation,
9216
9362
  });
9217
9363
  let currentPresentation = resolvePresentation({
9364
+ annotationAntialiasing: options.annotationAntialiasing,
9218
9365
  annotationOverlayStyle: options.annotationOverlayStyle,
9219
9366
  backgroundColor: options.backgroundColor,
9220
9367
  boxStyle: options.boxStyle,
@@ -10089,6 +10236,7 @@ async function createMediaRendererCore(options, providers) {
10089
10236
  const mediaDimensions = runtimeState.recordMediaMetadata(metadata);
10090
10237
  mediaSize = mediaDimensions;
10091
10238
  mediaScene = await providers.createScene({
10239
+ annotationAntialiasing: currentPresentation.annotationAntialiasing,
10092
10240
  annotationOverlayStyle: currentPresentation.annotationOverlayStyle,
10093
10241
  backgroundColor: currentPresentation.backgroundColor,
10094
10242
  boxStyle: currentPresentation.boxStyle,
@@ -10650,6 +10798,260 @@ function matches(left, right) {
10650
10798
  left.every((value, index) => Object.is(value, right[index])));
10651
10799
  }
10652
10800
 
10801
+ const BINS = 256;
10802
+ const TILE_COUNT = 8;
10803
+ const CLIP_LIMIT = 2;
10804
+ function computeMediaClahe(rgba, width, height) {
10805
+ const tilesX = Math.min(TILE_COUNT, width);
10806
+ const tilesY = Math.min(TILE_COUNT, height);
10807
+ const luts = new Uint8Array(tilesX * tilesY * BINS);
10808
+ const histogram = new Uint32Array(BINS);
10809
+ for (let ty = 0; ty < tilesY; ty++) {
10810
+ const y0 = Math.floor((ty * height) / tilesY);
10811
+ const y1 = Math.floor(((ty + 1) * height) / tilesY);
10812
+ for (let tx = 0; tx < tilesX; tx++) {
10813
+ const x0 = Math.floor((tx * width) / tilesX);
10814
+ const x1 = Math.floor(((tx + 1) * width) / tilesX);
10815
+ histogram.fill(0);
10816
+ for (let y = y0; y < y1; y++) {
10817
+ for (let x = x0; x < x1; x++) {
10818
+ const pixel = (y * width + x) * 4;
10819
+ const luma = Math.round(0.299 * rgba[pixel] +
10820
+ 0.587 * rgba[pixel + 1] +
10821
+ 0.114 * rgba[pixel + 2]);
10822
+ histogram[luma]++;
10823
+ }
10824
+ }
10825
+ const area = (x1 - x0) * (y1 - y0);
10826
+ const limit = Math.max(1, Math.floor((CLIP_LIMIT * area) / BINS));
10827
+ let excess = 0;
10828
+ for (let i = 0; i < BINS; i++) {
10829
+ if (histogram[i] > limit) {
10830
+ excess += histogram[i] - limit;
10831
+ histogram[i] = limit;
10832
+ }
10833
+ }
10834
+ const perBin = Math.floor(excess / BINS);
10835
+ let residual = excess - perBin * BINS;
10836
+ for (let i = 0; i < BINS; i++)
10837
+ histogram[i] += perBin;
10838
+ const residualStep = Math.max(Math.floor(BINS / Math.max(residual, 1)), 1);
10839
+ for (let i = 0; i < BINS && residual > 0; i += residualStep, residual--) {
10840
+ histogram[i]++;
10841
+ }
10842
+ const offset = (ty * tilesX + tx) * BINS;
10843
+ let cumulative = 0;
10844
+ for (let i = 0; i < BINS; i++) {
10845
+ cumulative += histogram[i];
10846
+ luts[offset + i] = Math.round((cumulative * (BINS - 1)) / area);
10847
+ }
10848
+ }
10849
+ }
10850
+ return { luts, tilesX, tilesY };
10851
+ }
10852
+
10853
+ /**
10854
+ * Pixi keys its program cache by source, so one program is shared by every
10855
+ * shader built from it. `destroy(true)` takes that program with it, leaving
10856
+ * both the peers still rendering through it and the next shader built from the
10857
+ * same source bound to nothing.
10858
+ */
10859
+ function destroyShaderKeepingProgram(shader) {
10860
+ try {
10861
+ shader.destroy();
10862
+ }
10863
+ catch {
10864
+ // Pixi has already invalidated this shader's resource group.
10865
+ }
10866
+ }
10867
+ /**
10868
+ * Pixi uploads a placeholder while it builds a shader's first bind group, and
10869
+ * WebGPU rejects a canvas that was never given a rendering context.
10870
+ */
10871
+ function createShaderPlaceholderCanvas() {
10872
+ if (typeof document === "undefined") {
10873
+ return { height: 1, width: 1 };
10874
+ }
10875
+ const canvas = document.createElement("canvas");
10876
+ canvas.height = 1;
10877
+ canvas.width = 1;
10878
+ canvas.getContext("2d");
10879
+ return canvas;
10880
+ }
10881
+
10882
+ const HISTOGRAM_MAX_DIMENSION = 1024;
10883
+ const vertex = `
10884
+ in vec2 aPosition;
10885
+ out vec2 vTextureCoord;
10886
+ out vec2 vImageCoord;
10887
+ uniform vec4 uInputSize;
10888
+ uniform vec4 uOutputFrame;
10889
+ uniform vec4 uOutputTexture;
10890
+ uniform mat3 uImageMatrix;
10891
+ void main() {
10892
+ vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
10893
+ position.x = position.x * (2.0 / uOutputTexture.x) - 1.0;
10894
+ position.y = position.y * (2.0 * uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z;
10895
+ gl_Position = vec4(position, 0.0, 1.0);
10896
+ vTextureCoord = aPosition * (uOutputFrame.zw * uInputSize.zw);
10897
+ vImageCoord = (uImageMatrix * vec3(vTextureCoord, 1.0)).xy;
10898
+ }
10899
+ `;
10900
+ const fragment = `
10901
+ in vec2 vTextureCoord;
10902
+ in vec2 vImageCoord;
10903
+ out vec4 finalColor;
10904
+ uniform sampler2D uTexture;
10905
+ uniform sampler2D uLut;
10906
+ uniform vec2 uTiles;
10907
+ uniform float uGamma;
10908
+ uniform float uAuto;
10909
+ float lookup(vec2 tile, float luma) {
10910
+ float row = tile.y * uTiles.x + tile.x;
10911
+ return texture(uLut, vec2((luma * 255.0 + 0.5) / 256.0,
10912
+ (row + 0.5) / (uTiles.x * uTiles.y))).r;
10913
+ }
10914
+ void main() {
10915
+ vec4 color = texture(uTexture, vTextureCoord);
10916
+ if (color.a == 0.0) { finalColor = color; return; }
10917
+ vec3 rgb = color.rgb / color.a;
10918
+ if (uAuto > 0.5) {
10919
+ float luma = dot(rgb, vec3(0.299, 0.587, 0.114));
10920
+ vec2 tilePosition = clamp(vImageCoord, 0.0, 1.0) * uTiles - 0.5;
10921
+ vec2 t0 = floor(tilePosition);
10922
+ vec2 f = tilePosition - t0;
10923
+ vec2 t1 = clamp(t0 + 1.0, vec2(0.0), uTiles - 1.0);
10924
+ t0 = clamp(t0, vec2(0.0), uTiles - 1.0);
10925
+ float top = mix(lookup(t0, luma), lookup(vec2(t1.x, t0.y), luma), f.x);
10926
+ float bottom = mix(lookup(vec2(t0.x, t1.y), luma), lookup(t1, luma), f.x);
10927
+ rgb = clamp(rgb + (mix(top, bottom, f.y) - luma), 0.0, 1.0);
10928
+ }
10929
+ finalColor = vec4(pow(rgb, vec3(1.0 / uGamma)) * color.a, color.a);
10930
+ }
10931
+ `;
10932
+ const gpuSource = `
10933
+ struct GlobalFilterUniforms {
10934
+ uInputSize: vec4<f32>, uInputPixel: vec4<f32>, uInputClamp: vec4<f32>,
10935
+ uOutputFrame: vec4<f32>, uGlobalFrame: vec4<f32>, uOutputTexture: vec4<f32>,
10936
+ };
10937
+ struct InspectionUniforms {
10938
+ uImageMatrix: mat3x3<f32>, uTiles: vec2<f32>, uGamma: f32, uAuto: f32,
10939
+ };
10940
+ @group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;
10941
+ @group(0) @binding(1) var uTexture: texture_2d<f32>;
10942
+ @group(0) @binding(2) var uSampler: sampler;
10943
+ @group(1) @binding(0) var<uniform> inspectionUniforms: InspectionUniforms;
10944
+ @group(1) @binding(1) var uLut: texture_2d<f32>;
10945
+ @group(1) @binding(2) var uLutSampler: sampler;
10946
+ struct VertexOutput {
10947
+ @builtin(position) position: vec4<f32>,
10948
+ @location(0) uv: vec2<f32>, @location(1) image: vec2<f32>,
10949
+ };
10950
+ @vertex fn mainVertex(@location(0) aPosition: vec2<f32>) -> VertexOutput {
10951
+ var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;
10952
+ position.x = position.x * (2.0 / gfu.uOutputTexture.x) - 1.0;
10953
+ position.y = position.y * (2.0 * gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z;
10954
+ let uv = aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw);
10955
+ return VertexOutput(vec4(position, 0.0, 1.0), uv,
10956
+ (inspectionUniforms.uImageMatrix * vec3(uv, 1.0)).xy);
10957
+ }
10958
+ fn lookup(tile: vec2<f32>, luma: f32) -> f32 {
10959
+ let tiles = inspectionUniforms.uTiles;
10960
+ let row = tile.y * tiles.x + tile.x;
10961
+ return textureSampleLevel(uLut, uLutSampler,
10962
+ vec2((luma * 255.0 + 0.5) / 256.0, (row + 0.5) / (tiles.x * tiles.y)), 0.0).r;
10963
+ }
10964
+ @fragment fn mainFragment(input: VertexOutput) -> @location(0) vec4<f32> {
10965
+ let color = textureSample(uTexture, uSampler, input.uv);
10966
+ if (color.a == 0.0) { return color; }
10967
+ var rgb = color.rgb / color.a;
10968
+ if (inspectionUniforms.uAuto > 0.5) {
10969
+ let luma = dot(rgb, vec3(0.299, 0.587, 0.114));
10970
+ let tiles = inspectionUniforms.uTiles;
10971
+ let tilePosition = clamp(input.image, vec2(0.0), vec2(1.0)) * tiles - 0.5;
10972
+ let unbounded = floor(tilePosition);
10973
+ let f = tilePosition - unbounded;
10974
+ let t0 = clamp(unbounded, vec2(0.0), tiles - 1.0);
10975
+ let t1 = clamp(unbounded + 1.0, vec2(0.0), tiles - 1.0);
10976
+ let top = mix(lookup(t0, luma), lookup(vec2(t1.x, t0.y), luma), f.x);
10977
+ let bottom = mix(lookup(vec2(t0.x, t1.y), luma), lookup(t1, luma), f.x);
10978
+ rgb = clamp(rgb + (mix(top, bottom, f.y) - luma), vec3(0.0), vec3(1.0));
10979
+ }
10980
+ return vec4(pow(rgb, vec3(1.0 / inspectionUniforms.uGamma)) * color.a, color.a);
10981
+ }
10982
+ `;
10983
+ function createPixiMediaInspectionFilter(sprite, pixi) {
10984
+ const { BufferImageSource, Filter, Matrix } = pixi;
10985
+ const pixels = new Uint8Array(256 * 64 * 4);
10986
+ const lutSource = new BufferImageSource({
10987
+ resource: pixels,
10988
+ width: 256,
10989
+ height: 64,
10990
+ scaleMode: "linear",
10991
+ alphaMode: "no-premultiply-alpha",
10992
+ });
10993
+ const filter = Filter.from({
10994
+ gl: { vertex: vertex, fragment: fragment },
10995
+ gpu: {
10996
+ vertex: { source: gpuSource, entryPoint: "mainVertex" },
10997
+ fragment: { source: gpuSource, entryPoint: "mainFragment" },
10998
+ },
10999
+ resolution: "inherit",
11000
+ resources: {
11001
+ inspectionUniforms: {
11002
+ uImageMatrix: { value: new Matrix(), type: "mat3x3<f32>" },
11003
+ uTiles: { value: new Float32Array([8, 8]), type: "vec2<f32>" },
11004
+ uGamma: { value: 1, type: "f32" },
11005
+ uAuto: { value: 0, type: "f32" },
11006
+ },
11007
+ uLut: lutSource,
11008
+ uLutSampler: lutSource.style,
11009
+ },
11010
+ });
11011
+ const uniforms = filter.resources.inspectionUniforms.uniforms;
11012
+ let hasHistogram = false;
11013
+ filter.apply = (manager, input, output, clear) => {
11014
+ // The filter input is clipped by the viewport; lookup tiles stay anchored
11015
+ // to the full image when the user zooms or pans.
11016
+ manager.calculateSpriteMatrix(uniforms.uImageMatrix, sprite);
11017
+ manager.applyFilter(filter, input, output, clear);
11018
+ };
11019
+ return {
11020
+ filter,
11021
+ invalidateImage() {
11022
+ hasHistogram = false;
11023
+ },
11024
+ update(gamma, autoContrast, image) {
11025
+ uniforms.uGamma = Math.max(gamma, 0.0001);
11026
+ if (autoContrast && image && !hasHistogram) {
11027
+ const scale = Math.min(1, HISTOGRAM_MAX_DIMENSION / Math.max(image.width, image.height));
11028
+ const sample = document.createElement("canvas");
11029
+ sample.width = Math.max(1, Math.round(image.width * scale));
11030
+ sample.height = Math.max(1, Math.round(image.height * scale));
11031
+ const context = sample.getContext("2d", { willReadFrequently: true });
11032
+ context.drawImage(image, 0, 0, sample.width, sample.height);
11033
+ const { luts, tilesX, tilesY } = computeMediaClahe(context.getImageData(0, 0, sample.width, sample.height).data, sample.width, sample.height);
11034
+ for (let i = 0; i < luts.length; i++) {
11035
+ pixels[i * 4] = luts[i];
11036
+ pixels[i * 4 + 1] = luts[i];
11037
+ pixels[i * 4 + 2] = luts[i];
11038
+ pixels[i * 4 + 3] = 255;
11039
+ }
11040
+ uniforms.uTiles.set([tilesX, tilesY]);
11041
+ lutSource.resource = pixels.subarray(0, luts.length * 4);
11042
+ lutSource.resize(256, tilesX * tilesY);
11043
+ lutSource.update();
11044
+ hasHistogram = true;
11045
+ }
11046
+ uniforms.uAuto = autoContrast && hasHistogram ? 1 : 0;
11047
+ },
11048
+ destroy() {
11049
+ destroyShaderKeepingProgram(filter);
11050
+ lutSource.destroy();
11051
+ },
11052
+ };
11053
+ }
11054
+
10653
11055
  /**
10654
11056
  * Which frame a media time draws and how much of it is cooked, as two separate
10655
11057
  * facts. Reporting readiness is what the snapshot and the token are for; a
@@ -10789,35 +11191,6 @@ void main(void) {
10789
11191
  }
10790
11192
  `;
10791
11193
 
10792
- /**
10793
- * Pixi keys its program cache by source, so one program is shared by every
10794
- * shader built from it. `destroy(true)` takes that program with it, leaving
10795
- * both the peers still rendering through it and the next shader built from the
10796
- * same source bound to nothing.
10797
- */
10798
- function destroyShaderKeepingProgram(shader) {
10799
- try {
10800
- shader.destroy();
10801
- }
10802
- catch {
10803
- // Pixi has already invalidated this shader's resource group.
10804
- }
10805
- }
10806
- /**
10807
- * Pixi uploads a placeholder while it builds a shader's first bind group, and
10808
- * WebGPU rejects a canvas that was never given a rendering context.
10809
- */
10810
- function createShaderPlaceholderCanvas() {
10811
- if (typeof document === "undefined") {
10812
- return { height: 1, width: 1 };
10813
- }
10814
- const canvas = document.createElement("canvas");
10815
- canvas.height = 1;
10816
- canvas.width = 1;
10817
- canvas.getContext("2d");
10818
- return canvas;
10819
- }
10820
-
10821
11194
  /**
10822
11195
  * The halo a detection actually paints, or `undefined`. A style may answer an
10823
11196
  * instruction that draws nothing, and both the coverage the halo prepares and
@@ -11184,6 +11557,8 @@ function createPixiBoxLayer(options) {
11184
11557
  let retainedContainer = options.container;
11185
11558
  const retainedEntries = new Map();
11186
11559
  const invalidatedDetections = new Set();
11560
+ const renderedRects = new Map();
11561
+ let renderedBoxFrame;
11187
11562
  const usingRetainedEntries = Boolean(options.Container && options.Graphics);
11188
11563
  return {
11189
11564
  createContainer() {
@@ -11212,6 +11587,10 @@ function createPixiBoxLayer(options) {
11212
11587
  boxGraphics?.clear();
11213
11588
  const activeDetectionIndexes = [];
11214
11589
  const activeKeys = new Set();
11590
+ renderedRects.clear();
11591
+ renderedBoxFrame = detectionFrame
11592
+ ? { frame: detectionFrame, rects: renderedRects }
11593
+ : undefined;
11215
11594
  if (boxStyle && detectionFrame) {
11216
11595
  const orderedDetections = detectionFrame.detections
11217
11596
  .map((detection, detectionIndex) => ({ detection, detectionIndex }))
@@ -11243,10 +11622,13 @@ function createPixiBoxLayer(options) {
11243
11622
  display.position?.set?.(0, 0);
11244
11623
  display.clear();
11245
11624
  drawBoxInstruction(display, instruction, viewportScale);
11625
+ retainRenderedBox(detectionIndex, instruction);
11246
11626
  invalidatedDetections.delete(key);
11247
11627
  }
11248
11628
  else {
11249
11629
  drawBoxInstruction(boxGraphics, instruction, viewportScale);
11630
+ if (boxGraphics)
11631
+ retainRenderedBox(detectionIndex, instruction);
11250
11632
  }
11251
11633
  }
11252
11634
  }
@@ -11259,6 +11641,9 @@ function createPixiBoxLayer(options) {
11259
11641
  lastDrawnState = getBoxLayerState(detectionFrame, activeDetectionIndexes);
11260
11642
  return lastDrawnState;
11261
11643
  },
11644
+ getRenderedBoxes() {
11645
+ return renderedBoxFrame;
11646
+ },
11262
11647
  setBoxStyle(nextBoxStyle) {
11263
11648
  if (nextBoxStyle === undefined) {
11264
11649
  return;
@@ -11281,6 +11666,16 @@ function createPixiBoxLayer(options) {
11281
11666
  return true;
11282
11667
  },
11283
11668
  };
11669
+ function retainRenderedBox(detectionIndex, instruction) {
11670
+ if (instruction.rect.width > 0 &&
11671
+ instruction.rect.height > 0 &&
11672
+ ((instruction.fill && (instruction.fill.alpha ?? 1) > 0) ||
11673
+ (instruction.stroke &&
11674
+ instruction.stroke.width > 0 &&
11675
+ (instruction.stroke.alpha ?? 1) > 0))) {
11676
+ renderedRects.set(detectionIndex, instruction.rect);
11677
+ }
11678
+ }
11284
11679
  function ensureRetainedEntry(key) {
11285
11680
  let display = retainedEntries.get(key);
11286
11681
  if (!display) {
@@ -12374,7 +12769,7 @@ function createPixiHeatmapLayer(options) {
12374
12769
  sprite.removeFromParent();
12375
12770
  sprite.destroy();
12376
12771
  if (temporaryTexture)
12377
- texture.destroy(true);
12772
+ releaseTexture(texture);
12378
12773
  }
12379
12774
  positionedSprites.length = 0;
12380
12775
  activeKeys.clear();
@@ -12388,7 +12783,7 @@ function createPixiHeatmapLayer(options) {
12388
12783
  }
12389
12784
  clear();
12390
12785
  for (const entry of cache.values()) {
12391
- entry.texture.destroy(true);
12786
+ releaseTexture(entry.texture);
12392
12787
  entry.image.close();
12393
12788
  }
12394
12789
  cache.clear();
@@ -12426,7 +12821,7 @@ function createPixiHeatmapLayer(options) {
12426
12821
  continue;
12427
12822
  cache.delete(key);
12428
12823
  cachedBytes -= entry.bytes;
12429
- entry.texture.destroy(true);
12824
+ releaseTexture(entry.texture);
12430
12825
  entry.image.close();
12431
12826
  }
12432
12827
  };
@@ -12474,7 +12869,7 @@ function createPixiHeatmapLayer(options) {
12474
12869
  (right.detection.zIndex ?? right.index) || left.index - right.index);
12475
12870
  const result = [];
12476
12871
  let pixels = 0;
12477
- for (const { detection } of orderedDetections) {
12872
+ for (const { detection, index } of orderedDetections) {
12478
12873
  const map = detection.heatmap;
12479
12874
  if (!map || !options.isVisible(detection))
12480
12875
  continue;
@@ -12487,7 +12882,7 @@ function createPixiHeatmapLayer(options) {
12487
12882
  continue;
12488
12883
  }
12489
12884
  pixels += map.width * map.height;
12490
- result.push({ detection, map, renderer });
12885
+ result.push({ detection, detectionIndex: index, map, renderer });
12491
12886
  }
12492
12887
  }
12493
12888
  return result;
@@ -12522,7 +12917,8 @@ function createPixiHeatmapLayer(options) {
12522
12917
  await options.prepareTexture?.(texture);
12523
12918
  }
12524
12919
  catch (error) {
12525
- texture?.destroy(true);
12920
+ if (texture)
12921
+ releaseTexture(texture);
12526
12922
  image.close();
12527
12923
  if (destroyed || job.cancelled || job.generation !== generation) {
12528
12924
  return;
@@ -12540,7 +12936,7 @@ function createPixiHeatmapLayer(options) {
12540
12936
  job.cancelled ||
12541
12937
  job.generation !== generation ||
12542
12938
  !wantedKeys.has(job.key)) {
12543
- texture.destroy(true);
12939
+ releaseTexture(texture);
12544
12940
  image.close();
12545
12941
  return;
12546
12942
  }
@@ -12596,6 +12992,20 @@ function createPixiHeatmapLayer(options) {
12596
12992
  };
12597
12993
  return {
12598
12994
  createContainer: () => container,
12995
+ getActiveFocusArtifacts(frame) {
12996
+ if (!frame || lastFrame !== frame)
12997
+ return [];
12998
+ return positionedSprites.map(({ sprite, detectionIndex }) => ({
12999
+ detectionIndex,
13000
+ texture: sprite.texture,
13001
+ bounds: {
13002
+ x: sprite.x + sprite.width / 2,
13003
+ y: sprite.y + sprite.height / 2,
13004
+ width: sprite.width,
13005
+ height: sprite.height,
13006
+ },
13007
+ }));
13008
+ },
12599
13009
  setRenderers(next) {
12600
13010
  if (next.length === renderers.length &&
12601
13011
  next.every((renderer, index) => renderer === renderers[index])) {
@@ -12661,7 +13071,7 @@ function createPixiHeatmapLayer(options) {
12661
13071
  }
12662
13072
  const maps = visibleMaps(frame);
12663
13073
  const visiblePixelCount = maps.reduce((total, { map }) => total + map.width * map.height, 0);
12664
- for (const { detection, map, renderer } of maps) {
13074
+ for (const { detection, detectionIndex, map, renderer } of maps) {
12665
13075
  const key = keyFor(map, renderer);
12666
13076
  if (shouldPrepareAsync(map, visiblePixelCount)) {
12667
13077
  wantedKeys.add(key);
@@ -12702,6 +13112,7 @@ function createPixiHeatmapLayer(options) {
12702
13112
  positionedSprites.push({
12703
13113
  sprite,
12704
13114
  detectionId: detection.id,
13115
+ detectionIndex,
12705
13116
  x: sprite.x,
12706
13117
  y: sprite.y,
12707
13118
  temporaryTexture,
@@ -12732,6 +13143,10 @@ function createPixiHeatmapLayer(options) {
12732
13143
  container.destroy();
12733
13144
  },
12734
13145
  };
13146
+ function releaseTexture(texture) {
13147
+ options.onTextureRelease?.(texture);
13148
+ texture.destroy(true);
13149
+ }
12735
13150
  }
12736
13151
 
12737
13152
  /**
@@ -12817,6 +13232,268 @@ function installPixiBatchTextureBindings(pixi, renderer) {
12817
13232
  };
12818
13233
  }
12819
13234
 
13235
+ const maskUniformOrigins = new WeakMap();
13236
+ /** Keeps nested mask cutouts aligned and filter disposal scene-owned. */
13237
+ function installPixiFilterBindings(pixi, renderer) {
13238
+ const releaseMaskTarget = installMaskTargetBindings(renderer);
13239
+ if (renderer.name !== "webgpu")
13240
+ return releaseMaskTarget;
13241
+ const system = renderer.filter;
13242
+ const originalApplyFilter = system.applyFilter;
13243
+ let globalGroup;
13244
+ const maskGroups = new Map();
13245
+ const applyFilter = (filter, input, output, clear) => {
13246
+ if (filter.enabled && filter instanceof pixi.MaskFilter) {
13247
+ const source = filter.resources.filterUniforms;
13248
+ let entry = maskGroups.get(filter);
13249
+ if (!entry) {
13250
+ const original = maskUniformOrigins.get(source) ?? source;
13251
+ const owned = new pixi.UniformGroup(original.uniformStructures, {
13252
+ isStatic: original.isStatic,
13253
+ ubo: original.ubo,
13254
+ });
13255
+ maskUniformOrigins.set(owned, original);
13256
+ entry = { original, owned };
13257
+ maskGroups.set(filter, entry);
13258
+ }
13259
+ // Pixi pools MaskFilters across renderers. Their uniform buffers need
13260
+ // one GPU upload owner, even when a second scene borrows the same filter.
13261
+ if (source !== entry.owned) {
13262
+ Object.assign(entry.owned.uniforms, source.uniforms);
13263
+ filter.resources.filterUniforms = entry.owned;
13264
+ if (entry.owned.isStatic)
13265
+ entry.owned.update();
13266
+ }
13267
+ }
13268
+ originalApplyFilter.call(system, filter, input, output, clear);
13269
+ // Pixi replaces group 0 with its renderer-owned group when applying an
13270
+ // enabled filter. Disabled filters use a separate passthrough shader.
13271
+ if (filter.enabled)
13272
+ globalGroup = filter.groups[0];
13273
+ };
13274
+ system.applyFilter = applyFilter;
13275
+ return () => {
13276
+ releaseMaskTarget();
13277
+ if (system.applyFilter === applyFilter) {
13278
+ system.applyFilter = originalApplyFilter;
13279
+ }
13280
+ globalGroup?.destroy();
13281
+ globalGroup = undefined;
13282
+ for (const [filter, { original, owned }] of maskGroups) {
13283
+ if (filter.resources?.filterUniforms === owned) {
13284
+ Object.assign(original.uniforms, owned.uniforms);
13285
+ filter.resources.filterUniforms = original;
13286
+ }
13287
+ const buffer = owned.buffer;
13288
+ owned.buffer = undefined;
13289
+ buffer?.destroy();
13290
+ }
13291
+ maskGroups.clear();
13292
+ };
13293
+ }
13294
+ function installMaskTargetBindings(renderer) {
13295
+ const pipe = renderer.renderPipes?.alphaMask;
13296
+ if (!pipe)
13297
+ return () => undefined;
13298
+ const targets = renderer.renderTarget;
13299
+ const originalExecute = pipe.execute;
13300
+ const captures = [];
13301
+ const execute = (instruction) => {
13302
+ if (!instruction.mask.renderMaskToTexture) {
13303
+ originalExecute.call(pipe, instruction);
13304
+ return;
13305
+ }
13306
+ if (instruction.action === "pushMaskBegin") {
13307
+ captures.push(targets.getBindState());
13308
+ }
13309
+ if (instruction.action !== "pushMaskEnd") {
13310
+ originalExecute.call(pipe, instruction);
13311
+ return;
13312
+ }
13313
+ const binding = captures.pop();
13314
+ const originalPop = targets.pop;
13315
+ // Filters bind their captures without pushing them onto the target stack.
13316
+ // Returning from an alpha-mask capture must restore that bound surface.
13317
+ targets.pop = () => {
13318
+ const target = originalPop.call(targets);
13319
+ return binding ? targets.bind(binding) : target;
13320
+ };
13321
+ try {
13322
+ originalExecute.call(pipe, instruction);
13323
+ }
13324
+ finally {
13325
+ targets.pop = originalPop;
13326
+ }
13327
+ };
13328
+ pipe.execute = execute;
13329
+ return () => {
13330
+ if (pipe.execute === execute)
13331
+ pipe.execute = originalExecute;
13332
+ captures.length = 0;
13333
+ };
13334
+ }
13335
+
13336
+ /** Follows visible heatmap samples while preserving faint anomaly interiors. */
13337
+ function createPixiFocusHeatmapCutout(options) {
13338
+ const geometry = new options.MeshGeometry({
13339
+ indices: new Uint32Array([0, 1, 2, 0, 2, 3]),
13340
+ positions: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]),
13341
+ shrinkBuffersToFit: true,
13342
+ topology: "triangle-list",
13343
+ uvs: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]),
13344
+ });
13345
+ const shader = options.Shader.from({
13346
+ gl: { vertex: tintedMaskVertexGlsl, fragment: heatmapCutoutGlsl },
13347
+ gpu: {
13348
+ vertex: { source: untintedMaskVertexWgsl, entryPoint: "mainVertex" },
13349
+ fragment: { source: heatmapCutoutWgsl, entryPoint: "mainFragment" },
13350
+ },
13351
+ resources: {
13352
+ uTexture: options.artifact.texture.source,
13353
+ uSampler: options.artifact.texture.source.style,
13354
+ heatmapCoverageUniforms: {
13355
+ uAntialiasing: { value: 0, type: "f32" },
13356
+ },
13357
+ },
13358
+ });
13359
+ const display = new options.Mesh({ geometry, shader });
13360
+ const uniforms = shader.resources.heatmapCoverageUniforms;
13361
+ return {
13362
+ display,
13363
+ render(artifact, antialiasing = false) {
13364
+ uniforms.uniforms.uAntialiasing = antialiasing ? 1 : 0;
13365
+ const { bounds } = artifact;
13366
+ display.position.set(bounds.x - bounds.width / 2, bounds.y - bounds.height / 2);
13367
+ display.scale.set(bounds.width, bounds.height);
13368
+ display.visible = true;
13369
+ },
13370
+ destroy() {
13371
+ display.removeFromParent();
13372
+ display.destroy();
13373
+ destroyShaderKeepingProgram(shader);
13374
+ const buffer = uniforms.buffer;
13375
+ uniforms.buffer = undefined;
13376
+ buffer?.destroy();
13377
+ geometry.destroy();
13378
+ },
13379
+ };
13380
+ }
13381
+ const heatmapCutoutGlsl = `#version 300 es
13382
+ precision highp float;
13383
+
13384
+ in vec2 vUV;
13385
+ uniform sampler2D uTexture;
13386
+ uniform float uAntialiasing;
13387
+ out vec4 finalColor;
13388
+
13389
+ float visibleSample(ivec2 position, ivec2 dimensions) {
13390
+ return texelFetch(uTexture, clamp(position, ivec2(0), dimensions - ivec2(1)), 0).a > 0.0 ? 1.0 : 0.0;
13391
+ }
13392
+
13393
+ void main(void) {
13394
+ float coverage;
13395
+ if (uAntialiasing > 0.0) {
13396
+ ivec2 dimensions = textureSize(uTexture, 0);
13397
+ vec2 position = vUV * vec2(dimensions) - 0.5;
13398
+ ivec2 base = ivec2(floor(position));
13399
+ vec2 weight = fract(position);
13400
+ coverage = mix(
13401
+ mix(visibleSample(base, dimensions), visibleSample(base + ivec2(1, 0), dimensions), weight.x),
13402
+ mix(visibleSample(base + ivec2(0, 1), dimensions), visibleSample(base + ivec2(1, 1), dimensions), weight.x),
13403
+ weight.y
13404
+ );
13405
+ } else {
13406
+ coverage = texture(uTexture, vUV).a > 0.0 ? 1.0 : 0.0;
13407
+ }
13408
+ finalColor = vec4(coverage);
13409
+ }
13410
+ `;
13411
+ const heatmapCutoutWgsl = `
13412
+ @group(2) @binding(0) var uTexture: texture_2d<f32>;
13413
+ @group(2) @binding(1) var uSampler: sampler;
13414
+ struct HeatmapCoverageUniforms {
13415
+ uAntialiasing: f32,
13416
+ };
13417
+ @group(2) @binding(2) var<uniform> heatmapCoverageUniforms: HeatmapCoverageUniforms;
13418
+
13419
+ fn visibleSample(position: vec2<i32>, dimensions: vec2<i32>) -> f32 {
13420
+ let alpha = textureLoad(uTexture, clamp(position, vec2<i32>(0), dimensions - vec2<i32>(1)), 0).a;
13421
+ return select(0.0, 1.0, alpha > 0.0);
13422
+ }
13423
+
13424
+ @fragment
13425
+ fn mainFragment(@location(0) vUV: vec2<f32>) -> @location(0) vec4<f32> {
13426
+ var coverage: f32;
13427
+ if (heatmapCoverageUniforms.uAntialiasing > 0.0) {
13428
+ let dimensions = vec2<i32>(textureDimensions(uTexture));
13429
+ let position = vUV * vec2<f32>(dimensions) - 0.5;
13430
+ let base = vec2<i32>(floor(position));
13431
+ let weight = fract(position);
13432
+ coverage = mix(
13433
+ mix(visibleSample(base, dimensions), visibleSample(base + vec2<i32>(1, 0), dimensions), weight.x),
13434
+ mix(visibleSample(base + vec2<i32>(0, 1), dimensions), visibleSample(base + vec2<i32>(1, 1), dimensions), weight.x),
13435
+ weight.y
13436
+ );
13437
+ } else {
13438
+ let alpha = textureSampleLevel(uTexture, uSampler, vUV, 0.0).a;
13439
+ coverage = select(0.0, 1.0, alpha > 0.0);
13440
+ }
13441
+ return vec4<f32>(coverage);
13442
+ }
13443
+ `;
13444
+
13445
+ function setPixiViewportBounds(display, bounds, viewport, Rectangle) {
13446
+ if (!viewport) {
13447
+ display.boundsArea = undefined;
13448
+ return;
13449
+ }
13450
+ const left = Math.max(bounds.x, viewport.x);
13451
+ const top = Math.max(bounds.y, viewport.y);
13452
+ const right = Math.min(bounds.x + bounds.width, viewport.x + viewport.width);
13453
+ const bottom = Math.min(bounds.y + bounds.height, viewport.y + viewport.height);
13454
+ const clipped = (display.boundsArea ??= new Rectangle());
13455
+ clipped.x = left;
13456
+ clipped.y = top;
13457
+ clipped.width = Math.max(0, right - left);
13458
+ clipped.height = Math.max(0, bottom - top);
13459
+ }
13460
+ /**
13461
+ * AlphaMask allocates from its mask's full bounds, including geometry outside
13462
+ * the canvas. A neutral parent clips that allocation without changing a
13463
+ * rotated or mirrored child's geometry or texture coordinates.
13464
+ */
13465
+ function syncPixiMaskViewportBounds(container, viewport, Rectangle) {
13466
+ if (!viewport) {
13467
+ container.boundsArea = undefined;
13468
+ return;
13469
+ }
13470
+ let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
13471
+ for (const child of container.children) {
13472
+ if (!child.visible)
13473
+ continue;
13474
+ const bounds = child.getLocalBounds();
13475
+ if (bounds.width <= 0 || bounds.height <= 0)
13476
+ continue;
13477
+ child.updateLocalTransform();
13478
+ const { a, b, c, d, tx, ty } = child.localTransform;
13479
+ left = Math.min(left, tx +
13480
+ Math.min(a * bounds.minX, a * bounds.maxX) +
13481
+ Math.min(c * bounds.minY, c * bounds.maxY));
13482
+ top = Math.min(top, ty +
13483
+ Math.min(b * bounds.minX, b * bounds.maxX) +
13484
+ Math.min(d * bounds.minY, d * bounds.maxY));
13485
+ right = Math.max(right, tx +
13486
+ Math.max(a * bounds.minX, a * bounds.maxX) +
13487
+ Math.max(c * bounds.minY, c * bounds.maxY));
13488
+ bottom = Math.max(bottom, ty +
13489
+ Math.max(b * bounds.minX, b * bounds.maxX) +
13490
+ Math.max(d * bounds.minY, d * bounds.maxY));
13491
+ }
13492
+ setPixiViewportBounds(container, left === Infinity
13493
+ ? { x: viewport.x, y: viewport.y, width: 0, height: 0 }
13494
+ : { x: left, y: top, width: right - left, height: bottom - top }, viewport, Rectangle);
13495
+ }
13496
+
12820
13497
  const MAX_FOCUS_MASK_IDS = 16;
12821
13498
  /**
12822
13499
  * How far the media may travel before a cutout drawn for an earlier frame stops
@@ -12839,6 +13516,11 @@ function createPixiFocusLayer(options) {
12839
13516
  : (options.focusStyle ?? new BaseFocusStyle());
12840
13517
  let focusGraphics;
12841
13518
  let focusMaskGraphics;
13519
+ let heatmapMaskContainer;
13520
+ let heatmapVectorGraphics;
13521
+ let usesHeatmapAlphaMask = false;
13522
+ let viewportBounds;
13523
+ const heatmapCutouts = new Map();
12842
13524
  let idMaskRenderer;
12843
13525
  let focusDisplay;
12844
13526
  let currentAlpha = 0;
@@ -12865,6 +13547,7 @@ function createPixiFocusLayer(options) {
12865
13547
  resetHeldFocus();
12866
13548
  focusGraphics = new options.Graphics();
12867
13549
  focusGraphics.visible = false;
13550
+ syncViewportBounds(true);
12868
13551
  idMaskRenderer = createIdMaskRenderer();
12869
13552
  if (!options.Container || !focusGraphics.setMask) {
12870
13553
  focusDisplay = focusGraphics;
@@ -12893,12 +13576,27 @@ function createPixiFocusLayer(options) {
12893
13576
  hide();
12894
13577
  idMaskRenderer?.destroy();
12895
13578
  idMaskRenderer = undefined;
13579
+ activateVectorMask();
13580
+ heatmapMaskContainer?.destroy({ children: true });
13581
+ heatmapMaskContainer = undefined;
12896
13582
  },
12897
13583
  releaseMaskTexture(texture) {
12898
- if (!isDestroyed)
13584
+ if (!isDestroyed) {
12899
13585
  idMaskRenderer?.releaseTexture(texture.source);
13586
+ const cutouts = heatmapCutouts.get(texture);
13587
+ if (cutouts) {
13588
+ for (const cutout of cutouts.values())
13589
+ cutout.destroy();
13590
+ heatmapCutouts.delete(texture);
13591
+ vectorFocusSignature = null;
13592
+ if (heatmapCutouts.size === 0 && usesHeatmapAlphaMask) {
13593
+ activateVectorMask()?.clear();
13594
+ }
13595
+ }
13596
+ }
12900
13597
  },
12901
13598
  drawFrame(context) {
13599
+ syncViewportBounds();
12902
13600
  if (isDestroyed || !focusStyle || mediaWidth <= 0 || mediaHeight <= 0) {
12903
13601
  hide();
12904
13602
  return;
@@ -12919,11 +13617,15 @@ function createPixiFocusLayer(options) {
12919
13617
  const instruction = resolvedInstruction
12920
13618
  ? {
12921
13619
  ...resolvedInstruction,
12922
- targets: resolvedInstruction.targets.filter(({ detection }) => options.isDetectionVisible?.(detection) ?? true),
13620
+ targets: resolvedInstruction.targets.filter(({ detection, frame }) => frame === context.frame &&
13621
+ (options.isDetectionVisible?.(detection) ?? true)),
12923
13622
  }
12924
13623
  : undefined;
12925
13624
  endHold();
12926
13625
  if (!instruction || instruction.targets.length === 0) {
13626
+ if (usesHeatmapAlphaMask && heldFill) {
13627
+ drawOverlayWithoutCutout(heldFill);
13628
+ }
12927
13629
  resetHeldFocus();
12928
13630
  transitionToHidden();
12929
13631
  return;
@@ -12933,12 +13635,13 @@ function createPixiFocusLayer(options) {
12933
13635
  if (focusDisplay)
12934
13636
  focusDisplay.visible = true;
12935
13637
  if (drawIdMaskFocus(context.idMaskArtifact, instruction)) {
13638
+ activateVectorMask();
12936
13639
  hideVectorFocus();
12937
13640
  markCutoutDrawn(context.mediaTime, context.frame.mediaTime);
12938
13641
  return;
12939
13642
  }
12940
13643
  idMaskRenderer?.hide();
12941
- if (drawVectorFocus(instruction, context.frame)) {
13644
+ if (drawVectorFocus(instruction, context)) {
12942
13645
  markCutoutDrawn(context.mediaTime, context.frame.mediaTime);
12943
13646
  }
12944
13647
  },
@@ -12966,7 +13669,29 @@ function createPixiFocusLayer(options) {
12966
13669
  }
12967
13670
  focusStyle = nextFocusStyle;
12968
13671
  },
13672
+ syncViewportBounds,
12969
13673
  };
13674
+ function syncViewportBounds(force = false) {
13675
+ if (!options.Rectangle)
13676
+ return;
13677
+ const next = options.getViewportBounds?.();
13678
+ if (!force &&
13679
+ next?.x === viewportBounds?.x &&
13680
+ next?.y === viewportBounds?.y &&
13681
+ next?.width === viewportBounds?.width &&
13682
+ next?.height === viewportBounds?.height)
13683
+ return;
13684
+ viewportBounds = next ? { ...next } : undefined;
13685
+ if (focusGraphics) {
13686
+ setPixiViewportBounds(focusGraphics, { x: 0, y: 0, width: mediaWidth, height: mediaHeight }, viewportBounds, options.Rectangle);
13687
+ }
13688
+ syncHeatmapViewportBounds();
13689
+ }
13690
+ function syncHeatmapViewportBounds() {
13691
+ if (heatmapMaskContainer && options.Rectangle) {
13692
+ syncPixiMaskViewportBounds(heatmapMaskContainer, viewportBounds, options.Rectangle);
13693
+ }
13694
+ }
12970
13695
  function drawIdMaskFocus(artifact, instruction) {
12971
13696
  if (!idMaskRenderer ||
12972
13697
  !artifact ||
@@ -12975,17 +13700,19 @@ function createPixiFocusLayer(options) {
12975
13700
  }
12976
13701
  const maskIds = getTargetMaskIds(instruction.targets);
12977
13702
  if (maskIds.length === 0 ||
13703
+ instruction.targets.some((target) => !target.detection.mask) ||
12978
13704
  (!instruction.ambient && maskIds.length !== instruction.targets.length)) {
12979
13705
  return false;
12980
13706
  }
12981
13707
  idMaskRenderer.render(artifact.frame, artifact.texture, maskIds, instruction.fill, instruction.ambient === true);
12982
13708
  return true;
12983
13709
  }
12984
- function drawVectorFocus(instruction, frame) {
13710
+ function drawVectorFocus(instruction, context) {
12985
13711
  if (!focusGraphics) {
12986
13712
  return false;
12987
13713
  }
12988
13714
  const targetsWithGeometry = instruction.targets.filter((target) => target.detection.mask ||
13715
+ target.detection.heatmap ||
12989
13716
  target.detection.rect ||
12990
13717
  target.detection.polygon);
12991
13718
  if (targetsWithGeometry.length === 0) {
@@ -12993,37 +13720,55 @@ function createPixiFocusLayer(options) {
12993
13720
  return false;
12994
13721
  }
12995
13722
  const signature = {
13723
+ annotationAntialiasing: options.getAnnotationAntialiasing?.() ?? false,
12996
13724
  cornerRadius: instruction.fallback?.cornerRadius,
12997
13725
  detections: targetsWithGeometry.map((target) => target.detection),
12998
13726
  fillAlpha: instruction.fill.alpha,
12999
13727
  fillColor: instruction.fill.color,
13000
- frame,
13728
+ frame: context.frame,
13729
+ heatmapsEnabled: (options.getHeatmapRenderers?.().length ?? 0) > 0,
13730
+ heatmapArtifacts: context.heatmapArtifacts ?? [],
13001
13731
  mediaHeight,
13002
13732
  mediaWidth,
13003
- shape: instruction.fallback?.shape,
13733
+ shape: instruction.fallback === null ? null : instruction.fallback?.shape,
13004
13734
  };
13005
13735
  focusGraphics.visible = true;
13006
13736
  if (focusMaskGraphics)
13007
- focusMaskGraphics.visible = true;
13737
+ focusMaskGraphics.visible = !usesHeatmapAlphaMask;
13008
13738
  // Tessellating thousands of mask runs on every draw is what makes this path
13009
13739
  // expensive, and nothing it draws moves while its inputs hold still.
13010
13740
  if (isSameVectorFocus(vectorFocusSignature, signature)) {
13011
13741
  return true;
13012
13742
  }
13013
13743
  vectorFocusSignature = signature;
13744
+ const gpuHeatmapTargets = context.heatmapArtifacts !== undefined &&
13745
+ signature.heatmapsEnabled &&
13746
+ options.AlphaMask &&
13747
+ options.Container &&
13748
+ options.Mesh &&
13749
+ options.MeshGeometry &&
13750
+ options.Shader &&
13751
+ focusMaskGraphics &&
13752
+ focusGraphics.setMask &&
13753
+ targetsWithGeometry.some(({ detection }) => detection.heatmap && !detection.mask);
13754
+ const cutoutGraphics = gpuHeatmapTargets
13755
+ ? activateHeatmapMask(instruction.targets, signature.annotationAntialiasing, signature.heatmapArtifacts)
13756
+ : activateVectorMask();
13014
13757
  focusGraphics.clear();
13015
13758
  focusGraphics.rect(0, 0, mediaWidth, mediaHeight);
13016
13759
  focusGraphics.fill(instruction.fill);
13017
- if (focusMaskGraphics) {
13018
- focusMaskGraphics.clear();
13019
- for (const target of targetsWithGeometry) {
13020
- if (drawCutoutShape(focusMaskGraphics, target, instruction) === "drawn") {
13021
- focusMaskGraphics.fill({ alpha: 1, color: 0xffffff });
13760
+ const vectorTargets = targetsWithGeometry.filter(({ detection }) => !signature.heatmapsEnabled || !detection.heatmap || detection.mask);
13761
+ if (cutoutGraphics) {
13762
+ cutoutGraphics.clear();
13763
+ for (const target of vectorTargets) {
13764
+ if (drawCutoutShape(cutoutGraphics, target, instruction) === "drawn") {
13765
+ cutoutGraphics.fill({ alpha: 1, color: 0xffffff });
13022
13766
  }
13023
13767
  }
13768
+ syncHeatmapViewportBounds();
13024
13769
  return true;
13025
13770
  }
13026
- for (const target of targetsWithGeometry) {
13771
+ for (const target of vectorTargets) {
13027
13772
  if (drawCutoutShape(focusGraphics, target, instruction) === "drawn") {
13028
13773
  focusGraphics.cut();
13029
13774
  }
@@ -13039,7 +13784,7 @@ function createPixiFocusLayer(options) {
13039
13784
  graphics.poly(target.detection.polygon.points.flatMap(({ x, y }) => [x, y]), true);
13040
13785
  return "drawn";
13041
13786
  }
13042
- if (!target.detection.rect) {
13787
+ if (!target.detection.rect || instruction.fallback === null) {
13043
13788
  return "empty";
13044
13789
  }
13045
13790
  const { rect } = target.detection;
@@ -13053,6 +13798,93 @@ function createPixiFocusLayer(options) {
13053
13798
  }
13054
13799
  return "drawn";
13055
13800
  }
13801
+ function activateVectorMask() {
13802
+ clearHeatmapCutouts();
13803
+ if (heatmapMaskContainer)
13804
+ heatmapMaskContainer.visible = false;
13805
+ if (usesHeatmapAlphaMask && focusMaskGraphics) {
13806
+ focusGraphics?.setMask?.({
13807
+ inverse: true,
13808
+ mask: focusMaskGraphics,
13809
+ });
13810
+ usesHeatmapAlphaMask = false;
13811
+ }
13812
+ if (focusMaskGraphics)
13813
+ focusMaskGraphics.visible = true;
13814
+ return focusMaskGraphics;
13815
+ }
13816
+ function activateHeatmapMask(targets, annotationAntialiasing, heatmapArtifacts) {
13817
+ const indexes = new Set(targets
13818
+ .filter(({ detection }) => !detection.mask)
13819
+ .map(({ detectionIndex }) => detectionIndex));
13820
+ const artifacts = heatmapArtifacts.filter((artifact) => indexes.has(artifact.detectionIndex));
13821
+ if (artifacts.length === 0)
13822
+ return activateVectorMask();
13823
+ if (!heatmapMaskContainer) {
13824
+ heatmapMaskContainer = new options.Container();
13825
+ heatmapVectorGraphics = new options.Graphics();
13826
+ heatmapMaskContainer.addChild(heatmapVectorGraphics);
13827
+ focusDisplay.addChild(heatmapMaskContainer);
13828
+ }
13829
+ heatmapMaskContainer.visible = true;
13830
+ if (!usesHeatmapAlphaMask) {
13831
+ focusGraphics?.setMask?.({
13832
+ inverse: true,
13833
+ channel: "alpha",
13834
+ mask: new options.AlphaMask({
13835
+ mask: heatmapMaskContainer,
13836
+ }),
13837
+ });
13838
+ usesHeatmapAlphaMask = true;
13839
+ }
13840
+ if (focusMaskGraphics)
13841
+ focusMaskGraphics.visible = false;
13842
+ const activeTextures = new Map();
13843
+ for (const artifact of artifacts) {
13844
+ let indexes = activeTextures.get(artifact.texture);
13845
+ if (!indexes) {
13846
+ indexes = new Set();
13847
+ activeTextures.set(artifact.texture, indexes);
13848
+ }
13849
+ indexes.add(artifact.detectionIndex);
13850
+ let textureCutouts = heatmapCutouts.get(artifact.texture);
13851
+ if (!textureCutouts) {
13852
+ textureCutouts = new Map();
13853
+ heatmapCutouts.set(artifact.texture, textureCutouts);
13854
+ }
13855
+ let cutout = textureCutouts.get(artifact.detectionIndex);
13856
+ if (!cutout) {
13857
+ cutout = createPixiFocusHeatmapCutout({
13858
+ artifact,
13859
+ Mesh: options.Mesh,
13860
+ MeshGeometry: options.MeshGeometry,
13861
+ Shader: options.Shader,
13862
+ });
13863
+ textureCutouts.set(artifact.detectionIndex, cutout);
13864
+ heatmapMaskContainer.addChild(cutout.display);
13865
+ }
13866
+ cutout.render(artifact, annotationAntialiasing);
13867
+ }
13868
+ for (const [texture, cutouts] of heatmapCutouts) {
13869
+ const activeIndexes = activeTextures.get(texture);
13870
+ for (const [index, cutout] of cutouts) {
13871
+ if (activeIndexes?.has(index))
13872
+ continue;
13873
+ cutout.destroy();
13874
+ cutouts.delete(index);
13875
+ }
13876
+ if (cutouts.size === 0)
13877
+ heatmapCutouts.delete(texture);
13878
+ }
13879
+ return heatmapVectorGraphics;
13880
+ }
13881
+ function clearHeatmapCutouts() {
13882
+ for (const cutouts of heatmapCutouts.values()) {
13883
+ for (const cutout of cutouts.values())
13884
+ cutout.destroy();
13885
+ }
13886
+ heatmapCutouts.clear();
13887
+ }
13056
13888
  function drawMaskCutout(graphics, target) {
13057
13889
  const mask = target.detection.mask;
13058
13890
  if (!mask) {
@@ -13114,7 +13946,8 @@ function createPixiFocusLayer(options) {
13114
13946
  targetAlpha = 1;
13115
13947
  if (focusDisplay)
13116
13948
  focusDisplay.visible = true;
13117
- if (isDrawnCutoutStillCurrent(mediaTime, heldMaskFrameTime)) {
13949
+ if (!usesHeatmapAlphaMask &&
13950
+ isDrawnCutoutStillCurrent(mediaTime, heldMaskFrameTime)) {
13118
13951
  return;
13119
13952
  }
13120
13953
  if (isMaskArtifactOwed) {
@@ -13197,6 +14030,7 @@ function createPixiFocusLayer(options) {
13197
14030
  cutoutMediaTime = null;
13198
14031
  cutoutFrameTime = null;
13199
14032
  drawnOverlayWithoutCutout = fill;
14033
+ activateVectorMask();
13200
14034
  if (idMaskRenderer) {
13201
14035
  hideVectorFocus();
13202
14036
  idMaskRenderer.renderWithoutCutout(fill);
@@ -13256,7 +14090,10 @@ function createPixiFocusLayer(options) {
13256
14090
  }
13257
14091
  function isSameVectorFocus(previous, next) {
13258
14092
  return (previous !== null &&
14093
+ previous.annotationAntialiasing === next.annotationAntialiasing &&
13259
14094
  previous.frame === next.frame &&
14095
+ previous.heatmapsEnabled === next.heatmapsEnabled &&
14096
+ sameHeatmapArtifacts(previous.heatmapArtifacts, next.heatmapArtifacts) &&
13260
14097
  previous.fillAlpha === next.fillAlpha &&
13261
14098
  previous.fillColor === next.fillColor &&
13262
14099
  previous.cornerRadius === next.cornerRadius &&
@@ -13266,6 +14103,18 @@ function isSameVectorFocus(previous, next) {
13266
14103
  previous.detections.length === next.detections.length &&
13267
14104
  previous.detections.every((detection, index) => detection === next.detections[index]));
13268
14105
  }
14106
+ function sameHeatmapArtifacts(left, right) {
14107
+ return (left.length === right.length &&
14108
+ left.every((artifact, index) => {
14109
+ const next = right[index];
14110
+ return (artifact.texture === next.texture &&
14111
+ artifact.detectionIndex === next.detectionIndex &&
14112
+ artifact.bounds.x === next.bounds.x &&
14113
+ artifact.bounds.y === next.bounds.y &&
14114
+ artifact.bounds.width === next.bounds.width &&
14115
+ artifact.bounds.height === next.bounds.height);
14116
+ }));
14117
+ }
13269
14118
  function getTargetMaskIds(targets) {
13270
14119
  return targets
13271
14120
  .filter((target) => target.detection.mask)
@@ -13550,6 +14399,416 @@ fn mainFragment(
13550
14399
  }
13551
14400
  `;
13552
14401
 
14402
+ /*!
14403
+ * The FXAA algorithm is adapted from PixiJS v7.4.3's official shader:
14404
+ * https://github.com/pixijs/pixijs/blob/v7.4.3/packages/filter-fxaa/src/fxaa.frag
14405
+ * It includes alpha in contrast, filters premultiplied RGBA, clamps samples,
14406
+ * limits the span to four pixels and mixes the center back for thin strokes.
14407
+ *
14408
+ * From: https://github.com/mitsuhiko/webgl-meincraft
14409
+ * Copyright (c) 2011 by Armin Ronacher. Some rights reserved.
14410
+ *
14411
+ * Redistribution and use in source and binary forms, with or without
14412
+ * modification, are permitted provided that the following conditions are met:
14413
+ *
14414
+ * Redistributions of source code must retain the above copyright notice,
14415
+ * this list of conditions and the following disclaimer.
14416
+ *
14417
+ * Redistributions in binary form must reproduce the above copyright notice,
14418
+ * this list of conditions and the following disclaimer in the documentation
14419
+ * and/or other materials provided with the distribution.
14420
+ *
14421
+ * The names of the contributors may not be used to endorse or promote
14422
+ * products derived from this software without specific prior written permission.
14423
+ *
14424
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14425
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
14426
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
14427
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
14428
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
14429
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
14430
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
14431
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
14432
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
14433
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
14434
+ * POSSIBILITY OF SUCH DAMAGE.
14435
+ */
14436
+ const annotationFxaaGl = `
14437
+ vec4 applyAnnotationFxaa(vec2 uv) {
14438
+ vec2 pixel = uInputPixel.zw;
14439
+ vec4 center = readColor(uv);
14440
+ float nw = contrastValue(readColor(uv + vec2(-1.0, -1.0) * pixel));
14441
+ float ne = contrastValue(readColor(uv + vec2( 1.0, -1.0) * pixel));
14442
+ float sw = contrastValue(readColor(uv + vec2(-1.0, 1.0) * pixel));
14443
+ float se = contrastValue(readColor(uv + vec2( 1.0, 1.0) * pixel));
14444
+ float middle = contrastValue(center);
14445
+ float low = min(middle, min(min(nw, ne), min(sw, se)));
14446
+ float high = max(middle, max(max(nw, ne), max(sw, se)));
14447
+ if (high - low < max(0.03125, high * 0.0625)) {
14448
+ return center;
14449
+ }
14450
+ vec2 direction = vec2(-((nw + ne) - (sw + se)), (nw + sw) - (ne + se));
14451
+ float reduction = max((nw + ne + sw + se) * 0.03125, 0.0078125);
14452
+ direction *= 1.0 / (min(abs(direction.x), abs(direction.y)) + reduction);
14453
+ direction = clamp(direction, vec2(-4.0), vec2(4.0)) * pixel;
14454
+ vec4 narrow = 0.5 * (
14455
+ readColor(uv - direction / 6.0) +
14456
+ readColor(uv + direction / 6.0)
14457
+ );
14458
+ vec4 wide = 0.5 * narrow + 0.25 * (
14459
+ readColor(uv - direction * 0.5) +
14460
+ readColor(uv + direction * 0.5)
14461
+ );
14462
+ float value = contrastValue(wide);
14463
+ vec4 filtered = value < low || value > high ? narrow : wide;
14464
+ return mix(center, filtered, 0.75);
14465
+ }
14466
+ `;
14467
+ const annotationFxaaWgsl = `
14468
+ fn applyAnnotationFxaa(uv: vec2<f32>) -> vec4<f32> {
14469
+ let pixel = gfu.uInputPixel.zw;
14470
+ let center = readColor(uv);
14471
+ let nw = contrastValue(readColor(uv + vec2<f32>(-1.0, -1.0) * pixel));
14472
+ let ne = contrastValue(readColor(uv + vec2<f32>( 1.0, -1.0) * pixel));
14473
+ let sw = contrastValue(readColor(uv + vec2<f32>(-1.0, 1.0) * pixel));
14474
+ let se = contrastValue(readColor(uv + vec2<f32>( 1.0, 1.0) * pixel));
14475
+ let middle = contrastValue(center);
14476
+ let low = min(middle, min(min(nw, ne), min(sw, se)));
14477
+ let high = max(middle, max(max(nw, ne), max(sw, se)));
14478
+ if (high - low < max(0.03125, high * 0.0625)) { return center; }
14479
+ var direction = vec2<f32>(-((nw + ne) - (sw + se)), (nw + sw) - (ne + se));
14480
+ let reduction = max((nw + ne + sw + se) * 0.03125, 0.0078125);
14481
+ direction *= 1.0 / (min(abs(direction.x), abs(direction.y)) + reduction);
14482
+ direction = clamp(direction, vec2<f32>(-4.0), vec2<f32>(4.0)) * pixel;
14483
+ let narrow = 0.5 * (readColor(uv - direction / 6.0) + readColor(uv + direction / 6.0));
14484
+ let wide = 0.5 * narrow + 0.25 * (readColor(uv - direction * 0.5) + readColor(uv + direction * 0.5));
14485
+ let value = contrastValue(wide);
14486
+ let filtered = select(wide, narrow, value < low || value > high);
14487
+ return mix(center, filtered, 0.75);
14488
+ }
14489
+ `;
14490
+
14491
+ /** Smooths a captured annotation group without reading the media texture. */
14492
+ function createPixiAnnotationAntialiasFilter(options) {
14493
+ const source = createGpuSource(options.maskCoverage ?? false);
14494
+ const filter = options.Filter.from({
14495
+ antialias: false,
14496
+ resolution: options.resolution ?? 1,
14497
+ padding: 2,
14498
+ resources: options.maskCoverage
14499
+ ? {
14500
+ coverageAaUniforms: {
14501
+ uOutputOffset: {
14502
+ type: "vec4<f32>",
14503
+ value: new Float32Array([0, 0, 1, 0]),
14504
+ },
14505
+ },
14506
+ }
14507
+ : {},
14508
+ gl: {
14509
+ vertex: options.maskCoverage
14510
+ ? coverageFilterVert
14511
+ : options.defaultFilterVert,
14512
+ fragment: `#version 300 es
14513
+ precision highp float;
14514
+ in vec2 vTextureCoord;
14515
+ uniform sampler2D uTexture;
14516
+ uniform vec4 uInputClamp;
14517
+ uniform vec4 uInputPixel;
14518
+ uniform vec4 uInputSize;
14519
+ uniform vec4 uOutputFrame;
14520
+ uniform vec4 uOutputTexture;
14521
+ out vec4 finalColor;
14522
+
14523
+ vec4 readColor(vec2 uv) {
14524
+ ${options.maskCoverage
14525
+ ? "return texture(uTexture, clamp(uv, uInputClamp.xy, uInputClamp.zw));"
14526
+ : `vec2 lower = max(uInputClamp.xy, -uOutputFrame.xy * uInputSize.zw + 0.5 * uInputPixel.zw);
14527
+ vec2 upper = min(uInputClamp.zw, (uOutputTexture.xy - uOutputFrame.xy) * uInputSize.zw - 0.5 * uInputPixel.zw);
14528
+ return texture(uTexture, clamp(uv, lower, max(lower, upper)));`}
14529
+ }
14530
+
14531
+ float contrastValue(vec4 color) {
14532
+ return dot(color.rgb, vec3(0.299, 0.587, 0.114)) + color.a;
14533
+ }
14534
+ ${annotationFxaaGl}
14535
+
14536
+ void main(void) {
14537
+ finalColor = applyAnnotationFxaa(vTextureCoord);
14538
+ }
14539
+
14540
+ `,
14541
+ },
14542
+ gpu: {
14543
+ vertex: { entryPoint: "mainVertex", source },
14544
+ fragment: { entryPoint: "mainFragment", source },
14545
+ },
14546
+ });
14547
+ if (options.maskCoverage) {
14548
+ filter.apply = (manager, input, output, clear) => {
14549
+ const uniforms = filter.resources.coverageAaUniforms;
14550
+ const offset = uniforms.uniforms.uOutputOffset;
14551
+ const renderer = manager.renderer;
14552
+ const target = renderer.renderTarget.getRenderTarget(output);
14553
+ const global = renderer.globalUniforms.globalUniformData;
14554
+ // A mask target has its own origin outside Pixi's filter stack. The
14555
+ // global uniforms identify that target after the capture offset pops.
14556
+ offset[0] = global.offset.x;
14557
+ offset[1] = global.offset.y;
14558
+ offset[2] = target.resolution;
14559
+ offset[3] = global.resolution === target.size ? 1 : 0;
14560
+ uniforms.update();
14561
+ manager.applyFilter(filter, input, output, clear);
14562
+ };
14563
+ }
14564
+ return filter;
14565
+ }
14566
+ /** Keeps Pixi's power-of-two capture textures within the backend's limit. */
14567
+ function resolvePixiAnnotationAntialiasResolution(outputResolution, viewport, maxTextureSize, captureScale = 1) {
14568
+ const pooledLimit = 2 ** Math.floor(Math.log2(maxTextureSize));
14569
+ // Nested masks round logical bounds; FXAA adds two padding pixels per edge.
14570
+ const roundedSide = Math.max(1, Math.ceil(viewport.width) + 6, Math.ceil(viewport.height) + 6);
14571
+ return Math.min(outputResolution * captureScale, Math.max(1, pooledLimit - 2) / roundedSide);
14572
+ }
14573
+ const coverageFilterVert = `#version 300 es
14574
+ precision highp float;
14575
+ in vec2 aPosition;
14576
+ out vec2 vTextureCoord;
14577
+ uniform vec4 uOutputFrame;
14578
+ uniform vec4 uOutputTexture;
14579
+ uniform vec4 uInputSize;
14580
+ uniform vec4 uGlobalFrame;
14581
+ uniform vec4 uOutputOffset;
14582
+
14583
+ void main(void) {
14584
+ vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
14585
+ if (uOutputOffset.w > 0.0) {
14586
+ position -= uOutputOffset.xy - uGlobalFrame.xy / uOutputOffset.z;
14587
+ }
14588
+ gl_Position = vec4(
14589
+ position.x * (2.0 / uOutputTexture.x) - 1.0,
14590
+ position.y * (2.0 * uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z,
14591
+ 0.0, 1.0
14592
+ );
14593
+ vTextureCoord = aPosition * (uOutputFrame.zw * uInputSize.zw);
14594
+ }
14595
+ `;
14596
+ function createGpuSource(maskCoverage) {
14597
+ return `
14598
+ struct GlobalFilterUniforms {
14599
+ uInputSize: vec4<f32>,
14600
+ uInputPixel: vec4<f32>,
14601
+ uInputClamp: vec4<f32>,
14602
+ uOutputFrame: vec4<f32>,
14603
+ uGlobalFrame: vec4<f32>,
14604
+ uOutputTexture: vec4<f32>,
14605
+ }
14606
+ @group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;
14607
+ @group(0) @binding(1) var uTexture: texture_2d<f32>;
14608
+ @group(0) @binding(2) var uSampler: sampler;
14609
+ ${maskCoverage
14610
+ ? `struct CoverageAaUniforms { uOutputOffset: vec4<f32>, }
14611
+ @group(1) @binding(0) var<uniform> coverageAaUniforms: CoverageAaUniforms;`
14612
+ : ""}
14613
+
14614
+ struct FilterOutput {
14615
+ @builtin(position) position: vec4<f32>,
14616
+ @location(0) uv: vec2<f32>,
14617
+ }
14618
+
14619
+ @vertex
14620
+ fn mainVertex(@location(0) aPosition: vec2<f32>) -> FilterOutput {
14621
+ var out: FilterOutput;
14622
+ var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;
14623
+ ${maskCoverage
14624
+ ? `let offset = coverageAaUniforms.uOutputOffset;
14625
+ if (offset.w > 0.0) {
14626
+ position -= offset.xy - gfu.uGlobalFrame.xy / offset.z;
14627
+ }`
14628
+ : ""}
14629
+ out.position = vec4<f32>(
14630
+ position.x * (2.0 / gfu.uOutputTexture.x) - 1.0,
14631
+ position.y * (2.0 * gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z,
14632
+ 0.0, 1.0
14633
+ );
14634
+ out.uv = aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw);
14635
+ return out;
14636
+ }
14637
+
14638
+ fn readColor(uv: vec2<f32>) -> vec4<f32> {
14639
+ ${maskCoverage
14640
+ ? "return textureSampleLevel(uTexture, uSampler, clamp(uv, gfu.uInputClamp.xy, gfu.uInputClamp.zw), 0.0);"
14641
+ : `let lower = max(gfu.uInputClamp.xy, -gfu.uOutputFrame.xy * gfu.uInputSize.zw + 0.5 * gfu.uInputPixel.zw);
14642
+ let upper = min(gfu.uInputClamp.zw, (gfu.uOutputTexture.xy - gfu.uOutputFrame.xy) * gfu.uInputSize.zw - 0.5 * gfu.uInputPixel.zw);
14643
+ return textureSampleLevel(uTexture, uSampler, clamp(uv, lower, max(lower, upper)), 0.0);`}
14644
+ }
14645
+
14646
+ fn contrastValue(color: vec4<f32>) -> f32 {
14647
+ return dot(color.rgb, vec3<f32>(0.299, 0.587, 0.114)) + color.a;
14648
+ }
14649
+ ${annotationFxaaWgsl}
14650
+
14651
+ @fragment
14652
+ fn mainFragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
14653
+ return applyAnnotationFxaa(uv);
14654
+ }
14655
+ `;
14656
+ }
14657
+
14658
+ var PixiSceneLayerKind;
14659
+ (function (PixiSceneLayerKind) {
14660
+ PixiSceneLayerKind["Media"] = "media";
14661
+ PixiSceneLayerKind["Depth"] = "depth";
14662
+ PixiSceneLayerKind["Heatmap"] = "heatmap";
14663
+ PixiSceneLayerKind["Mask"] = "mask";
14664
+ PixiSceneLayerKind["Box"] = "box";
14665
+ PixiSceneLayerKind["Vector"] = "vector";
14666
+ PixiSceneLayerKind["Region"] = "region";
14667
+ PixiSceneLayerKind["Focus"] = "focus";
14668
+ PixiSceneLayerKind["Preview"] = "preview";
14669
+ PixiSceneLayerKind["Guide"] = "guide";
14670
+ PixiSceneLayerKind["Handle"] = "handle";
14671
+ PixiSceneLayerKind["Interaction"] = "interaction";
14672
+ PixiSceneLayerKind["Label"] = "label";
14673
+ })(PixiSceneLayerKind || (PixiSceneLayerKind = {}));
14674
+ function createPixiSceneLayerSlot(kind, order = defaultLayerOrder(kind)) {
14675
+ let display;
14676
+ return {
14677
+ getDisplay() {
14678
+ return display;
14679
+ },
14680
+ kind,
14681
+ order,
14682
+ setDisplay(nextDisplay) {
14683
+ display = nextDisplay;
14684
+ },
14685
+ };
14686
+ }
14687
+ function syncPixiSceneLayerChildren(scene, slots) {
14688
+ if (!scene) {
14689
+ return;
14690
+ }
14691
+ const displays = [...slots]
14692
+ .sort((left, right) => left.order - right.order)
14693
+ .map((slot) => slot.getDisplay())
14694
+ .filter((display) => display !== undefined);
14695
+ clearSceneChildren(scene);
14696
+ if (displays.length === 0) {
14697
+ return;
14698
+ }
14699
+ scene.addChild(...displays);
14700
+ }
14701
+ function defaultLayerOrder(kind) {
14702
+ switch (kind) {
14703
+ case PixiSceneLayerKind.Media:
14704
+ return 0;
14705
+ case PixiSceneLayerKind.Focus:
14706
+ return 10;
14707
+ case PixiSceneLayerKind.Depth:
14708
+ return 25;
14709
+ case PixiSceneLayerKind.Heatmap:
14710
+ return 50;
14711
+ case PixiSceneLayerKind.Mask:
14712
+ return 100;
14713
+ case PixiSceneLayerKind.Box:
14714
+ return 200;
14715
+ case PixiSceneLayerKind.Vector:
14716
+ return 250;
14717
+ case PixiSceneLayerKind.Region:
14718
+ return 275;
14719
+ case PixiSceneLayerKind.Preview:
14720
+ return 400;
14721
+ case PixiSceneLayerKind.Guide:
14722
+ return 500;
14723
+ case PixiSceneLayerKind.Handle:
14724
+ return 750;
14725
+ case PixiSceneLayerKind.Interaction:
14726
+ return 700;
14727
+ case PixiSceneLayerKind.Label:
14728
+ return 800;
14729
+ default:
14730
+ return 350;
14731
+ }
14732
+ }
14733
+ function clearSceneChildren(scene) {
14734
+ if (typeof scene.removeChildren === "function") {
14735
+ scene.removeChildren();
14736
+ return;
14737
+ }
14738
+ const fakeScene = scene;
14739
+ if (Array.isArray(fakeScene.children)) {
14740
+ fakeScene.children.splice(0);
14741
+ }
14742
+ }
14743
+
14744
+ /** Keeps media pixels outside AA while retaining the scene's layer order. */
14745
+ function createPixiAnnotationAntialiasLayer(options) {
14746
+ const groups = [];
14747
+ let filter;
14748
+ let captureResolution = 1;
14749
+ return {
14750
+ getFxaaFilter() {
14751
+ return options.getEnabled() ? ensureFilter() : null;
14752
+ },
14753
+ setResolution(resolution) {
14754
+ captureResolution = resolution;
14755
+ if (filter)
14756
+ filter.resolution = resolution;
14757
+ },
14758
+ sync(scene, slots) {
14759
+ if (!scene)
14760
+ return;
14761
+ for (const group of groups) {
14762
+ group.removeChildren();
14763
+ group.filters = null;
14764
+ }
14765
+ if (!options.getEnabled()) {
14766
+ syncPixiSceneLayerChildren(scene, slots);
14767
+ return;
14768
+ }
14769
+ scene.removeChildren();
14770
+ let index = 0;
14771
+ let group;
14772
+ for (const slot of [...slots].sort((left, right) => left.order - right.order)) {
14773
+ const display = slot.getDisplay();
14774
+ if (!display)
14775
+ continue;
14776
+ if (slot.kind === PixiSceneLayerKind.Media ||
14777
+ slot.kind === PixiSceneLayerKind.Region ||
14778
+ slot.kind === PixiSceneLayerKind.Label) {
14779
+ // Regions smooth coverage separately from their copied video.
14780
+ // Labels capture backgrounds separately so text stays sharp.
14781
+ group = undefined;
14782
+ scene.addChild(display);
14783
+ continue;
14784
+ }
14785
+ if (!group) {
14786
+ const sharedFilter = ensureFilter();
14787
+ group = groups[index] ??= new options.Container();
14788
+ index += 1;
14789
+ group.filters = [sharedFilter];
14790
+ scene.addChild(group);
14791
+ }
14792
+ group.addChild(display);
14793
+ }
14794
+ },
14795
+ destroy() {
14796
+ for (const group of groups) {
14797
+ group.filters = null;
14798
+ if (!group.parent)
14799
+ group.destroy({ children: false });
14800
+ }
14801
+ filter?.destroy();
14802
+ },
14803
+ };
14804
+ function ensureFilter() {
14805
+ return (filter ??= createPixiAnnotationAntialiasFilter({
14806
+ ...options,
14807
+ resolution: captureResolution,
14808
+ }));
14809
+ }
14810
+ }
14811
+
13553
14812
  const DEFAULT_PICK_PADDING = 4;
13554
14813
  const POINT_PRECISE_PICK_TARGETS = new Set([
13555
14814
  DetectionPickTarget.Keypoint,
@@ -14119,7 +15378,7 @@ function createPixiMaskScreenStrokeRenderer(options) {
14119
15378
  let strokes = frame.screenStrokes ?? [];
14120
15379
  if (overrides) {
14121
15380
  strokes = overrides.flatMap(({ detectionIndex, stroke }) => {
14122
- if (stroke?.widthUnit !== "screen" ||
15381
+ if (stroke?.widthUnit === undefined ||
14123
15382
  stroke.width <= 0 ||
14124
15383
  stroke.alpha <= 0)
14125
15384
  return [];
@@ -14141,21 +15400,36 @@ function createPixiMaskScreenStrokeRenderer(options) {
14141
15400
  hide();
14142
15401
  return;
14143
15402
  }
14144
- const signature = `${viewportScale}:${strokes.map(({ detectionIndex, width, color, alpha }) => `${detectionIndex}:${width}:${color}:${alpha}`).join(";")}`;
15403
+ const signature = `${viewportScale}:${strokes.map(({ detectionIndex, width, widthUnit, alignment, color, alpha }) => `${detectionIndex}:${width}:${widthUnit}:${alignment}:${color}:${alpha}`).join(";")}`;
14145
15404
  if (lastFrame !== frame || lastSignature !== signature) {
14146
15405
  display.clear();
14147
15406
  const scaleX = options.mediaWidth / frame.width;
14148
15407
  const scaleY = options.mediaHeight / frame.height;
14149
15408
  for (const stroke of strokes) {
14150
- for (const path of stroke.paths) {
14151
- if (path.length < 6)
14152
- continue;
14153
- display.moveTo(path[0] * scaleX, path[1] * scaleY);
14154
- for (let index = 2; index < path.length; index += 2)
14155
- display.lineTo(path[index] * scaleX, path[index + 1] * scaleY);
14156
- display.closePath();
15409
+ for (const { paths, alignment } of resolveContourGroups(stroke)) {
15410
+ for (const path of paths) {
15411
+ if (path.length < 6)
15412
+ continue;
15413
+ display.moveTo(path[0] * scaleX, path[1] * scaleY);
15414
+ for (let index = 2; index < path.length; index += 2)
15415
+ display.lineTo(path[index] * scaleX, path[index + 1] * scaleY);
15416
+ display.closePath();
15417
+ }
15418
+ display.stroke(resolvePixiStroke({
15419
+ ...stroke,
15420
+ alignment,
15421
+ width: stroke.widthUnit === "mask"
15422
+ ? (2 *
15423
+ stroke.width *
15424
+ options.mediaWidth *
15425
+ viewportScale) /
15426
+ (frame.kind === PreparedMaskFrameKind.IdMask
15427
+ ? frame.sourceWidth
15428
+ : frame.width)
15429
+ : stroke.width,
15430
+ join: "round",
15431
+ }, viewportScale));
14157
15432
  }
14158
- display.stroke(resolvePixiStroke({ ...stroke, join: "round" }, viewportScale));
14159
15433
  }
14160
15434
  lastFrame = frame;
14161
15435
  lastSignature = signature;
@@ -14164,22 +15438,56 @@ function createPixiMaskScreenStrokeRenderer(options) {
14164
15438
  },
14165
15439
  };
14166
15440
  }
15441
+ function resolveContourGroups(stroke) {
15442
+ if (stroke.alignment === undefined ||
15443
+ stroke.alignment === StrokeAlignment.Center) {
15444
+ return [{ paths: stroke.paths, alignment: stroke.alignment }];
15445
+ }
15446
+ const outer = [];
15447
+ const holes = [];
15448
+ // Pixi aligns each contour to its enclosed area. Hole alignment must reverse
15449
+ // so Inside and Outside remain relative to the mask's foreground pixels.
15450
+ for (const path of stroke.paths) {
15451
+ let area = 0;
15452
+ for (let index = 0, previous = path.length - 2; index < path.length; index += 2) {
15453
+ area +=
15454
+ path[previous] * path[index + 1] - path[index] * path[previous + 1];
15455
+ previous = index;
15456
+ }
15457
+ (area < 0 ? holes : outer).push(path);
15458
+ }
15459
+ return [
15460
+ { paths: outer, alignment: stroke.alignment },
15461
+ {
15462
+ paths: holes,
15463
+ alignment: stroke.alignment === StrokeAlignment.Outside
15464
+ ? StrokeAlignment.Inside
15465
+ : StrokeAlignment.Outside,
15466
+ },
15467
+ ].filter(({ paths }) => paths.length > 0);
15468
+ }
14167
15469
 
14168
15470
  const coverageBody = `
14169
- return clamp(0.5 + (width - distance) / pixelWidth, 0.0, 1.0)
14170
- - clamp(0.5 - (width + distance) / pixelWidth, 0.0, 1.0);
15471
+ return clamp(0.5 + (width * (1.0 - alignment) - distance) / pixelWidth, 0.0, 1.0)
15472
+ - clamp(0.5 - (width * alignment + distance) / pixelWidth, 0.0, 1.0);
14171
15473
  `;
14172
15474
  const distanceBody = `
14173
15475
  return max(max(offset - position, position - offset - 1.0), 0.0);
14174
15476
  `;
14175
15477
  const radiusBody = `
14176
- if (fractionalWidth > 0.0) {
14177
- return min(max(floor(maxWidth), ceil(fractionalWidth + pixelWidth * 0.5)), ${MAX_ID_MASK_STROKE_WIDTH}.0);
15478
+ if (maxWidth < 0.0) {
15479
+ return 0.0;
14178
15480
  }
14179
- return min(floor(maxWidth), ${MAX_ID_MASK_STROKE_WIDTH}.0);
15481
+ return min(ceil(maxWidth + pixelWidth * 0.5), ${MAX_ID_MASK_STROKE_WIDTH + 1}.0);
14180
15482
  `;
14181
15483
  const idMaskStrokeCoverageGlsl = `
14182
- float subtexelStrokeCoverage(float distance, float width, float pixelWidth) {
15484
+ float sampleMaskIdCell(ivec2 cell) {
15485
+ int x = clamp(cell.x, 0, int(uTextureSize.x) - 1);
15486
+ int y = clamp(cell.y, 0, int(uTextureSize.y) - 1);
15487
+ return floor(texelFetch(uTexture, ivec2(x, y), 0).r * 255.0 + 0.5);
15488
+ }
15489
+
15490
+ float subtexelStrokeCoverage(float distance, float width, float alignment, float pixelWidth) {
14183
15491
  ${coverageBody}
14184
15492
  }
14185
15493
 
@@ -14187,42 +15495,55 @@ float neighborCellDistance(float offset, float position) {
14187
15495
  ${distanceBody}
14188
15496
  }
14189
15497
 
14190
- float strokeScanRadius(float maxWidth, float fractionalWidth, float pixelWidth) {
15498
+ float strokeScanRadius(float maxWidth, float pixelWidth) {
14191
15499
  ${radiusBody}
14192
15500
  }
14193
15501
 
14194
- bool isBoundary(float centerId, vec2 texel) {
14195
- return
14196
- differs(sampleMaskId(vUV + vec2(texel.x, 0.0)), centerId) ||
14197
- differs(sampleMaskId(vUV + vec2(-texel.x, 0.0)), centerId) ||
14198
- differs(sampleMaskId(vUV + vec2(0.0, texel.y)), centerId) ||
14199
- differs(sampleMaskId(vUV + vec2(0.0, -texel.y)), centerId);
15502
+ float strokeWidthInTexels(float maskId, float pixelWidth) {
15503
+ float alignment = readStrokeAlignment(maskId);
15504
+ float sideFraction = max(alignment, 1.0 - alignment);
15505
+ return min(readStrokeWidth(maskId) * pixelWidth * uStrokePixelRatio, ${MAX_ID_MASK_STROKE_WIDTH}.0 / sideFraction);
14200
15506
  }
14201
15507
 
14202
- float innerStrokeCoverage(float centerId, vec2 texel, vec2 cell, float width, float pixelWidth) {
14203
- if (width >= 1.0) {
14204
- return isBoundary(centerId, texel) ? 1.0 : 0.0;
14205
- }
15508
+ float innerStrokeCoverage(float centerId, ivec2 sourceCell, vec2 cell, float width, float pixelWidth) {
15509
+ if (width <= 0.0) return 0.0;
15510
+ float alignment = readStrokeAlignment(centerId);
15511
+ int radius = int(strokeScanRadius(width * alignment, pixelWidth));
15512
+ float nearestDistance = width * alignment + pixelWidth * 0.5;
15513
+ float minimumDistance = min(min(cell.x, 1.0 - cell.x), min(cell.y, 1.0 - cell.y));
14206
15514
 
14207
- float coverage = 0.0;
14208
- if (differs(sampleMaskId(vUV + vec2(texel.x, 0.0)), centerId)) {
14209
- coverage = max(coverage, subtexelStrokeCoverage(1.0 - cell.x, width, pixelWidth));
14210
- }
14211
- if (differs(sampleMaskId(vUV + vec2(-texel.x, 0.0)), centerId)) {
14212
- coverage = max(coverage, subtexelStrokeCoverage(cell.x, width, pixelWidth));
14213
- }
14214
- if (differs(sampleMaskId(vUV + vec2(0.0, texel.y)), centerId)) {
14215
- coverage = max(coverage, subtexelStrokeCoverage(1.0 - cell.y, width, pixelWidth));
14216
- }
14217
- if (differs(sampleMaskId(vUV + vec2(0.0, -texel.y)), centerId)) {
14218
- coverage = max(coverage, subtexelStrokeCoverage(cell.y, width, pixelWidth));
15515
+ for (int offsetY = radius; offsetY >= -radius; offsetY -= 1) {
15516
+ for (int offsetX = radius; offsetX >= -radius; offsetX -= 1) {
15517
+ if (offsetX == 0 && offsetY == 0) {
15518
+ continue;
15519
+ }
15520
+ float distance = max(
15521
+ neighborCellDistance(float(offsetX), cell.x),
15522
+ neighborCellDistance(float(offsetY), cell.y)
15523
+ );
15524
+ if (distance >= nearestDistance) {
15525
+ continue;
15526
+ }
15527
+ float maskId = sampleMaskIdCell(sourceCell + ivec2(offsetX, offsetY));
15528
+ if (differs(maskId, centerId)) {
15529
+ nearestDistance = distance;
15530
+ if (distance <= minimumDistance) {
15531
+ return subtexelStrokeCoverage(-distance, width, alignment, pixelWidth);
15532
+ }
15533
+ }
15534
+ }
14219
15535
  }
14220
- return coverage;
15536
+ return subtexelStrokeCoverage(-nearestDistance, width, alignment, pixelWidth);
14221
15537
  }
14222
15538
 
14223
15539
  // Descending offsets give the outer border its palette priority.
14224
- vec2 findNeighborStroke(float centerId, vec2 texel, vec2 cell, float pixelWidth) {
14225
- int radius = int(strokeScanRadius(uMaxStrokeWidth, uMaxFractionalStrokeWidth, pixelWidth));
15540
+ vec2 findNeighborStroke(float centerId, ivec2 sourceCell, vec2 cell, float pixelWidth) {
15541
+ float maxWidth = min(uMaxStrokeWidth * pixelWidth * uStrokePixelRatio, ${MAX_ID_MASK_STROKE_WIDTH}.0);
15542
+ int radius = int(strokeScanRadius(maxWidth, pixelWidth));
15543
+ vec2 stroke = vec2(0.0);
15544
+ float nearestDistance = maxWidth + pixelWidth * 0.5;
15545
+ float minimumDistance = min(min(cell.x, 1.0 - cell.x), min(cell.y, 1.0 - cell.y));
15546
+ if (uBorderEnabled <= 0.5) return stroke;
14226
15547
 
14227
15548
  for (int offsetY = radius; offsetY >= -radius; offsetY -= 1) {
14228
15549
  for (int offsetX = radius; offsetX >= -radius; offsetX -= 1) {
@@ -14230,35 +15551,44 @@ vec2 findNeighborStroke(float centerId, vec2 texel, vec2 cell, float pixelWidth)
14230
15551
  continue;
14231
15552
  }
14232
15553
 
14233
- float maskId = sampleMaskId(vUV + vec2(float(offsetX), float(offsetY)) * texel);
14234
- if (maskId < 0.5 || !differs(maskId, centerId)) {
15554
+ float distance = max(
15555
+ neighborCellDistance(float(offsetX), cell.x),
15556
+ neighborCellDistance(float(offsetY), cell.y)
15557
+ );
15558
+ if (distance >= nearestDistance) {
15559
+ continue;
15560
+ }
15561
+ float maskId = sampleMaskIdCell(sourceCell + ivec2(offsetX, offsetY));
15562
+ if (maskId < 0.5 || !differs(maskId, centerId) ||
15563
+ (stroke.x > 0.5 && differs(maskId, stroke.x))) {
14235
15564
  continue;
14236
15565
  }
14237
15566
 
14238
- float width = readStrokeWidth(maskId);
14239
- if (width >= 1.0) {
14240
- float distance = max(abs(float(offsetX)), abs(float(offsetY)));
14241
- if (width >= distance && readStroke(maskId).a > 0.0) {
14242
- return vec2(maskId, 1.0);
14243
- }
14244
- } else if (width > 0.0 && readStroke(maskId).a > 0.0) {
14245
- float distance = max(
14246
- neighborCellDistance(float(offsetX), cell.x),
14247
- neighborCellDistance(float(offsetY), cell.y)
14248
- );
14249
- float coverage = subtexelStrokeCoverage(distance, width, pixelWidth);
15567
+ float width = strokeWidthInTexels(maskId, pixelWidth);
15568
+ if (width > 0.0 && readStroke(maskId).a > 0.0) {
15569
+ float coverage = subtexelStrokeCoverage(distance, width, readStrokeAlignment(maskId), pixelWidth);
14250
15570
  if (coverage > 0.0) {
14251
- return vec2(maskId, coverage);
15571
+ stroke = vec2(maskId, coverage);
15572
+ nearestDistance = distance;
15573
+ if (distance <= minimumDistance) {
15574
+ return stroke;
15575
+ }
14252
15576
  }
14253
15577
  }
14254
15578
  }
14255
15579
  }
14256
15580
 
14257
- return vec2(0.0);
15581
+ return stroke;
14258
15582
  }
14259
15583
  `;
14260
15584
  const idMaskStrokeCoverageWgsl = `
14261
- fn subtexelStrokeCoverage(distance: f32, width: f32, pixelWidth: f32) -> f32 {
15585
+ fn sampleMaskIdCell(cell: vec2<i32>) -> f32 {
15586
+ let x = clamp(cell.x, 0, i32(maskUniforms.uTextureSize.x) - 1);
15587
+ let y = clamp(cell.y, 0, i32(maskUniforms.uTextureSize.y) - 1);
15588
+ return floor(textureLoad(uTexture, vec2<i32>(x, y), 0).r * 255.0 + 0.5);
15589
+ }
15590
+
15591
+ fn subtexelStrokeCoverage(distance: f32, width: f32, alignment: f32, pixelWidth: f32) -> f32 {
14262
15592
  ${coverageBody}
14263
15593
  }
14264
15594
 
@@ -14266,45 +15596,55 @@ fn neighborCellDistance(offset: f32, position: f32) -> f32 {
14266
15596
  ${distanceBody}
14267
15597
  }
14268
15598
 
14269
- fn strokeScanRadius(maxWidth: f32, fractionalWidth: f32, pixelWidth: f32) -> f32 {
15599
+ fn strokeScanRadius(maxWidth: f32, pixelWidth: f32) -> f32 {
14270
15600
  ${radiusBody}
14271
15601
  }
14272
15602
 
14273
- fn isBoundary(uv: vec2<f32>, centerId: f32, texel: vec2<f32>) -> bool {
14274
- return
14275
- differs(sampleMaskId(uv + vec2<f32>(texel.x, 0.0)), centerId) ||
14276
- differs(sampleMaskId(uv + vec2<f32>(-texel.x, 0.0)), centerId) ||
14277
- differs(sampleMaskId(uv + vec2<f32>(0.0, texel.y)), centerId) ||
14278
- differs(sampleMaskId(uv + vec2<f32>(0.0, -texel.y)), centerId);
15603
+ fn strokeWidthInTexels(maskId: f32, pixelWidth: f32) -> f32 {
15604
+ let alignment = readStrokeAlignment(maskId);
15605
+ let sideFraction = max(alignment, 1.0 - alignment);
15606
+ return min(readStrokeWidth(maskId) * pixelWidth * maskUniforms.uStrokePixelRatio, ${MAX_ID_MASK_STROKE_WIDTH}.0 / sideFraction);
14279
15607
  }
14280
15608
 
14281
- fn innerStrokeCoverage(uv: vec2<f32>, centerId: f32, texel: vec2<f32>, cell: vec2<f32>, width: f32, pixelWidth: f32) -> f32 {
14282
- if (width >= 1.0) {
14283
- if (isBoundary(uv, centerId, texel)) {
14284
- return 1.0;
14285
- }
14286
- return 0.0;
14287
- }
15609
+ fn innerStrokeCoverage(centerId: f32, sourceCell: vec2<i32>, cell: vec2<f32>, width: f32, pixelWidth: f32) -> f32 {
15610
+ if (width <= 0.0) { return 0.0; }
15611
+ let alignment = readStrokeAlignment(centerId);
15612
+ let radius = i32(strokeScanRadius(width * alignment, pixelWidth));
15613
+ var nearestDistance = width * alignment + pixelWidth * 0.5;
15614
+ let minimumDistance = min(min(cell.x, 1.0 - cell.x), min(cell.y, 1.0 - cell.y));
14288
15615
 
14289
- var coverage = 0.0;
14290
- if (differs(sampleMaskId(uv + vec2<f32>(texel.x, 0.0)), centerId)) {
14291
- coverage = max(coverage, subtexelStrokeCoverage(1.0 - cell.x, width, pixelWidth));
14292
- }
14293
- if (differs(sampleMaskId(uv + vec2<f32>(-texel.x, 0.0)), centerId)) {
14294
- coverage = max(coverage, subtexelStrokeCoverage(cell.x, width, pixelWidth));
14295
- }
14296
- if (differs(sampleMaskId(uv + vec2<f32>(0.0, texel.y)), centerId)) {
14297
- coverage = max(coverage, subtexelStrokeCoverage(1.0 - cell.y, width, pixelWidth));
14298
- }
14299
- if (differs(sampleMaskId(uv + vec2<f32>(0.0, -texel.y)), centerId)) {
14300
- coverage = max(coverage, subtexelStrokeCoverage(cell.y, width, pixelWidth));
15616
+ for (var offsetY = radius; offsetY >= -radius; offsetY -= 1) {
15617
+ for (var offsetX = radius; offsetX >= -radius; offsetX -= 1) {
15618
+ if (offsetX == 0 && offsetY == 0) {
15619
+ continue;
15620
+ }
15621
+ let distance = max(
15622
+ neighborCellDistance(f32(offsetX), cell.x),
15623
+ neighborCellDistance(f32(offsetY), cell.y)
15624
+ );
15625
+ if (distance >= nearestDistance) {
15626
+ continue;
15627
+ }
15628
+ let maskId = sampleMaskIdCell(sourceCell + vec2<i32>(offsetX, offsetY));
15629
+ if (differs(maskId, centerId)) {
15630
+ nearestDistance = distance;
15631
+ if (distance <= minimumDistance) {
15632
+ return subtexelStrokeCoverage(-distance, width, alignment, pixelWidth);
15633
+ }
15634
+ }
15635
+ }
14301
15636
  }
14302
- return coverage;
15637
+ return subtexelStrokeCoverage(-nearestDistance, width, alignment, pixelWidth);
14303
15638
  }
14304
15639
 
14305
15640
  // Descending offsets give the outer border its palette priority.
14306
- fn findNeighborStroke(uv: vec2<f32>, centerId: f32, texel: vec2<f32>, cell: vec2<f32>, pixelWidth: f32) -> vec2<f32> {
14307
- let radius = i32(strokeScanRadius(maskUniforms.uMaxStrokeWidth, maskUniforms.uMaxFractionalStrokeWidth, pixelWidth));
15641
+ fn findNeighborStroke(centerId: f32, sourceCell: vec2<i32>, cell: vec2<f32>, pixelWidth: f32) -> vec2<f32> {
15642
+ let maxWidth = min(maskUniforms.uMaxStrokeWidth * pixelWidth * maskUniforms.uStrokePixelRatio, ${MAX_ID_MASK_STROKE_WIDTH}.0);
15643
+ let radius = i32(strokeScanRadius(maxWidth, pixelWidth));
15644
+ var stroke = vec2<f32>(0.0);
15645
+ var nearestDistance = maxWidth + pixelWidth * 0.5;
15646
+ let minimumDistance = min(min(cell.x, 1.0 - cell.x), min(cell.y, 1.0 - cell.y));
15647
+ if (maskUniforms.uBorderEnabled <= 0.5) { return stroke; }
14308
15648
 
14309
15649
  for (var offsetY = radius; offsetY >= -radius; offsetY -= 1) {
14310
15650
  for (var offsetX = radius; offsetX >= -radius; offsetX -= 1) {
@@ -14312,45 +15652,44 @@ fn findNeighborStroke(uv: vec2<f32>, centerId: f32, texel: vec2<f32>, cell: vec2
14312
15652
  continue;
14313
15653
  }
14314
15654
 
14315
- let maskId = sampleMaskId(uv + vec2<f32>(f32(offsetX), f32(offsetY)) * texel);
14316
- if (maskId < 0.5 || !differs(maskId, centerId)) {
15655
+ let distance = max(
15656
+ neighborCellDistance(f32(offsetX), cell.x),
15657
+ neighborCellDistance(f32(offsetY), cell.y)
15658
+ );
15659
+ if (distance >= nearestDistance) {
15660
+ continue;
15661
+ }
15662
+ let maskId = sampleMaskIdCell(sourceCell + vec2<i32>(offsetX, offsetY));
15663
+ if (maskId < 0.5 || !differs(maskId, centerId) ||
15664
+ (stroke.x > 0.5 && differs(maskId, stroke.x))) {
14317
15665
  continue;
14318
15666
  }
14319
15667
 
14320
- let width = readStrokeWidth(maskId);
14321
- if (width >= 1.0) {
14322
- let distance = max(abs(f32(offsetX)), abs(f32(offsetY)));
14323
- if (width >= distance && readStroke(maskId).a > 0.0) {
14324
- return vec2<f32>(maskId, 1.0);
14325
- }
14326
- } else if (width > 0.0 && readStroke(maskId).a > 0.0) {
14327
- let distance = max(
14328
- neighborCellDistance(f32(offsetX), cell.x),
14329
- neighborCellDistance(f32(offsetY), cell.y)
14330
- );
14331
- let coverage = subtexelStrokeCoverage(distance, width, pixelWidth);
15668
+ let width = strokeWidthInTexels(maskId, pixelWidth);
15669
+ if (width > 0.0 && readStroke(maskId).a > 0.0) {
15670
+ let coverage = subtexelStrokeCoverage(distance, width, readStrokeAlignment(maskId), pixelWidth);
14332
15671
  if (coverage > 0.0) {
14333
- return vec2<f32>(maskId, coverage);
15672
+ stroke = vec2<f32>(maskId, coverage);
15673
+ nearestDistance = distance;
15674
+ if (distance <= minimumDistance) {
15675
+ return stroke;
15676
+ }
14334
15677
  }
14335
15678
  }
14336
15679
  }
14337
15680
  }
14338
15681
 
14339
- return vec2<f32>(0.0);
15682
+ return stroke;
14340
15683
  }
14341
15684
  `;
14342
15685
 
14343
- /**
14344
- * GLSL charges a uniform array of scalars one uniform vector per element, the
14345
- * same as an array of vec4, so the stroke widths ride four to a vector and an
14346
- * entry costs 2.25 vectors instead of 3. The lanes only line up while the entry
14347
- * count stays a multiple of four.
14348
- */
15686
+ /** Packs width and alignment scalars within the fragment uniform budget. */
14349
15687
  const ID_MASK_STROKE_WIDTH_LANES = MAX_ID_MASK_PALETTE_ENTRIES / 4;
14350
15688
  const idMaskPaletteGlsl = `
14351
15689
  uniform vec4 uFillPalette[${MAX_ID_MASK_PALETTE_ENTRIES}];
14352
15690
  uniform vec4 uStrokePalette[${MAX_ID_MASK_PALETTE_ENTRIES}];
14353
15691
  uniform vec4 uStrokeWidths[${ID_MASK_STROKE_WIDTH_LANES}];
15692
+ uniform vec4 uStrokeAlignments[${ID_MASK_STROKE_WIDTH_LANES}];
14354
15693
 
14355
15694
  int paletteIndex(float maskId) {
14356
15695
  return int(clamp(maskId, 0.0, float(${MAX_ID_MASK_PALETTE_ENTRIES - 1})));
@@ -14369,10 +15708,16 @@ float readStrokeWidth(float maskId) {
14369
15708
 
14370
15709
  return uStrokeWidths[index / 4][index % 4];
14371
15710
  }
15711
+
15712
+ float readStrokeAlignment(float maskId) {
15713
+ int index = paletteIndex(maskId);
15714
+ return uStrokeAlignments[index / 4][index % 4];
15715
+ }
14372
15716
  `;
14373
15717
  const idMaskFillPaletteWgslField = `uFillPalette: array<vec4<f32>, ${MAX_ID_MASK_PALETTE_ENTRIES}>,`;
14374
15718
  const idMaskStrokePaletteWgslField = `uStrokePalette: array<vec4<f32>, ${MAX_ID_MASK_PALETTE_ENTRIES}>,`;
14375
15719
  const idMaskStrokeWidthsWgslField = `uStrokeWidths: array<vec4<f32>, ${ID_MASK_STROKE_WIDTH_LANES}>,`;
15720
+ const idMaskStrokeAlignmentsWgslField = `uStrokeAlignments: array<vec4<f32>, ${ID_MASK_STROKE_WIDTH_LANES}>,`;
14376
15721
  /** Reads the uniform group a fragment stage must bind under the name maskUniforms. */
14377
15722
  const idMaskPaletteWgsl = `
14378
15723
  fn paletteIndex(maskId: f32) -> i32 {
@@ -14392,20 +15737,31 @@ fn readStrokeWidth(maskId: f32) -> f32 {
14392
15737
 
14393
15738
  return maskUniforms.uStrokeWidths[index / 4][index % 4];
14394
15739
  }
15740
+
15741
+ fn readStrokeAlignment(maskId: f32) -> f32 {
15742
+ let index = paletteIndex(maskId);
15743
+ return maskUniforms.uStrokeAlignments[index / 4][index % 4];
15744
+ }
14395
15745
  `;
14396
15746
 
14397
- function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, labelStyle, resolveContextState, }) {
15747
+ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, labelStyle, getRenderedBoxes, resolveContextState, }) {
14398
15748
  const entries = [];
14399
15749
  let container;
14400
15750
  let currentLabelStyle = labelStyle;
14401
15751
  // A versioned source can replace a frame without changing its media time or
14402
15752
  // frame index. Buffered frames are immutable snapshots, so retain by object.
14403
15753
  let lastFrame;
15754
+ let lastBoxFrame;
14404
15755
  let styleVersion = 0;
14405
15756
  let drawnStyleVersion = -1;
14406
15757
  let lastViewportScale = 0;
14407
15758
  let hitRects = [];
14408
15759
  let previewEntry;
15760
+ let previewIndex = 0;
15761
+ let backgroundFilter = null;
15762
+ let backgroundFilters = null;
15763
+ let backgroundsGrouped = false;
15764
+ const backgroundGroups = [];
14409
15765
  const entriesByDetectionKey = new Map();
14410
15766
  const boundsByDetectionKey = new Map();
14411
15767
  const clearLayout = () => {
@@ -14417,6 +15773,7 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14417
15773
  for (let index = startIndex; index < entries.length; index += 1) {
14418
15774
  entries[index].background.visible = false;
14419
15775
  entries[index].label.visible = false;
15776
+ entries[index].instruction = undefined;
14420
15777
  }
14421
15778
  };
14422
15779
  const createEntry = () => {
@@ -14433,6 +15790,9 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14433
15790
  labelBaseY: 0,
14434
15791
  text: null,
14435
15792
  textStyleKey: null,
15793
+ textHeight: 0,
15794
+ textWidth: 0,
15795
+ instruction: undefined,
14436
15796
  };
14437
15797
  container?.addChild(entry.background, entry.label);
14438
15798
  return entry;
@@ -14450,8 +15810,69 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14450
15810
  return;
14451
15811
  previewEntry.background.visible = false;
14452
15812
  previewEntry.label.visible = false;
15813
+ previewEntry.instruction = undefined;
15814
+ };
15815
+ const syncBackgroundCaptures = () => {
15816
+ if (!container || (!backgroundFilter && !backgroundsGrouped))
15817
+ return;
15818
+ for (const group of backgroundGroups) {
15819
+ group.removeChildren();
15820
+ group.filters = null;
15821
+ }
15822
+ container.removeChildren();
15823
+ backgroundsGrouped = backgroundFilter !== null;
15824
+ const orderedEntries = previewEntry
15825
+ ? [
15826
+ ...entries.slice(0, previewIndex),
15827
+ previewEntry,
15828
+ ...entries.slice(previewIndex),
15829
+ ]
15830
+ : entries;
15831
+ if (!backgroundFilter) {
15832
+ for (const entry of orderedEntries) {
15833
+ container.addChild(entry.background, entry.label);
15834
+ }
15835
+ return;
15836
+ }
15837
+ let groupIndex = 0;
15838
+ const run = [];
15839
+ let runBounds;
15840
+ const padding = backgroundFilter.padding / Math.max(lastViewportScale, 1e-6);
15841
+ const flushRun = () => {
15842
+ if (run.length === 0)
15843
+ return;
15844
+ if (run.some((entry) => entry.background.visible)) {
15845
+ const group = (backgroundGroups[groupIndex] ??= new Container());
15846
+ groupIndex += 1;
15847
+ group.label = "label-background-aa";
15848
+ group.filters = backgroundFilters;
15849
+ container.addChild(group);
15850
+ for (const entry of run)
15851
+ group.addChild(entry.background);
15852
+ }
15853
+ else {
15854
+ for (const entry of run)
15855
+ container.addChild(entry.background);
15856
+ }
15857
+ for (const entry of run)
15858
+ container.addChild(entry.label);
15859
+ run.length = 0;
15860
+ runBounds = undefined;
15861
+ };
15862
+ for (const entry of orderedEntries) {
15863
+ if (entry.background.visible || entry.label.visible) {
15864
+ const padded = resolveCaptureBounds(entry, padding);
15865
+ // Reordering a disjoint run leaves translucent overlapping chips and
15866
+ // their text in the same paint order as the original label pairs.
15867
+ if (runBounds && rectanglesOverlap(runBounds, padded))
15868
+ flushRun();
15869
+ runBounds = runBounds ? unionCaptureBounds(runBounds, padded) : padded;
15870
+ }
15871
+ run.push(entry);
15872
+ }
15873
+ flushRun();
14453
15874
  };
14454
- const redrawFrame = (frame, mediaTime, viewportScale) => {
15875
+ const redrawFrame = (frame, mediaTime, viewportScale, renderedBoxes) => {
14455
15876
  let drawnCount = 0;
14456
15877
  clearLayout();
14457
15878
  if (!currentLabelStyle) {
@@ -14474,7 +15895,7 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14474
15895
  continue;
14475
15896
  }
14476
15897
  const entry = ensureEntry(drawnCount);
14477
- const hitRect = drawInstruction(entry, instruction, viewportScale);
15898
+ const hitRect = drawInstruction(entry, instruction, viewportScale, renderedBoxes?.get(detectionIndex));
14478
15899
  const key = detectionKey$1(detection, detectionIndex);
14479
15900
  entriesByDetectionKey.set(key, entry);
14480
15901
  boundsByDetectionKey.set(key, {
@@ -14497,24 +15918,31 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14497
15918
  drawFrame(mediaTime, viewportScale) {
14498
15919
  const resolvedViewportScale = viewportScale ?? 1;
14499
15920
  const frame = detectionTimeline.selectFrame(mediaTime);
15921
+ const boxFrame = getRenderedBoxes?.();
15922
+ const matchingBoxFrame = boxFrame?.frame === frame ? boxFrame : undefined;
14500
15923
  if (frame === lastFrame &&
15924
+ matchingBoxFrame === lastBoxFrame &&
14501
15925
  drawnStyleVersion === styleVersion &&
14502
15926
  resolvedViewportScale === lastViewportScale) {
14503
15927
  return;
14504
15928
  }
14505
15929
  lastFrame = frame;
15930
+ lastBoxFrame = matchingBoxFrame;
14506
15931
  drawnStyleVersion = styleVersion;
14507
15932
  lastViewportScale = resolvedViewportScale;
14508
15933
  if (!frame) {
14509
15934
  clearLayout();
14510
15935
  hideEntriesFrom(0);
15936
+ syncBackgroundCaptures();
14511
15937
  return;
14512
15938
  }
14513
- redrawFrame(frame, mediaTime, resolvedViewportScale);
15939
+ redrawFrame(frame, mediaTime, resolvedViewportScale, matchingBoxFrame?.rects);
15940
+ syncBackgroundCaptures();
14514
15941
  },
14515
- drawCreationPreview(detection, mediaTime, viewportScale) {
15942
+ drawCreationPreview(detection, mediaTime, viewportScale, renderedBox) {
14516
15943
  if (!detection || !currentLabelStyle) {
14517
15944
  hideCreationPreview();
15945
+ syncBackgroundCaptures();
14518
15946
  return;
14519
15947
  }
14520
15948
  const resolvedViewportScale = viewportScale ?? 1;
@@ -14530,15 +15958,39 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14530
15958
  });
14531
15959
  if (!instruction) {
14532
15960
  hideCreationPreview();
15961
+ syncBackgroundCaptures();
14533
15962
  return;
14534
15963
  }
14535
- previewEntry ??= createEntry();
14536
- drawInstruction(previewEntry, instruction, resolvedViewportScale);
15964
+ if (!previewEntry) {
15965
+ previewIndex = entries.length;
15966
+ previewEntry = createEntry();
15967
+ }
15968
+ drawInstruction(previewEntry, instruction, resolvedViewportScale, renderedBox);
15969
+ syncBackgroundCaptures();
15970
+ },
15971
+ updateEditingBox(id, renderedBox) {
15972
+ const key = `id:${String(id)}`;
15973
+ const entry = entriesByDetectionKey.get(key);
15974
+ const bounds = boundsByDetectionKey.get(key);
15975
+ const instruction = entry?.instruction;
15976
+ if (!entry || !bounds || !instruction?.background)
15977
+ return;
15978
+ const corners = resolveRoundedCorners(instruction.background, instruction.placement ?? LabelPlacement.Top, bounds, renderedBox, lastViewportScale);
15979
+ drawBackground(entry, instruction.background, bounds.baseX, bounds.baseY, bounds.width, bounds.height, lastViewportScale, corners);
15980
+ entry.background.x = bounds.x;
15981
+ entry.background.y = bounds.y;
14537
15982
  },
14538
15983
  setLabelStyle(nextLabelStyle) {
14539
15984
  currentLabelStyle = nextLabelStyle ?? undefined;
14540
15985
  styleVersion += 1;
14541
15986
  },
15987
+ setBackgroundAntialiasFilter(filter) {
15988
+ if (filter === backgroundFilter)
15989
+ return;
15990
+ backgroundFilter = filter;
15991
+ backgroundFilters = filter ? [filter] : null;
15992
+ syncBackgroundCaptures();
15993
+ },
14542
15994
  translateDetection(id, x, y) {
14543
15995
  const key = `id:${String(id)}`;
14544
15996
  const entry = entriesByDetectionKey.get(key);
@@ -14556,6 +16008,7 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14556
16008
  y: bounds.baseY + y,
14557
16009
  });
14558
16010
  }
16011
+ syncBackgroundCaptures();
14559
16012
  return true;
14560
16013
  },
14561
16014
  getDetectionLabelBounds(id) {
@@ -14592,6 +16045,13 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14592
16045
  : null;
14593
16046
  },
14594
16047
  destroy() {
16048
+ for (const group of backgroundGroups) {
16049
+ group.filters = null;
16050
+ if (!group.parent)
16051
+ group.destroy({ children: false });
16052
+ }
16053
+ backgroundFilter = null;
16054
+ backgroundFilters = null;
14595
16055
  hideEntriesFrom(0);
14596
16056
  hideCreationPreview();
14597
16057
  entries.length = 0;
@@ -14601,7 +16061,8 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14601
16061
  },
14602
16062
  };
14603
16063
  }
14604
- function drawInstruction(entry, instruction, viewportScale) {
16064
+ function drawInstruction(entry, instruction, viewportScale, renderedBox) {
16065
+ entry.instruction = instruction;
14605
16066
  const textStyle = resolveTextStyle(instruction.textStyle, viewportScale);
14606
16067
  const textStyleKey = createTextStyleKey(textStyle);
14607
16068
  const textAlpha = instruction.textStyle?.alpha ?? 1;
@@ -14621,8 +16082,10 @@ function drawInstruction(entry, instruction, viewportScale) {
14621
16082
  const background = instruction.background;
14622
16083
  const paddingX = (background?.paddingX ?? 0) / viewportScale;
14623
16084
  const paddingY = (background?.paddingY ?? 0) / viewportScale;
14624
- const width = entry.label.width + paddingX * 2;
14625
- const height = entry.label.height + paddingY * 2;
16085
+ entry.textWidth = entry.label.width;
16086
+ entry.textHeight = entry.label.height;
16087
+ const width = entry.textWidth + paddingX * 2;
16088
+ const height = entry.textHeight + paddingY * 2;
14626
16089
  const { x, y } = resolveLabelPosition(instruction, width, height);
14627
16090
  entry.label.x = x + paddingX;
14628
16091
  entry.label.y = y + paddingY;
@@ -14633,9 +16096,41 @@ function drawInstruction(entry, instruction, viewportScale) {
14633
16096
  entry.backgroundKey = null;
14634
16097
  return { height, width, x, y };
14635
16098
  }
14636
- drawBackground(entry, background, x, y, width, height, viewportScale);
16099
+ const corners = resolveRoundedCorners(background, instruction.placement ?? LabelPlacement.Top, { x, y, width, height }, renderedBox, viewportScale);
16100
+ drawBackground(entry, background, x, y, width, height, viewportScale, corners);
14637
16101
  return { height, width, x, y };
14638
16102
  }
16103
+ function resolveCaptureBounds(entry, padding) {
16104
+ const { background, label } = entry;
16105
+ const x = Math.min(background.visible ? background.x : Infinity, label.visible ? label.x : Infinity);
16106
+ const y = Math.min(background.visible ? background.y : Infinity, label.visible ? label.y : Infinity);
16107
+ return {
16108
+ height: Math.max(background.visible ? background.y + entry.backgroundHeight : -Infinity, label.visible ? label.y + entry.textHeight : -Infinity) -
16109
+ y +
16110
+ padding * 2,
16111
+ width: Math.max(background.visible ? background.x + entry.backgroundWidth : -Infinity, label.visible ? label.x + entry.textWidth : -Infinity) -
16112
+ x +
16113
+ padding * 2,
16114
+ x: x - padding,
16115
+ y: y - padding,
16116
+ };
16117
+ }
16118
+ function rectanglesOverlap(left, right) {
16119
+ return (left.x < right.x + right.width &&
16120
+ right.x < left.x + left.width &&
16121
+ left.y < right.y + right.height &&
16122
+ right.y < left.y + left.height);
16123
+ }
16124
+ function unionCaptureBounds(left, right) {
16125
+ const x = Math.min(left.x, right.x);
16126
+ const y = Math.min(left.y, right.y);
16127
+ return {
16128
+ height: Math.max(left.y + left.height, right.y + right.height) - y,
16129
+ width: Math.max(left.x + left.width, right.x + right.width) - x,
16130
+ x,
16131
+ y,
16132
+ };
16133
+ }
14639
16134
  function resolveLabelPosition(instruction, width, height) {
14640
16135
  const { rect } = instruction;
14641
16136
  const offsetX = instruction.offsetX ?? 0;
@@ -14669,9 +16164,9 @@ function resolveLabelPosition(instruction, width, height) {
14669
16164
  };
14670
16165
  }
14671
16166
  }
14672
- function drawBackground(entry, background, x, y, width, height, viewportScale) {
16167
+ function drawBackground(entry, background, x, y, width, height, viewportScale, corners) {
14673
16168
  const graphics = entry.background;
14674
- const backgroundKey = createBackgroundKey(background);
16169
+ const backgroundKey = createBackgroundKey(background, corners);
14675
16170
  graphics.visible = true;
14676
16171
  graphics.x = x;
14677
16172
  graphics.y = y;
@@ -14686,8 +16181,8 @@ function drawBackground(entry, background, x, y, width, height, viewportScale) {
14686
16181
  entry.backgroundWidth = width;
14687
16182
  entry.backgroundHeight = height;
14688
16183
  graphics.clear();
14689
- const radius = (background.cornerRadius ?? 0) / viewportScale;
14690
- if (background.topCornersOnly && radius > 0) {
16184
+ const radius = Math.min((background.cornerRadius ?? 0) / viewportScale, width / 2, height / 2);
16185
+ if (corners === "top" && radius > 0) {
14691
16186
  graphics
14692
16187
  .moveTo(0, height)
14693
16188
  .lineTo(0, radius)
@@ -14697,6 +16192,16 @@ function drawBackground(entry, background, x, y, width, height, viewportScale) {
14697
16192
  .lineTo(width, height)
14698
16193
  .closePath();
14699
16194
  }
16195
+ else if (corners === "bottom" && radius > 0) {
16196
+ graphics
16197
+ .moveTo(0, 0)
16198
+ .lineTo(width, 0)
16199
+ .lineTo(width, height - radius)
16200
+ .quadraticCurveTo(width, height, width - radius, height)
16201
+ .lineTo(radius, height)
16202
+ .quadraticCurveTo(0, height, 0, height - radius)
16203
+ .closePath();
16204
+ }
14700
16205
  else {
14701
16206
  graphics.roundRect(0, 0, width, height, radius);
14702
16207
  }
@@ -14721,14 +16226,46 @@ function createTextStyleKey(textStyle) {
14721
16226
  textStyle.fontWeight,
14722
16227
  ].join(":");
14723
16228
  }
14724
- function createBackgroundKey(background) {
16229
+ function resolveRoundedCorners(background, placement, label, renderedBox, viewportScale) {
16230
+ if (background.topCornersOnly !== undefined) {
16231
+ return background.topCornersOnly ? "top" : "all";
16232
+ }
16233
+ if (!renderedBox)
16234
+ return "all";
16235
+ const box = centerRectToTopLeftRect(renderedBox);
16236
+ const epsilon = 1e-6 / viewportScale;
16237
+ if (Math.min(label.x + label.width, box.x + box.width) -
16238
+ Math.max(label.x, box.x) <=
16239
+ epsilon) {
16240
+ return "all";
16241
+ }
16242
+ switch (placement) {
16243
+ case LabelPlacement.Top:
16244
+ return Math.abs(label.y + label.height - box.y) <= epsilon
16245
+ ? "top"
16246
+ : "all";
16247
+ case LabelPlacement.Bottom:
16248
+ return Math.abs(label.y - box.y - box.height) <= epsilon
16249
+ ? "bottom"
16250
+ : "all";
16251
+ case LabelPlacement.InsideTop:
16252
+ return Math.abs(label.y - box.y) <= epsilon ? "bottom" : "all";
16253
+ case LabelPlacement.InsideBottom:
16254
+ return Math.abs(label.y + label.height - box.y - box.height) <= epsilon
16255
+ ? "top"
16256
+ : "all";
16257
+ case LabelPlacement.Center:
16258
+ return "all";
16259
+ }
16260
+ }
16261
+ function createBackgroundKey(background, corners) {
14725
16262
  return [
14726
16263
  background.alpha,
14727
16264
  background.color,
14728
16265
  background.cornerRadius ?? 0,
14729
16266
  background.paddingX ?? 0,
14730
16267
  background.paddingY ?? 0,
14731
- background.topCornersOnly ? 1 : 0,
16268
+ corners,
14732
16269
  ].join(":");
14733
16270
  }
14734
16271
  function detectionKey$1(detection, detectionIndex) {
@@ -15031,7 +16568,9 @@ function createPixiInteractionPresentationLayer(options) {
15031
16568
  let activePicks = [];
15032
16569
  let currentMediaTime = 0;
15033
16570
  let viewportScale = 1;
16571
+ let strokePixelRatio = 1;
15034
16572
  let maskRenderer;
16573
+ let labelDisplay = null;
15035
16574
  let screenStrokeRenderer;
15036
16575
  let isDestroyed = false;
15037
16576
  const syntheticTimeline = createSyntheticTimeline(() => syntheticFrame);
@@ -15065,17 +16604,20 @@ function createPixiInteractionPresentationLayer(options) {
15065
16604
  mediaHeight = height;
15066
16605
  const container = new options.Container();
15067
16606
  const boxGraphics = new options.Graphics();
15068
- const labels = labelLayer.createContainer();
16607
+ labelDisplay = labelLayer.createContainer();
15069
16608
  const vectors = vectorLayer.createContainer();
15070
16609
  boxLayer.attachGraphics(boxGraphics);
15071
16610
  maskRenderer = createMaskRenderer();
15072
16611
  if (maskRenderer) {
15073
16612
  container.addChild(maskRenderer.mesh);
15074
16613
  }
15075
- container.addChild(boxGraphics, vectors, labels);
16614
+ container.addChild(boxGraphics, vectors);
15076
16615
  displayContainer = container;
15077
16616
  return container;
15078
16617
  },
16618
+ getLabelDisplay() {
16619
+ return labelDisplay;
16620
+ },
15079
16621
  destroy() {
15080
16622
  if (isDestroyed) {
15081
16623
  return;
@@ -15086,6 +16628,7 @@ function createPixiInteractionPresentationLayer(options) {
15086
16628
  maskRenderer?.destroy();
15087
16629
  screenStrokeRenderer?.destroy();
15088
16630
  labelLayer.destroy();
16631
+ labelDisplay = null;
15089
16632
  vectorLayer.destroy();
15090
16633
  },
15091
16634
  releaseMaskTexture(texture) {
@@ -15095,6 +16638,7 @@ function createPixiInteractionPresentationLayer(options) {
15095
16638
  drawFrame(context) {
15096
16639
  currentMediaTime = context.mediaTime;
15097
16640
  viewportScale = context.viewportScale ?? 1;
16641
+ strokePixelRatio = context.strokePixelRatio ?? 1;
15098
16642
  if (isDestroyed || !interactionStyle || !context.frame) {
15099
16643
  clear();
15100
16644
  return;
@@ -15128,6 +16672,9 @@ function createPixiInteractionPresentationLayer(options) {
15128
16672
  labelLayer.setLabelStyle(labelStyle);
15129
16673
  vectorLayer.setStyles({ polygonStyle, polylineStyle, keypointStyle });
15130
16674
  },
16675
+ setBackgroundAntialiasFilter(filter) {
16676
+ labelLayer.setBackgroundAntialiasFilter(filter);
16677
+ },
15131
16678
  };
15132
16679
  function getActivePicks({ frame, hoveredPick, mediaTime, selectedPick, selectedPicks, }) {
15133
16680
  if (!frame || !interactionStyle) {
@@ -15239,9 +16786,9 @@ function createPixiInteractionPresentationLayer(options) {
15239
16786
  screenStrokeRenderer?.hide();
15240
16787
  return;
15241
16788
  }
15242
- maskRenderer.render(artifact.frame, artifact.texture, instructions);
16789
+ maskRenderer.render(artifact.frame, artifact.texture, instructions, strokePixelRatio);
15243
16790
  if (!screenStrokeRenderer &&
15244
- instructions.some(({ instruction }) => instruction.stroke?.widthUnit === "screen")) {
16791
+ instructions.some(({ instruction }) => instruction.stroke?.widthUnit !== undefined)) {
15245
16792
  screenStrokeRenderer = createPixiMaskScreenStrokeRenderer({
15246
16793
  Graphics: options.Graphics,
15247
16794
  mediaWidth,
@@ -15408,6 +16955,7 @@ function createInteractionMaskRenderer(options) {
15408
16955
  const fillPalette = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4);
15409
16956
  const strokePalette = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4);
15410
16957
  const strokeWidths = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES);
16958
+ const strokeAlignments = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES);
15411
16959
  const uniforms = new options.UniformGroup({
15412
16960
  uFillPalette: {
15413
16961
  size: MAX_ID_MASK_PALETTE_ENTRIES,
@@ -15415,7 +16963,8 @@ function createInteractionMaskRenderer(options) {
15415
16963
  value: fillPalette,
15416
16964
  },
15417
16965
  uMaxStrokeWidth: { type: "f32", value: 0 },
15418
- uMaxFractionalStrokeWidth: { type: "f32", value: 0 },
16966
+ uBorderEnabled: { type: "f32", value: 0 },
16967
+ uStrokePixelRatio: { type: "f32", value: 1 },
15419
16968
  uStrokePalette: {
15420
16969
  size: MAX_ID_MASK_PALETTE_ENTRIES,
15421
16970
  type: "vec4<f32>",
@@ -15426,6 +16975,11 @@ function createInteractionMaskRenderer(options) {
15426
16975
  type: "vec4<f32>",
15427
16976
  value: strokeWidths,
15428
16977
  },
16978
+ uStrokeAlignments: {
16979
+ size: ID_MASK_STROKE_WIDTH_LANES,
16980
+ type: "vec4<f32>",
16981
+ value: strokeAlignments,
16982
+ },
15429
16983
  uTextureSize: {
15430
16984
  type: "vec2<f32>",
15431
16985
  value: new Float32Array([options.mediaWidth, options.mediaHeight]),
@@ -15475,7 +17029,7 @@ function createInteractionMaskRenderer(options) {
15475
17029
  mesh.visible = false;
15476
17030
  },
15477
17031
  mesh,
15478
- render(frame, texture, instructions) {
17032
+ render(frame, texture, instructions, strokePixelRatio) {
15479
17033
  if (frame.kind !== PreparedMaskFrameKind.IdMask) {
15480
17034
  mesh.visible = false;
15481
17035
  return;
@@ -15484,8 +17038,9 @@ function createInteractionMaskRenderer(options) {
15484
17038
  fillPalette.fill(0);
15485
17039
  strokePalette.fill(0);
15486
17040
  strokeWidths.fill(0);
17041
+ strokeAlignments.fill(0);
15487
17042
  let maxStrokeWidth = 0;
15488
- let maxFractionalStrokeWidth = 0;
17043
+ let hasStroke = false;
15489
17044
  for (const { detectionIndex, instruction } of instructions) {
15490
17045
  const maskId = resolveIdMaskPaletteId(detectionIndex);
15491
17046
  // A raster naming this detection is one the palette accepted, so an id
@@ -15495,26 +17050,31 @@ function createInteractionMaskRenderer(options) {
15495
17050
  }
15496
17051
  writeIdMaskPaletteEntry(fillPalette, maskId, instruction.color, instruction.alpha);
15497
17052
  if (instruction.stroke &&
15498
- instruction.stroke.widthUnit !== "screen" &&
17053
+ instruction.stroke.widthUnit === undefined &&
15499
17054
  instruction.stroke.width > 0) {
15500
17055
  writeIdMaskPaletteEntry(strokePalette, maskId, instruction.stroke.color, instruction.stroke.alpha);
15501
- strokeWidths[maskId] = resolveIdMaskStrokeTexels(instruction.stroke.width, frame.sourceWidth, frame.width);
15502
- maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidths[maskId] ?? 0);
15503
- const width = strokeWidths[maskId] ?? 0;
15504
- if (width > 0 && width < 1) {
15505
- maxFractionalStrokeWidth = Math.max(maxFractionalStrokeWidth, width);
15506
- }
17056
+ const alignment = instruction.stroke.alignment === StrokeAlignment.Inside
17057
+ ? 1
17058
+ : instruction.stroke.alignment === StrokeAlignment.Center
17059
+ ? 0.5
17060
+ : 0;
17061
+ strokeWidths[maskId] = instruction.stroke.width;
17062
+ strokeAlignments[maskId] = alignment;
17063
+ hasStroke = true;
17064
+ maxStrokeWidth = Math.max(maxStrokeWidth, instruction.stroke.width * (1 - alignment));
15507
17065
  }
15508
17066
  }
15509
17067
  uniforms.uniforms.uFillPalette = fillPalette;
15510
17068
  uniforms.uniforms.uStrokePalette = strokePalette;
15511
17069
  uniforms.uniforms.uStrokeWidths = strokeWidths;
17070
+ uniforms.uniforms.uStrokeAlignments = strokeAlignments;
17071
+ uniforms.uniforms.uBorderEnabled = hasStroke ? 1 : 0;
17072
+ uniforms.uniforms.uStrokePixelRatio = strokePixelRatio;
15512
17073
  uniforms.uniforms.uTextureSize = new Float32Array([
15513
17074
  frame.width,
15514
17075
  frame.height,
15515
17076
  ]);
15516
17077
  uniforms.uniforms.uMaxStrokeWidth = maxStrokeWidth;
15517
- uniforms.uniforms.uMaxFractionalStrokeWidth = maxFractionalStrokeWidth;
15518
17078
  uniforms.update();
15519
17079
  mesh.visible = true;
15520
17080
  },
@@ -15569,14 +17129,11 @@ in vec4 vColor;
15569
17129
  uniform sampler2D uTexture;
15570
17130
  uniform vec2 uTextureSize;
15571
17131
  uniform float uMaxStrokeWidth;
15572
- uniform float uMaxFractionalStrokeWidth;
17132
+ uniform float uBorderEnabled;
17133
+ uniform float uStrokePixelRatio;
15573
17134
  ${idMaskPaletteGlsl}
15574
17135
  out vec4 finalColor;
15575
17136
 
15576
- float sampleMaskId(vec2 uv) {
15577
- return floor(texture(uTexture, uv).r * 255.0 + 0.5);
15578
- }
15579
-
15580
17137
  vec4 premultiplyAlpha(vec4 color) {
15581
17138
  return vec4(color.rgb * color.a, color.a);
15582
17139
  }
@@ -15588,19 +17145,20 @@ bool differs(float left, float right) {
15588
17145
  ${idMaskStrokeCoverageGlsl}
15589
17146
 
15590
17147
  void main(void) {
15591
- float centerId = sampleMaskId(vUV);
15592
- vec2 texel = 1.0 / uTextureSize;
15593
- vec2 cell = fract(vUV * uTextureSize);
15594
- vec2 footprint = fwidth(vUV * uTextureSize);
17148
+ vec2 position = vUV * uTextureSize;
17149
+ ivec2 sourceCell = ivec2(floor(position));
17150
+ float centerId = sampleMaskIdCell(sourceCell);
17151
+ vec2 cell = fract(position);
17152
+ vec2 footprint = fwidth(position);
15595
17153
  float pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
15596
17154
 
15597
17155
  if (centerId > 0.5) {
15598
17156
  vec4 fill = readFill(centerId);
15599
17157
  vec4 stroke = readStroke(centerId);
15600
17158
 
15601
- float width = readStrokeWidth(centerId);
15602
- if (uMaxStrokeWidth > 0.0 && width > 0.0 && stroke.a > 0.0) {
15603
- float coverage = innerStrokeCoverage(centerId, texel, cell, width, pixelWidth);
17159
+ float width = strokeWidthInTexels(centerId, pixelWidth);
17160
+ if (uBorderEnabled > 0.5 && width > 0.0 && stroke.a > 0.0) {
17161
+ float coverage = innerStrokeCoverage(centerId, sourceCell, cell, width, pixelWidth);
15604
17162
  if (coverage >= 1.0) {
15605
17163
  finalColor = premultiplyAlpha(stroke * vColor);
15606
17164
  return;
@@ -15615,8 +17173,8 @@ void main(void) {
15615
17173
  return;
15616
17174
  }
15617
17175
 
15618
- if (uMaxStrokeWidth > 0.0) {
15619
- vec2 border = findNeighborStroke(centerId, texel, cell, pixelWidth);
17176
+ if (uBorderEnabled > 0.5) {
17177
+ vec2 border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
15620
17178
 
15621
17179
  if (border.x > 0.5) {
15622
17180
  finalColor = premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
@@ -15631,9 +17189,11 @@ const interactionMaskFragmentWgsl = `
15631
17189
  struct InteractionMaskUniforms {
15632
17190
  ${idMaskFillPaletteWgslField}
15633
17191
  uMaxStrokeWidth: f32,
15634
- uMaxFractionalStrokeWidth: f32,
17192
+ uBorderEnabled: f32,
17193
+ uStrokePixelRatio: f32,
15635
17194
  ${idMaskStrokePaletteWgslField}
15636
17195
  ${idMaskStrokeWidthsWgslField}
17196
+ ${idMaskStrokeAlignmentsWgslField}
15637
17197
  uTextureSize: vec2<f32>,
15638
17198
  }
15639
17199
 
@@ -15641,9 +17201,6 @@ struct InteractionMaskUniforms {
15641
17201
  @group(2) @binding(1) var uTexture: texture_2d<f32>;
15642
17202
  @group(2) @binding(2) var uSampler: sampler;
15643
17203
 
15644
- fn sampleMaskId(uv: vec2<f32>) -> f32 {
15645
- return floor(textureSampleLevel(uTexture, uSampler, uv, 0.0).r * 255.0 + 0.5);
15646
- }
15647
17204
  ${idMaskPaletteWgsl}
15648
17205
  fn premultiplyAlpha(color: vec4<f32>) -> vec4<f32> {
15649
17206
  return vec4<f32>(color.rgb * color.a, color.a);
@@ -15660,19 +17217,20 @@ fn mainFragment(
15660
17217
  @location(0) vUV: vec2<f32>,
15661
17218
  @location(1) vColor: vec4<f32>,
15662
17219
  ) -> @location(0) vec4<f32> {
15663
- let centerId = sampleMaskId(vUV);
15664
- let texel = 1.0 / maskUniforms.uTextureSize;
15665
- let cell = fract(vUV * maskUniforms.uTextureSize);
15666
- let footprint = fwidth(vUV * maskUniforms.uTextureSize);
17220
+ let position = vUV * maskUniforms.uTextureSize;
17221
+ let sourceCell = vec2<i32>(floor(position));
17222
+ let centerId = sampleMaskIdCell(sourceCell);
17223
+ let cell = fract(position);
17224
+ let footprint = fwidth(position);
15667
17225
  let pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
15668
17226
 
15669
17227
  if (centerId > 0.5) {
15670
17228
  let fill = readFill(centerId);
15671
17229
  let stroke = readStroke(centerId);
15672
17230
 
15673
- let width = readStrokeWidth(centerId);
15674
- if (maskUniforms.uMaxStrokeWidth > 0.0 && width > 0.0 && stroke.a > 0.0) {
15675
- let coverage = innerStrokeCoverage(vUV, centerId, texel, cell, width, pixelWidth);
17231
+ let width = strokeWidthInTexels(centerId, pixelWidth);
17232
+ if (maskUniforms.uBorderEnabled > 0.5 && width > 0.0 && stroke.a > 0.0) {
17233
+ let coverage = innerStrokeCoverage(centerId, sourceCell, cell, width, pixelWidth);
15676
17234
  if (coverage >= 1.0) {
15677
17235
  return premultiplyAlpha(stroke * vColor);
15678
17236
  }
@@ -15684,8 +17242,8 @@ fn mainFragment(
15684
17242
  return premultiplyAlpha(fill * vColor);
15685
17243
  }
15686
17244
 
15687
- if (maskUniforms.uMaxStrokeWidth > 0.0) {
15688
- let border = findNeighborStroke(vUV, centerId, texel, cell, pixelWidth);
17245
+ if (maskUniforms.uBorderEnabled > 0.5) {
17246
+ let border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
15689
17247
 
15690
17248
  if (border.x > 0.5) {
15691
17249
  return premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
@@ -15697,6 +17255,7 @@ fn mainFragment(
15697
17255
  `;
15698
17256
 
15699
17257
  function createPixiIdMaskShaderRenderer(options) {
17258
+ const defaultStrokeAlignments = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES);
15700
17259
  const uniforms = new options.UniformGroup({
15701
17260
  uBorderEnabled: { type: "f32", value: 0 },
15702
17261
  uFillPalette: {
@@ -15705,7 +17264,7 @@ function createPixiIdMaskShaderRenderer(options) {
15705
17264
  value: new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4),
15706
17265
  },
15707
17266
  uMaxStrokeWidth: { type: "f32", value: 0 },
15708
- uMaxFractionalStrokeWidth: { type: "f32", value: 0 },
17267
+ uStrokePixelRatio: { type: "f32", value: 1 },
15709
17268
  uStrokePalette: {
15710
17269
  size: MAX_ID_MASK_PALETTE_ENTRIES,
15711
17270
  type: "vec4<f32>",
@@ -15716,6 +17275,11 @@ function createPixiIdMaskShaderRenderer(options) {
15716
17275
  type: "vec4<f32>",
15717
17276
  value: new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES),
15718
17277
  },
17278
+ uStrokeAlignments: {
17279
+ size: ID_MASK_STROKE_WIDTH_LANES,
17280
+ type: "vec4<f32>",
17281
+ value: defaultStrokeAlignments,
17282
+ },
15719
17283
  uTextureSize: {
15720
17284
  type: "vec2<f32>",
15721
17285
  value: new Float32Array([options.mediaWidth, options.mediaHeight]),
@@ -15768,24 +17332,30 @@ function createPixiIdMaskShaderRenderer(options) {
15768
17332
  mesh.visible = false;
15769
17333
  },
15770
17334
  mesh,
15771
- render(frame, texture) {
17335
+ render(frame, texture, strokePixelRatio = 1) {
15772
17336
  bindTexture(texture.source);
15773
17337
  uniforms.uniforms.uFillPalette = frame.fillPalette;
15774
17338
  uniforms.uniforms.uStrokePalette = frame.strokePalette;
15775
17339
  uniforms.uniforms.uStrokeWidths = frame.strokeWidths;
17340
+ uniforms.uniforms.uStrokeAlignments =
17341
+ frame.strokeAlignments ?? defaultStrokeAlignments;
17342
+ uniforms.uniforms.uStrokePixelRatio = strokePixelRatio;
15776
17343
  uniforms.uniforms.uTextureSize = new Float32Array([
15777
17344
  frame.width,
15778
17345
  frame.height,
15779
17346
  ]);
15780
17347
  uniforms.uniforms.uBorderEnabled = frame.hasStroke ? 1 : 0;
15781
- uniforms.uniforms.uMaxStrokeWidth = Math.min(frame.maxStrokeWidth, MAX_ID_MASK_STROKE_WIDTH);
15782
- uniforms.uniforms.uMaxFractionalStrokeWidth = frame.strokeWidths.reduce((maximum, width) => width > 0 && width < 1 ? Math.max(maximum, width) : maximum, 0);
17348
+ uniforms.uniforms.uMaxStrokeWidth = frame.maxStrokeWidth;
15783
17349
  uniforms.update();
15784
17350
  mesh.visible = true;
15785
17351
  },
15786
17352
  setOpacity(opacity) {
15787
17353
  mesh.alpha = opacity;
15788
17354
  },
17355
+ setStrokePixelRatio(pixelRatio) {
17356
+ uniforms.uniforms.uStrokePixelRatio = pixelRatio;
17357
+ uniforms.update();
17358
+ },
15789
17359
  };
15790
17360
  function bindTexture(source) {
15791
17361
  try {
@@ -15838,14 +17408,10 @@ uniform sampler2D uTexture;
15838
17408
  uniform vec2 uTextureSize;
15839
17409
  uniform float uBorderEnabled;
15840
17410
  uniform float uMaxStrokeWidth;
15841
- uniform float uMaxFractionalStrokeWidth;
17411
+ uniform float uStrokePixelRatio;
15842
17412
  ${idMaskPaletteGlsl}
15843
17413
  out vec4 finalColor;
15844
17414
 
15845
- float sampleMaskId(vec2 uv) {
15846
- return floor(texture(uTexture, uv).r * 255.0 + 0.5);
15847
- }
15848
-
15849
17415
  vec4 premultiplyAlpha(vec4 color) {
15850
17416
  return vec4(color.rgb * color.a, color.a);
15851
17417
  }
@@ -15857,15 +17423,16 @@ bool differs(float left, float right) {
15857
17423
  ${idMaskStrokeCoverageGlsl}
15858
17424
 
15859
17425
  void main(void) {
15860
- float centerId = sampleMaskId(vUV);
15861
- vec2 texel = 1.0 / uTextureSize;
15862
- vec2 cell = fract(vUV * uTextureSize);
15863
- vec2 footprint = fwidth(vUV * uTextureSize);
17426
+ vec2 position = vUV * uTextureSize;
17427
+ ivec2 sourceCell = ivec2(floor(position));
17428
+ float centerId = sampleMaskIdCell(sourceCell);
17429
+ vec2 cell = fract(position);
17430
+ vec2 footprint = fwidth(position);
15864
17431
  float pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
15865
17432
 
15866
17433
  if (centerId < 0.5) {
15867
- if (uBorderEnabled > 0.5 && uMaxStrokeWidth > 0.0) {
15868
- vec2 border = findNeighborStroke(centerId, texel, cell, pixelWidth);
17434
+ if (uBorderEnabled > 0.5) {
17435
+ vec2 border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
15869
17436
 
15870
17437
  if (border.x > 0.5) {
15871
17438
  finalColor = premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
@@ -15878,9 +17445,9 @@ void main(void) {
15878
17445
  }
15879
17446
 
15880
17447
  if (uBorderEnabled > 0.5) {
15881
- float width = readStrokeWidth(centerId);
17448
+ float width = strokeWidthInTexels(centerId, pixelWidth);
15882
17449
  if (width > 0.0 && readStroke(centerId).a > 0.0) {
15883
- float coverage = innerStrokeCoverage(centerId, texel, cell, width, pixelWidth);
17450
+ float coverage = innerStrokeCoverage(centerId, sourceCell, cell, width, pixelWidth);
15884
17451
  if (coverage >= 1.0) {
15885
17452
  finalColor = premultiplyAlpha(readStroke(centerId) * vColor);
15886
17453
  return;
@@ -15904,9 +17471,10 @@ struct MaskUniforms {
15904
17471
  uBorderEnabled: f32,
15905
17472
  ${idMaskFillPaletteWgslField}
15906
17473
  uMaxStrokeWidth: f32,
15907
- uMaxFractionalStrokeWidth: f32,
17474
+ uStrokePixelRatio: f32,
15908
17475
  ${idMaskStrokePaletteWgslField}
15909
17476
  ${idMaskStrokeWidthsWgslField}
17477
+ ${idMaskStrokeAlignmentsWgslField}
15910
17478
  uTextureSize: vec2<f32>,
15911
17479
  }
15912
17480
 
@@ -15914,9 +17482,6 @@ struct MaskUniforms {
15914
17482
  @group(2) @binding(1) var uTexture: texture_2d<f32>;
15915
17483
  @group(2) @binding(2) var uSampler: sampler;
15916
17484
 
15917
- fn sampleMaskId(uv: vec2<f32>) -> f32 {
15918
- return floor(textureSampleLevel(uTexture, uSampler, uv, 0.0).r * 255.0 + 0.5);
15919
- }
15920
17485
  ${idMaskPaletteWgsl}
15921
17486
  fn premultiplyAlpha(color: vec4<f32>) -> vec4<f32> {
15922
17487
  return vec4<f32>(color.rgb * color.a, color.a);
@@ -15933,15 +17498,16 @@ fn mainFragment(
15933
17498
  @location(0) vUV: vec2<f32>,
15934
17499
  @location(1) vColor: vec4<f32>,
15935
17500
  ) -> @location(0) vec4<f32> {
15936
- let centerId = sampleMaskId(vUV);
15937
- let texel = 1.0 / maskUniforms.uTextureSize;
15938
- let cell = fract(vUV * maskUniforms.uTextureSize);
15939
- let footprint = fwidth(vUV * maskUniforms.uTextureSize);
17501
+ let position = vUV * maskUniforms.uTextureSize;
17502
+ let sourceCell = vec2<i32>(floor(position));
17503
+ let centerId = sampleMaskIdCell(sourceCell);
17504
+ let cell = fract(position);
17505
+ let footprint = fwidth(position);
15940
17506
  let pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
15941
17507
 
15942
17508
  if (centerId < 0.5) {
15943
- if (maskUniforms.uBorderEnabled > 0.5 && maskUniforms.uMaxStrokeWidth > 0.0) {
15944
- let border = findNeighborStroke(vUV, centerId, texel, cell, pixelWidth);
17509
+ if (maskUniforms.uBorderEnabled > 0.5) {
17510
+ let border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
15945
17511
 
15946
17512
  if (border.x > 0.5) {
15947
17513
  return premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
@@ -15952,9 +17518,9 @@ fn mainFragment(
15952
17518
  }
15953
17519
 
15954
17520
  if (maskUniforms.uBorderEnabled > 0.5) {
15955
- let width = readStrokeWidth(centerId);
17521
+ let width = strokeWidthInTexels(centerId, pixelWidth);
15956
17522
  if (width > 0.0 && readStroke(centerId).a > 0.0) {
15957
- let coverage = innerStrokeCoverage(vUV, centerId, texel, cell, width, pixelWidth);
17523
+ let coverage = innerStrokeCoverage(centerId, sourceCell, cell, width, pixelWidth);
15958
17524
  if (coverage >= 1.0) {
15959
17525
  return premultiplyAlpha(readStroke(centerId) * vColor);
15960
17526
  }
@@ -16020,7 +17586,10 @@ function createPixiMaskLayer(options) {
16020
17586
  const haloTextures = new Map();
16021
17587
  const regionMaskTextures = new Map();
16022
17588
  const maskFrameOptions = options.renderPreparation?.maskFrame;
17589
+ const capsRasterToDisplay = maskFrameOptions?.display !== undefined;
16023
17590
  let rasterDisplay = maskFrameOptions?.display;
17591
+ let rasterResolutionScale = 1;
17592
+ let strokeViewportScale;
16024
17593
  const preparedRenderWindow = createPreparedRenderWindow({
16025
17594
  artifactKind: options.artifactKind,
16026
17595
  detectionTimeline: options.detectionTimeline,
@@ -16044,6 +17613,7 @@ function createPixiMaskLayer(options) {
16044
17613
  resolveInstructions: options.resolveInstructions,
16045
17614
  resolveMaxRasterWidth,
16046
17615
  resolveMaskFrameBackingBytes,
17616
+ resolveMaskDisplayWidth,
16047
17617
  });
16048
17618
  return {
16049
17619
  createSprite({ width, height }) {
@@ -16162,16 +17732,24 @@ function createPixiMaskLayer(options) {
16162
17732
  setPlaybackActive(active) {
16163
17733
  preparedRenderWindow.setPlaybackActive(active);
16164
17734
  },
16165
- setRasterDisplay(display) {
16166
- if (!rasterDisplay || isDestroyed)
17735
+ setRasterDisplay(display, resolutionScale = 1) {
17736
+ if (isDestroyed)
16167
17737
  return;
16168
17738
  const previousWidth = resolveMaxRasterWidth();
16169
17739
  rasterDisplay = display;
17740
+ rasterResolutionScale = resolutionScale;
17741
+ idMaskRenderer?.setStrokePixelRatio(resolveDisplayPixelRatio(display) * resolutionScale);
16170
17742
  const nextWidth = resolveMaxRasterWidth();
16171
17743
  if (mediaWidth > 0 && previousWidth !== nextWidth) {
16172
17744
  preparedRenderWindow.invalidateRasterSize();
16173
17745
  }
16174
17746
  },
17747
+ setViewportScale(scale) {
17748
+ if (isDestroyed || scale <= 0 || scale === strokeViewportScale)
17749
+ return;
17750
+ strokeViewportScale = scale;
17751
+ preparedRenderWindow.invalidateMaskDisplayWidth();
17752
+ },
16175
17753
  setTimelineContext(context) {
16176
17754
  preparedRenderWindow.setTimelineContext(context);
16177
17755
  },
@@ -16261,14 +17839,22 @@ function createPixiMaskLayer(options) {
16261
17839
  maskTextures.set(maskFrame.key, texture);
16262
17840
  return texture;
16263
17841
  }
16264
- /**
16265
- * The widest raster the picture can show. Polygon frames rasterize geometry
16266
- * at a size of their own choosing, so this sizes mask frames only.
16267
- */
17842
+ function resolveMaskDisplayWidth() {
17843
+ if (strokeViewportScale !== undefined && mediaWidth > 0) {
17844
+ return mediaWidth * strokeViewportScale;
17845
+ }
17846
+ const display = rasterDisplay;
17847
+ if (!display || mediaWidth <= 0 || mediaHeight <= 0)
17848
+ return undefined;
17849
+ const fit = Math.min(display.boxWidth / mediaWidth, display.boxHeight / mediaHeight);
17850
+ return fit > 0 ? mediaWidth * fit : undefined;
17851
+ }
17852
+ /** Mask detail follows the annotation capture density. */
16268
17853
  function resolveMaxRasterWidth() {
16269
17854
  const display = rasterDisplay;
16270
17855
  const artifactKind = options.artifactKind ?? RenderPreparationArtifactKind.MaskFrame;
16271
17856
  if (!display ||
17857
+ !capsRasterToDisplay ||
16272
17858
  artifactKind !== RenderPreparationArtifactKind.MaskFrame ||
16273
17859
  mediaWidth <= 0 ||
16274
17860
  mediaHeight <= 0) {
@@ -16277,7 +17863,7 @@ function createPixiMaskLayer(options) {
16277
17863
  const fit = Math.min(display.boxWidth / mediaWidth, display.boxHeight / mediaHeight);
16278
17864
  const pixelRatio = resolveDisplayPixelRatio(display);
16279
17865
  return fit > 0 && pixelRatio > 0
16280
- ? alignRasterWidth(Math.ceil(mediaWidth * fit * pixelRatio))
17866
+ ? alignRasterWidth(Math.ceil(mediaWidth * fit * pixelRatio * rasterResolutionScale))
16281
17867
  : undefined;
16282
17868
  }
16283
17869
  /**
@@ -16358,7 +17944,9 @@ function createPixiMaskLayer(options) {
16358
17944
  return;
16359
17945
  }
16360
17946
  maskSprite.visible = false;
16361
- idMaskRenderer.render(maskFrame, texture);
17947
+ idMaskRenderer.render(maskFrame, texture, rasterDisplay
17948
+ ? resolveDisplayPixelRatio(rasterDisplay) * rasterResolutionScale
17949
+ : 1);
16362
17950
  }
16363
17951
  function hideFill() {
16364
17952
  if (maskSprite) {
@@ -16797,11 +18385,15 @@ function createPixiPolygonLayer(options) {
16797
18385
  rasterLayer.setMaskStyle(nextPolygonStyle ? createArtifactStyle() : nextPolygonStyle);
16798
18386
  },
16799
18387
  needsRenderPreparationWait: rasterLayer.needsRenderPreparationWait,
18388
+ setPlaybackActive: rasterLayer.setPlaybackActive,
18389
+ setRasterDisplay: rasterLayer.setRasterDisplay,
16800
18390
  setTimelineContext: rasterLayer.setTimelineContext,
18391
+ setViewportScale: adoptViewportScale,
16801
18392
  waitForRenderPreparation: rasterLayer.waitForRenderPreparation,
16802
18393
  };
16803
18394
  function adoptViewportScale(nextViewportScale) {
16804
18395
  const normalizedViewportScale = Math.max(nextViewportScale, Number.EPSILON);
18396
+ rasterLayer.setViewportScale(normalizedViewportScale);
16805
18397
  if (normalizedViewportScale === viewportScale) {
16806
18398
  return;
16807
18399
  }
@@ -16865,9 +18457,10 @@ function resolvePreparedPolygonInstructions(options) {
16865
18457
  },
16866
18458
  stroke: instruction.stroke
16867
18459
  ? {
18460
+ alignment: BoxStrokeAlignment.Center,
16868
18461
  alpha: instruction.stroke.alpha,
16869
18462
  color: instruction.stroke.color,
16870
- width: resolveScreenLength(instruction.stroke.width, options.viewportScale),
18463
+ width: instruction.stroke.width,
16871
18464
  }
16872
18465
  : undefined,
16873
18466
  });
@@ -17012,17 +18605,30 @@ function createPixiRegionCoverageMask(options) {
17012
18605
  });
17013
18606
  let shader = createShader();
17014
18607
  const display = new options.Mesh({ geometry, shader });
17015
- const effect = new options.AlphaMask({ mask: display });
18608
+ const container = new options.Container();
18609
+ container.addChild(display);
18610
+ const effect = new options.AlphaMask({ mask: container });
18611
+ let antialiasFilter;
17016
18612
  return {
17017
18613
  destroy() {
18614
+ if (antialiasFilter && display.filters?.includes(antialiasFilter)) {
18615
+ display.filters = null;
18616
+ }
18617
+ antialiasFilter?.destroy();
17018
18618
  effect.destroy();
17019
18619
  display.destroy();
18620
+ container.destroy();
17020
18621
  destroyShaderKeepingProgram(shader);
17021
18622
  geometry.destroy();
17022
18623
  placeholderSource.destroy();
17023
18624
  },
18625
+ container,
17024
18626
  display,
17025
18627
  effect,
18628
+ setAntialiasResolution(resolution) {
18629
+ if (antialiasFilter)
18630
+ antialiasFilter.resolution = resolution;
18631
+ },
17026
18632
  releaseTexture(source) {
17027
18633
  if (shader.resources.uTexture !== source)
17028
18634
  return;
@@ -17031,6 +18637,7 @@ function createPixiRegionCoverageMask(options) {
17031
18637
  },
17032
18638
  render(renderOptions) {
17033
18639
  bindTexture(renderOptions.artifact.texture.source);
18640
+ const alphaCoverage = updateAntialiasing();
17034
18641
  display.visible = true;
17035
18642
  uniforms.uniforms.uCrop = new Float32Array([
17036
18643
  renderOptions.crop.x,
@@ -17048,8 +18655,34 @@ function createPixiRegionCoverageMask(options) {
17048
18655
  display.position.set(renderOptions.x, renderOptions.y);
17049
18656
  display.scale.set(renderOptions.width * (renderOptions.flipHorizontal ? -1 : 1), renderOptions.height * (renderOptions.flipVertical ? -1 : 1));
17050
18657
  display.rotation = renderOptions.rotation;
18658
+ syncViewportBounds();
18659
+ return alphaCoverage;
17051
18660
  },
18661
+ syncViewportBounds,
17052
18662
  };
18663
+ function syncViewportBounds() {
18664
+ syncPixiMaskViewportBounds(container, options.getViewportBounds?.(), options.Rectangle);
18665
+ }
18666
+ function updateAntialiasing() {
18667
+ if (options.getAnnotationAntialiasing?.() &&
18668
+ options.Filter &&
18669
+ options.defaultFilterVert) {
18670
+ antialiasFilter ??= createPixiAnnotationAntialiasFilter({
18671
+ maskCoverage: true,
18672
+ Filter: options.Filter,
18673
+ defaultFilterVert: options.defaultFilterVert,
18674
+ resolution: options.getAntialiasResolution?.(),
18675
+ });
18676
+ if (display.filters?.[0] !== antialiasFilter) {
18677
+ display.filters = [antialiasFilter];
18678
+ }
18679
+ return true;
18680
+ }
18681
+ if (antialiasFilter && display.filters?.includes(antialiasFilter)) {
18682
+ display.filters = null;
18683
+ }
18684
+ return false;
18685
+ }
17053
18686
  function bindTexture(source) {
17054
18687
  try {
17055
18688
  shader.resources.uTexture = source;
@@ -17351,6 +18984,7 @@ function createPixiRegionLayer(options) {
17351
18984
  let currentMediaTime = 0;
17352
18985
  let currentViewportScale = 1;
17353
18986
  let destroyed = false;
18987
+ let viewportBounds;
17354
18988
  let renderers = [...options.regionRenderers];
17355
18989
  const assets = new Map();
17356
18990
  const entries = new Map();
@@ -17431,6 +19065,7 @@ function createPixiRegionLayer(options) {
17431
19065
  }
17432
19066
  const position = positionSprite(entry.display, renderer, region, sourceSize, viewportScale);
17433
19067
  updateEffect(entry, renderer, viewportScale);
19068
+ updateAssetAntialiasing(entry, renderer);
17434
19069
  if (renderer.source.kind === RegionRendererSourceKind.Media &&
17435
19070
  mediaCrop &&
17436
19071
  !updateMediaCoverage(entry, renderer, detection, detectionIndex, mediaCrop)) {
@@ -17482,6 +19117,15 @@ function createPixiRegionLayer(options) {
17482
19117
  }
17483
19118
  }
17484
19119
  },
19120
+ setAntialiasResolution(resolution) {
19121
+ for (const entry of entries.values()) {
19122
+ updateEntryResolution(entry, resolution);
19123
+ }
19124
+ for (const pool of pools.values()) {
19125
+ for (const entry of pool)
19126
+ updateEntryResolution(entry, resolution);
19127
+ }
19128
+ },
17485
19129
  setRenderers(nextRenderers) {
17486
19130
  renderers = [...nextRenderers];
17487
19131
  syncSources();
@@ -17495,10 +19139,24 @@ function createPixiRegionLayer(options) {
17495
19139
  entry.display.position.set(entry.baseX + x, entry.baseY + y);
17496
19140
  entry.coverageMask?.position.set(entry.baseX + x, entry.baseY + y);
17497
19141
  entry.exactCoverageMask?.display.position.set(entry.baseX + x, entry.baseY + y);
19142
+ syncEntryViewportBounds(entry);
17498
19143
  translated = true;
17499
19144
  }
17500
19145
  return translated;
17501
19146
  },
19147
+ syncViewportBounds() {
19148
+ const next = options.getViewportBounds?.();
19149
+ if (next?.x === viewportBounds?.x &&
19150
+ next?.y === viewportBounds?.y &&
19151
+ next?.width === viewportBounds?.width &&
19152
+ next?.height === viewportBounds?.height)
19153
+ return;
19154
+ viewportBounds = next ? { ...next } : undefined;
19155
+ for (const entry of entries.values()) {
19156
+ if (entry.active)
19157
+ syncEntryViewportBounds(entry);
19158
+ }
19159
+ },
17502
19160
  destroy() {
17503
19161
  destroyed = true;
17504
19162
  destroyAllDisplays();
@@ -17620,11 +19278,18 @@ function createPixiRegionLayer(options) {
17620
19278
  return entry;
17621
19279
  }
17622
19280
  function updateMediaCoverage(entry, renderer, detection, detectionIndex, crop) {
17623
- if (renderer.source.kind !== RegionRendererSourceKind.Media ||
17624
- renderer.source.coverage === undefined) {
19281
+ if (renderer.source.kind !== RegionRendererSourceKind.Media) {
17625
19282
  removeCoverageMask(entry);
17626
19283
  return true;
17627
19284
  }
19285
+ if (renderer.source.coverage === undefined) {
19286
+ removeExactCoverageMask(entry);
19287
+ if (!annotationAntialiasingEnabled()) {
19288
+ removePolygonCoverageMask(entry);
19289
+ return true;
19290
+ }
19291
+ return updateGraphicsCoverage(entry, renderer, crop);
19292
+ }
17628
19293
  if (renderer.source.coverage.kind === RegionRendererCoverageKind.Mask) {
17629
19294
  removePolygonCoverageMask(entry);
17630
19295
  const artifact = options.getActiveRegionMaskCoverage(currentFrame?.mediaTime ?? null);
@@ -17642,7 +19307,14 @@ function createPixiRegionLayer(options) {
17642
19307
  }
17643
19308
  const mask = entry.exactCoverageMask ??
17644
19309
  createPixiRegionCoverageMask({
19310
+ getAnnotationAntialiasing: options.getAnnotationAntialiasing,
19311
+ getAntialiasResolution: options.getAntialiasResolution,
19312
+ getViewportBounds: options.getViewportBounds,
19313
+ defaultFilterVert: options.defaultFilterVert,
19314
+ Filter: options.Filter,
17645
19315
  AlphaMask: options.AlphaMask,
19316
+ Container: options.Container,
19317
+ Rectangle: options.Rectangle,
17646
19318
  ImageSource: options.ImageSource,
17647
19319
  Mesh: options.Mesh,
17648
19320
  MeshGeometry: options.MeshGeometry,
@@ -17651,7 +19323,7 @@ function createPixiRegionLayer(options) {
17651
19323
  });
17652
19324
  if (!entry.exactCoverageMask) {
17653
19325
  entry.exactCoverageMask = mask;
17654
- container?.addChild(mask.display);
19326
+ container?.addChild(mask.container);
17655
19327
  // A Mesh assigned through `display.mask` becomes a stencil mask in
17656
19328
  // Pixi v8, which clips by the quad geometry and ignores shader alpha.
17657
19329
  // AlphaMask renders the id-sampling mesh to a GPU texture first, so
@@ -17663,7 +19335,7 @@ function createPixiRegionLayer(options) {
17663
19335
  removeExactCoverageMask(entry);
17664
19336
  return false;
17665
19337
  }
17666
- mask.render({
19338
+ const alphaCoverage = mask.render({
17667
19339
  artifact: { texture: coverageTexture },
17668
19340
  coverage: coverageEntry,
17669
19341
  crop,
@@ -17675,6 +19347,7 @@ function createPixiRegionLayer(options) {
17675
19347
  x: entry.display.position.x,
17676
19348
  y: entry.display.position.y,
17677
19349
  });
19350
+ setCoverageChannel(entry, alphaCoverage);
17678
19351
  return true;
17679
19352
  }
17680
19353
  if (renderer.source.coverage.kind !== RegionRendererCoverageKind.Polygon ||
@@ -17684,26 +19357,95 @@ function createPixiRegionLayer(options) {
17684
19357
  return false;
17685
19358
  }
17686
19359
  removeExactCoverageMask(entry);
19360
+ return updateGraphicsCoverage(entry, renderer, crop, detection.polygon.points);
19361
+ }
19362
+ function updateGraphicsCoverage(entry, renderer, crop, points) {
17687
19363
  const mask = entry.coverageMask ?? new options.Graphics();
17688
19364
  if (!entry.coverageMask) {
17689
19365
  entry.coverageMask = mask;
17690
- container?.addChild(mask);
17691
- entry.display.mask = mask;
19366
+ entry.coverageMaskContainer = new options.Container();
19367
+ entry.coverageMaskContainer.addChild(mask);
19368
+ container?.addChild(entry.coverageMaskContainer);
19369
+ }
19370
+ mask.clear();
19371
+ if (points) {
19372
+ mask.poly(points.flatMap(({ x, y }) => [
19373
+ x - crop.x - crop.width / 2,
19374
+ y - crop.y - crop.height / 2,
19375
+ ]), true);
17692
19376
  }
17693
- mask
17694
- .clear()
17695
- .poly(detection.polygon.points.flatMap(({ x, y }) => [
17696
- x - crop.x - crop.width / 2,
17697
- y - crop.y - crop.height / 2,
17698
- ]), true)
17699
- .fill({ alpha: 1, color: 0xffffff });
19377
+ else {
19378
+ mask.rect(-crop.width / 2, -crop.height / 2, crop.width, crop.height);
19379
+ }
19380
+ mask.fill({ alpha: 1, color: 0xffffff });
17700
19381
  mask.position.set(entry.display.position.x, entry.display.position.y);
17701
19382
  mask.scale.set((entry.display.width / crop.width) *
17702
19383
  (renderer.transform?.flip?.horizontal ? -1 : 1), (entry.display.height / crop.height) *
17703
19384
  (renderer.transform?.flip?.vertical ? -1 : 1));
17704
19385
  mask.rotation = entry.display.rotation;
19386
+ if (annotationAntialiasingEnabled()) {
19387
+ entry.display.mask = null;
19388
+ const effect = entry.coverageMaskEffect ??
19389
+ new options.AlphaMask({ mask: entry.coverageMaskContainer });
19390
+ if (!entry.coverageMaskEffect)
19391
+ entry.coverageMaskEffect = effect;
19392
+ if (!entry.display.effects?.includes(effect)) {
19393
+ effect.init(entry.coverageMaskContainer);
19394
+ entry.display.addEffect(effect);
19395
+ }
19396
+ const filter = ensureAntialiasFilter(entry, true);
19397
+ if (filter && mask.filters?.[0] !== filter)
19398
+ mask.filters = [filter];
19399
+ syncEntryViewportBounds(entry);
19400
+ setCoverageChannel(entry, true);
19401
+ }
19402
+ else {
19403
+ if (entry.coverageMaskEffect) {
19404
+ entry.display.removeEffect(entry.coverageMaskEffect);
19405
+ entry.coverageMaskEffect.reset();
19406
+ }
19407
+ if (mask.filters?.length)
19408
+ mask.filters = null;
19409
+ setCoverageChannel(entry, false);
19410
+ entry.display.mask = mask;
19411
+ }
17705
19412
  return true;
17706
19413
  }
19414
+ function syncEntryViewportBounds(entry) {
19415
+ entry.exactCoverageMask?.syncViewportBounds();
19416
+ if (entry.coverageMaskContainer) {
19417
+ syncPixiMaskViewportBounds(entry.coverageMaskContainer, options.getViewportBounds?.(), options.Rectangle);
19418
+ }
19419
+ }
19420
+ function annotationAntialiasingEnabled() {
19421
+ return Boolean(options.getAnnotationAntialiasing?.() &&
19422
+ options.Filter &&
19423
+ options.defaultFilterVert);
19424
+ }
19425
+ function ensureAntialiasFilter(entry, maskCoverage = false) {
19426
+ if (!options.Filter || !options.defaultFilterVert)
19427
+ return undefined;
19428
+ entry.antialiasFilter ??= createPixiAnnotationAntialiasFilter({
19429
+ maskCoverage,
19430
+ Filter: options.Filter,
19431
+ defaultFilterVert: options.defaultFilterVert,
19432
+ resolution: options.getAntialiasResolution?.(),
19433
+ });
19434
+ return entry.antialiasFilter;
19435
+ }
19436
+ function updateAssetAntialiasing(entry, renderer) {
19437
+ if (renderer.source.kind !== RegionRendererSourceKind.Asset)
19438
+ return;
19439
+ if (annotationAntialiasingEnabled()) {
19440
+ const filter = ensureAntialiasFilter(entry);
19441
+ if (filter && entry.display.filters?.[0] !== filter) {
19442
+ entry.display.filters = [filter];
19443
+ }
19444
+ }
19445
+ else if (entry.antialiasFilter && entry.display.filters?.length) {
19446
+ entry.display.filters = null;
19447
+ }
19448
+ }
17707
19449
  function updateEffect(entry, renderer, viewportScale) {
17708
19450
  const effect = renderer.source.kind === RegionRendererSourceKind.Media
17709
19451
  ? renderer.source.effect
@@ -17723,26 +19465,6 @@ function createPixiRegionLayer(options) {
17723
19465
  }
17724
19466
  entry.effect?.apply(entry.display, viewportScale);
17725
19467
  }
17726
- function removeCoverageMask(entry) {
17727
- removePolygonCoverageMask(entry);
17728
- removeExactCoverageMask(entry);
17729
- }
17730
- function removePolygonCoverageMask(entry) {
17731
- if (!entry.coverageMask)
17732
- return;
17733
- entry.display.mask = null;
17734
- entry.coverageMask.removeFromParent();
17735
- entry.coverageMask.destroy();
17736
- entry.coverageMask = undefined;
17737
- }
17738
- function removeExactCoverageMask(entry) {
17739
- if (!entry.exactCoverageMask)
17740
- return;
17741
- entry.display.removeEffect(entry.exactCoverageMask.effect);
17742
- entry.exactCoverageMask.display.removeFromParent();
17743
- entry.exactCoverageMask.destroy();
17744
- entry.exactCoverageMask = undefined;
17745
- }
17746
19468
  function destroyRendererDisplays(rendererId) {
17747
19469
  for (const [key, entry] of entries) {
17748
19470
  if (entry.rendererId !== rendererId)
@@ -17774,17 +19496,53 @@ function createPixiRegionLayer(options) {
17774
19496
  pools.clear();
17775
19497
  }
17776
19498
  }
19499
+ function setCoverageChannel(entry, alpha) {
19500
+ if ((entry.alphaCoverage ?? false) === alpha)
19501
+ return;
19502
+ entry.display.setMask({ channel: alpha ? "alpha" : "red" });
19503
+ entry.alphaCoverage = alpha;
19504
+ }
19505
+ function removeCoverageMask(entry) {
19506
+ removePolygonCoverageMask(entry);
19507
+ removeExactCoverageMask(entry);
19508
+ }
19509
+ function removePolygonCoverageMask(entry) {
19510
+ if (!entry.coverageMask)
19511
+ return;
19512
+ entry.display.mask = null;
19513
+ if (entry.coverageMaskEffect) {
19514
+ entry.display.removeEffect(entry.coverageMaskEffect);
19515
+ entry.coverageMaskEffect.destroy();
19516
+ entry.coverageMaskEffect = undefined;
19517
+ }
19518
+ destroyEntryAntialiasFilter(entry);
19519
+ entry.coverageMask.removeFromParent();
19520
+ entry.coverageMask.destroy();
19521
+ entry.coverageMask = undefined;
19522
+ entry.coverageMaskContainer?.removeFromParent();
19523
+ entry.coverageMaskContainer?.destroy();
19524
+ entry.coverageMaskContainer = undefined;
19525
+ setCoverageChannel(entry, false);
19526
+ }
19527
+ function removeExactCoverageMask(entry) {
19528
+ if (!entry.exactCoverageMask)
19529
+ return;
19530
+ entry.display.removeEffect(entry.exactCoverageMask.effect);
19531
+ entry.exactCoverageMask.container.removeFromParent();
19532
+ entry.exactCoverageMask.destroy();
19533
+ entry.exactCoverageMask = undefined;
19534
+ setCoverageChannel(entry, false);
19535
+ }
19536
+ function updateEntryResolution(entry, resolution) {
19537
+ if (entry.antialiasFilter)
19538
+ entry.antialiasFilter.resolution = resolution;
19539
+ entry.exactCoverageMask?.setAntialiasResolution(resolution);
19540
+ }
17777
19541
  function destroyEntry(entry) {
17778
19542
  entry.effect?.destroy();
17779
19543
  entry.effect = undefined;
17780
- entry.display.mask = null;
17781
- entry.coverageMask?.removeFromParent();
17782
- entry.coverageMask?.destroy();
17783
- if (entry.exactCoverageMask) {
17784
- entry.display.removeEffect(entry.exactCoverageMask.effect);
17785
- entry.exactCoverageMask.display.removeFromParent();
17786
- entry.exactCoverageMask.destroy();
17787
- }
19544
+ removeCoverageMask(entry);
19545
+ destroyEntryAntialiasFilter(entry);
17788
19546
  entry.display.removeFromParent?.();
17789
19547
  // GifSprite sources are shared and released through Assets.unload().
17790
19548
  entry.display.destroy();
@@ -17792,6 +19550,18 @@ function destroyEntry(entry) {
17792
19550
  // lightweight crop texture; the scene destroys the shared source.
17793
19551
  entry.ownedTexture?.destroy(false);
17794
19552
  }
19553
+ function destroyEntryAntialiasFilter(entry) {
19554
+ if (!entry.antialiasFilter)
19555
+ return;
19556
+ if (entry.display.filters?.includes(entry.antialiasFilter)) {
19557
+ entry.display.filters = null;
19558
+ }
19559
+ if (entry.coverageMask?.filters?.includes(entry.antialiasFilter)) {
19560
+ entry.coverageMask.filters = null;
19561
+ }
19562
+ entry.antialiasFilter.destroy();
19563
+ entry.antialiasFilter = undefined;
19564
+ }
17795
19565
  function isAnimatedDisplay(display) {
17796
19566
  return "play" in display && typeof display.play === "function";
17797
19567
  }
@@ -17987,6 +19757,7 @@ function acquireAsset(loader, src) {
17987
19757
 
17988
19758
  function createPixiAnnotationOverlayLayer(editingEngine, initialStyle, initialKeypointStyle) {
17989
19759
  let graphics;
19760
+ let renderedEditingBox;
17990
19761
  let style = resolveAnnotationOverlayStyle(initialStyle);
17991
19762
  let keypointStyle = initialKeypointStyle === undefined
17992
19763
  ? new BaseKeypointStyle()
@@ -18003,12 +19774,16 @@ function createPixiAnnotationOverlayLayer(editingEngine, initialStyle, initialKe
18003
19774
  if (next !== undefined)
18004
19775
  keypointStyle = next;
18005
19776
  },
19777
+ getRenderedEditingBox() {
19778
+ return renderedEditingBox;
19779
+ },
18006
19780
  draw(context) {
19781
+ renderedEditingBox = undefined;
18007
19782
  if (!graphics)
18008
19783
  return;
18009
19784
  graphics.clear();
18010
19785
  drawExternalPreview(graphics, context.previewOverlay, context.viewportScale, style);
18011
- drawEditingPreview(graphics, editingEngine, context.frame, context.viewportScale, style, keypointStyle);
19786
+ renderedEditingBox = drawEditingPreview(graphics, editingEngine, context.frame, context.viewportScale, style, keypointStyle);
18012
19787
  drawSelectionHandles(graphics, context, style, editingEngine);
18013
19788
  drawMarquee(graphics, context.marquee, style);
18014
19789
  drawGuides(graphics, editingEngine, context, style);
@@ -18021,6 +19796,7 @@ function drawEditingPreview(graphics, engine, frame, viewportScale, style, keypo
18021
19796
  const detection = state?.preview;
18022
19797
  if (!detection)
18023
19798
  return;
19799
+ let renderedBox;
18024
19800
  const styleContext = {
18025
19801
  gestureKind: state.kind,
18026
19802
  viewportScale,
@@ -18047,11 +19823,16 @@ function drawEditingPreview(graphics, engine, frame, viewportScale, style, keypo
18047
19823
  { x: x + width / 2, y: y + height / 2 },
18048
19824
  { x: x - width / 2, y: y + height / 2 },
18049
19825
  ], true, stroke, viewportScale);
19826
+ if (width > 0 &&
19827
+ height > 0 &&
19828
+ (boxFill.alpha > 0 || (stroke.alpha > 0 && stroke.width > 0))) {
19829
+ renderedBox = { detection, rect: detection.rect };
19830
+ }
18050
19831
  }
18051
19832
  if (detection.polygon) {
18052
19833
  if (state.kind === AnnotationGestureStateKind.Creating) {
18053
19834
  drawCreatingPolygon(graphics, detection.polygon.points, viewportScale, stroke, closeZoneStroke);
18054
- return;
19835
+ return renderedBox;
18055
19836
  }
18056
19837
  graphics
18057
19838
  .poly(detection.polygon.points.flatMap(({ x, y }) => [x, y]), true)
@@ -18083,6 +19864,7 @@ function drawEditingPreview(graphics, engine, frame, viewportScale, style, keypo
18083
19864
  if (instruction)
18084
19865
  drawPixiKeypointInstruction(graphics, instruction, viewportScale);
18085
19866
  }
19867
+ return renderedBox;
18086
19868
  }
18087
19869
  const CREATION_VERTEX_RADIUS = 6;
18088
19870
  const CREATION_VERTEX_STROKE_WIDTH = 1.5;
@@ -18335,92 +20117,6 @@ function resolveAnnotationOverlayStyle(style) {
18335
20117
  };
18336
20118
  }
18337
20119
 
18338
- var PixiSceneLayerKind;
18339
- (function (PixiSceneLayerKind) {
18340
- PixiSceneLayerKind["Media"] = "media";
18341
- PixiSceneLayerKind["Depth"] = "depth";
18342
- PixiSceneLayerKind["Heatmap"] = "heatmap";
18343
- PixiSceneLayerKind["Mask"] = "mask";
18344
- PixiSceneLayerKind["Box"] = "box";
18345
- PixiSceneLayerKind["Vector"] = "vector";
18346
- PixiSceneLayerKind["Region"] = "region";
18347
- PixiSceneLayerKind["Focus"] = "focus";
18348
- PixiSceneLayerKind["Preview"] = "preview";
18349
- PixiSceneLayerKind["Guide"] = "guide";
18350
- PixiSceneLayerKind["Handle"] = "handle";
18351
- PixiSceneLayerKind["Interaction"] = "interaction";
18352
- PixiSceneLayerKind["Label"] = "label";
18353
- })(PixiSceneLayerKind || (PixiSceneLayerKind = {}));
18354
- function createPixiSceneLayerSlot(kind, order = defaultLayerOrder(kind)) {
18355
- let display;
18356
- return {
18357
- getDisplay() {
18358
- return display;
18359
- },
18360
- kind,
18361
- order,
18362
- setDisplay(nextDisplay) {
18363
- display = nextDisplay;
18364
- },
18365
- };
18366
- }
18367
- function syncPixiSceneLayerChildren(scene, slots) {
18368
- if (!scene) {
18369
- return;
18370
- }
18371
- const displays = [...slots]
18372
- .sort((left, right) => left.order - right.order)
18373
- .map((slot) => slot.getDisplay())
18374
- .filter((display) => display !== undefined);
18375
- clearSceneChildren(scene);
18376
- if (displays.length === 0) {
18377
- return;
18378
- }
18379
- scene.addChild(...displays);
18380
- }
18381
- function defaultLayerOrder(kind) {
18382
- switch (kind) {
18383
- case PixiSceneLayerKind.Media:
18384
- return 0;
18385
- case PixiSceneLayerKind.Depth:
18386
- return 25;
18387
- case PixiSceneLayerKind.Heatmap:
18388
- return 50;
18389
- case PixiSceneLayerKind.Mask:
18390
- return 100;
18391
- case PixiSceneLayerKind.Box:
18392
- return 200;
18393
- case PixiSceneLayerKind.Vector:
18394
- return 250;
18395
- case PixiSceneLayerKind.Region:
18396
- return 275;
18397
- case PixiSceneLayerKind.Focus:
18398
- return 300;
18399
- case PixiSceneLayerKind.Preview:
18400
- return 400;
18401
- case PixiSceneLayerKind.Guide:
18402
- return 500;
18403
- case PixiSceneLayerKind.Handle:
18404
- return 750;
18405
- case PixiSceneLayerKind.Interaction:
18406
- return 700;
18407
- case PixiSceneLayerKind.Label:
18408
- return 800;
18409
- default:
18410
- return 350;
18411
- }
18412
- }
18413
- function clearSceneChildren(scene) {
18414
- if (typeof scene.removeChildren === "function") {
18415
- scene.removeChildren();
18416
- return;
18417
- }
18418
- const fakeScene = scene;
18419
- if (Array.isArray(fakeScene.children)) {
18420
- fakeScene.children.splice(0);
18421
- }
18422
- }
18423
-
18424
20120
  function calculatePixiSceneFit(options) {
18425
20121
  const { fit, mediaHeight, mediaWidth, screenHeight, screenWidth } = options;
18426
20122
  if (mediaWidth <= 0 ||
@@ -18456,6 +20152,7 @@ const REGION_COVERAGE_ONLY_MASK_STYLE = {
18456
20152
  const STATIC_FOCUS_SETTLE_MS = 10_000;
18457
20153
  /** The presentation fields a render answers to that no renderer kind owns. */
18458
20154
  const RENDERED_PRESENTATION_TAIL = [
20155
+ "annotationAntialiasing",
18459
20156
  "annotationOverlayStyle",
18460
20157
  "backgroundColor",
18461
20158
  "focusStyle",
@@ -18551,6 +20248,14 @@ async function createPixiMediaScene(options) {
18551
20248
  let currentFocusStyle = options.focusStyle ?? null;
18552
20249
  let currentLabelStyle = options.labelStyle ?? null;
18553
20250
  let currentMaskStyle = options.maskStyle ?? null;
20251
+ let annotationAntialiasing = options.annotationAntialiasing ?? false;
20252
+ let annotationAntialiasResolution = 1;
20253
+ const annotationAntialiasLayer = createPixiAnnotationAntialiasLayer({
20254
+ Container,
20255
+ Filter,
20256
+ defaultFilterVert,
20257
+ getEnabled: () => Boolean(annotationAntialiasing),
20258
+ });
18554
20259
  let currentMaskHaloStyle = options.maskHaloStyle ?? null;
18555
20260
  let currentBoxCornerStyle = options.boxCornerStyle ?? null;
18556
20261
  let currentEllipseStyle = options.ellipseStyle ?? null;
@@ -18572,6 +20277,9 @@ async function createPixiMediaScene(options) {
18572
20277
  let isScrubbing = false;
18573
20278
  let displayBrightness = 1;
18574
20279
  let displayContrast = 1;
20280
+ let displayGamma = 1;
20281
+ let displayAutoContrast = false;
20282
+ let inspectionFilter;
18575
20283
  let viewportScale = 1;
18576
20284
  let hasPresentedSample = false;
18577
20285
  let mediaHeight = 0;
@@ -18642,6 +20350,7 @@ async function createPixiMediaScene(options) {
18642
20350
  isVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
18643
20351
  // A partial heatmap changes pixels before the all-ready token changes.
18644
20352
  onPreparedWindowChange: () => redrawAnnotationsNow(true),
20353
+ onTextureRelease: (texture) => focusLayer?.releaseMaskTexture(texture),
18645
20354
  prepareTexture: (texture) => app.renderer.prepare.upload(texture),
18646
20355
  renderPreparation: options.renderPreparation,
18647
20356
  renderers: currentHeatmapRenderers,
@@ -18703,6 +20412,7 @@ async function createPixiMediaScene(options) {
18703
20412
  resolveContextState,
18704
20413
  })
18705
20414
  : undefined;
20415
+ polygonLayer?.setPlaybackActive(isPlaybackActive);
18706
20416
  const vectorLayer = createPixiVectorLayer({
18707
20417
  container: retainedGeometry,
18708
20418
  Container,
@@ -18732,6 +20442,9 @@ async function createPixiMediaScene(options) {
18732
20442
  Texture,
18733
20443
  UniformGroup,
18734
20444
  detectionTimeline: annotationDetectionTimeline,
20445
+ getAnnotationAntialiasing: () => Boolean(annotationAntialiasing),
20446
+ getAntialiasResolution: () => annotationAntialiasResolution,
20447
+ getViewportBounds: resolveViewportBounds,
18735
20448
  getActiveRegionMaskCoverage: (detectionFrameTime) => maskLayer?.getActiveRegionMaskCoverage(detectionFrameTime) ?? null,
18736
20449
  // Under GPU compositing the decoded frame lands in a texture the
18737
20450
  // compositor swaps onto the media sprite, leaving the canvas that
@@ -18783,6 +20496,7 @@ async function createPixiMediaScene(options) {
18783
20496
  Text,
18784
20497
  detectionTimeline: annotationDetectionTimeline,
18785
20498
  labelStyle: options.labelStyle,
20499
+ getRenderedBoxes: () => boxLayer.getRenderedBoxes(),
18786
20500
  resolveContextState: resolveLabelContextState,
18787
20501
  })
18788
20502
  : undefined;
@@ -18796,6 +20510,7 @@ async function createPixiMediaScene(options) {
18796
20510
  resolution: resolvePixiResolution(options.maxDevicePixelRatio),
18797
20511
  });
18798
20512
  const destroyBatchTextureBindings = installPixiBatchTextureBindings(pixi, app.renderer);
20513
+ const destroyFilterBindings = installPixiFilterBindings(pixi, app.renderer);
18799
20514
  const rendererCanvas = app.canvas;
18800
20515
  rendererCanvas.style.display = "block";
18801
20516
  rendererCanvas.style.position = "absolute";
@@ -18841,6 +20556,7 @@ async function createPixiMediaScene(options) {
18841
20556
  const focusSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Focus);
18842
20557
  const previewSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Preview);
18843
20558
  const interactionSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Interaction);
20559
+ const interactionLabelSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Label, interactionSlot.order);
18844
20560
  const handleSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Handle);
18845
20561
  const labelSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Label);
18846
20562
  const layerSlots = [
@@ -18855,6 +20571,7 @@ async function createPixiMediaScene(options) {
18855
20571
  previewSlot,
18856
20572
  handleSlot,
18857
20573
  interactionSlot,
20574
+ interactionLabelSlot,
18858
20575
  labelSlot,
18859
20576
  ];
18860
20577
  const viewport = createViewportController({ scale: 1 });
@@ -19026,13 +20743,18 @@ async function createPixiMediaScene(options) {
19026
20743
  }
19027
20744
  let focusLayer = options.focusStyle
19028
20745
  ? createPixiFocusLayer({
20746
+ AlphaMask,
19029
20747
  Container,
19030
20748
  Graphics,
19031
20749
  ImageSource,
19032
20750
  Mesh,
19033
20751
  MeshGeometry,
20752
+ Rectangle,
19034
20753
  Shader,
19035
20754
  UniformGroup,
20755
+ getHeatmapRenderers: () => currentHeatmapRenderers,
20756
+ getAnnotationAntialiasing: () => Boolean(annotationAntialiasing),
20757
+ getViewportBounds: resolveViewportBounds,
19036
20758
  focusStyle: options.focusStyle,
19037
20759
  isDetectionVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
19038
20760
  })
@@ -19042,7 +20764,9 @@ async function createPixiMediaScene(options) {
19042
20764
  let stagingTextureSource;
19043
20765
  const collectFrameTimings = options.diagnostics?.frameTimings === true;
19044
20766
  const syncSceneChildren = () => {
19045
- syncPixiSceneLayerChildren(mediaScene, layerSlots);
20767
+ labelLayer?.setBackgroundAntialiasFilter(annotationAntialiasLayer.getFxaaFilter());
20768
+ interactionPresentationLayer?.setBackgroundAntialiasFilter(annotationAntialiasLayer.getFxaaFilter());
20769
+ annotationAntialiasLayer.sync(mediaScene, layerSlots);
19046
20770
  };
19047
20771
  const updateMediaSceneFit = () => {
19048
20772
  if (!mediaScene || mediaWidth <= 0 || mediaHeight <= 0) {
@@ -19108,8 +20832,24 @@ async function createPixiMediaScene(options) {
19108
20832
  rendererCanvas.style.left = `${next.left}px`;
19109
20833
  rendererCanvas.style.top = `${next.top}px`;
19110
20834
  app.renderer.resize(next.width, next.height, presentationResolution);
20835
+ syncAnnotationAntialiasResolution();
19111
20836
  return presentationBox;
19112
20837
  };
20838
+ const syncAnnotationAntialiasResolution = () => {
20839
+ annotationAntialiasResolution = resolvePixiAnnotationAntialiasResolution(presentationResolution, containerSize, maxTextureSize(), annotationAntialiasing === 2 ? 2 : 1);
20840
+ annotationAntialiasLayer.setResolution(annotationAntialiasResolution);
20841
+ regionLayer.setAntialiasResolution(annotationAntialiasResolution);
20842
+ syncMaskRasterDisplay();
20843
+ };
20844
+ function syncMaskRasterDisplay() {
20845
+ if (!rasterDisplay)
20846
+ return;
20847
+ const scale = annotationAntialiasing
20848
+ ? annotationAntialiasResolution / presentationResolution
20849
+ : 1;
20850
+ maskLayer?.setRasterDisplay(rasterDisplay, scale);
20851
+ polygonLayer?.setRasterDisplay(rasterDisplay, scale);
20852
+ }
19113
20853
  const syncDisplaySizing = () => {
19114
20854
  if (!baseFit || mediaWidth <= 0 || mediaHeight <= 0)
19115
20855
  return;
@@ -19126,7 +20866,7 @@ async function createPixiMediaScene(options) {
19126
20866
  return;
19127
20867
  }
19128
20868
  rasterDisplay = display;
19129
- maskLayer?.setRasterDisplay(display);
20869
+ syncMaskRasterDisplay();
19130
20870
  options.onDisplayChange?.(display);
19131
20871
  };
19132
20872
  const updateMediaSampling = () => {
@@ -19147,12 +20887,26 @@ async function createPixiMediaScene(options) {
19147
20887
  return;
19148
20888
  const transform = viewport.getTransform();
19149
20889
  viewportScale = baseFit.scale * transform.scale;
20890
+ maskLayer?.setViewportScale(viewportScale);
20891
+ polygonLayer?.setViewportScale(viewportScale);
19150
20892
  updateMediaSampling();
19151
20893
  mediaScene.scale.set(viewportScale);
19152
20894
  const box = syncPresentationBox();
19153
20895
  mediaScene.position.set(baseFit.x + transform.x - box.left, baseFit.y + transform.y - box.top);
20896
+ focusLayer?.syncViewportBounds();
20897
+ regionLayer.syncViewportBounds();
19154
20898
  maskBrushPreview?.setViewportScale(viewportScale);
19155
20899
  };
20900
+ function resolveViewportBounds() {
20901
+ if (!mediaScene || viewportScale <= 0)
20902
+ return undefined;
20903
+ return {
20904
+ x: -mediaScene.x / viewportScale,
20905
+ y: -mediaScene.y / viewportScale,
20906
+ width: app.renderer.screen.width / viewportScale,
20907
+ height: app.renderer.screen.height / viewportScale,
20908
+ };
20909
+ }
19156
20910
  const mediaPointToScreen = (point) => {
19157
20911
  const fit = baseFit ?? { scale: 1, x: 0, y: 0 };
19158
20912
  const screenPoint = viewport.mediaToScreen({
@@ -19174,6 +20928,7 @@ async function createPixiMediaScene(options) {
19174
20928
  syncPresentationBox();
19175
20929
  updateMediaSceneFit();
19176
20930
  syncDisplaySizing();
20931
+ drawInteractionPresentationLayer(currentMediaTime);
19177
20932
  renderOnChange();
19178
20933
  });
19179
20934
  measureContainer();
@@ -19389,6 +21144,7 @@ async function createPixiMediaScene(options) {
19389
21144
  interactionSlot.setDisplay(interactionPresentationDisplay || interactionHitDisplay
19390
21145
  ? interactionDisplay
19391
21146
  : undefined);
21147
+ interactionLabelSlot.setDisplay(interactionPresentationLayer?.getLabelDisplay() ?? undefined);
19392
21148
  attachFocusLayerDisplay();
19393
21149
  attachMaskLayerDisplay();
19394
21150
  attachLabelLayerDisplay();
@@ -19405,6 +21161,7 @@ async function createPixiMediaScene(options) {
19405
21161
  setPlaybackActive(active) {
19406
21162
  isPlaybackActive = active;
19407
21163
  maskLayer?.setPlaybackActive(active);
21164
+ polygonLayer?.setPlaybackActive(active);
19408
21165
  depthSource?.setPlaybackActive?.(active);
19409
21166
  },
19410
21167
  setScrubbing(scrubbing) {
@@ -19429,6 +21186,7 @@ async function createPixiMediaScene(options) {
19429
21186
  if (!collectFrameTimings) {
19430
21187
  sample.draw(stagingContext, 0, 0, mediaWidth, mediaHeight);
19431
21188
  presentedSampleTimestamp = sample.timestamp;
21189
+ refreshInspectionImage();
19432
21190
  stagingTextureSource?.update();
19433
21191
  stagingTexture?.update();
19434
21192
  const { boxState, regionState } = drawFramePresentLayers(annotationDrawLayers, sample.timestamp);
@@ -19444,6 +21202,7 @@ async function createPixiMediaScene(options) {
19444
21202
  const mediaUploadMs = measure(() => {
19445
21203
  sample.draw(stagingContext, 0, 0, mediaWidth, mediaHeight);
19446
21204
  presentedSampleTimestamp = sample.timestamp;
21205
+ refreshInspectionImage();
19447
21206
  stagingTextureSource?.update();
19448
21207
  stagingTexture?.update();
19449
21208
  });
@@ -19503,19 +21262,32 @@ async function createPixiMediaScene(options) {
19503
21262
  syncPresentationBox();
19504
21263
  updateMediaSceneFit();
19505
21264
  syncDisplaySizing();
21265
+ drawInteractionPresentationLayer(currentMediaTime);
19506
21266
  renderOnChange();
19507
21267
  },
19508
21268
  setDisplayAdjustments(adjustments) {
19509
21269
  void import('pixi.js').then(({ ColorMatrixFilter }) => {
21270
+ if (isDestroyed)
21271
+ return;
19510
21272
  const brightness = adjustments.brightness ?? 1;
19511
21273
  const contrast = adjustments.contrast ?? 1;
19512
21274
  const filter = new ColorMatrixFilter();
19513
21275
  // Preserve the renderer's pixel grid through the adjustment pass.
19514
21276
  filter.resolution = "inherit";
19515
21277
  filter.brightness(brightness, false);
19516
- filter.contrast(contrast - 1, true);
19517
- if (mediaSprite)
19518
- mediaSprite.filters = [filter];
21278
+ displayGamma = adjustments.gamma ?? 1;
21279
+ displayAutoContrast = stillFrame && adjustments.autoContrast === true;
21280
+ filter.contrast(displayAutoContrast ? 0 : contrast - 1, true);
21281
+ if (mediaSprite) {
21282
+ if (displayGamma !== 1 || displayAutoContrast) {
21283
+ inspectionFilter ??= createPixiMediaInspectionFilter(mediaSprite, pixi);
21284
+ inspectionFilter.update(displayGamma, displayAutoContrast, hasPresentedSample ? stagingCanvas : null);
21285
+ mediaSprite.filters = [inspectionFilter.filter, filter];
21286
+ }
21287
+ else {
21288
+ mediaSprite.filters = [filter];
21289
+ }
21290
+ }
19519
21291
  displayBrightness = brightness;
19520
21292
  displayContrast = contrast;
19521
21293
  renderOnChange();
@@ -19620,6 +21392,12 @@ async function createPixiMediaScene(options) {
19620
21392
  setPresentation(presentation, mediaTime) {
19621
21393
  appliedPresentation = presentation;
19622
21394
  currentMediaTime = mediaTime;
21395
+ if (annotationAntialiasing !==
21396
+ (presentation.annotationAntialiasing ?? false)) {
21397
+ annotationAntialiasing = presentation.annotationAntialiasing ?? false;
21398
+ syncAnnotationAntialiasResolution();
21399
+ syncSceneChildren();
21400
+ }
19623
21401
  if (presentation.backgroundColor !== undefined) {
19624
21402
  app.renderer.background.color = presentation.backgroundColor;
19625
21403
  applyBackdropColor(backdrop, presentation.backgroundColor);
@@ -19822,6 +21600,11 @@ async function createPixiMediaScene(options) {
19822
21600
  // a decoder buffer in a producer that outlives the scene.
19823
21601
  frameChannel?.onPresentedFrame((presented) => presented.frame.close());
19824
21602
  destroyBatchTextureBindings();
21603
+ destroyFilterBindings();
21604
+ labelLayer?.setBackgroundAntialiasFilter(null);
21605
+ interactionPresentationLayer?.setBackgroundAntialiasFilter(null);
21606
+ annotationAntialiasLayer.destroy();
21607
+ inspectionFilter?.destroy();
19825
21608
  mediaCompositor?.destroy();
19826
21609
  interactionLayer?.destroy();
19827
21610
  interactionPresentationLayer?.destroy();
@@ -19929,8 +21712,8 @@ async function createPixiMediaScene(options) {
19929
21712
  renderPreparation: options.renderPreparation,
19930
21713
  resolveInstructions: resolveArtifactMaskInstructions,
19931
21714
  });
19932
- if (rasterDisplay)
19933
- maskLayer.setRasterDisplay(rasterDisplay);
21715
+ syncMaskRasterDisplay();
21716
+ maskLayer.setViewportScale(viewportScale);
19934
21717
  maskLayer.setPlaybackActive(isPlaybackActive);
19935
21718
  if (timelineContext) {
19936
21719
  maskLayer.setTimelineContext(timelineContext);
@@ -20184,6 +21967,9 @@ async function createPixiMediaScene(options) {
20184
21967
  renderPreparation: options.renderPreparation,
20185
21968
  resolveContextState,
20186
21969
  });
21970
+ polygonLayer.setPlaybackActive(isPlaybackActive);
21971
+ syncMaskRasterDisplay();
21972
+ polygonLayer.setViewportScale(viewportScale);
20187
21973
  if (timelineContext) {
20188
21974
  polygonLayer.setTimelineContext(timelineContext);
20189
21975
  }
@@ -20224,14 +22010,19 @@ async function createPixiMediaScene(options) {
20224
22010
  function ensureFocusLayer(focusStyle) {
20225
22011
  if (!focusLayer) {
20226
22012
  focusLayer = createPixiFocusLayer({
22013
+ AlphaMask,
20227
22014
  Container,
20228
22015
  Graphics,
20229
22016
  ImageSource,
20230
22017
  Mesh,
20231
22018
  MeshGeometry,
22019
+ Rectangle,
20232
22020
  Shader,
20233
22021
  UniformGroup,
20234
22022
  focusStyle,
22023
+ getHeatmapRenderers: () => currentHeatmapRenderers,
22024
+ getAnnotationAntialiasing: () => Boolean(annotationAntialiasing),
22025
+ getViewportBounds: resolveViewportBounds,
20235
22026
  isDetectionVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
20236
22027
  });
20237
22028
  }
@@ -20272,6 +22063,7 @@ async function createPixiMediaScene(options) {
20272
22063
  const interactionState = interactionLayer?.getState();
20273
22064
  focusLayer.drawFrame({
20274
22065
  frame,
22066
+ heatmapArtifacts: heatmapLayer?.getActiveFocusArtifacts(frame) ?? [],
20275
22067
  hoveredPick: withEditingPreview(filterVisiblePick(interactionState?.hoveredPick ?? null)),
20276
22068
  idMaskArtifact: maskLayer?.getActiveIdMaskFrameTexture(frame?.mediaTime ?? null) ??
20277
22069
  null,
@@ -20304,6 +22096,9 @@ async function createPixiMediaScene(options) {
20304
22096
  mediaTime,
20305
22097
  selectedPick: interactionState?.selectedPick ?? null,
20306
22098
  selectedPicks: interactionState?.selectedPicks,
22099
+ strokePixelRatio: annotationAntialiasing
22100
+ ? annotationAntialiasResolution
22101
+ : presentationResolution,
20307
22102
  viewportScale,
20308
22103
  });
20309
22104
  }
@@ -20382,9 +22177,6 @@ async function createPixiMediaScene(options) {
20382
22177
  const frame = annotationDetectionTimeline.selectFrame(mediaTime);
20383
22178
  const interactionState = interactionLayer?.getState();
20384
22179
  const editingState = options.editingEngine?.getState();
20385
- labelLayer?.drawCreationPreview(editingState?.kind === AnnotationGestureStateKind.Creating
20386
- ? editingState.preview
20387
- : null, mediaTime, viewportScale);
20388
22180
  const previewOverlay = options.previewOverlay?.() ?? null;
20389
22181
  drawnEditingState = editingState;
20390
22182
  // Only a scene that renders on change compares signatures.
@@ -20404,6 +22196,18 @@ async function createPixiMediaScene(options) {
20404
22196
  viewportScale,
20405
22197
  visibility: currentVisibility,
20406
22198
  });
22199
+ const editingBox = annotationOverlayLayer.getRenderedEditingBox();
22200
+ const creationPreview = editingState?.kind === AnnotationGestureStateKind.Creating
22201
+ ? editingState.preview
22202
+ : null;
22203
+ labelLayer?.drawCreationPreview(creationPreview, mediaTime, viewportScale, editingBox && editingBox.detection === creationPreview
22204
+ ? editingBox.rect
22205
+ : undefined);
22206
+ if (editingState?.activeDetectionId != null) {
22207
+ labelLayer?.updateEditingBox(editingState.activeDetectionId, editingBox?.detection.id === editingState.activeDetectionId
22208
+ ? editingBox.rect
22209
+ : undefined);
22210
+ }
20407
22211
  }
20408
22212
  /**
20409
22213
  * Milliseconds for animations that are not annotation geometry. A pull scene
@@ -20532,6 +22336,12 @@ async function createPixiMediaScene(options) {
20532
22336
  app.ticker.remove(drawAnnotationOverlayNow);
20533
22337
  }
20534
22338
  }
22339
+ function refreshInspectionImage() {
22340
+ if (!stillFrame || !inspectionFilter)
22341
+ return;
22342
+ inspectionFilter.invalidateImage();
22343
+ inspectionFilter.update(displayGamma, displayAutoContrast, stagingCanvas);
22344
+ }
20535
22345
  function hasLoadingDetections() {
20536
22346
  const loading = currentVisibility?.loadingDetectionIds;
20537
22347
  if (!loading)
@@ -20563,6 +22373,8 @@ async function createPixiMediaScene(options) {
20563
22373
  app.renderer.resolution,
20564
22374
  displayBrightness,
20565
22375
  displayContrast,
22376
+ displayGamma,
22377
+ displayAutoContrast,
20566
22378
  ...RENDERED_PRESENTATION_FIELDS.map((field) => appliedPresentation?.[field]),
20567
22379
  // Styles above compare by identity; mask opacity is applied after
20568
22380
  // preparation, so a host can move it inside a style object it keeps.
@@ -20663,6 +22475,7 @@ async function createPixiMediaScene(options) {
20663
22475
  Text,
20664
22476
  detectionTimeline: annotationDetectionTimeline,
20665
22477
  labelStyle,
22478
+ getRenderedBoxes: () => boxLayer.getRenderedBoxes(),
20666
22479
  resolveContextState: resolveLabelContextState,
20667
22480
  });
20668
22481
  }
@@ -21879,6 +23692,7 @@ async function createMediaSession(options) {
21879
23692
  ...options.renderer,
21880
23693
  ...sessionMedia.rendererSourceOption,
21881
23694
  backgroundColor: initialPresentation.backgroundColor,
23695
+ annotationAntialiasing: initialPresentation.annotationAntialiasing,
21882
23696
  annotationOverlayStyle: initialPresentation.annotationOverlayStyle,
21883
23697
  boxStyle: initialPresentation.boxStyle,
21884
23698
  boxCornerStyle: initialPresentation.boxCornerStyle,