supervision 0.2.9 → 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.
- package/dist/{depth-preview-probe-DG7McQEN.js → depth-preview-probe-BG0k-33U.js} +3 -3
- package/dist/{depth-preview-probe-DG7McQEN.js.map → depth-preview-probe-BG0k-33U.js.map} +1 -1
- package/dist/{depth-preview-track-BvgwNEE9.js → depth-preview-track-BEag1ZWF.js} +2 -2
- package/dist/{depth-preview-track-BvgwNEE9.js.map → depth-preview-track-BEag1ZWF.js.map} +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2041 -478
- package/dist/index.js.map +1 -1
- package/dist/mask-preparation.worker.js +166 -45
- package/dist/mask-preparation.worker.js.map +1 -1
- package/dist/media/display-pixel-ratio.d.ts +3 -8
- package/dist/media/display-pixel-ratio.d.ts.map +1 -1
- package/dist/media/video-engine-media-source.js +1 -1
- package/dist/render-preparation/mask-frame-artifact.d.ts +7 -1
- package/dist/render-preparation/mask-frame-artifact.d.ts.map +1 -1
- package/dist/render-preparation/mask-frame-compositor.d.ts +1 -1
- package/dist/render-preparation/mask-frame-compositor.d.ts.map +1 -1
- package/dist/render-preparation/mask-preparation-worker-protocol.d.ts +3 -0
- package/dist/render-preparation/mask-preparation-worker-protocol.d.ts.map +1 -1
- package/dist/render-preparation/prepared-render-window.d.ts +2 -0
- package/dist/render-preparation/prepared-render-window.d.ts.map +1 -1
- package/dist/renderers/annotation-fxaa.d.ts +37 -0
- package/dist/renderers/annotation-fxaa.d.ts.map +1 -0
- package/dist/renderers/id-mask-stroke-coverage.d.ts +2 -2
- package/dist/renderers/id-mask-stroke-coverage.d.ts.map +1 -1
- package/dist/renderers/mask-palette.d.ts +4 -8
- package/dist/renderers/mask-palette.d.ts.map +1 -1
- package/dist/renderers/media-renderer-core.d.ts.map +1 -1
- package/dist/renderers/media-renderer-scene.d.ts +1 -0
- package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
- package/dist/renderers/pixi-annotation-antialias-layer.d.ts +15 -0
- package/dist/renderers/pixi-annotation-antialias-layer.d.ts.map +1 -0
- package/dist/renderers/pixi-annotation-antialias.d.ts +14 -0
- package/dist/renderers/pixi-annotation-antialias.d.ts.map +1 -0
- package/dist/renderers/pixi-annotation-overlay-layer.d.ts +6 -1
- package/dist/renderers/pixi-annotation-overlay-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-box-layer.d.ts +6 -1
- package/dist/renderers/pixi-box-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-filter-bindings.d.ts +6 -0
- package/dist/renderers/pixi-filter-bindings.d.ts.map +1 -0
- package/dist/renderers/pixi-focus-heatmap-cutout.d.ts +17 -0
- package/dist/renderers/pixi-focus-heatmap-cutout.d.ts.map +1 -0
- package/dist/renderers/pixi-focus-layer.d.ts +18 -1
- package/dist/renderers/pixi-focus-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-heatmap-layer.d.ts +4 -1
- package/dist/renderers/pixi-heatmap-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-id-mask-shader.d.ts +2 -1
- package/dist/renderers/pixi-id-mask-shader.d.ts.map +1 -1
- package/dist/renderers/pixi-interaction-presentation-layer.d.ts +5 -1
- package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-label-layer.d.ts +8 -4
- package/dist/renderers/pixi-label-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-mask-layer.d.ts +2 -1
- package/dist/renderers/pixi-mask-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-mask-screen-stroke.d.ts +1 -1
- package/dist/renderers/pixi-mask-screen-stroke.d.ts.map +1 -1
- package/dist/renderers/pixi-mask-viewport.d.ts +21 -0
- package/dist/renderers/pixi-mask-viewport.d.ts.map +1 -0
- package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
- package/dist/renderers/pixi-polygon-layer.d.ts +3 -0
- package/dist/renderers/pixi-polygon-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-region-coverage-mask.d.ts +14 -3
- package/dist/renderers/pixi-region-coverage-mask.d.ts.map +1 -1
- package/dist/renderers/pixi-region-layer.d.ts +7 -1
- package/dist/renderers/pixi-region-layer.d.ts.map +1 -1
- package/dist/sessions/media-session.d.ts.map +1 -1
- package/dist/tracking.worker.js.map +1 -1
- package/dist/types/media-renderer.d.ts +1 -1
- package/dist/types/media-renderer.d.ts.map +1 -1
- package/dist/{video-engine-media-source-DOFsuGS4.js → video-engine-media-source-MFthAkbe.js} +4 -9
- package/dist/video-engine-media-source-MFthAkbe.js.map +1 -0
- package/node_modules/supervision-js-core/dist/index.d.ts +2 -2
- package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/index.js +57 -25
- package/node_modules/supervision-js-core/dist/index.js.map +1 -1
- package/node_modules/supervision-js-core/dist/styles/default-annotation-presentation.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/styles/focus-style.d.ts +4 -3
- package/node_modules/supervision-js-core/dist/styles/focus-style.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/styles/mask-style.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/focus-style.d.ts +2 -1
- package/node_modules/supervision-js-core/dist/types/focus-style.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/label-style.d.ts +4 -1
- package/node_modules/supervision-js-core/dist/types/label-style.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/mask-style.d.ts +8 -1
- package/node_modules/supervision-js-core/dist/types/mask-style.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +14 -0
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/utils/detection-conversions.d.ts +1 -0
- package/node_modules/supervision-js-core/dist/utils/detection-conversions.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts +21 -7
- package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts.map +1 -1
- package/package.json +1 -1
- 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,
|
|
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-
|
|
4
|
-
export { a as createWebVideoEngineMediaRendererSource, g as getMediaErrorKind, i as isMediaSourceError, o as openWebVideoEngineMediaSource } from './video-engine-media-source-
|
|
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-
|
|
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-
|
|
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(
|
|
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
|
|
5309
|
-
bounds
|
|
5310
|
-
|
|
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
|
|
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
|
|
5411
|
-
const
|
|
5412
|
-
|
|
5413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6190
|
+
let tier = scheduleOptions.tier ?? getRequestedRasterTier();
|
|
6099
6191
|
if (emptyMaskFrameKeys.has(key)) {
|
|
6100
6192
|
return false;
|
|
6101
6193
|
}
|
|
6102
|
-
|
|
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
|
-
|
|
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 ? (
|
|
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
|
|
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,
|
|
@@ -11409,6 +11557,8 @@ function createPixiBoxLayer(options) {
|
|
|
11409
11557
|
let retainedContainer = options.container;
|
|
11410
11558
|
const retainedEntries = new Map();
|
|
11411
11559
|
const invalidatedDetections = new Set();
|
|
11560
|
+
const renderedRects = new Map();
|
|
11561
|
+
let renderedBoxFrame;
|
|
11412
11562
|
const usingRetainedEntries = Boolean(options.Container && options.Graphics);
|
|
11413
11563
|
return {
|
|
11414
11564
|
createContainer() {
|
|
@@ -11437,6 +11587,10 @@ function createPixiBoxLayer(options) {
|
|
|
11437
11587
|
boxGraphics?.clear();
|
|
11438
11588
|
const activeDetectionIndexes = [];
|
|
11439
11589
|
const activeKeys = new Set();
|
|
11590
|
+
renderedRects.clear();
|
|
11591
|
+
renderedBoxFrame = detectionFrame
|
|
11592
|
+
? { frame: detectionFrame, rects: renderedRects }
|
|
11593
|
+
: undefined;
|
|
11440
11594
|
if (boxStyle && detectionFrame) {
|
|
11441
11595
|
const orderedDetections = detectionFrame.detections
|
|
11442
11596
|
.map((detection, detectionIndex) => ({ detection, detectionIndex }))
|
|
@@ -11468,10 +11622,13 @@ function createPixiBoxLayer(options) {
|
|
|
11468
11622
|
display.position?.set?.(0, 0);
|
|
11469
11623
|
display.clear();
|
|
11470
11624
|
drawBoxInstruction(display, instruction, viewportScale);
|
|
11625
|
+
retainRenderedBox(detectionIndex, instruction);
|
|
11471
11626
|
invalidatedDetections.delete(key);
|
|
11472
11627
|
}
|
|
11473
11628
|
else {
|
|
11474
11629
|
drawBoxInstruction(boxGraphics, instruction, viewportScale);
|
|
11630
|
+
if (boxGraphics)
|
|
11631
|
+
retainRenderedBox(detectionIndex, instruction);
|
|
11475
11632
|
}
|
|
11476
11633
|
}
|
|
11477
11634
|
}
|
|
@@ -11484,6 +11641,9 @@ function createPixiBoxLayer(options) {
|
|
|
11484
11641
|
lastDrawnState = getBoxLayerState(detectionFrame, activeDetectionIndexes);
|
|
11485
11642
|
return lastDrawnState;
|
|
11486
11643
|
},
|
|
11644
|
+
getRenderedBoxes() {
|
|
11645
|
+
return renderedBoxFrame;
|
|
11646
|
+
},
|
|
11487
11647
|
setBoxStyle(nextBoxStyle) {
|
|
11488
11648
|
if (nextBoxStyle === undefined) {
|
|
11489
11649
|
return;
|
|
@@ -11506,6 +11666,16 @@ function createPixiBoxLayer(options) {
|
|
|
11506
11666
|
return true;
|
|
11507
11667
|
},
|
|
11508
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
|
+
}
|
|
11509
11679
|
function ensureRetainedEntry(key) {
|
|
11510
11680
|
let display = retainedEntries.get(key);
|
|
11511
11681
|
if (!display) {
|
|
@@ -12599,7 +12769,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12599
12769
|
sprite.removeFromParent();
|
|
12600
12770
|
sprite.destroy();
|
|
12601
12771
|
if (temporaryTexture)
|
|
12602
|
-
texture
|
|
12772
|
+
releaseTexture(texture);
|
|
12603
12773
|
}
|
|
12604
12774
|
positionedSprites.length = 0;
|
|
12605
12775
|
activeKeys.clear();
|
|
@@ -12613,7 +12783,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12613
12783
|
}
|
|
12614
12784
|
clear();
|
|
12615
12785
|
for (const entry of cache.values()) {
|
|
12616
|
-
entry.texture
|
|
12786
|
+
releaseTexture(entry.texture);
|
|
12617
12787
|
entry.image.close();
|
|
12618
12788
|
}
|
|
12619
12789
|
cache.clear();
|
|
@@ -12651,7 +12821,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12651
12821
|
continue;
|
|
12652
12822
|
cache.delete(key);
|
|
12653
12823
|
cachedBytes -= entry.bytes;
|
|
12654
|
-
entry.texture
|
|
12824
|
+
releaseTexture(entry.texture);
|
|
12655
12825
|
entry.image.close();
|
|
12656
12826
|
}
|
|
12657
12827
|
};
|
|
@@ -12699,7 +12869,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12699
12869
|
(right.detection.zIndex ?? right.index) || left.index - right.index);
|
|
12700
12870
|
const result = [];
|
|
12701
12871
|
let pixels = 0;
|
|
12702
|
-
for (const { detection } of orderedDetections) {
|
|
12872
|
+
for (const { detection, index } of orderedDetections) {
|
|
12703
12873
|
const map = detection.heatmap;
|
|
12704
12874
|
if (!map || !options.isVisible(detection))
|
|
12705
12875
|
continue;
|
|
@@ -12712,7 +12882,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12712
12882
|
continue;
|
|
12713
12883
|
}
|
|
12714
12884
|
pixels += map.width * map.height;
|
|
12715
|
-
result.push({ detection, map, renderer });
|
|
12885
|
+
result.push({ detection, detectionIndex: index, map, renderer });
|
|
12716
12886
|
}
|
|
12717
12887
|
}
|
|
12718
12888
|
return result;
|
|
@@ -12747,7 +12917,8 @@ function createPixiHeatmapLayer(options) {
|
|
|
12747
12917
|
await options.prepareTexture?.(texture);
|
|
12748
12918
|
}
|
|
12749
12919
|
catch (error) {
|
|
12750
|
-
texture
|
|
12920
|
+
if (texture)
|
|
12921
|
+
releaseTexture(texture);
|
|
12751
12922
|
image.close();
|
|
12752
12923
|
if (destroyed || job.cancelled || job.generation !== generation) {
|
|
12753
12924
|
return;
|
|
@@ -12765,7 +12936,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12765
12936
|
job.cancelled ||
|
|
12766
12937
|
job.generation !== generation ||
|
|
12767
12938
|
!wantedKeys.has(job.key)) {
|
|
12768
|
-
texture
|
|
12939
|
+
releaseTexture(texture);
|
|
12769
12940
|
image.close();
|
|
12770
12941
|
return;
|
|
12771
12942
|
}
|
|
@@ -12821,6 +12992,20 @@ function createPixiHeatmapLayer(options) {
|
|
|
12821
12992
|
};
|
|
12822
12993
|
return {
|
|
12823
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
|
+
},
|
|
12824
13009
|
setRenderers(next) {
|
|
12825
13010
|
if (next.length === renderers.length &&
|
|
12826
13011
|
next.every((renderer, index) => renderer === renderers[index])) {
|
|
@@ -12886,7 +13071,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12886
13071
|
}
|
|
12887
13072
|
const maps = visibleMaps(frame);
|
|
12888
13073
|
const visiblePixelCount = maps.reduce((total, { map }) => total + map.width * map.height, 0);
|
|
12889
|
-
for (const { detection, map, renderer } of maps) {
|
|
13074
|
+
for (const { detection, detectionIndex, map, renderer } of maps) {
|
|
12890
13075
|
const key = keyFor(map, renderer);
|
|
12891
13076
|
if (shouldPrepareAsync(map, visiblePixelCount)) {
|
|
12892
13077
|
wantedKeys.add(key);
|
|
@@ -12927,6 +13112,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12927
13112
|
positionedSprites.push({
|
|
12928
13113
|
sprite,
|
|
12929
13114
|
detectionId: detection.id,
|
|
13115
|
+
detectionIndex,
|
|
12930
13116
|
x: sprite.x,
|
|
12931
13117
|
y: sprite.y,
|
|
12932
13118
|
temporaryTexture,
|
|
@@ -12957,6 +13143,10 @@ function createPixiHeatmapLayer(options) {
|
|
|
12957
13143
|
container.destroy();
|
|
12958
13144
|
},
|
|
12959
13145
|
};
|
|
13146
|
+
function releaseTexture(texture) {
|
|
13147
|
+
options.onTextureRelease?.(texture);
|
|
13148
|
+
texture.destroy(true);
|
|
13149
|
+
}
|
|
12960
13150
|
}
|
|
12961
13151
|
|
|
12962
13152
|
/**
|
|
@@ -13042,6 +13232,268 @@ function installPixiBatchTextureBindings(pixi, renderer) {
|
|
|
13042
13232
|
};
|
|
13043
13233
|
}
|
|
13044
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
|
+
|
|
13045
13497
|
const MAX_FOCUS_MASK_IDS = 16;
|
|
13046
13498
|
/**
|
|
13047
13499
|
* How far the media may travel before a cutout drawn for an earlier frame stops
|
|
@@ -13064,6 +13516,11 @@ function createPixiFocusLayer(options) {
|
|
|
13064
13516
|
: (options.focusStyle ?? new BaseFocusStyle());
|
|
13065
13517
|
let focusGraphics;
|
|
13066
13518
|
let focusMaskGraphics;
|
|
13519
|
+
let heatmapMaskContainer;
|
|
13520
|
+
let heatmapVectorGraphics;
|
|
13521
|
+
let usesHeatmapAlphaMask = false;
|
|
13522
|
+
let viewportBounds;
|
|
13523
|
+
const heatmapCutouts = new Map();
|
|
13067
13524
|
let idMaskRenderer;
|
|
13068
13525
|
let focusDisplay;
|
|
13069
13526
|
let currentAlpha = 0;
|
|
@@ -13090,6 +13547,7 @@ function createPixiFocusLayer(options) {
|
|
|
13090
13547
|
resetHeldFocus();
|
|
13091
13548
|
focusGraphics = new options.Graphics();
|
|
13092
13549
|
focusGraphics.visible = false;
|
|
13550
|
+
syncViewportBounds(true);
|
|
13093
13551
|
idMaskRenderer = createIdMaskRenderer();
|
|
13094
13552
|
if (!options.Container || !focusGraphics.setMask) {
|
|
13095
13553
|
focusDisplay = focusGraphics;
|
|
@@ -13118,12 +13576,27 @@ function createPixiFocusLayer(options) {
|
|
|
13118
13576
|
hide();
|
|
13119
13577
|
idMaskRenderer?.destroy();
|
|
13120
13578
|
idMaskRenderer = undefined;
|
|
13579
|
+
activateVectorMask();
|
|
13580
|
+
heatmapMaskContainer?.destroy({ children: true });
|
|
13581
|
+
heatmapMaskContainer = undefined;
|
|
13121
13582
|
},
|
|
13122
13583
|
releaseMaskTexture(texture) {
|
|
13123
|
-
if (!isDestroyed)
|
|
13584
|
+
if (!isDestroyed) {
|
|
13124
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
|
+
}
|
|
13125
13597
|
},
|
|
13126
13598
|
drawFrame(context) {
|
|
13599
|
+
syncViewportBounds();
|
|
13127
13600
|
if (isDestroyed || !focusStyle || mediaWidth <= 0 || mediaHeight <= 0) {
|
|
13128
13601
|
hide();
|
|
13129
13602
|
return;
|
|
@@ -13144,11 +13617,15 @@ function createPixiFocusLayer(options) {
|
|
|
13144
13617
|
const instruction = resolvedInstruction
|
|
13145
13618
|
? {
|
|
13146
13619
|
...resolvedInstruction,
|
|
13147
|
-
targets: resolvedInstruction.targets.filter(({ detection }) =>
|
|
13620
|
+
targets: resolvedInstruction.targets.filter(({ detection, frame }) => frame === context.frame &&
|
|
13621
|
+
(options.isDetectionVisible?.(detection) ?? true)),
|
|
13148
13622
|
}
|
|
13149
13623
|
: undefined;
|
|
13150
13624
|
endHold();
|
|
13151
13625
|
if (!instruction || instruction.targets.length === 0) {
|
|
13626
|
+
if (usesHeatmapAlphaMask && heldFill) {
|
|
13627
|
+
drawOverlayWithoutCutout(heldFill);
|
|
13628
|
+
}
|
|
13152
13629
|
resetHeldFocus();
|
|
13153
13630
|
transitionToHidden();
|
|
13154
13631
|
return;
|
|
@@ -13158,12 +13635,13 @@ function createPixiFocusLayer(options) {
|
|
|
13158
13635
|
if (focusDisplay)
|
|
13159
13636
|
focusDisplay.visible = true;
|
|
13160
13637
|
if (drawIdMaskFocus(context.idMaskArtifact, instruction)) {
|
|
13638
|
+
activateVectorMask();
|
|
13161
13639
|
hideVectorFocus();
|
|
13162
13640
|
markCutoutDrawn(context.mediaTime, context.frame.mediaTime);
|
|
13163
13641
|
return;
|
|
13164
13642
|
}
|
|
13165
13643
|
idMaskRenderer?.hide();
|
|
13166
|
-
if (drawVectorFocus(instruction, context
|
|
13644
|
+
if (drawVectorFocus(instruction, context)) {
|
|
13167
13645
|
markCutoutDrawn(context.mediaTime, context.frame.mediaTime);
|
|
13168
13646
|
}
|
|
13169
13647
|
},
|
|
@@ -13191,7 +13669,29 @@ function createPixiFocusLayer(options) {
|
|
|
13191
13669
|
}
|
|
13192
13670
|
focusStyle = nextFocusStyle;
|
|
13193
13671
|
},
|
|
13672
|
+
syncViewportBounds,
|
|
13194
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
|
+
}
|
|
13195
13695
|
function drawIdMaskFocus(artifact, instruction) {
|
|
13196
13696
|
if (!idMaskRenderer ||
|
|
13197
13697
|
!artifact ||
|
|
@@ -13200,17 +13700,19 @@ function createPixiFocusLayer(options) {
|
|
|
13200
13700
|
}
|
|
13201
13701
|
const maskIds = getTargetMaskIds(instruction.targets);
|
|
13202
13702
|
if (maskIds.length === 0 ||
|
|
13703
|
+
instruction.targets.some((target) => !target.detection.mask) ||
|
|
13203
13704
|
(!instruction.ambient && maskIds.length !== instruction.targets.length)) {
|
|
13204
13705
|
return false;
|
|
13205
13706
|
}
|
|
13206
13707
|
idMaskRenderer.render(artifact.frame, artifact.texture, maskIds, instruction.fill, instruction.ambient === true);
|
|
13207
13708
|
return true;
|
|
13208
13709
|
}
|
|
13209
|
-
function drawVectorFocus(instruction,
|
|
13710
|
+
function drawVectorFocus(instruction, context) {
|
|
13210
13711
|
if (!focusGraphics) {
|
|
13211
13712
|
return false;
|
|
13212
13713
|
}
|
|
13213
13714
|
const targetsWithGeometry = instruction.targets.filter((target) => target.detection.mask ||
|
|
13715
|
+
target.detection.heatmap ||
|
|
13214
13716
|
target.detection.rect ||
|
|
13215
13717
|
target.detection.polygon);
|
|
13216
13718
|
if (targetsWithGeometry.length === 0) {
|
|
@@ -13218,37 +13720,55 @@ function createPixiFocusLayer(options) {
|
|
|
13218
13720
|
return false;
|
|
13219
13721
|
}
|
|
13220
13722
|
const signature = {
|
|
13723
|
+
annotationAntialiasing: options.getAnnotationAntialiasing?.() ?? false,
|
|
13221
13724
|
cornerRadius: instruction.fallback?.cornerRadius,
|
|
13222
13725
|
detections: targetsWithGeometry.map((target) => target.detection),
|
|
13223
13726
|
fillAlpha: instruction.fill.alpha,
|
|
13224
13727
|
fillColor: instruction.fill.color,
|
|
13225
|
-
frame,
|
|
13728
|
+
frame: context.frame,
|
|
13729
|
+
heatmapsEnabled: (options.getHeatmapRenderers?.().length ?? 0) > 0,
|
|
13730
|
+
heatmapArtifacts: context.heatmapArtifacts ?? [],
|
|
13226
13731
|
mediaHeight,
|
|
13227
13732
|
mediaWidth,
|
|
13228
|
-
shape: instruction.fallback?.shape,
|
|
13733
|
+
shape: instruction.fallback === null ? null : instruction.fallback?.shape,
|
|
13229
13734
|
};
|
|
13230
13735
|
focusGraphics.visible = true;
|
|
13231
13736
|
if (focusMaskGraphics)
|
|
13232
|
-
focusMaskGraphics.visible =
|
|
13737
|
+
focusMaskGraphics.visible = !usesHeatmapAlphaMask;
|
|
13233
13738
|
// Tessellating thousands of mask runs on every draw is what makes this path
|
|
13234
13739
|
// expensive, and nothing it draws moves while its inputs hold still.
|
|
13235
13740
|
if (isSameVectorFocus(vectorFocusSignature, signature)) {
|
|
13236
13741
|
return true;
|
|
13237
13742
|
}
|
|
13238
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();
|
|
13239
13757
|
focusGraphics.clear();
|
|
13240
13758
|
focusGraphics.rect(0, 0, mediaWidth, mediaHeight);
|
|
13241
13759
|
focusGraphics.fill(instruction.fill);
|
|
13242
|
-
|
|
13243
|
-
|
|
13244
|
-
|
|
13245
|
-
|
|
13246
|
-
|
|
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 });
|
|
13247
13766
|
}
|
|
13248
13767
|
}
|
|
13768
|
+
syncHeatmapViewportBounds();
|
|
13249
13769
|
return true;
|
|
13250
13770
|
}
|
|
13251
|
-
for (const target of
|
|
13771
|
+
for (const target of vectorTargets) {
|
|
13252
13772
|
if (drawCutoutShape(focusGraphics, target, instruction) === "drawn") {
|
|
13253
13773
|
focusGraphics.cut();
|
|
13254
13774
|
}
|
|
@@ -13264,7 +13784,7 @@ function createPixiFocusLayer(options) {
|
|
|
13264
13784
|
graphics.poly(target.detection.polygon.points.flatMap(({ x, y }) => [x, y]), true);
|
|
13265
13785
|
return "drawn";
|
|
13266
13786
|
}
|
|
13267
|
-
if (!target.detection.rect) {
|
|
13787
|
+
if (!target.detection.rect || instruction.fallback === null) {
|
|
13268
13788
|
return "empty";
|
|
13269
13789
|
}
|
|
13270
13790
|
const { rect } = target.detection;
|
|
@@ -13278,6 +13798,93 @@ function createPixiFocusLayer(options) {
|
|
|
13278
13798
|
}
|
|
13279
13799
|
return "drawn";
|
|
13280
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
|
+
}
|
|
13281
13888
|
function drawMaskCutout(graphics, target) {
|
|
13282
13889
|
const mask = target.detection.mask;
|
|
13283
13890
|
if (!mask) {
|
|
@@ -13339,7 +13946,8 @@ function createPixiFocusLayer(options) {
|
|
|
13339
13946
|
targetAlpha = 1;
|
|
13340
13947
|
if (focusDisplay)
|
|
13341
13948
|
focusDisplay.visible = true;
|
|
13342
|
-
if (
|
|
13949
|
+
if (!usesHeatmapAlphaMask &&
|
|
13950
|
+
isDrawnCutoutStillCurrent(mediaTime, heldMaskFrameTime)) {
|
|
13343
13951
|
return;
|
|
13344
13952
|
}
|
|
13345
13953
|
if (isMaskArtifactOwed) {
|
|
@@ -13422,6 +14030,7 @@ function createPixiFocusLayer(options) {
|
|
|
13422
14030
|
cutoutMediaTime = null;
|
|
13423
14031
|
cutoutFrameTime = null;
|
|
13424
14032
|
drawnOverlayWithoutCutout = fill;
|
|
14033
|
+
activateVectorMask();
|
|
13425
14034
|
if (idMaskRenderer) {
|
|
13426
14035
|
hideVectorFocus();
|
|
13427
14036
|
idMaskRenderer.renderWithoutCutout(fill);
|
|
@@ -13481,7 +14090,10 @@ function createPixiFocusLayer(options) {
|
|
|
13481
14090
|
}
|
|
13482
14091
|
function isSameVectorFocus(previous, next) {
|
|
13483
14092
|
return (previous !== null &&
|
|
14093
|
+
previous.annotationAntialiasing === next.annotationAntialiasing &&
|
|
13484
14094
|
previous.frame === next.frame &&
|
|
14095
|
+
previous.heatmapsEnabled === next.heatmapsEnabled &&
|
|
14096
|
+
sameHeatmapArtifacts(previous.heatmapArtifacts, next.heatmapArtifacts) &&
|
|
13485
14097
|
previous.fillAlpha === next.fillAlpha &&
|
|
13486
14098
|
previous.fillColor === next.fillColor &&
|
|
13487
14099
|
previous.cornerRadius === next.cornerRadius &&
|
|
@@ -13491,6 +14103,18 @@ function isSameVectorFocus(previous, next) {
|
|
|
13491
14103
|
previous.detections.length === next.detections.length &&
|
|
13492
14104
|
previous.detections.every((detection, index) => detection === next.detections[index]));
|
|
13493
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
|
+
}
|
|
13494
14118
|
function getTargetMaskIds(targets) {
|
|
13495
14119
|
return targets
|
|
13496
14120
|
.filter((target) => target.detection.mask)
|
|
@@ -13775,68 +14399,478 @@ fn mainFragment(
|
|
|
13775
14399
|
}
|
|
13776
14400
|
`;
|
|
13777
14401
|
|
|
13778
|
-
|
|
13779
|
-
|
|
13780
|
-
|
|
13781
|
-
|
|
13782
|
-
|
|
13783
|
-
|
|
13784
|
-
|
|
13785
|
-
|
|
13786
|
-
|
|
13787
|
-
|
|
13788
|
-
|
|
13789
|
-
|
|
13790
|
-
|
|
13791
|
-
|
|
13792
|
-
|
|
13793
|
-
|
|
13794
|
-
|
|
13795
|
-
|
|
13796
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13802
|
-
|
|
13803
|
-
|
|
13804
|
-
|
|
13805
|
-
|
|
13806
|
-
|
|
13807
|
-
|
|
13808
|
-
|
|
13809
|
-
|
|
13810
|
-
|
|
13811
|
-
|
|
13812
|
-
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
13816
|
-
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
|
|
13820
|
-
|
|
13821
|
-
|
|
13822
|
-
|
|
13823
|
-
|
|
13824
|
-
|
|
13825
|
-
|
|
13826
|
-
|
|
13827
|
-
|
|
13828
|
-
|
|
13829
|
-
|
|
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
|
+
},
|
|
13830
14506
|
}
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
13839
|
-
|
|
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
|
+
|
|
14812
|
+
const DEFAULT_PICK_PADDING = 4;
|
|
14813
|
+
const POINT_PRECISE_PICK_TARGETS = new Set([
|
|
14814
|
+
DetectionPickTarget.Keypoint,
|
|
14815
|
+
DetectionPickTarget.Edge,
|
|
14816
|
+
DetectionPickTarget.Polyline,
|
|
14817
|
+
]);
|
|
14818
|
+
function createPixiInteractionLayer(options) {
|
|
14819
|
+
const mode = options.interaction.mode ?? MediaInteractionMode.PausedOnly;
|
|
14820
|
+
const pickPadding = options.interaction.padding ?? DEFAULT_PICK_PADDING;
|
|
14821
|
+
let container;
|
|
14822
|
+
let displayDimensions;
|
|
14823
|
+
let currentMediaTime = 0;
|
|
14824
|
+
let hoveredPick = null;
|
|
14825
|
+
let hoveredPickKey = null;
|
|
14826
|
+
let selectedPick = null;
|
|
14827
|
+
let selectedPicks = [];
|
|
14828
|
+
let marqueeStart = null;
|
|
14829
|
+
let marqueeRect = null;
|
|
14830
|
+
let didDragMarquee = false;
|
|
14831
|
+
let pointerPoint = null;
|
|
14832
|
+
let suppressNextTap = false;
|
|
14833
|
+
let activeFrame;
|
|
14834
|
+
let isDestroyed = false;
|
|
14835
|
+
return {
|
|
14836
|
+
createDisplay({ width, height }) {
|
|
14837
|
+
displayDimensions = { width, height };
|
|
14838
|
+
container = new options.Container();
|
|
14839
|
+
container.eventMode = "static";
|
|
14840
|
+
container.cursor = "default";
|
|
14841
|
+
container.hitArea = new options.Rectangle(0, 0, width, height);
|
|
14842
|
+
container.on("pointermove", handlePointerMove);
|
|
14843
|
+
container.on("globalpointermove", handleGlobalPointerMove);
|
|
14844
|
+
container.on("pointerout", handlePointerOut);
|
|
14845
|
+
container.on("pointertap", handlePointerTap);
|
|
14846
|
+
container.on("pointerdown", handlePointerDown);
|
|
14847
|
+
container.on("pointerup", handlePointerUp);
|
|
14848
|
+
container.on("pointerupoutside", handlePointerUp);
|
|
14849
|
+
return container;
|
|
14850
|
+
},
|
|
14851
|
+
drawFrame(mediaTime) {
|
|
14852
|
+
currentMediaTime = mediaTime;
|
|
14853
|
+
activeFrame = options.detectionTimeline.selectFrame(mediaTime);
|
|
14854
|
+
if (canHandleInteraction()) {
|
|
14855
|
+
clearStalePicks(activeFrame);
|
|
14856
|
+
return;
|
|
14857
|
+
}
|
|
14858
|
+
pointerPoint = null;
|
|
14859
|
+
marqueeRect = null;
|
|
14860
|
+
setHoveredPick(null);
|
|
14861
|
+
if (isDestroyed || mode === MediaInteractionMode.Disabled) {
|
|
14862
|
+
setSelectedPick(null);
|
|
14863
|
+
return;
|
|
14864
|
+
}
|
|
14865
|
+
// Paused-only interaction gates new picks during playback, but an
|
|
14866
|
+
// existing selection keeps following its detection so selection-driven
|
|
14867
|
+
// presentation such as focus survives frame advances.
|
|
14868
|
+
followSelectedPicks(activeFrame);
|
|
14869
|
+
},
|
|
14870
|
+
setSelectedDetection(selection) {
|
|
14871
|
+
if (selection === null) {
|
|
14872
|
+
setSelectedPick(null);
|
|
14873
|
+
return null;
|
|
13840
14874
|
}
|
|
13841
14875
|
if (!canHandleInteraction()) {
|
|
13842
14876
|
setSelectedPick(null);
|
|
@@ -14344,7 +15378,7 @@ function createPixiMaskScreenStrokeRenderer(options) {
|
|
|
14344
15378
|
let strokes = frame.screenStrokes ?? [];
|
|
14345
15379
|
if (overrides) {
|
|
14346
15380
|
strokes = overrides.flatMap(({ detectionIndex, stroke }) => {
|
|
14347
|
-
if (stroke?.widthUnit
|
|
15381
|
+
if (stroke?.widthUnit === undefined ||
|
|
14348
15382
|
stroke.width <= 0 ||
|
|
14349
15383
|
stroke.alpha <= 0)
|
|
14350
15384
|
return [];
|
|
@@ -14366,21 +15400,36 @@ function createPixiMaskScreenStrokeRenderer(options) {
|
|
|
14366
15400
|
hide();
|
|
14367
15401
|
return;
|
|
14368
15402
|
}
|
|
14369
|
-
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(";")}`;
|
|
14370
15404
|
if (lastFrame !== frame || lastSignature !== signature) {
|
|
14371
15405
|
display.clear();
|
|
14372
15406
|
const scaleX = options.mediaWidth / frame.width;
|
|
14373
15407
|
const scaleY = options.mediaHeight / frame.height;
|
|
14374
15408
|
for (const stroke of strokes) {
|
|
14375
|
-
for (const
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
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));
|
|
14382
15432
|
}
|
|
14383
|
-
display.stroke(resolvePixiStroke({ ...stroke, join: "round" }, viewportScale));
|
|
14384
15433
|
}
|
|
14385
15434
|
lastFrame = frame;
|
|
14386
15435
|
lastSignature = signature;
|
|
@@ -14389,22 +15438,56 @@ function createPixiMaskScreenStrokeRenderer(options) {
|
|
|
14389
15438
|
},
|
|
14390
15439
|
};
|
|
14391
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
|
+
}
|
|
14392
15469
|
|
|
14393
15470
|
const coverageBody = `
|
|
14394
|
-
return clamp(0.5 + (width - distance) / pixelWidth, 0.0, 1.0)
|
|
14395
|
-
- 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);
|
|
14396
15473
|
`;
|
|
14397
15474
|
const distanceBody = `
|
|
14398
15475
|
return max(max(offset - position, position - offset - 1.0), 0.0);
|
|
14399
15476
|
`;
|
|
14400
15477
|
const radiusBody = `
|
|
14401
|
-
if (
|
|
14402
|
-
return
|
|
15478
|
+
if (maxWidth < 0.0) {
|
|
15479
|
+
return 0.0;
|
|
14403
15480
|
}
|
|
14404
|
-
return min(
|
|
15481
|
+
return min(ceil(maxWidth + pixelWidth * 0.5), ${MAX_ID_MASK_STROKE_WIDTH + 1}.0);
|
|
14405
15482
|
`;
|
|
14406
15483
|
const idMaskStrokeCoverageGlsl = `
|
|
14407
|
-
float
|
|
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) {
|
|
14408
15491
|
${coverageBody}
|
|
14409
15492
|
}
|
|
14410
15493
|
|
|
@@ -14412,42 +15495,55 @@ float neighborCellDistance(float offset, float position) {
|
|
|
14412
15495
|
${distanceBody}
|
|
14413
15496
|
}
|
|
14414
15497
|
|
|
14415
|
-
float strokeScanRadius(float maxWidth, float
|
|
15498
|
+
float strokeScanRadius(float maxWidth, float pixelWidth) {
|
|
14416
15499
|
${radiusBody}
|
|
14417
15500
|
}
|
|
14418
15501
|
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
|
|
14423
|
-
differs(sampleMaskId(vUV + vec2(0.0, texel.y)), centerId) ||
|
|
14424
|
-
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);
|
|
14425
15506
|
}
|
|
14426
15507
|
|
|
14427
|
-
float innerStrokeCoverage(float centerId,
|
|
14428
|
-
if (width
|
|
14429
|
-
|
|
14430
|
-
|
|
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));
|
|
14431
15514
|
|
|
14432
|
-
|
|
14433
|
-
|
|
14434
|
-
|
|
14435
|
-
|
|
14436
|
-
|
|
14437
|
-
|
|
14438
|
-
|
|
14439
|
-
|
|
14440
|
-
|
|
14441
|
-
|
|
14442
|
-
|
|
14443
|
-
|
|
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
|
+
}
|
|
14444
15535
|
}
|
|
14445
|
-
return
|
|
15536
|
+
return subtexelStrokeCoverage(-nearestDistance, width, alignment, pixelWidth);
|
|
14446
15537
|
}
|
|
14447
15538
|
|
|
14448
15539
|
// Descending offsets give the outer border its palette priority.
|
|
14449
|
-
vec2 findNeighborStroke(float centerId,
|
|
14450
|
-
|
|
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;
|
|
14451
15547
|
|
|
14452
15548
|
for (int offsetY = radius; offsetY >= -radius; offsetY -= 1) {
|
|
14453
15549
|
for (int offsetX = radius; offsetX >= -radius; offsetX -= 1) {
|
|
@@ -14455,35 +15551,44 @@ vec2 findNeighborStroke(float centerId, vec2 texel, vec2 cell, float pixelWidth)
|
|
|
14455
15551
|
continue;
|
|
14456
15552
|
}
|
|
14457
15553
|
|
|
14458
|
-
float
|
|
14459
|
-
|
|
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))) {
|
|
14460
15564
|
continue;
|
|
14461
15565
|
}
|
|
14462
15566
|
|
|
14463
|
-
float width =
|
|
14464
|
-
if (width
|
|
14465
|
-
float
|
|
14466
|
-
if (width >= distance && readStroke(maskId).a > 0.0) {
|
|
14467
|
-
return vec2(maskId, 1.0);
|
|
14468
|
-
}
|
|
14469
|
-
} else if (width > 0.0 && readStroke(maskId).a > 0.0) {
|
|
14470
|
-
float distance = max(
|
|
14471
|
-
neighborCellDistance(float(offsetX), cell.x),
|
|
14472
|
-
neighborCellDistance(float(offsetY), cell.y)
|
|
14473
|
-
);
|
|
14474
|
-
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);
|
|
14475
15570
|
if (coverage > 0.0) {
|
|
14476
|
-
|
|
15571
|
+
stroke = vec2(maskId, coverage);
|
|
15572
|
+
nearestDistance = distance;
|
|
15573
|
+
if (distance <= minimumDistance) {
|
|
15574
|
+
return stroke;
|
|
15575
|
+
}
|
|
14477
15576
|
}
|
|
14478
15577
|
}
|
|
14479
15578
|
}
|
|
14480
15579
|
}
|
|
14481
15580
|
|
|
14482
|
-
return
|
|
15581
|
+
return stroke;
|
|
14483
15582
|
}
|
|
14484
15583
|
`;
|
|
14485
15584
|
const idMaskStrokeCoverageWgsl = `
|
|
14486
|
-
fn
|
|
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 {
|
|
14487
15592
|
${coverageBody}
|
|
14488
15593
|
}
|
|
14489
15594
|
|
|
@@ -14491,45 +15596,55 @@ fn neighborCellDistance(offset: f32, position: f32) -> f32 {
|
|
|
14491
15596
|
${distanceBody}
|
|
14492
15597
|
}
|
|
14493
15598
|
|
|
14494
|
-
fn strokeScanRadius(maxWidth: f32,
|
|
15599
|
+
fn strokeScanRadius(maxWidth: f32, pixelWidth: f32) -> f32 {
|
|
14495
15600
|
${radiusBody}
|
|
14496
15601
|
}
|
|
14497
15602
|
|
|
14498
|
-
fn
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
differs(sampleMaskId(uv + vec2<f32>(0.0, texel.y)), centerId) ||
|
|
14503
|
-
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);
|
|
14504
15607
|
}
|
|
14505
15608
|
|
|
14506
|
-
fn innerStrokeCoverage(
|
|
14507
|
-
if (width
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
}
|
|
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));
|
|
14513
15615
|
|
|
14514
|
-
var
|
|
14515
|
-
|
|
14516
|
-
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
|
|
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
|
+
}
|
|
14526
15636
|
}
|
|
14527
|
-
return
|
|
15637
|
+
return subtexelStrokeCoverage(-nearestDistance, width, alignment, pixelWidth);
|
|
14528
15638
|
}
|
|
14529
15639
|
|
|
14530
15640
|
// Descending offsets give the outer border its palette priority.
|
|
14531
|
-
fn findNeighborStroke(
|
|
14532
|
-
let
|
|
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; }
|
|
14533
15648
|
|
|
14534
15649
|
for (var offsetY = radius; offsetY >= -radius; offsetY -= 1) {
|
|
14535
15650
|
for (var offsetX = radius; offsetX >= -radius; offsetX -= 1) {
|
|
@@ -14537,45 +15652,44 @@ fn findNeighborStroke(uv: vec2<f32>, centerId: f32, texel: vec2<f32>, cell: vec2
|
|
|
14537
15652
|
continue;
|
|
14538
15653
|
}
|
|
14539
15654
|
|
|
14540
|
-
let
|
|
14541
|
-
|
|
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))) {
|
|
14542
15665
|
continue;
|
|
14543
15666
|
}
|
|
14544
15667
|
|
|
14545
|
-
let width =
|
|
14546
|
-
if (width
|
|
14547
|
-
let
|
|
14548
|
-
if (width >= distance && readStroke(maskId).a > 0.0) {
|
|
14549
|
-
return vec2<f32>(maskId, 1.0);
|
|
14550
|
-
}
|
|
14551
|
-
} else if (width > 0.0 && readStroke(maskId).a > 0.0) {
|
|
14552
|
-
let distance = max(
|
|
14553
|
-
neighborCellDistance(f32(offsetX), cell.x),
|
|
14554
|
-
neighborCellDistance(f32(offsetY), cell.y)
|
|
14555
|
-
);
|
|
14556
|
-
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);
|
|
14557
15671
|
if (coverage > 0.0) {
|
|
14558
|
-
|
|
15672
|
+
stroke = vec2<f32>(maskId, coverage);
|
|
15673
|
+
nearestDistance = distance;
|
|
15674
|
+
if (distance <= minimumDistance) {
|
|
15675
|
+
return stroke;
|
|
15676
|
+
}
|
|
14559
15677
|
}
|
|
14560
15678
|
}
|
|
14561
15679
|
}
|
|
14562
15680
|
}
|
|
14563
15681
|
|
|
14564
|
-
return
|
|
15682
|
+
return stroke;
|
|
14565
15683
|
}
|
|
14566
15684
|
`;
|
|
14567
15685
|
|
|
14568
|
-
/**
|
|
14569
|
-
* GLSL charges a uniform array of scalars one uniform vector per element, the
|
|
14570
|
-
* same as an array of vec4, so the stroke widths ride four to a vector and an
|
|
14571
|
-
* entry costs 2.25 vectors instead of 3. The lanes only line up while the entry
|
|
14572
|
-
* count stays a multiple of four.
|
|
14573
|
-
*/
|
|
15686
|
+
/** Packs width and alignment scalars within the fragment uniform budget. */
|
|
14574
15687
|
const ID_MASK_STROKE_WIDTH_LANES = MAX_ID_MASK_PALETTE_ENTRIES / 4;
|
|
14575
15688
|
const idMaskPaletteGlsl = `
|
|
14576
15689
|
uniform vec4 uFillPalette[${MAX_ID_MASK_PALETTE_ENTRIES}];
|
|
14577
15690
|
uniform vec4 uStrokePalette[${MAX_ID_MASK_PALETTE_ENTRIES}];
|
|
14578
15691
|
uniform vec4 uStrokeWidths[${ID_MASK_STROKE_WIDTH_LANES}];
|
|
15692
|
+
uniform vec4 uStrokeAlignments[${ID_MASK_STROKE_WIDTH_LANES}];
|
|
14579
15693
|
|
|
14580
15694
|
int paletteIndex(float maskId) {
|
|
14581
15695
|
return int(clamp(maskId, 0.0, float(${MAX_ID_MASK_PALETTE_ENTRIES - 1})));
|
|
@@ -14594,10 +15708,16 @@ float readStrokeWidth(float maskId) {
|
|
|
14594
15708
|
|
|
14595
15709
|
return uStrokeWidths[index / 4][index % 4];
|
|
14596
15710
|
}
|
|
15711
|
+
|
|
15712
|
+
float readStrokeAlignment(float maskId) {
|
|
15713
|
+
int index = paletteIndex(maskId);
|
|
15714
|
+
return uStrokeAlignments[index / 4][index % 4];
|
|
15715
|
+
}
|
|
14597
15716
|
`;
|
|
14598
15717
|
const idMaskFillPaletteWgslField = `uFillPalette: array<vec4<f32>, ${MAX_ID_MASK_PALETTE_ENTRIES}>,`;
|
|
14599
15718
|
const idMaskStrokePaletteWgslField = `uStrokePalette: array<vec4<f32>, ${MAX_ID_MASK_PALETTE_ENTRIES}>,`;
|
|
14600
15719
|
const idMaskStrokeWidthsWgslField = `uStrokeWidths: array<vec4<f32>, ${ID_MASK_STROKE_WIDTH_LANES}>,`;
|
|
15720
|
+
const idMaskStrokeAlignmentsWgslField = `uStrokeAlignments: array<vec4<f32>, ${ID_MASK_STROKE_WIDTH_LANES}>,`;
|
|
14601
15721
|
/** Reads the uniform group a fragment stage must bind under the name maskUniforms. */
|
|
14602
15722
|
const idMaskPaletteWgsl = `
|
|
14603
15723
|
fn paletteIndex(maskId: f32) -> i32 {
|
|
@@ -14617,20 +15737,31 @@ fn readStrokeWidth(maskId: f32) -> f32 {
|
|
|
14617
15737
|
|
|
14618
15738
|
return maskUniforms.uStrokeWidths[index / 4][index % 4];
|
|
14619
15739
|
}
|
|
15740
|
+
|
|
15741
|
+
fn readStrokeAlignment(maskId: f32) -> f32 {
|
|
15742
|
+
let index = paletteIndex(maskId);
|
|
15743
|
+
return maskUniforms.uStrokeAlignments[index / 4][index % 4];
|
|
15744
|
+
}
|
|
14620
15745
|
`;
|
|
14621
15746
|
|
|
14622
|
-
function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, labelStyle, resolveContextState, }) {
|
|
15747
|
+
function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, labelStyle, getRenderedBoxes, resolveContextState, }) {
|
|
14623
15748
|
const entries = [];
|
|
14624
15749
|
let container;
|
|
14625
15750
|
let currentLabelStyle = labelStyle;
|
|
14626
15751
|
// A versioned source can replace a frame without changing its media time or
|
|
14627
15752
|
// frame index. Buffered frames are immutable snapshots, so retain by object.
|
|
14628
15753
|
let lastFrame;
|
|
15754
|
+
let lastBoxFrame;
|
|
14629
15755
|
let styleVersion = 0;
|
|
14630
15756
|
let drawnStyleVersion = -1;
|
|
14631
15757
|
let lastViewportScale = 0;
|
|
14632
15758
|
let hitRects = [];
|
|
14633
15759
|
let previewEntry;
|
|
15760
|
+
let previewIndex = 0;
|
|
15761
|
+
let backgroundFilter = null;
|
|
15762
|
+
let backgroundFilters = null;
|
|
15763
|
+
let backgroundsGrouped = false;
|
|
15764
|
+
const backgroundGroups = [];
|
|
14634
15765
|
const entriesByDetectionKey = new Map();
|
|
14635
15766
|
const boundsByDetectionKey = new Map();
|
|
14636
15767
|
const clearLayout = () => {
|
|
@@ -14642,6 +15773,7 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14642
15773
|
for (let index = startIndex; index < entries.length; index += 1) {
|
|
14643
15774
|
entries[index].background.visible = false;
|
|
14644
15775
|
entries[index].label.visible = false;
|
|
15776
|
+
entries[index].instruction = undefined;
|
|
14645
15777
|
}
|
|
14646
15778
|
};
|
|
14647
15779
|
const createEntry = () => {
|
|
@@ -14658,6 +15790,9 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14658
15790
|
labelBaseY: 0,
|
|
14659
15791
|
text: null,
|
|
14660
15792
|
textStyleKey: null,
|
|
15793
|
+
textHeight: 0,
|
|
15794
|
+
textWidth: 0,
|
|
15795
|
+
instruction: undefined,
|
|
14661
15796
|
};
|
|
14662
15797
|
container?.addChild(entry.background, entry.label);
|
|
14663
15798
|
return entry;
|
|
@@ -14673,10 +15808,71 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14673
15808
|
const hideCreationPreview = () => {
|
|
14674
15809
|
if (!previewEntry)
|
|
14675
15810
|
return;
|
|
14676
|
-
previewEntry.background.visible = false;
|
|
14677
|
-
previewEntry.label.visible = false;
|
|
15811
|
+
previewEntry.background.visible = false;
|
|
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();
|
|
14678
15874
|
};
|
|
14679
|
-
const redrawFrame = (frame, mediaTime, viewportScale) => {
|
|
15875
|
+
const redrawFrame = (frame, mediaTime, viewportScale, renderedBoxes) => {
|
|
14680
15876
|
let drawnCount = 0;
|
|
14681
15877
|
clearLayout();
|
|
14682
15878
|
if (!currentLabelStyle) {
|
|
@@ -14699,7 +15895,7 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14699
15895
|
continue;
|
|
14700
15896
|
}
|
|
14701
15897
|
const entry = ensureEntry(drawnCount);
|
|
14702
|
-
const hitRect = drawInstruction(entry, instruction, viewportScale);
|
|
15898
|
+
const hitRect = drawInstruction(entry, instruction, viewportScale, renderedBoxes?.get(detectionIndex));
|
|
14703
15899
|
const key = detectionKey$1(detection, detectionIndex);
|
|
14704
15900
|
entriesByDetectionKey.set(key, entry);
|
|
14705
15901
|
boundsByDetectionKey.set(key, {
|
|
@@ -14722,24 +15918,31 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14722
15918
|
drawFrame(mediaTime, viewportScale) {
|
|
14723
15919
|
const resolvedViewportScale = viewportScale ?? 1;
|
|
14724
15920
|
const frame = detectionTimeline.selectFrame(mediaTime);
|
|
15921
|
+
const boxFrame = getRenderedBoxes?.();
|
|
15922
|
+
const matchingBoxFrame = boxFrame?.frame === frame ? boxFrame : undefined;
|
|
14725
15923
|
if (frame === lastFrame &&
|
|
15924
|
+
matchingBoxFrame === lastBoxFrame &&
|
|
14726
15925
|
drawnStyleVersion === styleVersion &&
|
|
14727
15926
|
resolvedViewportScale === lastViewportScale) {
|
|
14728
15927
|
return;
|
|
14729
15928
|
}
|
|
14730
15929
|
lastFrame = frame;
|
|
15930
|
+
lastBoxFrame = matchingBoxFrame;
|
|
14731
15931
|
drawnStyleVersion = styleVersion;
|
|
14732
15932
|
lastViewportScale = resolvedViewportScale;
|
|
14733
15933
|
if (!frame) {
|
|
14734
15934
|
clearLayout();
|
|
14735
15935
|
hideEntriesFrom(0);
|
|
15936
|
+
syncBackgroundCaptures();
|
|
14736
15937
|
return;
|
|
14737
15938
|
}
|
|
14738
|
-
redrawFrame(frame, mediaTime, resolvedViewportScale);
|
|
15939
|
+
redrawFrame(frame, mediaTime, resolvedViewportScale, matchingBoxFrame?.rects);
|
|
15940
|
+
syncBackgroundCaptures();
|
|
14739
15941
|
},
|
|
14740
|
-
drawCreationPreview(detection, mediaTime, viewportScale) {
|
|
15942
|
+
drawCreationPreview(detection, mediaTime, viewportScale, renderedBox) {
|
|
14741
15943
|
if (!detection || !currentLabelStyle) {
|
|
14742
15944
|
hideCreationPreview();
|
|
15945
|
+
syncBackgroundCaptures();
|
|
14743
15946
|
return;
|
|
14744
15947
|
}
|
|
14745
15948
|
const resolvedViewportScale = viewportScale ?? 1;
|
|
@@ -14755,15 +15958,39 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14755
15958
|
});
|
|
14756
15959
|
if (!instruction) {
|
|
14757
15960
|
hideCreationPreview();
|
|
15961
|
+
syncBackgroundCaptures();
|
|
14758
15962
|
return;
|
|
14759
15963
|
}
|
|
14760
|
-
|
|
14761
|
-
|
|
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;
|
|
14762
15982
|
},
|
|
14763
15983
|
setLabelStyle(nextLabelStyle) {
|
|
14764
15984
|
currentLabelStyle = nextLabelStyle ?? undefined;
|
|
14765
15985
|
styleVersion += 1;
|
|
14766
15986
|
},
|
|
15987
|
+
setBackgroundAntialiasFilter(filter) {
|
|
15988
|
+
if (filter === backgroundFilter)
|
|
15989
|
+
return;
|
|
15990
|
+
backgroundFilter = filter;
|
|
15991
|
+
backgroundFilters = filter ? [filter] : null;
|
|
15992
|
+
syncBackgroundCaptures();
|
|
15993
|
+
},
|
|
14767
15994
|
translateDetection(id, x, y) {
|
|
14768
15995
|
const key = `id:${String(id)}`;
|
|
14769
15996
|
const entry = entriesByDetectionKey.get(key);
|
|
@@ -14781,6 +16008,7 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14781
16008
|
y: bounds.baseY + y,
|
|
14782
16009
|
});
|
|
14783
16010
|
}
|
|
16011
|
+
syncBackgroundCaptures();
|
|
14784
16012
|
return true;
|
|
14785
16013
|
},
|
|
14786
16014
|
getDetectionLabelBounds(id) {
|
|
@@ -14817,6 +16045,13 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14817
16045
|
: null;
|
|
14818
16046
|
},
|
|
14819
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;
|
|
14820
16055
|
hideEntriesFrom(0);
|
|
14821
16056
|
hideCreationPreview();
|
|
14822
16057
|
entries.length = 0;
|
|
@@ -14826,7 +16061,8 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14826
16061
|
},
|
|
14827
16062
|
};
|
|
14828
16063
|
}
|
|
14829
|
-
function drawInstruction(entry, instruction, viewportScale) {
|
|
16064
|
+
function drawInstruction(entry, instruction, viewportScale, renderedBox) {
|
|
16065
|
+
entry.instruction = instruction;
|
|
14830
16066
|
const textStyle = resolveTextStyle(instruction.textStyle, viewportScale);
|
|
14831
16067
|
const textStyleKey = createTextStyleKey(textStyle);
|
|
14832
16068
|
const textAlpha = instruction.textStyle?.alpha ?? 1;
|
|
@@ -14846,8 +16082,10 @@ function drawInstruction(entry, instruction, viewportScale) {
|
|
|
14846
16082
|
const background = instruction.background;
|
|
14847
16083
|
const paddingX = (background?.paddingX ?? 0) / viewportScale;
|
|
14848
16084
|
const paddingY = (background?.paddingY ?? 0) / viewportScale;
|
|
14849
|
-
|
|
14850
|
-
|
|
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;
|
|
14851
16089
|
const { x, y } = resolveLabelPosition(instruction, width, height);
|
|
14852
16090
|
entry.label.x = x + paddingX;
|
|
14853
16091
|
entry.label.y = y + paddingY;
|
|
@@ -14858,9 +16096,41 @@ function drawInstruction(entry, instruction, viewportScale) {
|
|
|
14858
16096
|
entry.backgroundKey = null;
|
|
14859
16097
|
return { height, width, x, y };
|
|
14860
16098
|
}
|
|
14861
|
-
|
|
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);
|
|
14862
16101
|
return { height, width, x, y };
|
|
14863
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
|
+
}
|
|
14864
16134
|
function resolveLabelPosition(instruction, width, height) {
|
|
14865
16135
|
const { rect } = instruction;
|
|
14866
16136
|
const offsetX = instruction.offsetX ?? 0;
|
|
@@ -14894,9 +16164,9 @@ function resolveLabelPosition(instruction, width, height) {
|
|
|
14894
16164
|
};
|
|
14895
16165
|
}
|
|
14896
16166
|
}
|
|
14897
|
-
function drawBackground(entry, background, x, y, width, height, viewportScale) {
|
|
16167
|
+
function drawBackground(entry, background, x, y, width, height, viewportScale, corners) {
|
|
14898
16168
|
const graphics = entry.background;
|
|
14899
|
-
const backgroundKey = createBackgroundKey(background);
|
|
16169
|
+
const backgroundKey = createBackgroundKey(background, corners);
|
|
14900
16170
|
graphics.visible = true;
|
|
14901
16171
|
graphics.x = x;
|
|
14902
16172
|
graphics.y = y;
|
|
@@ -14911,8 +16181,8 @@ function drawBackground(entry, background, x, y, width, height, viewportScale) {
|
|
|
14911
16181
|
entry.backgroundWidth = width;
|
|
14912
16182
|
entry.backgroundHeight = height;
|
|
14913
16183
|
graphics.clear();
|
|
14914
|
-
const radius = (background.cornerRadius ?? 0) / viewportScale;
|
|
14915
|
-
if (
|
|
16184
|
+
const radius = Math.min((background.cornerRadius ?? 0) / viewportScale, width / 2, height / 2);
|
|
16185
|
+
if (corners === "top" && radius > 0) {
|
|
14916
16186
|
graphics
|
|
14917
16187
|
.moveTo(0, height)
|
|
14918
16188
|
.lineTo(0, radius)
|
|
@@ -14922,6 +16192,16 @@ function drawBackground(entry, background, x, y, width, height, viewportScale) {
|
|
|
14922
16192
|
.lineTo(width, height)
|
|
14923
16193
|
.closePath();
|
|
14924
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
|
+
}
|
|
14925
16205
|
else {
|
|
14926
16206
|
graphics.roundRect(0, 0, width, height, radius);
|
|
14927
16207
|
}
|
|
@@ -14946,14 +16226,46 @@ function createTextStyleKey(textStyle) {
|
|
|
14946
16226
|
textStyle.fontWeight,
|
|
14947
16227
|
].join(":");
|
|
14948
16228
|
}
|
|
14949
|
-
function
|
|
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) {
|
|
14950
16262
|
return [
|
|
14951
16263
|
background.alpha,
|
|
14952
16264
|
background.color,
|
|
14953
16265
|
background.cornerRadius ?? 0,
|
|
14954
16266
|
background.paddingX ?? 0,
|
|
14955
16267
|
background.paddingY ?? 0,
|
|
14956
|
-
|
|
16268
|
+
corners,
|
|
14957
16269
|
].join(":");
|
|
14958
16270
|
}
|
|
14959
16271
|
function detectionKey$1(detection, detectionIndex) {
|
|
@@ -15256,7 +16568,9 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
15256
16568
|
let activePicks = [];
|
|
15257
16569
|
let currentMediaTime = 0;
|
|
15258
16570
|
let viewportScale = 1;
|
|
16571
|
+
let strokePixelRatio = 1;
|
|
15259
16572
|
let maskRenderer;
|
|
16573
|
+
let labelDisplay = null;
|
|
15260
16574
|
let screenStrokeRenderer;
|
|
15261
16575
|
let isDestroyed = false;
|
|
15262
16576
|
const syntheticTimeline = createSyntheticTimeline(() => syntheticFrame);
|
|
@@ -15290,17 +16604,20 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
15290
16604
|
mediaHeight = height;
|
|
15291
16605
|
const container = new options.Container();
|
|
15292
16606
|
const boxGraphics = new options.Graphics();
|
|
15293
|
-
|
|
16607
|
+
labelDisplay = labelLayer.createContainer();
|
|
15294
16608
|
const vectors = vectorLayer.createContainer();
|
|
15295
16609
|
boxLayer.attachGraphics(boxGraphics);
|
|
15296
16610
|
maskRenderer = createMaskRenderer();
|
|
15297
16611
|
if (maskRenderer) {
|
|
15298
16612
|
container.addChild(maskRenderer.mesh);
|
|
15299
16613
|
}
|
|
15300
|
-
container.addChild(boxGraphics, vectors
|
|
16614
|
+
container.addChild(boxGraphics, vectors);
|
|
15301
16615
|
displayContainer = container;
|
|
15302
16616
|
return container;
|
|
15303
16617
|
},
|
|
16618
|
+
getLabelDisplay() {
|
|
16619
|
+
return labelDisplay;
|
|
16620
|
+
},
|
|
15304
16621
|
destroy() {
|
|
15305
16622
|
if (isDestroyed) {
|
|
15306
16623
|
return;
|
|
@@ -15311,6 +16628,7 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
15311
16628
|
maskRenderer?.destroy();
|
|
15312
16629
|
screenStrokeRenderer?.destroy();
|
|
15313
16630
|
labelLayer.destroy();
|
|
16631
|
+
labelDisplay = null;
|
|
15314
16632
|
vectorLayer.destroy();
|
|
15315
16633
|
},
|
|
15316
16634
|
releaseMaskTexture(texture) {
|
|
@@ -15320,6 +16638,7 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
15320
16638
|
drawFrame(context) {
|
|
15321
16639
|
currentMediaTime = context.mediaTime;
|
|
15322
16640
|
viewportScale = context.viewportScale ?? 1;
|
|
16641
|
+
strokePixelRatio = context.strokePixelRatio ?? 1;
|
|
15323
16642
|
if (isDestroyed || !interactionStyle || !context.frame) {
|
|
15324
16643
|
clear();
|
|
15325
16644
|
return;
|
|
@@ -15353,6 +16672,9 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
15353
16672
|
labelLayer.setLabelStyle(labelStyle);
|
|
15354
16673
|
vectorLayer.setStyles({ polygonStyle, polylineStyle, keypointStyle });
|
|
15355
16674
|
},
|
|
16675
|
+
setBackgroundAntialiasFilter(filter) {
|
|
16676
|
+
labelLayer.setBackgroundAntialiasFilter(filter);
|
|
16677
|
+
},
|
|
15356
16678
|
};
|
|
15357
16679
|
function getActivePicks({ frame, hoveredPick, mediaTime, selectedPick, selectedPicks, }) {
|
|
15358
16680
|
if (!frame || !interactionStyle) {
|
|
@@ -15464,9 +16786,9 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
15464
16786
|
screenStrokeRenderer?.hide();
|
|
15465
16787
|
return;
|
|
15466
16788
|
}
|
|
15467
|
-
maskRenderer.render(artifact.frame, artifact.texture, instructions);
|
|
16789
|
+
maskRenderer.render(artifact.frame, artifact.texture, instructions, strokePixelRatio);
|
|
15468
16790
|
if (!screenStrokeRenderer &&
|
|
15469
|
-
instructions.some(({ instruction }) => instruction.stroke?.widthUnit
|
|
16791
|
+
instructions.some(({ instruction }) => instruction.stroke?.widthUnit !== undefined)) {
|
|
15470
16792
|
screenStrokeRenderer = createPixiMaskScreenStrokeRenderer({
|
|
15471
16793
|
Graphics: options.Graphics,
|
|
15472
16794
|
mediaWidth,
|
|
@@ -15633,6 +16955,7 @@ function createInteractionMaskRenderer(options) {
|
|
|
15633
16955
|
const fillPalette = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4);
|
|
15634
16956
|
const strokePalette = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4);
|
|
15635
16957
|
const strokeWidths = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES);
|
|
16958
|
+
const strokeAlignments = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES);
|
|
15636
16959
|
const uniforms = new options.UniformGroup({
|
|
15637
16960
|
uFillPalette: {
|
|
15638
16961
|
size: MAX_ID_MASK_PALETTE_ENTRIES,
|
|
@@ -15640,7 +16963,8 @@ function createInteractionMaskRenderer(options) {
|
|
|
15640
16963
|
value: fillPalette,
|
|
15641
16964
|
},
|
|
15642
16965
|
uMaxStrokeWidth: { type: "f32", value: 0 },
|
|
15643
|
-
|
|
16966
|
+
uBorderEnabled: { type: "f32", value: 0 },
|
|
16967
|
+
uStrokePixelRatio: { type: "f32", value: 1 },
|
|
15644
16968
|
uStrokePalette: {
|
|
15645
16969
|
size: MAX_ID_MASK_PALETTE_ENTRIES,
|
|
15646
16970
|
type: "vec4<f32>",
|
|
@@ -15651,6 +16975,11 @@ function createInteractionMaskRenderer(options) {
|
|
|
15651
16975
|
type: "vec4<f32>",
|
|
15652
16976
|
value: strokeWidths,
|
|
15653
16977
|
},
|
|
16978
|
+
uStrokeAlignments: {
|
|
16979
|
+
size: ID_MASK_STROKE_WIDTH_LANES,
|
|
16980
|
+
type: "vec4<f32>",
|
|
16981
|
+
value: strokeAlignments,
|
|
16982
|
+
},
|
|
15654
16983
|
uTextureSize: {
|
|
15655
16984
|
type: "vec2<f32>",
|
|
15656
16985
|
value: new Float32Array([options.mediaWidth, options.mediaHeight]),
|
|
@@ -15700,7 +17029,7 @@ function createInteractionMaskRenderer(options) {
|
|
|
15700
17029
|
mesh.visible = false;
|
|
15701
17030
|
},
|
|
15702
17031
|
mesh,
|
|
15703
|
-
render(frame, texture, instructions) {
|
|
17032
|
+
render(frame, texture, instructions, strokePixelRatio) {
|
|
15704
17033
|
if (frame.kind !== PreparedMaskFrameKind.IdMask) {
|
|
15705
17034
|
mesh.visible = false;
|
|
15706
17035
|
return;
|
|
@@ -15709,8 +17038,9 @@ function createInteractionMaskRenderer(options) {
|
|
|
15709
17038
|
fillPalette.fill(0);
|
|
15710
17039
|
strokePalette.fill(0);
|
|
15711
17040
|
strokeWidths.fill(0);
|
|
17041
|
+
strokeAlignments.fill(0);
|
|
15712
17042
|
let maxStrokeWidth = 0;
|
|
15713
|
-
let
|
|
17043
|
+
let hasStroke = false;
|
|
15714
17044
|
for (const { detectionIndex, instruction } of instructions) {
|
|
15715
17045
|
const maskId = resolveIdMaskPaletteId(detectionIndex);
|
|
15716
17046
|
// A raster naming this detection is one the palette accepted, so an id
|
|
@@ -15720,26 +17050,31 @@ function createInteractionMaskRenderer(options) {
|
|
|
15720
17050
|
}
|
|
15721
17051
|
writeIdMaskPaletteEntry(fillPalette, maskId, instruction.color, instruction.alpha);
|
|
15722
17052
|
if (instruction.stroke &&
|
|
15723
|
-
instruction.stroke.widthUnit
|
|
17053
|
+
instruction.stroke.widthUnit === undefined &&
|
|
15724
17054
|
instruction.stroke.width > 0) {
|
|
15725
17055
|
writeIdMaskPaletteEntry(strokePalette, maskId, instruction.stroke.color, instruction.stroke.alpha);
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
|
|
15729
|
-
|
|
15730
|
-
|
|
15731
|
-
|
|
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));
|
|
15732
17065
|
}
|
|
15733
17066
|
}
|
|
15734
17067
|
uniforms.uniforms.uFillPalette = fillPalette;
|
|
15735
17068
|
uniforms.uniforms.uStrokePalette = strokePalette;
|
|
15736
17069
|
uniforms.uniforms.uStrokeWidths = strokeWidths;
|
|
17070
|
+
uniforms.uniforms.uStrokeAlignments = strokeAlignments;
|
|
17071
|
+
uniforms.uniforms.uBorderEnabled = hasStroke ? 1 : 0;
|
|
17072
|
+
uniforms.uniforms.uStrokePixelRatio = strokePixelRatio;
|
|
15737
17073
|
uniforms.uniforms.uTextureSize = new Float32Array([
|
|
15738
17074
|
frame.width,
|
|
15739
17075
|
frame.height,
|
|
15740
17076
|
]);
|
|
15741
17077
|
uniforms.uniforms.uMaxStrokeWidth = maxStrokeWidth;
|
|
15742
|
-
uniforms.uniforms.uMaxFractionalStrokeWidth = maxFractionalStrokeWidth;
|
|
15743
17078
|
uniforms.update();
|
|
15744
17079
|
mesh.visible = true;
|
|
15745
17080
|
},
|
|
@@ -15794,14 +17129,11 @@ in vec4 vColor;
|
|
|
15794
17129
|
uniform sampler2D uTexture;
|
|
15795
17130
|
uniform vec2 uTextureSize;
|
|
15796
17131
|
uniform float uMaxStrokeWidth;
|
|
15797
|
-
uniform float
|
|
17132
|
+
uniform float uBorderEnabled;
|
|
17133
|
+
uniform float uStrokePixelRatio;
|
|
15798
17134
|
${idMaskPaletteGlsl}
|
|
15799
17135
|
out vec4 finalColor;
|
|
15800
17136
|
|
|
15801
|
-
float sampleMaskId(vec2 uv) {
|
|
15802
|
-
return floor(texture(uTexture, uv).r * 255.0 + 0.5);
|
|
15803
|
-
}
|
|
15804
|
-
|
|
15805
17137
|
vec4 premultiplyAlpha(vec4 color) {
|
|
15806
17138
|
return vec4(color.rgb * color.a, color.a);
|
|
15807
17139
|
}
|
|
@@ -15813,19 +17145,20 @@ bool differs(float left, float right) {
|
|
|
15813
17145
|
${idMaskStrokeCoverageGlsl}
|
|
15814
17146
|
|
|
15815
17147
|
void main(void) {
|
|
15816
|
-
|
|
15817
|
-
|
|
15818
|
-
|
|
15819
|
-
vec2
|
|
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);
|
|
15820
17153
|
float pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
|
|
15821
17154
|
|
|
15822
17155
|
if (centerId > 0.5) {
|
|
15823
17156
|
vec4 fill = readFill(centerId);
|
|
15824
17157
|
vec4 stroke = readStroke(centerId);
|
|
15825
17158
|
|
|
15826
|
-
float width =
|
|
15827
|
-
if (
|
|
15828
|
-
float coverage = innerStrokeCoverage(centerId,
|
|
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);
|
|
15829
17162
|
if (coverage >= 1.0) {
|
|
15830
17163
|
finalColor = premultiplyAlpha(stroke * vColor);
|
|
15831
17164
|
return;
|
|
@@ -15840,8 +17173,8 @@ void main(void) {
|
|
|
15840
17173
|
return;
|
|
15841
17174
|
}
|
|
15842
17175
|
|
|
15843
|
-
if (
|
|
15844
|
-
vec2 border = findNeighborStroke(centerId,
|
|
17176
|
+
if (uBorderEnabled > 0.5) {
|
|
17177
|
+
vec2 border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
|
|
15845
17178
|
|
|
15846
17179
|
if (border.x > 0.5) {
|
|
15847
17180
|
finalColor = premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
|
|
@@ -15856,9 +17189,11 @@ const interactionMaskFragmentWgsl = `
|
|
|
15856
17189
|
struct InteractionMaskUniforms {
|
|
15857
17190
|
${idMaskFillPaletteWgslField}
|
|
15858
17191
|
uMaxStrokeWidth: f32,
|
|
15859
|
-
|
|
17192
|
+
uBorderEnabled: f32,
|
|
17193
|
+
uStrokePixelRatio: f32,
|
|
15860
17194
|
${idMaskStrokePaletteWgslField}
|
|
15861
17195
|
${idMaskStrokeWidthsWgslField}
|
|
17196
|
+
${idMaskStrokeAlignmentsWgslField}
|
|
15862
17197
|
uTextureSize: vec2<f32>,
|
|
15863
17198
|
}
|
|
15864
17199
|
|
|
@@ -15866,9 +17201,6 @@ struct InteractionMaskUniforms {
|
|
|
15866
17201
|
@group(2) @binding(1) var uTexture: texture_2d<f32>;
|
|
15867
17202
|
@group(2) @binding(2) var uSampler: sampler;
|
|
15868
17203
|
|
|
15869
|
-
fn sampleMaskId(uv: vec2<f32>) -> f32 {
|
|
15870
|
-
return floor(textureSampleLevel(uTexture, uSampler, uv, 0.0).r * 255.0 + 0.5);
|
|
15871
|
-
}
|
|
15872
17204
|
${idMaskPaletteWgsl}
|
|
15873
17205
|
fn premultiplyAlpha(color: vec4<f32>) -> vec4<f32> {
|
|
15874
17206
|
return vec4<f32>(color.rgb * color.a, color.a);
|
|
@@ -15885,19 +17217,20 @@ fn mainFragment(
|
|
|
15885
17217
|
@location(0) vUV: vec2<f32>,
|
|
15886
17218
|
@location(1) vColor: vec4<f32>,
|
|
15887
17219
|
) -> @location(0) vec4<f32> {
|
|
15888
|
-
let
|
|
15889
|
-
let
|
|
15890
|
-
let
|
|
15891
|
-
let
|
|
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);
|
|
15892
17225
|
let pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
|
|
15893
17226
|
|
|
15894
17227
|
if (centerId > 0.5) {
|
|
15895
17228
|
let fill = readFill(centerId);
|
|
15896
17229
|
let stroke = readStroke(centerId);
|
|
15897
17230
|
|
|
15898
|
-
let width =
|
|
15899
|
-
if (maskUniforms.
|
|
15900
|
-
let coverage = innerStrokeCoverage(
|
|
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);
|
|
15901
17234
|
if (coverage >= 1.0) {
|
|
15902
17235
|
return premultiplyAlpha(stroke * vColor);
|
|
15903
17236
|
}
|
|
@@ -15909,8 +17242,8 @@ fn mainFragment(
|
|
|
15909
17242
|
return premultiplyAlpha(fill * vColor);
|
|
15910
17243
|
}
|
|
15911
17244
|
|
|
15912
|
-
if (maskUniforms.
|
|
15913
|
-
let border = findNeighborStroke(
|
|
17245
|
+
if (maskUniforms.uBorderEnabled > 0.5) {
|
|
17246
|
+
let border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
|
|
15914
17247
|
|
|
15915
17248
|
if (border.x > 0.5) {
|
|
15916
17249
|
return premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
|
|
@@ -15922,6 +17255,7 @@ fn mainFragment(
|
|
|
15922
17255
|
`;
|
|
15923
17256
|
|
|
15924
17257
|
function createPixiIdMaskShaderRenderer(options) {
|
|
17258
|
+
const defaultStrokeAlignments = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES);
|
|
15925
17259
|
const uniforms = new options.UniformGroup({
|
|
15926
17260
|
uBorderEnabled: { type: "f32", value: 0 },
|
|
15927
17261
|
uFillPalette: {
|
|
@@ -15930,7 +17264,7 @@ function createPixiIdMaskShaderRenderer(options) {
|
|
|
15930
17264
|
value: new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4),
|
|
15931
17265
|
},
|
|
15932
17266
|
uMaxStrokeWidth: { type: "f32", value: 0 },
|
|
15933
|
-
|
|
17267
|
+
uStrokePixelRatio: { type: "f32", value: 1 },
|
|
15934
17268
|
uStrokePalette: {
|
|
15935
17269
|
size: MAX_ID_MASK_PALETTE_ENTRIES,
|
|
15936
17270
|
type: "vec4<f32>",
|
|
@@ -15941,6 +17275,11 @@ function createPixiIdMaskShaderRenderer(options) {
|
|
|
15941
17275
|
type: "vec4<f32>",
|
|
15942
17276
|
value: new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES),
|
|
15943
17277
|
},
|
|
17278
|
+
uStrokeAlignments: {
|
|
17279
|
+
size: ID_MASK_STROKE_WIDTH_LANES,
|
|
17280
|
+
type: "vec4<f32>",
|
|
17281
|
+
value: defaultStrokeAlignments,
|
|
17282
|
+
},
|
|
15944
17283
|
uTextureSize: {
|
|
15945
17284
|
type: "vec2<f32>",
|
|
15946
17285
|
value: new Float32Array([options.mediaWidth, options.mediaHeight]),
|
|
@@ -15993,24 +17332,30 @@ function createPixiIdMaskShaderRenderer(options) {
|
|
|
15993
17332
|
mesh.visible = false;
|
|
15994
17333
|
},
|
|
15995
17334
|
mesh,
|
|
15996
|
-
render(frame, texture) {
|
|
17335
|
+
render(frame, texture, strokePixelRatio = 1) {
|
|
15997
17336
|
bindTexture(texture.source);
|
|
15998
17337
|
uniforms.uniforms.uFillPalette = frame.fillPalette;
|
|
15999
17338
|
uniforms.uniforms.uStrokePalette = frame.strokePalette;
|
|
16000
17339
|
uniforms.uniforms.uStrokeWidths = frame.strokeWidths;
|
|
17340
|
+
uniforms.uniforms.uStrokeAlignments =
|
|
17341
|
+
frame.strokeAlignments ?? defaultStrokeAlignments;
|
|
17342
|
+
uniforms.uniforms.uStrokePixelRatio = strokePixelRatio;
|
|
16001
17343
|
uniforms.uniforms.uTextureSize = new Float32Array([
|
|
16002
17344
|
frame.width,
|
|
16003
17345
|
frame.height,
|
|
16004
17346
|
]);
|
|
16005
17347
|
uniforms.uniforms.uBorderEnabled = frame.hasStroke ? 1 : 0;
|
|
16006
|
-
uniforms.uniforms.uMaxStrokeWidth =
|
|
16007
|
-
uniforms.uniforms.uMaxFractionalStrokeWidth = frame.strokeWidths.reduce((maximum, width) => width > 0 && width < 1 ? Math.max(maximum, width) : maximum, 0);
|
|
17348
|
+
uniforms.uniforms.uMaxStrokeWidth = frame.maxStrokeWidth;
|
|
16008
17349
|
uniforms.update();
|
|
16009
17350
|
mesh.visible = true;
|
|
16010
17351
|
},
|
|
16011
17352
|
setOpacity(opacity) {
|
|
16012
17353
|
mesh.alpha = opacity;
|
|
16013
17354
|
},
|
|
17355
|
+
setStrokePixelRatio(pixelRatio) {
|
|
17356
|
+
uniforms.uniforms.uStrokePixelRatio = pixelRatio;
|
|
17357
|
+
uniforms.update();
|
|
17358
|
+
},
|
|
16014
17359
|
};
|
|
16015
17360
|
function bindTexture(source) {
|
|
16016
17361
|
try {
|
|
@@ -16063,14 +17408,10 @@ uniform sampler2D uTexture;
|
|
|
16063
17408
|
uniform vec2 uTextureSize;
|
|
16064
17409
|
uniform float uBorderEnabled;
|
|
16065
17410
|
uniform float uMaxStrokeWidth;
|
|
16066
|
-
uniform float
|
|
17411
|
+
uniform float uStrokePixelRatio;
|
|
16067
17412
|
${idMaskPaletteGlsl}
|
|
16068
17413
|
out vec4 finalColor;
|
|
16069
17414
|
|
|
16070
|
-
float sampleMaskId(vec2 uv) {
|
|
16071
|
-
return floor(texture(uTexture, uv).r * 255.0 + 0.5);
|
|
16072
|
-
}
|
|
16073
|
-
|
|
16074
17415
|
vec4 premultiplyAlpha(vec4 color) {
|
|
16075
17416
|
return vec4(color.rgb * color.a, color.a);
|
|
16076
17417
|
}
|
|
@@ -16082,15 +17423,16 @@ bool differs(float left, float right) {
|
|
|
16082
17423
|
${idMaskStrokeCoverageGlsl}
|
|
16083
17424
|
|
|
16084
17425
|
void main(void) {
|
|
16085
|
-
|
|
16086
|
-
|
|
16087
|
-
|
|
16088
|
-
vec2
|
|
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);
|
|
16089
17431
|
float pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
|
|
16090
17432
|
|
|
16091
17433
|
if (centerId < 0.5) {
|
|
16092
|
-
if (uBorderEnabled > 0.5
|
|
16093
|
-
vec2 border = findNeighborStroke(centerId,
|
|
17434
|
+
if (uBorderEnabled > 0.5) {
|
|
17435
|
+
vec2 border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
|
|
16094
17436
|
|
|
16095
17437
|
if (border.x > 0.5) {
|
|
16096
17438
|
finalColor = premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
|
|
@@ -16103,9 +17445,9 @@ void main(void) {
|
|
|
16103
17445
|
}
|
|
16104
17446
|
|
|
16105
17447
|
if (uBorderEnabled > 0.5) {
|
|
16106
|
-
float width =
|
|
17448
|
+
float width = strokeWidthInTexels(centerId, pixelWidth);
|
|
16107
17449
|
if (width > 0.0 && readStroke(centerId).a > 0.0) {
|
|
16108
|
-
float coverage = innerStrokeCoverage(centerId,
|
|
17450
|
+
float coverage = innerStrokeCoverage(centerId, sourceCell, cell, width, pixelWidth);
|
|
16109
17451
|
if (coverage >= 1.0) {
|
|
16110
17452
|
finalColor = premultiplyAlpha(readStroke(centerId) * vColor);
|
|
16111
17453
|
return;
|
|
@@ -16129,9 +17471,10 @@ struct MaskUniforms {
|
|
|
16129
17471
|
uBorderEnabled: f32,
|
|
16130
17472
|
${idMaskFillPaletteWgslField}
|
|
16131
17473
|
uMaxStrokeWidth: f32,
|
|
16132
|
-
|
|
17474
|
+
uStrokePixelRatio: f32,
|
|
16133
17475
|
${idMaskStrokePaletteWgslField}
|
|
16134
17476
|
${idMaskStrokeWidthsWgslField}
|
|
17477
|
+
${idMaskStrokeAlignmentsWgslField}
|
|
16135
17478
|
uTextureSize: vec2<f32>,
|
|
16136
17479
|
}
|
|
16137
17480
|
|
|
@@ -16139,9 +17482,6 @@ struct MaskUniforms {
|
|
|
16139
17482
|
@group(2) @binding(1) var uTexture: texture_2d<f32>;
|
|
16140
17483
|
@group(2) @binding(2) var uSampler: sampler;
|
|
16141
17484
|
|
|
16142
|
-
fn sampleMaskId(uv: vec2<f32>) -> f32 {
|
|
16143
|
-
return floor(textureSampleLevel(uTexture, uSampler, uv, 0.0).r * 255.0 + 0.5);
|
|
16144
|
-
}
|
|
16145
17485
|
${idMaskPaletteWgsl}
|
|
16146
17486
|
fn premultiplyAlpha(color: vec4<f32>) -> vec4<f32> {
|
|
16147
17487
|
return vec4<f32>(color.rgb * color.a, color.a);
|
|
@@ -16158,15 +17498,16 @@ fn mainFragment(
|
|
|
16158
17498
|
@location(0) vUV: vec2<f32>,
|
|
16159
17499
|
@location(1) vColor: vec4<f32>,
|
|
16160
17500
|
) -> @location(0) vec4<f32> {
|
|
16161
|
-
let
|
|
16162
|
-
let
|
|
16163
|
-
let
|
|
16164
|
-
let
|
|
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);
|
|
16165
17506
|
let pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
|
|
16166
17507
|
|
|
16167
17508
|
if (centerId < 0.5) {
|
|
16168
|
-
if (maskUniforms.uBorderEnabled > 0.5
|
|
16169
|
-
let border = findNeighborStroke(
|
|
17509
|
+
if (maskUniforms.uBorderEnabled > 0.5) {
|
|
17510
|
+
let border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
|
|
16170
17511
|
|
|
16171
17512
|
if (border.x > 0.5) {
|
|
16172
17513
|
return premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
|
|
@@ -16177,9 +17518,9 @@ fn mainFragment(
|
|
|
16177
17518
|
}
|
|
16178
17519
|
|
|
16179
17520
|
if (maskUniforms.uBorderEnabled > 0.5) {
|
|
16180
|
-
let width =
|
|
17521
|
+
let width = strokeWidthInTexels(centerId, pixelWidth);
|
|
16181
17522
|
if (width > 0.0 && readStroke(centerId).a > 0.0) {
|
|
16182
|
-
let coverage = innerStrokeCoverage(
|
|
17523
|
+
let coverage = innerStrokeCoverage(centerId, sourceCell, cell, width, pixelWidth);
|
|
16183
17524
|
if (coverage >= 1.0) {
|
|
16184
17525
|
return premultiplyAlpha(readStroke(centerId) * vColor);
|
|
16185
17526
|
}
|
|
@@ -16245,7 +17586,10 @@ function createPixiMaskLayer(options) {
|
|
|
16245
17586
|
const haloTextures = new Map();
|
|
16246
17587
|
const regionMaskTextures = new Map();
|
|
16247
17588
|
const maskFrameOptions = options.renderPreparation?.maskFrame;
|
|
17589
|
+
const capsRasterToDisplay = maskFrameOptions?.display !== undefined;
|
|
16248
17590
|
let rasterDisplay = maskFrameOptions?.display;
|
|
17591
|
+
let rasterResolutionScale = 1;
|
|
17592
|
+
let strokeViewportScale;
|
|
16249
17593
|
const preparedRenderWindow = createPreparedRenderWindow({
|
|
16250
17594
|
artifactKind: options.artifactKind,
|
|
16251
17595
|
detectionTimeline: options.detectionTimeline,
|
|
@@ -16269,6 +17613,7 @@ function createPixiMaskLayer(options) {
|
|
|
16269
17613
|
resolveInstructions: options.resolveInstructions,
|
|
16270
17614
|
resolveMaxRasterWidth,
|
|
16271
17615
|
resolveMaskFrameBackingBytes,
|
|
17616
|
+
resolveMaskDisplayWidth,
|
|
16272
17617
|
});
|
|
16273
17618
|
return {
|
|
16274
17619
|
createSprite({ width, height }) {
|
|
@@ -16387,16 +17732,24 @@ function createPixiMaskLayer(options) {
|
|
|
16387
17732
|
setPlaybackActive(active) {
|
|
16388
17733
|
preparedRenderWindow.setPlaybackActive(active);
|
|
16389
17734
|
},
|
|
16390
|
-
setRasterDisplay(display) {
|
|
16391
|
-
if (
|
|
17735
|
+
setRasterDisplay(display, resolutionScale = 1) {
|
|
17736
|
+
if (isDestroyed)
|
|
16392
17737
|
return;
|
|
16393
17738
|
const previousWidth = resolveMaxRasterWidth();
|
|
16394
17739
|
rasterDisplay = display;
|
|
17740
|
+
rasterResolutionScale = resolutionScale;
|
|
17741
|
+
idMaskRenderer?.setStrokePixelRatio(resolveDisplayPixelRatio(display) * resolutionScale);
|
|
16395
17742
|
const nextWidth = resolveMaxRasterWidth();
|
|
16396
17743
|
if (mediaWidth > 0 && previousWidth !== nextWidth) {
|
|
16397
17744
|
preparedRenderWindow.invalidateRasterSize();
|
|
16398
17745
|
}
|
|
16399
17746
|
},
|
|
17747
|
+
setViewportScale(scale) {
|
|
17748
|
+
if (isDestroyed || scale <= 0 || scale === strokeViewportScale)
|
|
17749
|
+
return;
|
|
17750
|
+
strokeViewportScale = scale;
|
|
17751
|
+
preparedRenderWindow.invalidateMaskDisplayWidth();
|
|
17752
|
+
},
|
|
16400
17753
|
setTimelineContext(context) {
|
|
16401
17754
|
preparedRenderWindow.setTimelineContext(context);
|
|
16402
17755
|
},
|
|
@@ -16486,14 +17839,22 @@ function createPixiMaskLayer(options) {
|
|
|
16486
17839
|
maskTextures.set(maskFrame.key, texture);
|
|
16487
17840
|
return texture;
|
|
16488
17841
|
}
|
|
16489
|
-
|
|
16490
|
-
|
|
16491
|
-
|
|
16492
|
-
|
|
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. */
|
|
16493
17853
|
function resolveMaxRasterWidth() {
|
|
16494
17854
|
const display = rasterDisplay;
|
|
16495
17855
|
const artifactKind = options.artifactKind ?? RenderPreparationArtifactKind.MaskFrame;
|
|
16496
17856
|
if (!display ||
|
|
17857
|
+
!capsRasterToDisplay ||
|
|
16497
17858
|
artifactKind !== RenderPreparationArtifactKind.MaskFrame ||
|
|
16498
17859
|
mediaWidth <= 0 ||
|
|
16499
17860
|
mediaHeight <= 0) {
|
|
@@ -16502,7 +17863,7 @@ function createPixiMaskLayer(options) {
|
|
|
16502
17863
|
const fit = Math.min(display.boxWidth / mediaWidth, display.boxHeight / mediaHeight);
|
|
16503
17864
|
const pixelRatio = resolveDisplayPixelRatio(display);
|
|
16504
17865
|
return fit > 0 && pixelRatio > 0
|
|
16505
|
-
? alignRasterWidth(Math.ceil(mediaWidth * fit * pixelRatio))
|
|
17866
|
+
? alignRasterWidth(Math.ceil(mediaWidth * fit * pixelRatio * rasterResolutionScale))
|
|
16506
17867
|
: undefined;
|
|
16507
17868
|
}
|
|
16508
17869
|
/**
|
|
@@ -16583,7 +17944,9 @@ function createPixiMaskLayer(options) {
|
|
|
16583
17944
|
return;
|
|
16584
17945
|
}
|
|
16585
17946
|
maskSprite.visible = false;
|
|
16586
|
-
idMaskRenderer.render(maskFrame, texture
|
|
17947
|
+
idMaskRenderer.render(maskFrame, texture, rasterDisplay
|
|
17948
|
+
? resolveDisplayPixelRatio(rasterDisplay) * rasterResolutionScale
|
|
17949
|
+
: 1);
|
|
16587
17950
|
}
|
|
16588
17951
|
function hideFill() {
|
|
16589
17952
|
if (maskSprite) {
|
|
@@ -17022,11 +18385,15 @@ function createPixiPolygonLayer(options) {
|
|
|
17022
18385
|
rasterLayer.setMaskStyle(nextPolygonStyle ? createArtifactStyle() : nextPolygonStyle);
|
|
17023
18386
|
},
|
|
17024
18387
|
needsRenderPreparationWait: rasterLayer.needsRenderPreparationWait,
|
|
18388
|
+
setPlaybackActive: rasterLayer.setPlaybackActive,
|
|
18389
|
+
setRasterDisplay: rasterLayer.setRasterDisplay,
|
|
17025
18390
|
setTimelineContext: rasterLayer.setTimelineContext,
|
|
18391
|
+
setViewportScale: adoptViewportScale,
|
|
17026
18392
|
waitForRenderPreparation: rasterLayer.waitForRenderPreparation,
|
|
17027
18393
|
};
|
|
17028
18394
|
function adoptViewportScale(nextViewportScale) {
|
|
17029
18395
|
const normalizedViewportScale = Math.max(nextViewportScale, Number.EPSILON);
|
|
18396
|
+
rasterLayer.setViewportScale(normalizedViewportScale);
|
|
17030
18397
|
if (normalizedViewportScale === viewportScale) {
|
|
17031
18398
|
return;
|
|
17032
18399
|
}
|
|
@@ -17090,9 +18457,10 @@ function resolvePreparedPolygonInstructions(options) {
|
|
|
17090
18457
|
},
|
|
17091
18458
|
stroke: instruction.stroke
|
|
17092
18459
|
? {
|
|
18460
|
+
alignment: BoxStrokeAlignment.Center,
|
|
17093
18461
|
alpha: instruction.stroke.alpha,
|
|
17094
18462
|
color: instruction.stroke.color,
|
|
17095
|
-
width:
|
|
18463
|
+
width: instruction.stroke.width,
|
|
17096
18464
|
}
|
|
17097
18465
|
: undefined,
|
|
17098
18466
|
});
|
|
@@ -17237,17 +18605,30 @@ function createPixiRegionCoverageMask(options) {
|
|
|
17237
18605
|
});
|
|
17238
18606
|
let shader = createShader();
|
|
17239
18607
|
const display = new options.Mesh({ geometry, shader });
|
|
17240
|
-
const
|
|
18608
|
+
const container = new options.Container();
|
|
18609
|
+
container.addChild(display);
|
|
18610
|
+
const effect = new options.AlphaMask({ mask: container });
|
|
18611
|
+
let antialiasFilter;
|
|
17241
18612
|
return {
|
|
17242
18613
|
destroy() {
|
|
18614
|
+
if (antialiasFilter && display.filters?.includes(antialiasFilter)) {
|
|
18615
|
+
display.filters = null;
|
|
18616
|
+
}
|
|
18617
|
+
antialiasFilter?.destroy();
|
|
17243
18618
|
effect.destroy();
|
|
17244
18619
|
display.destroy();
|
|
18620
|
+
container.destroy();
|
|
17245
18621
|
destroyShaderKeepingProgram(shader);
|
|
17246
18622
|
geometry.destroy();
|
|
17247
18623
|
placeholderSource.destroy();
|
|
17248
18624
|
},
|
|
18625
|
+
container,
|
|
17249
18626
|
display,
|
|
17250
18627
|
effect,
|
|
18628
|
+
setAntialiasResolution(resolution) {
|
|
18629
|
+
if (antialiasFilter)
|
|
18630
|
+
antialiasFilter.resolution = resolution;
|
|
18631
|
+
},
|
|
17251
18632
|
releaseTexture(source) {
|
|
17252
18633
|
if (shader.resources.uTexture !== source)
|
|
17253
18634
|
return;
|
|
@@ -17256,6 +18637,7 @@ function createPixiRegionCoverageMask(options) {
|
|
|
17256
18637
|
},
|
|
17257
18638
|
render(renderOptions) {
|
|
17258
18639
|
bindTexture(renderOptions.artifact.texture.source);
|
|
18640
|
+
const alphaCoverage = updateAntialiasing();
|
|
17259
18641
|
display.visible = true;
|
|
17260
18642
|
uniforms.uniforms.uCrop = new Float32Array([
|
|
17261
18643
|
renderOptions.crop.x,
|
|
@@ -17273,8 +18655,34 @@ function createPixiRegionCoverageMask(options) {
|
|
|
17273
18655
|
display.position.set(renderOptions.x, renderOptions.y);
|
|
17274
18656
|
display.scale.set(renderOptions.width * (renderOptions.flipHorizontal ? -1 : 1), renderOptions.height * (renderOptions.flipVertical ? -1 : 1));
|
|
17275
18657
|
display.rotation = renderOptions.rotation;
|
|
18658
|
+
syncViewportBounds();
|
|
18659
|
+
return alphaCoverage;
|
|
17276
18660
|
},
|
|
18661
|
+
syncViewportBounds,
|
|
17277
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
|
+
}
|
|
17278
18686
|
function bindTexture(source) {
|
|
17279
18687
|
try {
|
|
17280
18688
|
shader.resources.uTexture = source;
|
|
@@ -17576,6 +18984,7 @@ function createPixiRegionLayer(options) {
|
|
|
17576
18984
|
let currentMediaTime = 0;
|
|
17577
18985
|
let currentViewportScale = 1;
|
|
17578
18986
|
let destroyed = false;
|
|
18987
|
+
let viewportBounds;
|
|
17579
18988
|
let renderers = [...options.regionRenderers];
|
|
17580
18989
|
const assets = new Map();
|
|
17581
18990
|
const entries = new Map();
|
|
@@ -17656,6 +19065,7 @@ function createPixiRegionLayer(options) {
|
|
|
17656
19065
|
}
|
|
17657
19066
|
const position = positionSprite(entry.display, renderer, region, sourceSize, viewportScale);
|
|
17658
19067
|
updateEffect(entry, renderer, viewportScale);
|
|
19068
|
+
updateAssetAntialiasing(entry, renderer);
|
|
17659
19069
|
if (renderer.source.kind === RegionRendererSourceKind.Media &&
|
|
17660
19070
|
mediaCrop &&
|
|
17661
19071
|
!updateMediaCoverage(entry, renderer, detection, detectionIndex, mediaCrop)) {
|
|
@@ -17707,6 +19117,15 @@ function createPixiRegionLayer(options) {
|
|
|
17707
19117
|
}
|
|
17708
19118
|
}
|
|
17709
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
|
+
},
|
|
17710
19129
|
setRenderers(nextRenderers) {
|
|
17711
19130
|
renderers = [...nextRenderers];
|
|
17712
19131
|
syncSources();
|
|
@@ -17720,10 +19139,24 @@ function createPixiRegionLayer(options) {
|
|
|
17720
19139
|
entry.display.position.set(entry.baseX + x, entry.baseY + y);
|
|
17721
19140
|
entry.coverageMask?.position.set(entry.baseX + x, entry.baseY + y);
|
|
17722
19141
|
entry.exactCoverageMask?.display.position.set(entry.baseX + x, entry.baseY + y);
|
|
19142
|
+
syncEntryViewportBounds(entry);
|
|
17723
19143
|
translated = true;
|
|
17724
19144
|
}
|
|
17725
19145
|
return translated;
|
|
17726
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
|
+
},
|
|
17727
19160
|
destroy() {
|
|
17728
19161
|
destroyed = true;
|
|
17729
19162
|
destroyAllDisplays();
|
|
@@ -17845,11 +19278,18 @@ function createPixiRegionLayer(options) {
|
|
|
17845
19278
|
return entry;
|
|
17846
19279
|
}
|
|
17847
19280
|
function updateMediaCoverage(entry, renderer, detection, detectionIndex, crop) {
|
|
17848
|
-
if (renderer.source.kind !== RegionRendererSourceKind.Media
|
|
17849
|
-
renderer.source.coverage === undefined) {
|
|
19281
|
+
if (renderer.source.kind !== RegionRendererSourceKind.Media) {
|
|
17850
19282
|
removeCoverageMask(entry);
|
|
17851
19283
|
return true;
|
|
17852
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
|
+
}
|
|
17853
19293
|
if (renderer.source.coverage.kind === RegionRendererCoverageKind.Mask) {
|
|
17854
19294
|
removePolygonCoverageMask(entry);
|
|
17855
19295
|
const artifact = options.getActiveRegionMaskCoverage(currentFrame?.mediaTime ?? null);
|
|
@@ -17867,7 +19307,14 @@ function createPixiRegionLayer(options) {
|
|
|
17867
19307
|
}
|
|
17868
19308
|
const mask = entry.exactCoverageMask ??
|
|
17869
19309
|
createPixiRegionCoverageMask({
|
|
19310
|
+
getAnnotationAntialiasing: options.getAnnotationAntialiasing,
|
|
19311
|
+
getAntialiasResolution: options.getAntialiasResolution,
|
|
19312
|
+
getViewportBounds: options.getViewportBounds,
|
|
19313
|
+
defaultFilterVert: options.defaultFilterVert,
|
|
19314
|
+
Filter: options.Filter,
|
|
17870
19315
|
AlphaMask: options.AlphaMask,
|
|
19316
|
+
Container: options.Container,
|
|
19317
|
+
Rectangle: options.Rectangle,
|
|
17871
19318
|
ImageSource: options.ImageSource,
|
|
17872
19319
|
Mesh: options.Mesh,
|
|
17873
19320
|
MeshGeometry: options.MeshGeometry,
|
|
@@ -17876,7 +19323,7 @@ function createPixiRegionLayer(options) {
|
|
|
17876
19323
|
});
|
|
17877
19324
|
if (!entry.exactCoverageMask) {
|
|
17878
19325
|
entry.exactCoverageMask = mask;
|
|
17879
|
-
container?.addChild(mask.
|
|
19326
|
+
container?.addChild(mask.container);
|
|
17880
19327
|
// A Mesh assigned through `display.mask` becomes a stencil mask in
|
|
17881
19328
|
// Pixi v8, which clips by the quad geometry and ignores shader alpha.
|
|
17882
19329
|
// AlphaMask renders the id-sampling mesh to a GPU texture first, so
|
|
@@ -17888,7 +19335,7 @@ function createPixiRegionLayer(options) {
|
|
|
17888
19335
|
removeExactCoverageMask(entry);
|
|
17889
19336
|
return false;
|
|
17890
19337
|
}
|
|
17891
|
-
mask.render({
|
|
19338
|
+
const alphaCoverage = mask.render({
|
|
17892
19339
|
artifact: { texture: coverageTexture },
|
|
17893
19340
|
coverage: coverageEntry,
|
|
17894
19341
|
crop,
|
|
@@ -17900,6 +19347,7 @@ function createPixiRegionLayer(options) {
|
|
|
17900
19347
|
x: entry.display.position.x,
|
|
17901
19348
|
y: entry.display.position.y,
|
|
17902
19349
|
});
|
|
19350
|
+
setCoverageChannel(entry, alphaCoverage);
|
|
17903
19351
|
return true;
|
|
17904
19352
|
}
|
|
17905
19353
|
if (renderer.source.coverage.kind !== RegionRendererCoverageKind.Polygon ||
|
|
@@ -17909,26 +19357,95 @@ function createPixiRegionLayer(options) {
|
|
|
17909
19357
|
return false;
|
|
17910
19358
|
}
|
|
17911
19359
|
removeExactCoverageMask(entry);
|
|
19360
|
+
return updateGraphicsCoverage(entry, renderer, crop, detection.polygon.points);
|
|
19361
|
+
}
|
|
19362
|
+
function updateGraphicsCoverage(entry, renderer, crop, points) {
|
|
17912
19363
|
const mask = entry.coverageMask ?? new options.Graphics();
|
|
17913
19364
|
if (!entry.coverageMask) {
|
|
17914
19365
|
entry.coverageMask = mask;
|
|
17915
|
-
|
|
17916
|
-
entry.
|
|
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);
|
|
17917
19376
|
}
|
|
17918
|
-
|
|
17919
|
-
.
|
|
17920
|
-
|
|
17921
|
-
|
|
17922
|
-
y - crop.y - crop.height / 2,
|
|
17923
|
-
]), true)
|
|
17924
|
-
.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 });
|
|
17925
19381
|
mask.position.set(entry.display.position.x, entry.display.position.y);
|
|
17926
19382
|
mask.scale.set((entry.display.width / crop.width) *
|
|
17927
19383
|
(renderer.transform?.flip?.horizontal ? -1 : 1), (entry.display.height / crop.height) *
|
|
17928
19384
|
(renderer.transform?.flip?.vertical ? -1 : 1));
|
|
17929
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
|
+
}
|
|
17930
19412
|
return true;
|
|
17931
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
|
+
}
|
|
17932
19449
|
function updateEffect(entry, renderer, viewportScale) {
|
|
17933
19450
|
const effect = renderer.source.kind === RegionRendererSourceKind.Media
|
|
17934
19451
|
? renderer.source.effect
|
|
@@ -17948,26 +19465,6 @@ function createPixiRegionLayer(options) {
|
|
|
17948
19465
|
}
|
|
17949
19466
|
entry.effect?.apply(entry.display, viewportScale);
|
|
17950
19467
|
}
|
|
17951
|
-
function removeCoverageMask(entry) {
|
|
17952
|
-
removePolygonCoverageMask(entry);
|
|
17953
|
-
removeExactCoverageMask(entry);
|
|
17954
|
-
}
|
|
17955
|
-
function removePolygonCoverageMask(entry) {
|
|
17956
|
-
if (!entry.coverageMask)
|
|
17957
|
-
return;
|
|
17958
|
-
entry.display.mask = null;
|
|
17959
|
-
entry.coverageMask.removeFromParent();
|
|
17960
|
-
entry.coverageMask.destroy();
|
|
17961
|
-
entry.coverageMask = undefined;
|
|
17962
|
-
}
|
|
17963
|
-
function removeExactCoverageMask(entry) {
|
|
17964
|
-
if (!entry.exactCoverageMask)
|
|
17965
|
-
return;
|
|
17966
|
-
entry.display.removeEffect(entry.exactCoverageMask.effect);
|
|
17967
|
-
entry.exactCoverageMask.display.removeFromParent();
|
|
17968
|
-
entry.exactCoverageMask.destroy();
|
|
17969
|
-
entry.exactCoverageMask = undefined;
|
|
17970
|
-
}
|
|
17971
19468
|
function destroyRendererDisplays(rendererId) {
|
|
17972
19469
|
for (const [key, entry] of entries) {
|
|
17973
19470
|
if (entry.rendererId !== rendererId)
|
|
@@ -17999,17 +19496,53 @@ function createPixiRegionLayer(options) {
|
|
|
17999
19496
|
pools.clear();
|
|
18000
19497
|
}
|
|
18001
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
|
+
}
|
|
18002
19541
|
function destroyEntry(entry) {
|
|
18003
19542
|
entry.effect?.destroy();
|
|
18004
19543
|
entry.effect = undefined;
|
|
18005
|
-
entry
|
|
18006
|
-
entry
|
|
18007
|
-
entry.coverageMask?.destroy();
|
|
18008
|
-
if (entry.exactCoverageMask) {
|
|
18009
|
-
entry.display.removeEffect(entry.exactCoverageMask.effect);
|
|
18010
|
-
entry.exactCoverageMask.display.removeFromParent();
|
|
18011
|
-
entry.exactCoverageMask.destroy();
|
|
18012
|
-
}
|
|
19544
|
+
removeCoverageMask(entry);
|
|
19545
|
+
destroyEntryAntialiasFilter(entry);
|
|
18013
19546
|
entry.display.removeFromParent?.();
|
|
18014
19547
|
// GifSprite sources are shared and released through Assets.unload().
|
|
18015
19548
|
entry.display.destroy();
|
|
@@ -18017,6 +19550,18 @@ function destroyEntry(entry) {
|
|
|
18017
19550
|
// lightweight crop texture; the scene destroys the shared source.
|
|
18018
19551
|
entry.ownedTexture?.destroy(false);
|
|
18019
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
|
+
}
|
|
18020
19565
|
function isAnimatedDisplay(display) {
|
|
18021
19566
|
return "play" in display && typeof display.play === "function";
|
|
18022
19567
|
}
|
|
@@ -18212,6 +19757,7 @@ function acquireAsset(loader, src) {
|
|
|
18212
19757
|
|
|
18213
19758
|
function createPixiAnnotationOverlayLayer(editingEngine, initialStyle, initialKeypointStyle) {
|
|
18214
19759
|
let graphics;
|
|
19760
|
+
let renderedEditingBox;
|
|
18215
19761
|
let style = resolveAnnotationOverlayStyle(initialStyle);
|
|
18216
19762
|
let keypointStyle = initialKeypointStyle === undefined
|
|
18217
19763
|
? new BaseKeypointStyle()
|
|
@@ -18228,12 +19774,16 @@ function createPixiAnnotationOverlayLayer(editingEngine, initialStyle, initialKe
|
|
|
18228
19774
|
if (next !== undefined)
|
|
18229
19775
|
keypointStyle = next;
|
|
18230
19776
|
},
|
|
19777
|
+
getRenderedEditingBox() {
|
|
19778
|
+
return renderedEditingBox;
|
|
19779
|
+
},
|
|
18231
19780
|
draw(context) {
|
|
19781
|
+
renderedEditingBox = undefined;
|
|
18232
19782
|
if (!graphics)
|
|
18233
19783
|
return;
|
|
18234
19784
|
graphics.clear();
|
|
18235
19785
|
drawExternalPreview(graphics, context.previewOverlay, context.viewportScale, style);
|
|
18236
|
-
drawEditingPreview(graphics, editingEngine, context.frame, context.viewportScale, style, keypointStyle);
|
|
19786
|
+
renderedEditingBox = drawEditingPreview(graphics, editingEngine, context.frame, context.viewportScale, style, keypointStyle);
|
|
18237
19787
|
drawSelectionHandles(graphics, context, style, editingEngine);
|
|
18238
19788
|
drawMarquee(graphics, context.marquee, style);
|
|
18239
19789
|
drawGuides(graphics, editingEngine, context, style);
|
|
@@ -18246,6 +19796,7 @@ function drawEditingPreview(graphics, engine, frame, viewportScale, style, keypo
|
|
|
18246
19796
|
const detection = state?.preview;
|
|
18247
19797
|
if (!detection)
|
|
18248
19798
|
return;
|
|
19799
|
+
let renderedBox;
|
|
18249
19800
|
const styleContext = {
|
|
18250
19801
|
gestureKind: state.kind,
|
|
18251
19802
|
viewportScale,
|
|
@@ -18272,11 +19823,16 @@ function drawEditingPreview(graphics, engine, frame, viewportScale, style, keypo
|
|
|
18272
19823
|
{ x: x + width / 2, y: y + height / 2 },
|
|
18273
19824
|
{ x: x - width / 2, y: y + height / 2 },
|
|
18274
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
|
+
}
|
|
18275
19831
|
}
|
|
18276
19832
|
if (detection.polygon) {
|
|
18277
19833
|
if (state.kind === AnnotationGestureStateKind.Creating) {
|
|
18278
19834
|
drawCreatingPolygon(graphics, detection.polygon.points, viewportScale, stroke, closeZoneStroke);
|
|
18279
|
-
return;
|
|
19835
|
+
return renderedBox;
|
|
18280
19836
|
}
|
|
18281
19837
|
graphics
|
|
18282
19838
|
.poly(detection.polygon.points.flatMap(({ x, y }) => [x, y]), true)
|
|
@@ -18308,6 +19864,7 @@ function drawEditingPreview(graphics, engine, frame, viewportScale, style, keypo
|
|
|
18308
19864
|
if (instruction)
|
|
18309
19865
|
drawPixiKeypointInstruction(graphics, instruction, viewportScale);
|
|
18310
19866
|
}
|
|
19867
|
+
return renderedBox;
|
|
18311
19868
|
}
|
|
18312
19869
|
const CREATION_VERTEX_RADIUS = 6;
|
|
18313
19870
|
const CREATION_VERTEX_STROKE_WIDTH = 1.5;
|
|
@@ -18560,92 +20117,6 @@ function resolveAnnotationOverlayStyle(style) {
|
|
|
18560
20117
|
};
|
|
18561
20118
|
}
|
|
18562
20119
|
|
|
18563
|
-
var PixiSceneLayerKind;
|
|
18564
|
-
(function (PixiSceneLayerKind) {
|
|
18565
|
-
PixiSceneLayerKind["Media"] = "media";
|
|
18566
|
-
PixiSceneLayerKind["Depth"] = "depth";
|
|
18567
|
-
PixiSceneLayerKind["Heatmap"] = "heatmap";
|
|
18568
|
-
PixiSceneLayerKind["Mask"] = "mask";
|
|
18569
|
-
PixiSceneLayerKind["Box"] = "box";
|
|
18570
|
-
PixiSceneLayerKind["Vector"] = "vector";
|
|
18571
|
-
PixiSceneLayerKind["Region"] = "region";
|
|
18572
|
-
PixiSceneLayerKind["Focus"] = "focus";
|
|
18573
|
-
PixiSceneLayerKind["Preview"] = "preview";
|
|
18574
|
-
PixiSceneLayerKind["Guide"] = "guide";
|
|
18575
|
-
PixiSceneLayerKind["Handle"] = "handle";
|
|
18576
|
-
PixiSceneLayerKind["Interaction"] = "interaction";
|
|
18577
|
-
PixiSceneLayerKind["Label"] = "label";
|
|
18578
|
-
})(PixiSceneLayerKind || (PixiSceneLayerKind = {}));
|
|
18579
|
-
function createPixiSceneLayerSlot(kind, order = defaultLayerOrder(kind)) {
|
|
18580
|
-
let display;
|
|
18581
|
-
return {
|
|
18582
|
-
getDisplay() {
|
|
18583
|
-
return display;
|
|
18584
|
-
},
|
|
18585
|
-
kind,
|
|
18586
|
-
order,
|
|
18587
|
-
setDisplay(nextDisplay) {
|
|
18588
|
-
display = nextDisplay;
|
|
18589
|
-
},
|
|
18590
|
-
};
|
|
18591
|
-
}
|
|
18592
|
-
function syncPixiSceneLayerChildren(scene, slots) {
|
|
18593
|
-
if (!scene) {
|
|
18594
|
-
return;
|
|
18595
|
-
}
|
|
18596
|
-
const displays = [...slots]
|
|
18597
|
-
.sort((left, right) => left.order - right.order)
|
|
18598
|
-
.map((slot) => slot.getDisplay())
|
|
18599
|
-
.filter((display) => display !== undefined);
|
|
18600
|
-
clearSceneChildren(scene);
|
|
18601
|
-
if (displays.length === 0) {
|
|
18602
|
-
return;
|
|
18603
|
-
}
|
|
18604
|
-
scene.addChild(...displays);
|
|
18605
|
-
}
|
|
18606
|
-
function defaultLayerOrder(kind) {
|
|
18607
|
-
switch (kind) {
|
|
18608
|
-
case PixiSceneLayerKind.Media:
|
|
18609
|
-
return 0;
|
|
18610
|
-
case PixiSceneLayerKind.Depth:
|
|
18611
|
-
return 25;
|
|
18612
|
-
case PixiSceneLayerKind.Heatmap:
|
|
18613
|
-
return 50;
|
|
18614
|
-
case PixiSceneLayerKind.Mask:
|
|
18615
|
-
return 100;
|
|
18616
|
-
case PixiSceneLayerKind.Box:
|
|
18617
|
-
return 200;
|
|
18618
|
-
case PixiSceneLayerKind.Vector:
|
|
18619
|
-
return 250;
|
|
18620
|
-
case PixiSceneLayerKind.Region:
|
|
18621
|
-
return 275;
|
|
18622
|
-
case PixiSceneLayerKind.Focus:
|
|
18623
|
-
return 300;
|
|
18624
|
-
case PixiSceneLayerKind.Preview:
|
|
18625
|
-
return 400;
|
|
18626
|
-
case PixiSceneLayerKind.Guide:
|
|
18627
|
-
return 500;
|
|
18628
|
-
case PixiSceneLayerKind.Handle:
|
|
18629
|
-
return 750;
|
|
18630
|
-
case PixiSceneLayerKind.Interaction:
|
|
18631
|
-
return 700;
|
|
18632
|
-
case PixiSceneLayerKind.Label:
|
|
18633
|
-
return 800;
|
|
18634
|
-
default:
|
|
18635
|
-
return 350;
|
|
18636
|
-
}
|
|
18637
|
-
}
|
|
18638
|
-
function clearSceneChildren(scene) {
|
|
18639
|
-
if (typeof scene.removeChildren === "function") {
|
|
18640
|
-
scene.removeChildren();
|
|
18641
|
-
return;
|
|
18642
|
-
}
|
|
18643
|
-
const fakeScene = scene;
|
|
18644
|
-
if (Array.isArray(fakeScene.children)) {
|
|
18645
|
-
fakeScene.children.splice(0);
|
|
18646
|
-
}
|
|
18647
|
-
}
|
|
18648
|
-
|
|
18649
20120
|
function calculatePixiSceneFit(options) {
|
|
18650
20121
|
const { fit, mediaHeight, mediaWidth, screenHeight, screenWidth } = options;
|
|
18651
20122
|
if (mediaWidth <= 0 ||
|
|
@@ -18681,6 +20152,7 @@ const REGION_COVERAGE_ONLY_MASK_STYLE = {
|
|
|
18681
20152
|
const STATIC_FOCUS_SETTLE_MS = 10_000;
|
|
18682
20153
|
/** The presentation fields a render answers to that no renderer kind owns. */
|
|
18683
20154
|
const RENDERED_PRESENTATION_TAIL = [
|
|
20155
|
+
"annotationAntialiasing",
|
|
18684
20156
|
"annotationOverlayStyle",
|
|
18685
20157
|
"backgroundColor",
|
|
18686
20158
|
"focusStyle",
|
|
@@ -18776,6 +20248,14 @@ async function createPixiMediaScene(options) {
|
|
|
18776
20248
|
let currentFocusStyle = options.focusStyle ?? null;
|
|
18777
20249
|
let currentLabelStyle = options.labelStyle ?? null;
|
|
18778
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
|
+
});
|
|
18779
20259
|
let currentMaskHaloStyle = options.maskHaloStyle ?? null;
|
|
18780
20260
|
let currentBoxCornerStyle = options.boxCornerStyle ?? null;
|
|
18781
20261
|
let currentEllipseStyle = options.ellipseStyle ?? null;
|
|
@@ -18870,6 +20350,7 @@ async function createPixiMediaScene(options) {
|
|
|
18870
20350
|
isVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
|
|
18871
20351
|
// A partial heatmap changes pixels before the all-ready token changes.
|
|
18872
20352
|
onPreparedWindowChange: () => redrawAnnotationsNow(true),
|
|
20353
|
+
onTextureRelease: (texture) => focusLayer?.releaseMaskTexture(texture),
|
|
18873
20354
|
prepareTexture: (texture) => app.renderer.prepare.upload(texture),
|
|
18874
20355
|
renderPreparation: options.renderPreparation,
|
|
18875
20356
|
renderers: currentHeatmapRenderers,
|
|
@@ -18931,6 +20412,7 @@ async function createPixiMediaScene(options) {
|
|
|
18931
20412
|
resolveContextState,
|
|
18932
20413
|
})
|
|
18933
20414
|
: undefined;
|
|
20415
|
+
polygonLayer?.setPlaybackActive(isPlaybackActive);
|
|
18934
20416
|
const vectorLayer = createPixiVectorLayer({
|
|
18935
20417
|
container: retainedGeometry,
|
|
18936
20418
|
Container,
|
|
@@ -18960,6 +20442,9 @@ async function createPixiMediaScene(options) {
|
|
|
18960
20442
|
Texture,
|
|
18961
20443
|
UniformGroup,
|
|
18962
20444
|
detectionTimeline: annotationDetectionTimeline,
|
|
20445
|
+
getAnnotationAntialiasing: () => Boolean(annotationAntialiasing),
|
|
20446
|
+
getAntialiasResolution: () => annotationAntialiasResolution,
|
|
20447
|
+
getViewportBounds: resolveViewportBounds,
|
|
18963
20448
|
getActiveRegionMaskCoverage: (detectionFrameTime) => maskLayer?.getActiveRegionMaskCoverage(detectionFrameTime) ?? null,
|
|
18964
20449
|
// Under GPU compositing the decoded frame lands in a texture the
|
|
18965
20450
|
// compositor swaps onto the media sprite, leaving the canvas that
|
|
@@ -19011,6 +20496,7 @@ async function createPixiMediaScene(options) {
|
|
|
19011
20496
|
Text,
|
|
19012
20497
|
detectionTimeline: annotationDetectionTimeline,
|
|
19013
20498
|
labelStyle: options.labelStyle,
|
|
20499
|
+
getRenderedBoxes: () => boxLayer.getRenderedBoxes(),
|
|
19014
20500
|
resolveContextState: resolveLabelContextState,
|
|
19015
20501
|
})
|
|
19016
20502
|
: undefined;
|
|
@@ -19024,6 +20510,7 @@ async function createPixiMediaScene(options) {
|
|
|
19024
20510
|
resolution: resolvePixiResolution(options.maxDevicePixelRatio),
|
|
19025
20511
|
});
|
|
19026
20512
|
const destroyBatchTextureBindings = installPixiBatchTextureBindings(pixi, app.renderer);
|
|
20513
|
+
const destroyFilterBindings = installPixiFilterBindings(pixi, app.renderer);
|
|
19027
20514
|
const rendererCanvas = app.canvas;
|
|
19028
20515
|
rendererCanvas.style.display = "block";
|
|
19029
20516
|
rendererCanvas.style.position = "absolute";
|
|
@@ -19069,6 +20556,7 @@ async function createPixiMediaScene(options) {
|
|
|
19069
20556
|
const focusSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Focus);
|
|
19070
20557
|
const previewSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Preview);
|
|
19071
20558
|
const interactionSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Interaction);
|
|
20559
|
+
const interactionLabelSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Label, interactionSlot.order);
|
|
19072
20560
|
const handleSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Handle);
|
|
19073
20561
|
const labelSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Label);
|
|
19074
20562
|
const layerSlots = [
|
|
@@ -19083,6 +20571,7 @@ async function createPixiMediaScene(options) {
|
|
|
19083
20571
|
previewSlot,
|
|
19084
20572
|
handleSlot,
|
|
19085
20573
|
interactionSlot,
|
|
20574
|
+
interactionLabelSlot,
|
|
19086
20575
|
labelSlot,
|
|
19087
20576
|
];
|
|
19088
20577
|
const viewport = createViewportController({ scale: 1 });
|
|
@@ -19254,13 +20743,18 @@ async function createPixiMediaScene(options) {
|
|
|
19254
20743
|
}
|
|
19255
20744
|
let focusLayer = options.focusStyle
|
|
19256
20745
|
? createPixiFocusLayer({
|
|
20746
|
+
AlphaMask,
|
|
19257
20747
|
Container,
|
|
19258
20748
|
Graphics,
|
|
19259
20749
|
ImageSource,
|
|
19260
20750
|
Mesh,
|
|
19261
20751
|
MeshGeometry,
|
|
20752
|
+
Rectangle,
|
|
19262
20753
|
Shader,
|
|
19263
20754
|
UniformGroup,
|
|
20755
|
+
getHeatmapRenderers: () => currentHeatmapRenderers,
|
|
20756
|
+
getAnnotationAntialiasing: () => Boolean(annotationAntialiasing),
|
|
20757
|
+
getViewportBounds: resolveViewportBounds,
|
|
19264
20758
|
focusStyle: options.focusStyle,
|
|
19265
20759
|
isDetectionVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
|
|
19266
20760
|
})
|
|
@@ -19270,7 +20764,9 @@ async function createPixiMediaScene(options) {
|
|
|
19270
20764
|
let stagingTextureSource;
|
|
19271
20765
|
const collectFrameTimings = options.diagnostics?.frameTimings === true;
|
|
19272
20766
|
const syncSceneChildren = () => {
|
|
19273
|
-
|
|
20767
|
+
labelLayer?.setBackgroundAntialiasFilter(annotationAntialiasLayer.getFxaaFilter());
|
|
20768
|
+
interactionPresentationLayer?.setBackgroundAntialiasFilter(annotationAntialiasLayer.getFxaaFilter());
|
|
20769
|
+
annotationAntialiasLayer.sync(mediaScene, layerSlots);
|
|
19274
20770
|
};
|
|
19275
20771
|
const updateMediaSceneFit = () => {
|
|
19276
20772
|
if (!mediaScene || mediaWidth <= 0 || mediaHeight <= 0) {
|
|
@@ -19336,8 +20832,24 @@ async function createPixiMediaScene(options) {
|
|
|
19336
20832
|
rendererCanvas.style.left = `${next.left}px`;
|
|
19337
20833
|
rendererCanvas.style.top = `${next.top}px`;
|
|
19338
20834
|
app.renderer.resize(next.width, next.height, presentationResolution);
|
|
20835
|
+
syncAnnotationAntialiasResolution();
|
|
19339
20836
|
return presentationBox;
|
|
19340
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
|
+
}
|
|
19341
20853
|
const syncDisplaySizing = () => {
|
|
19342
20854
|
if (!baseFit || mediaWidth <= 0 || mediaHeight <= 0)
|
|
19343
20855
|
return;
|
|
@@ -19354,7 +20866,7 @@ async function createPixiMediaScene(options) {
|
|
|
19354
20866
|
return;
|
|
19355
20867
|
}
|
|
19356
20868
|
rasterDisplay = display;
|
|
19357
|
-
|
|
20869
|
+
syncMaskRasterDisplay();
|
|
19358
20870
|
options.onDisplayChange?.(display);
|
|
19359
20871
|
};
|
|
19360
20872
|
const updateMediaSampling = () => {
|
|
@@ -19375,12 +20887,26 @@ async function createPixiMediaScene(options) {
|
|
|
19375
20887
|
return;
|
|
19376
20888
|
const transform = viewport.getTransform();
|
|
19377
20889
|
viewportScale = baseFit.scale * transform.scale;
|
|
20890
|
+
maskLayer?.setViewportScale(viewportScale);
|
|
20891
|
+
polygonLayer?.setViewportScale(viewportScale);
|
|
19378
20892
|
updateMediaSampling();
|
|
19379
20893
|
mediaScene.scale.set(viewportScale);
|
|
19380
20894
|
const box = syncPresentationBox();
|
|
19381
20895
|
mediaScene.position.set(baseFit.x + transform.x - box.left, baseFit.y + transform.y - box.top);
|
|
20896
|
+
focusLayer?.syncViewportBounds();
|
|
20897
|
+
regionLayer.syncViewportBounds();
|
|
19382
20898
|
maskBrushPreview?.setViewportScale(viewportScale);
|
|
19383
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
|
+
}
|
|
19384
20910
|
const mediaPointToScreen = (point) => {
|
|
19385
20911
|
const fit = baseFit ?? { scale: 1, x: 0, y: 0 };
|
|
19386
20912
|
const screenPoint = viewport.mediaToScreen({
|
|
@@ -19402,6 +20928,7 @@ async function createPixiMediaScene(options) {
|
|
|
19402
20928
|
syncPresentationBox();
|
|
19403
20929
|
updateMediaSceneFit();
|
|
19404
20930
|
syncDisplaySizing();
|
|
20931
|
+
drawInteractionPresentationLayer(currentMediaTime);
|
|
19405
20932
|
renderOnChange();
|
|
19406
20933
|
});
|
|
19407
20934
|
measureContainer();
|
|
@@ -19617,6 +21144,7 @@ async function createPixiMediaScene(options) {
|
|
|
19617
21144
|
interactionSlot.setDisplay(interactionPresentationDisplay || interactionHitDisplay
|
|
19618
21145
|
? interactionDisplay
|
|
19619
21146
|
: undefined);
|
|
21147
|
+
interactionLabelSlot.setDisplay(interactionPresentationLayer?.getLabelDisplay() ?? undefined);
|
|
19620
21148
|
attachFocusLayerDisplay();
|
|
19621
21149
|
attachMaskLayerDisplay();
|
|
19622
21150
|
attachLabelLayerDisplay();
|
|
@@ -19633,6 +21161,7 @@ async function createPixiMediaScene(options) {
|
|
|
19633
21161
|
setPlaybackActive(active) {
|
|
19634
21162
|
isPlaybackActive = active;
|
|
19635
21163
|
maskLayer?.setPlaybackActive(active);
|
|
21164
|
+
polygonLayer?.setPlaybackActive(active);
|
|
19636
21165
|
depthSource?.setPlaybackActive?.(active);
|
|
19637
21166
|
},
|
|
19638
21167
|
setScrubbing(scrubbing) {
|
|
@@ -19733,6 +21262,7 @@ async function createPixiMediaScene(options) {
|
|
|
19733
21262
|
syncPresentationBox();
|
|
19734
21263
|
updateMediaSceneFit();
|
|
19735
21264
|
syncDisplaySizing();
|
|
21265
|
+
drawInteractionPresentationLayer(currentMediaTime);
|
|
19736
21266
|
renderOnChange();
|
|
19737
21267
|
},
|
|
19738
21268
|
setDisplayAdjustments(adjustments) {
|
|
@@ -19862,6 +21392,12 @@ async function createPixiMediaScene(options) {
|
|
|
19862
21392
|
setPresentation(presentation, mediaTime) {
|
|
19863
21393
|
appliedPresentation = presentation;
|
|
19864
21394
|
currentMediaTime = mediaTime;
|
|
21395
|
+
if (annotationAntialiasing !==
|
|
21396
|
+
(presentation.annotationAntialiasing ?? false)) {
|
|
21397
|
+
annotationAntialiasing = presentation.annotationAntialiasing ?? false;
|
|
21398
|
+
syncAnnotationAntialiasResolution();
|
|
21399
|
+
syncSceneChildren();
|
|
21400
|
+
}
|
|
19865
21401
|
if (presentation.backgroundColor !== undefined) {
|
|
19866
21402
|
app.renderer.background.color = presentation.backgroundColor;
|
|
19867
21403
|
applyBackdropColor(backdrop, presentation.backgroundColor);
|
|
@@ -20064,6 +21600,10 @@ async function createPixiMediaScene(options) {
|
|
|
20064
21600
|
// a decoder buffer in a producer that outlives the scene.
|
|
20065
21601
|
frameChannel?.onPresentedFrame((presented) => presented.frame.close());
|
|
20066
21602
|
destroyBatchTextureBindings();
|
|
21603
|
+
destroyFilterBindings();
|
|
21604
|
+
labelLayer?.setBackgroundAntialiasFilter(null);
|
|
21605
|
+
interactionPresentationLayer?.setBackgroundAntialiasFilter(null);
|
|
21606
|
+
annotationAntialiasLayer.destroy();
|
|
20067
21607
|
inspectionFilter?.destroy();
|
|
20068
21608
|
mediaCompositor?.destroy();
|
|
20069
21609
|
interactionLayer?.destroy();
|
|
@@ -20172,8 +21712,8 @@ async function createPixiMediaScene(options) {
|
|
|
20172
21712
|
renderPreparation: options.renderPreparation,
|
|
20173
21713
|
resolveInstructions: resolveArtifactMaskInstructions,
|
|
20174
21714
|
});
|
|
20175
|
-
|
|
20176
|
-
|
|
21715
|
+
syncMaskRasterDisplay();
|
|
21716
|
+
maskLayer.setViewportScale(viewportScale);
|
|
20177
21717
|
maskLayer.setPlaybackActive(isPlaybackActive);
|
|
20178
21718
|
if (timelineContext) {
|
|
20179
21719
|
maskLayer.setTimelineContext(timelineContext);
|
|
@@ -20427,6 +21967,9 @@ async function createPixiMediaScene(options) {
|
|
|
20427
21967
|
renderPreparation: options.renderPreparation,
|
|
20428
21968
|
resolveContextState,
|
|
20429
21969
|
});
|
|
21970
|
+
polygonLayer.setPlaybackActive(isPlaybackActive);
|
|
21971
|
+
syncMaskRasterDisplay();
|
|
21972
|
+
polygonLayer.setViewportScale(viewportScale);
|
|
20430
21973
|
if (timelineContext) {
|
|
20431
21974
|
polygonLayer.setTimelineContext(timelineContext);
|
|
20432
21975
|
}
|
|
@@ -20467,14 +22010,19 @@ async function createPixiMediaScene(options) {
|
|
|
20467
22010
|
function ensureFocusLayer(focusStyle) {
|
|
20468
22011
|
if (!focusLayer) {
|
|
20469
22012
|
focusLayer = createPixiFocusLayer({
|
|
22013
|
+
AlphaMask,
|
|
20470
22014
|
Container,
|
|
20471
22015
|
Graphics,
|
|
20472
22016
|
ImageSource,
|
|
20473
22017
|
Mesh,
|
|
20474
22018
|
MeshGeometry,
|
|
22019
|
+
Rectangle,
|
|
20475
22020
|
Shader,
|
|
20476
22021
|
UniformGroup,
|
|
20477
22022
|
focusStyle,
|
|
22023
|
+
getHeatmapRenderers: () => currentHeatmapRenderers,
|
|
22024
|
+
getAnnotationAntialiasing: () => Boolean(annotationAntialiasing),
|
|
22025
|
+
getViewportBounds: resolveViewportBounds,
|
|
20478
22026
|
isDetectionVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
|
|
20479
22027
|
});
|
|
20480
22028
|
}
|
|
@@ -20515,6 +22063,7 @@ async function createPixiMediaScene(options) {
|
|
|
20515
22063
|
const interactionState = interactionLayer?.getState();
|
|
20516
22064
|
focusLayer.drawFrame({
|
|
20517
22065
|
frame,
|
|
22066
|
+
heatmapArtifacts: heatmapLayer?.getActiveFocusArtifacts(frame) ?? [],
|
|
20518
22067
|
hoveredPick: withEditingPreview(filterVisiblePick(interactionState?.hoveredPick ?? null)),
|
|
20519
22068
|
idMaskArtifact: maskLayer?.getActiveIdMaskFrameTexture(frame?.mediaTime ?? null) ??
|
|
20520
22069
|
null,
|
|
@@ -20547,6 +22096,9 @@ async function createPixiMediaScene(options) {
|
|
|
20547
22096
|
mediaTime,
|
|
20548
22097
|
selectedPick: interactionState?.selectedPick ?? null,
|
|
20549
22098
|
selectedPicks: interactionState?.selectedPicks,
|
|
22099
|
+
strokePixelRatio: annotationAntialiasing
|
|
22100
|
+
? annotationAntialiasResolution
|
|
22101
|
+
: presentationResolution,
|
|
20550
22102
|
viewportScale,
|
|
20551
22103
|
});
|
|
20552
22104
|
}
|
|
@@ -20625,9 +22177,6 @@ async function createPixiMediaScene(options) {
|
|
|
20625
22177
|
const frame = annotationDetectionTimeline.selectFrame(mediaTime);
|
|
20626
22178
|
const interactionState = interactionLayer?.getState();
|
|
20627
22179
|
const editingState = options.editingEngine?.getState();
|
|
20628
|
-
labelLayer?.drawCreationPreview(editingState?.kind === AnnotationGestureStateKind.Creating
|
|
20629
|
-
? editingState.preview
|
|
20630
|
-
: null, mediaTime, viewportScale);
|
|
20631
22180
|
const previewOverlay = options.previewOverlay?.() ?? null;
|
|
20632
22181
|
drawnEditingState = editingState;
|
|
20633
22182
|
// Only a scene that renders on change compares signatures.
|
|
@@ -20647,6 +22196,18 @@ async function createPixiMediaScene(options) {
|
|
|
20647
22196
|
viewportScale,
|
|
20648
22197
|
visibility: currentVisibility,
|
|
20649
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
|
+
}
|
|
20650
22211
|
}
|
|
20651
22212
|
/**
|
|
20652
22213
|
* Milliseconds for animations that are not annotation geometry. A pull scene
|
|
@@ -20914,6 +22475,7 @@ async function createPixiMediaScene(options) {
|
|
|
20914
22475
|
Text,
|
|
20915
22476
|
detectionTimeline: annotationDetectionTimeline,
|
|
20916
22477
|
labelStyle,
|
|
22478
|
+
getRenderedBoxes: () => boxLayer.getRenderedBoxes(),
|
|
20917
22479
|
resolveContextState: resolveLabelContextState,
|
|
20918
22480
|
});
|
|
20919
22481
|
}
|
|
@@ -22130,6 +23692,7 @@ async function createMediaSession(options) {
|
|
|
22130
23692
|
...options.renderer,
|
|
22131
23693
|
...sessionMedia.rendererSourceOption,
|
|
22132
23694
|
backgroundColor: initialPresentation.backgroundColor,
|
|
23695
|
+
annotationAntialiasing: initialPresentation.annotationAntialiasing,
|
|
22133
23696
|
annotationOverlayStyle: initialPresentation.annotationOverlayStyle,
|
|
22134
23697
|
boxStyle: initialPresentation.boxStyle,
|
|
22135
23698
|
boxCornerStyle: initialPresentation.boxCornerStyle,
|