supervision 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +2261 -447
- 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-clahe.d.ts +6 -0
- package/dist/renderers/media-clahe.d.ts.map +1 -0
- 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-inspection-filter.d.ts +8 -0
- package/dist/renderers/pixi-media-inspection-filter.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 +18 -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,
|
|
@@ -10650,6 +10798,260 @@ function matches(left, right) {
|
|
|
10650
10798
|
left.every((value, index) => Object.is(value, right[index])));
|
|
10651
10799
|
}
|
|
10652
10800
|
|
|
10801
|
+
const BINS = 256;
|
|
10802
|
+
const TILE_COUNT = 8;
|
|
10803
|
+
const CLIP_LIMIT = 2;
|
|
10804
|
+
function computeMediaClahe(rgba, width, height) {
|
|
10805
|
+
const tilesX = Math.min(TILE_COUNT, width);
|
|
10806
|
+
const tilesY = Math.min(TILE_COUNT, height);
|
|
10807
|
+
const luts = new Uint8Array(tilesX * tilesY * BINS);
|
|
10808
|
+
const histogram = new Uint32Array(BINS);
|
|
10809
|
+
for (let ty = 0; ty < tilesY; ty++) {
|
|
10810
|
+
const y0 = Math.floor((ty * height) / tilesY);
|
|
10811
|
+
const y1 = Math.floor(((ty + 1) * height) / tilesY);
|
|
10812
|
+
for (let tx = 0; tx < tilesX; tx++) {
|
|
10813
|
+
const x0 = Math.floor((tx * width) / tilesX);
|
|
10814
|
+
const x1 = Math.floor(((tx + 1) * width) / tilesX);
|
|
10815
|
+
histogram.fill(0);
|
|
10816
|
+
for (let y = y0; y < y1; y++) {
|
|
10817
|
+
for (let x = x0; x < x1; x++) {
|
|
10818
|
+
const pixel = (y * width + x) * 4;
|
|
10819
|
+
const luma = Math.round(0.299 * rgba[pixel] +
|
|
10820
|
+
0.587 * rgba[pixel + 1] +
|
|
10821
|
+
0.114 * rgba[pixel + 2]);
|
|
10822
|
+
histogram[luma]++;
|
|
10823
|
+
}
|
|
10824
|
+
}
|
|
10825
|
+
const area = (x1 - x0) * (y1 - y0);
|
|
10826
|
+
const limit = Math.max(1, Math.floor((CLIP_LIMIT * area) / BINS));
|
|
10827
|
+
let excess = 0;
|
|
10828
|
+
for (let i = 0; i < BINS; i++) {
|
|
10829
|
+
if (histogram[i] > limit) {
|
|
10830
|
+
excess += histogram[i] - limit;
|
|
10831
|
+
histogram[i] = limit;
|
|
10832
|
+
}
|
|
10833
|
+
}
|
|
10834
|
+
const perBin = Math.floor(excess / BINS);
|
|
10835
|
+
let residual = excess - perBin * BINS;
|
|
10836
|
+
for (let i = 0; i < BINS; i++)
|
|
10837
|
+
histogram[i] += perBin;
|
|
10838
|
+
const residualStep = Math.max(Math.floor(BINS / Math.max(residual, 1)), 1);
|
|
10839
|
+
for (let i = 0; i < BINS && residual > 0; i += residualStep, residual--) {
|
|
10840
|
+
histogram[i]++;
|
|
10841
|
+
}
|
|
10842
|
+
const offset = (ty * tilesX + tx) * BINS;
|
|
10843
|
+
let cumulative = 0;
|
|
10844
|
+
for (let i = 0; i < BINS; i++) {
|
|
10845
|
+
cumulative += histogram[i];
|
|
10846
|
+
luts[offset + i] = Math.round((cumulative * (BINS - 1)) / area);
|
|
10847
|
+
}
|
|
10848
|
+
}
|
|
10849
|
+
}
|
|
10850
|
+
return { luts, tilesX, tilesY };
|
|
10851
|
+
}
|
|
10852
|
+
|
|
10853
|
+
/**
|
|
10854
|
+
* Pixi keys its program cache by source, so one program is shared by every
|
|
10855
|
+
* shader built from it. `destroy(true)` takes that program with it, leaving
|
|
10856
|
+
* both the peers still rendering through it and the next shader built from the
|
|
10857
|
+
* same source bound to nothing.
|
|
10858
|
+
*/
|
|
10859
|
+
function destroyShaderKeepingProgram(shader) {
|
|
10860
|
+
try {
|
|
10861
|
+
shader.destroy();
|
|
10862
|
+
}
|
|
10863
|
+
catch {
|
|
10864
|
+
// Pixi has already invalidated this shader's resource group.
|
|
10865
|
+
}
|
|
10866
|
+
}
|
|
10867
|
+
/**
|
|
10868
|
+
* Pixi uploads a placeholder while it builds a shader's first bind group, and
|
|
10869
|
+
* WebGPU rejects a canvas that was never given a rendering context.
|
|
10870
|
+
*/
|
|
10871
|
+
function createShaderPlaceholderCanvas() {
|
|
10872
|
+
if (typeof document === "undefined") {
|
|
10873
|
+
return { height: 1, width: 1 };
|
|
10874
|
+
}
|
|
10875
|
+
const canvas = document.createElement("canvas");
|
|
10876
|
+
canvas.height = 1;
|
|
10877
|
+
canvas.width = 1;
|
|
10878
|
+
canvas.getContext("2d");
|
|
10879
|
+
return canvas;
|
|
10880
|
+
}
|
|
10881
|
+
|
|
10882
|
+
const HISTOGRAM_MAX_DIMENSION = 1024;
|
|
10883
|
+
const vertex = `
|
|
10884
|
+
in vec2 aPosition;
|
|
10885
|
+
out vec2 vTextureCoord;
|
|
10886
|
+
out vec2 vImageCoord;
|
|
10887
|
+
uniform vec4 uInputSize;
|
|
10888
|
+
uniform vec4 uOutputFrame;
|
|
10889
|
+
uniform vec4 uOutputTexture;
|
|
10890
|
+
uniform mat3 uImageMatrix;
|
|
10891
|
+
void main() {
|
|
10892
|
+
vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
|
|
10893
|
+
position.x = position.x * (2.0 / uOutputTexture.x) - 1.0;
|
|
10894
|
+
position.y = position.y * (2.0 * uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z;
|
|
10895
|
+
gl_Position = vec4(position, 0.0, 1.0);
|
|
10896
|
+
vTextureCoord = aPosition * (uOutputFrame.zw * uInputSize.zw);
|
|
10897
|
+
vImageCoord = (uImageMatrix * vec3(vTextureCoord, 1.0)).xy;
|
|
10898
|
+
}
|
|
10899
|
+
`;
|
|
10900
|
+
const fragment = `
|
|
10901
|
+
in vec2 vTextureCoord;
|
|
10902
|
+
in vec2 vImageCoord;
|
|
10903
|
+
out vec4 finalColor;
|
|
10904
|
+
uniform sampler2D uTexture;
|
|
10905
|
+
uniform sampler2D uLut;
|
|
10906
|
+
uniform vec2 uTiles;
|
|
10907
|
+
uniform float uGamma;
|
|
10908
|
+
uniform float uAuto;
|
|
10909
|
+
float lookup(vec2 tile, float luma) {
|
|
10910
|
+
float row = tile.y * uTiles.x + tile.x;
|
|
10911
|
+
return texture(uLut, vec2((luma * 255.0 + 0.5) / 256.0,
|
|
10912
|
+
(row + 0.5) / (uTiles.x * uTiles.y))).r;
|
|
10913
|
+
}
|
|
10914
|
+
void main() {
|
|
10915
|
+
vec4 color = texture(uTexture, vTextureCoord);
|
|
10916
|
+
if (color.a == 0.0) { finalColor = color; return; }
|
|
10917
|
+
vec3 rgb = color.rgb / color.a;
|
|
10918
|
+
if (uAuto > 0.5) {
|
|
10919
|
+
float luma = dot(rgb, vec3(0.299, 0.587, 0.114));
|
|
10920
|
+
vec2 tilePosition = clamp(vImageCoord, 0.0, 1.0) * uTiles - 0.5;
|
|
10921
|
+
vec2 t0 = floor(tilePosition);
|
|
10922
|
+
vec2 f = tilePosition - t0;
|
|
10923
|
+
vec2 t1 = clamp(t0 + 1.0, vec2(0.0), uTiles - 1.0);
|
|
10924
|
+
t0 = clamp(t0, vec2(0.0), uTiles - 1.0);
|
|
10925
|
+
float top = mix(lookup(t0, luma), lookup(vec2(t1.x, t0.y), luma), f.x);
|
|
10926
|
+
float bottom = mix(lookup(vec2(t0.x, t1.y), luma), lookup(t1, luma), f.x);
|
|
10927
|
+
rgb = clamp(rgb + (mix(top, bottom, f.y) - luma), 0.0, 1.0);
|
|
10928
|
+
}
|
|
10929
|
+
finalColor = vec4(pow(rgb, vec3(1.0 / uGamma)) * color.a, color.a);
|
|
10930
|
+
}
|
|
10931
|
+
`;
|
|
10932
|
+
const gpuSource = `
|
|
10933
|
+
struct GlobalFilterUniforms {
|
|
10934
|
+
uInputSize: vec4<f32>, uInputPixel: vec4<f32>, uInputClamp: vec4<f32>,
|
|
10935
|
+
uOutputFrame: vec4<f32>, uGlobalFrame: vec4<f32>, uOutputTexture: vec4<f32>,
|
|
10936
|
+
};
|
|
10937
|
+
struct InspectionUniforms {
|
|
10938
|
+
uImageMatrix: mat3x3<f32>, uTiles: vec2<f32>, uGamma: f32, uAuto: f32,
|
|
10939
|
+
};
|
|
10940
|
+
@group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;
|
|
10941
|
+
@group(0) @binding(1) var uTexture: texture_2d<f32>;
|
|
10942
|
+
@group(0) @binding(2) var uSampler: sampler;
|
|
10943
|
+
@group(1) @binding(0) var<uniform> inspectionUniforms: InspectionUniforms;
|
|
10944
|
+
@group(1) @binding(1) var uLut: texture_2d<f32>;
|
|
10945
|
+
@group(1) @binding(2) var uLutSampler: sampler;
|
|
10946
|
+
struct VertexOutput {
|
|
10947
|
+
@builtin(position) position: vec4<f32>,
|
|
10948
|
+
@location(0) uv: vec2<f32>, @location(1) image: vec2<f32>,
|
|
10949
|
+
};
|
|
10950
|
+
@vertex fn mainVertex(@location(0) aPosition: vec2<f32>) -> VertexOutput {
|
|
10951
|
+
var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;
|
|
10952
|
+
position.x = position.x * (2.0 / gfu.uOutputTexture.x) - 1.0;
|
|
10953
|
+
position.y = position.y * (2.0 * gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z;
|
|
10954
|
+
let uv = aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw);
|
|
10955
|
+
return VertexOutput(vec4(position, 0.0, 1.0), uv,
|
|
10956
|
+
(inspectionUniforms.uImageMatrix * vec3(uv, 1.0)).xy);
|
|
10957
|
+
}
|
|
10958
|
+
fn lookup(tile: vec2<f32>, luma: f32) -> f32 {
|
|
10959
|
+
let tiles = inspectionUniforms.uTiles;
|
|
10960
|
+
let row = tile.y * tiles.x + tile.x;
|
|
10961
|
+
return textureSampleLevel(uLut, uLutSampler,
|
|
10962
|
+
vec2((luma * 255.0 + 0.5) / 256.0, (row + 0.5) / (tiles.x * tiles.y)), 0.0).r;
|
|
10963
|
+
}
|
|
10964
|
+
@fragment fn mainFragment(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
10965
|
+
let color = textureSample(uTexture, uSampler, input.uv);
|
|
10966
|
+
if (color.a == 0.0) { return color; }
|
|
10967
|
+
var rgb = color.rgb / color.a;
|
|
10968
|
+
if (inspectionUniforms.uAuto > 0.5) {
|
|
10969
|
+
let luma = dot(rgb, vec3(0.299, 0.587, 0.114));
|
|
10970
|
+
let tiles = inspectionUniforms.uTiles;
|
|
10971
|
+
let tilePosition = clamp(input.image, vec2(0.0), vec2(1.0)) * tiles - 0.5;
|
|
10972
|
+
let unbounded = floor(tilePosition);
|
|
10973
|
+
let f = tilePosition - unbounded;
|
|
10974
|
+
let t0 = clamp(unbounded, vec2(0.0), tiles - 1.0);
|
|
10975
|
+
let t1 = clamp(unbounded + 1.0, vec2(0.0), tiles - 1.0);
|
|
10976
|
+
let top = mix(lookup(t0, luma), lookup(vec2(t1.x, t0.y), luma), f.x);
|
|
10977
|
+
let bottom = mix(lookup(vec2(t0.x, t1.y), luma), lookup(t1, luma), f.x);
|
|
10978
|
+
rgb = clamp(rgb + (mix(top, bottom, f.y) - luma), vec3(0.0), vec3(1.0));
|
|
10979
|
+
}
|
|
10980
|
+
return vec4(pow(rgb, vec3(1.0 / inspectionUniforms.uGamma)) * color.a, color.a);
|
|
10981
|
+
}
|
|
10982
|
+
`;
|
|
10983
|
+
function createPixiMediaInspectionFilter(sprite, pixi) {
|
|
10984
|
+
const { BufferImageSource, Filter, Matrix } = pixi;
|
|
10985
|
+
const pixels = new Uint8Array(256 * 64 * 4);
|
|
10986
|
+
const lutSource = new BufferImageSource({
|
|
10987
|
+
resource: pixels,
|
|
10988
|
+
width: 256,
|
|
10989
|
+
height: 64,
|
|
10990
|
+
scaleMode: "linear",
|
|
10991
|
+
alphaMode: "no-premultiply-alpha",
|
|
10992
|
+
});
|
|
10993
|
+
const filter = Filter.from({
|
|
10994
|
+
gl: { vertex: vertex, fragment: fragment },
|
|
10995
|
+
gpu: {
|
|
10996
|
+
vertex: { source: gpuSource, entryPoint: "mainVertex" },
|
|
10997
|
+
fragment: { source: gpuSource, entryPoint: "mainFragment" },
|
|
10998
|
+
},
|
|
10999
|
+
resolution: "inherit",
|
|
11000
|
+
resources: {
|
|
11001
|
+
inspectionUniforms: {
|
|
11002
|
+
uImageMatrix: { value: new Matrix(), type: "mat3x3<f32>" },
|
|
11003
|
+
uTiles: { value: new Float32Array([8, 8]), type: "vec2<f32>" },
|
|
11004
|
+
uGamma: { value: 1, type: "f32" },
|
|
11005
|
+
uAuto: { value: 0, type: "f32" },
|
|
11006
|
+
},
|
|
11007
|
+
uLut: lutSource,
|
|
11008
|
+
uLutSampler: lutSource.style,
|
|
11009
|
+
},
|
|
11010
|
+
});
|
|
11011
|
+
const uniforms = filter.resources.inspectionUniforms.uniforms;
|
|
11012
|
+
let hasHistogram = false;
|
|
11013
|
+
filter.apply = (manager, input, output, clear) => {
|
|
11014
|
+
// The filter input is clipped by the viewport; lookup tiles stay anchored
|
|
11015
|
+
// to the full image when the user zooms or pans.
|
|
11016
|
+
manager.calculateSpriteMatrix(uniforms.uImageMatrix, sprite);
|
|
11017
|
+
manager.applyFilter(filter, input, output, clear);
|
|
11018
|
+
};
|
|
11019
|
+
return {
|
|
11020
|
+
filter,
|
|
11021
|
+
invalidateImage() {
|
|
11022
|
+
hasHistogram = false;
|
|
11023
|
+
},
|
|
11024
|
+
update(gamma, autoContrast, image) {
|
|
11025
|
+
uniforms.uGamma = Math.max(gamma, 0.0001);
|
|
11026
|
+
if (autoContrast && image && !hasHistogram) {
|
|
11027
|
+
const scale = Math.min(1, HISTOGRAM_MAX_DIMENSION / Math.max(image.width, image.height));
|
|
11028
|
+
const sample = document.createElement("canvas");
|
|
11029
|
+
sample.width = Math.max(1, Math.round(image.width * scale));
|
|
11030
|
+
sample.height = Math.max(1, Math.round(image.height * scale));
|
|
11031
|
+
const context = sample.getContext("2d", { willReadFrequently: true });
|
|
11032
|
+
context.drawImage(image, 0, 0, sample.width, sample.height);
|
|
11033
|
+
const { luts, tilesX, tilesY } = computeMediaClahe(context.getImageData(0, 0, sample.width, sample.height).data, sample.width, sample.height);
|
|
11034
|
+
for (let i = 0; i < luts.length; i++) {
|
|
11035
|
+
pixels[i * 4] = luts[i];
|
|
11036
|
+
pixels[i * 4 + 1] = luts[i];
|
|
11037
|
+
pixels[i * 4 + 2] = luts[i];
|
|
11038
|
+
pixels[i * 4 + 3] = 255;
|
|
11039
|
+
}
|
|
11040
|
+
uniforms.uTiles.set([tilesX, tilesY]);
|
|
11041
|
+
lutSource.resource = pixels.subarray(0, luts.length * 4);
|
|
11042
|
+
lutSource.resize(256, tilesX * tilesY);
|
|
11043
|
+
lutSource.update();
|
|
11044
|
+
hasHistogram = true;
|
|
11045
|
+
}
|
|
11046
|
+
uniforms.uAuto = autoContrast && hasHistogram ? 1 : 0;
|
|
11047
|
+
},
|
|
11048
|
+
destroy() {
|
|
11049
|
+
destroyShaderKeepingProgram(filter);
|
|
11050
|
+
lutSource.destroy();
|
|
11051
|
+
},
|
|
11052
|
+
};
|
|
11053
|
+
}
|
|
11054
|
+
|
|
10653
11055
|
/**
|
|
10654
11056
|
* Which frame a media time draws and how much of it is cooked, as two separate
|
|
10655
11057
|
* facts. Reporting readiness is what the snapshot and the token are for; a
|
|
@@ -10789,35 +11191,6 @@ void main(void) {
|
|
|
10789
11191
|
}
|
|
10790
11192
|
`;
|
|
10791
11193
|
|
|
10792
|
-
/**
|
|
10793
|
-
* Pixi keys its program cache by source, so one program is shared by every
|
|
10794
|
-
* shader built from it. `destroy(true)` takes that program with it, leaving
|
|
10795
|
-
* both the peers still rendering through it and the next shader built from the
|
|
10796
|
-
* same source bound to nothing.
|
|
10797
|
-
*/
|
|
10798
|
-
function destroyShaderKeepingProgram(shader) {
|
|
10799
|
-
try {
|
|
10800
|
-
shader.destroy();
|
|
10801
|
-
}
|
|
10802
|
-
catch {
|
|
10803
|
-
// Pixi has already invalidated this shader's resource group.
|
|
10804
|
-
}
|
|
10805
|
-
}
|
|
10806
|
-
/**
|
|
10807
|
-
* Pixi uploads a placeholder while it builds a shader's first bind group, and
|
|
10808
|
-
* WebGPU rejects a canvas that was never given a rendering context.
|
|
10809
|
-
*/
|
|
10810
|
-
function createShaderPlaceholderCanvas() {
|
|
10811
|
-
if (typeof document === "undefined") {
|
|
10812
|
-
return { height: 1, width: 1 };
|
|
10813
|
-
}
|
|
10814
|
-
const canvas = document.createElement("canvas");
|
|
10815
|
-
canvas.height = 1;
|
|
10816
|
-
canvas.width = 1;
|
|
10817
|
-
canvas.getContext("2d");
|
|
10818
|
-
return canvas;
|
|
10819
|
-
}
|
|
10820
|
-
|
|
10821
11194
|
/**
|
|
10822
11195
|
* The halo a detection actually paints, or `undefined`. A style may answer an
|
|
10823
11196
|
* instruction that draws nothing, and both the coverage the halo prepares and
|
|
@@ -11184,6 +11557,8 @@ function createPixiBoxLayer(options) {
|
|
|
11184
11557
|
let retainedContainer = options.container;
|
|
11185
11558
|
const retainedEntries = new Map();
|
|
11186
11559
|
const invalidatedDetections = new Set();
|
|
11560
|
+
const renderedRects = new Map();
|
|
11561
|
+
let renderedBoxFrame;
|
|
11187
11562
|
const usingRetainedEntries = Boolean(options.Container && options.Graphics);
|
|
11188
11563
|
return {
|
|
11189
11564
|
createContainer() {
|
|
@@ -11212,6 +11587,10 @@ function createPixiBoxLayer(options) {
|
|
|
11212
11587
|
boxGraphics?.clear();
|
|
11213
11588
|
const activeDetectionIndexes = [];
|
|
11214
11589
|
const activeKeys = new Set();
|
|
11590
|
+
renderedRects.clear();
|
|
11591
|
+
renderedBoxFrame = detectionFrame
|
|
11592
|
+
? { frame: detectionFrame, rects: renderedRects }
|
|
11593
|
+
: undefined;
|
|
11215
11594
|
if (boxStyle && detectionFrame) {
|
|
11216
11595
|
const orderedDetections = detectionFrame.detections
|
|
11217
11596
|
.map((detection, detectionIndex) => ({ detection, detectionIndex }))
|
|
@@ -11243,10 +11622,13 @@ function createPixiBoxLayer(options) {
|
|
|
11243
11622
|
display.position?.set?.(0, 0);
|
|
11244
11623
|
display.clear();
|
|
11245
11624
|
drawBoxInstruction(display, instruction, viewportScale);
|
|
11625
|
+
retainRenderedBox(detectionIndex, instruction);
|
|
11246
11626
|
invalidatedDetections.delete(key);
|
|
11247
11627
|
}
|
|
11248
11628
|
else {
|
|
11249
11629
|
drawBoxInstruction(boxGraphics, instruction, viewportScale);
|
|
11630
|
+
if (boxGraphics)
|
|
11631
|
+
retainRenderedBox(detectionIndex, instruction);
|
|
11250
11632
|
}
|
|
11251
11633
|
}
|
|
11252
11634
|
}
|
|
@@ -11259,6 +11641,9 @@ function createPixiBoxLayer(options) {
|
|
|
11259
11641
|
lastDrawnState = getBoxLayerState(detectionFrame, activeDetectionIndexes);
|
|
11260
11642
|
return lastDrawnState;
|
|
11261
11643
|
},
|
|
11644
|
+
getRenderedBoxes() {
|
|
11645
|
+
return renderedBoxFrame;
|
|
11646
|
+
},
|
|
11262
11647
|
setBoxStyle(nextBoxStyle) {
|
|
11263
11648
|
if (nextBoxStyle === undefined) {
|
|
11264
11649
|
return;
|
|
@@ -11281,6 +11666,16 @@ function createPixiBoxLayer(options) {
|
|
|
11281
11666
|
return true;
|
|
11282
11667
|
},
|
|
11283
11668
|
};
|
|
11669
|
+
function retainRenderedBox(detectionIndex, instruction) {
|
|
11670
|
+
if (instruction.rect.width > 0 &&
|
|
11671
|
+
instruction.rect.height > 0 &&
|
|
11672
|
+
((instruction.fill && (instruction.fill.alpha ?? 1) > 0) ||
|
|
11673
|
+
(instruction.stroke &&
|
|
11674
|
+
instruction.stroke.width > 0 &&
|
|
11675
|
+
(instruction.stroke.alpha ?? 1) > 0))) {
|
|
11676
|
+
renderedRects.set(detectionIndex, instruction.rect);
|
|
11677
|
+
}
|
|
11678
|
+
}
|
|
11284
11679
|
function ensureRetainedEntry(key) {
|
|
11285
11680
|
let display = retainedEntries.get(key);
|
|
11286
11681
|
if (!display) {
|
|
@@ -12374,7 +12769,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12374
12769
|
sprite.removeFromParent();
|
|
12375
12770
|
sprite.destroy();
|
|
12376
12771
|
if (temporaryTexture)
|
|
12377
|
-
texture
|
|
12772
|
+
releaseTexture(texture);
|
|
12378
12773
|
}
|
|
12379
12774
|
positionedSprites.length = 0;
|
|
12380
12775
|
activeKeys.clear();
|
|
@@ -12388,7 +12783,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12388
12783
|
}
|
|
12389
12784
|
clear();
|
|
12390
12785
|
for (const entry of cache.values()) {
|
|
12391
|
-
entry.texture
|
|
12786
|
+
releaseTexture(entry.texture);
|
|
12392
12787
|
entry.image.close();
|
|
12393
12788
|
}
|
|
12394
12789
|
cache.clear();
|
|
@@ -12426,7 +12821,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12426
12821
|
continue;
|
|
12427
12822
|
cache.delete(key);
|
|
12428
12823
|
cachedBytes -= entry.bytes;
|
|
12429
|
-
entry.texture
|
|
12824
|
+
releaseTexture(entry.texture);
|
|
12430
12825
|
entry.image.close();
|
|
12431
12826
|
}
|
|
12432
12827
|
};
|
|
@@ -12474,7 +12869,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12474
12869
|
(right.detection.zIndex ?? right.index) || left.index - right.index);
|
|
12475
12870
|
const result = [];
|
|
12476
12871
|
let pixels = 0;
|
|
12477
|
-
for (const { detection } of orderedDetections) {
|
|
12872
|
+
for (const { detection, index } of orderedDetections) {
|
|
12478
12873
|
const map = detection.heatmap;
|
|
12479
12874
|
if (!map || !options.isVisible(detection))
|
|
12480
12875
|
continue;
|
|
@@ -12487,7 +12882,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12487
12882
|
continue;
|
|
12488
12883
|
}
|
|
12489
12884
|
pixels += map.width * map.height;
|
|
12490
|
-
result.push({ detection, map, renderer });
|
|
12885
|
+
result.push({ detection, detectionIndex: index, map, renderer });
|
|
12491
12886
|
}
|
|
12492
12887
|
}
|
|
12493
12888
|
return result;
|
|
@@ -12522,7 +12917,8 @@ function createPixiHeatmapLayer(options) {
|
|
|
12522
12917
|
await options.prepareTexture?.(texture);
|
|
12523
12918
|
}
|
|
12524
12919
|
catch (error) {
|
|
12525
|
-
texture
|
|
12920
|
+
if (texture)
|
|
12921
|
+
releaseTexture(texture);
|
|
12526
12922
|
image.close();
|
|
12527
12923
|
if (destroyed || job.cancelled || job.generation !== generation) {
|
|
12528
12924
|
return;
|
|
@@ -12540,7 +12936,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12540
12936
|
job.cancelled ||
|
|
12541
12937
|
job.generation !== generation ||
|
|
12542
12938
|
!wantedKeys.has(job.key)) {
|
|
12543
|
-
texture
|
|
12939
|
+
releaseTexture(texture);
|
|
12544
12940
|
image.close();
|
|
12545
12941
|
return;
|
|
12546
12942
|
}
|
|
@@ -12596,6 +12992,20 @@ function createPixiHeatmapLayer(options) {
|
|
|
12596
12992
|
};
|
|
12597
12993
|
return {
|
|
12598
12994
|
createContainer: () => container,
|
|
12995
|
+
getActiveFocusArtifacts(frame) {
|
|
12996
|
+
if (!frame || lastFrame !== frame)
|
|
12997
|
+
return [];
|
|
12998
|
+
return positionedSprites.map(({ sprite, detectionIndex }) => ({
|
|
12999
|
+
detectionIndex,
|
|
13000
|
+
texture: sprite.texture,
|
|
13001
|
+
bounds: {
|
|
13002
|
+
x: sprite.x + sprite.width / 2,
|
|
13003
|
+
y: sprite.y + sprite.height / 2,
|
|
13004
|
+
width: sprite.width,
|
|
13005
|
+
height: sprite.height,
|
|
13006
|
+
},
|
|
13007
|
+
}));
|
|
13008
|
+
},
|
|
12599
13009
|
setRenderers(next) {
|
|
12600
13010
|
if (next.length === renderers.length &&
|
|
12601
13011
|
next.every((renderer, index) => renderer === renderers[index])) {
|
|
@@ -12661,7 +13071,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12661
13071
|
}
|
|
12662
13072
|
const maps = visibleMaps(frame);
|
|
12663
13073
|
const visiblePixelCount = maps.reduce((total, { map }) => total + map.width * map.height, 0);
|
|
12664
|
-
for (const { detection, map, renderer } of maps) {
|
|
13074
|
+
for (const { detection, detectionIndex, map, renderer } of maps) {
|
|
12665
13075
|
const key = keyFor(map, renderer);
|
|
12666
13076
|
if (shouldPrepareAsync(map, visiblePixelCount)) {
|
|
12667
13077
|
wantedKeys.add(key);
|
|
@@ -12702,6 +13112,7 @@ function createPixiHeatmapLayer(options) {
|
|
|
12702
13112
|
positionedSprites.push({
|
|
12703
13113
|
sprite,
|
|
12704
13114
|
detectionId: detection.id,
|
|
13115
|
+
detectionIndex,
|
|
12705
13116
|
x: sprite.x,
|
|
12706
13117
|
y: sprite.y,
|
|
12707
13118
|
temporaryTexture,
|
|
@@ -12732,6 +13143,10 @@ function createPixiHeatmapLayer(options) {
|
|
|
12732
13143
|
container.destroy();
|
|
12733
13144
|
},
|
|
12734
13145
|
};
|
|
13146
|
+
function releaseTexture(texture) {
|
|
13147
|
+
options.onTextureRelease?.(texture);
|
|
13148
|
+
texture.destroy(true);
|
|
13149
|
+
}
|
|
12735
13150
|
}
|
|
12736
13151
|
|
|
12737
13152
|
/**
|
|
@@ -12817,6 +13232,268 @@ function installPixiBatchTextureBindings(pixi, renderer) {
|
|
|
12817
13232
|
};
|
|
12818
13233
|
}
|
|
12819
13234
|
|
|
13235
|
+
const maskUniformOrigins = new WeakMap();
|
|
13236
|
+
/** Keeps nested mask cutouts aligned and filter disposal scene-owned. */
|
|
13237
|
+
function installPixiFilterBindings(pixi, renderer) {
|
|
13238
|
+
const releaseMaskTarget = installMaskTargetBindings(renderer);
|
|
13239
|
+
if (renderer.name !== "webgpu")
|
|
13240
|
+
return releaseMaskTarget;
|
|
13241
|
+
const system = renderer.filter;
|
|
13242
|
+
const originalApplyFilter = system.applyFilter;
|
|
13243
|
+
let globalGroup;
|
|
13244
|
+
const maskGroups = new Map();
|
|
13245
|
+
const applyFilter = (filter, input, output, clear) => {
|
|
13246
|
+
if (filter.enabled && filter instanceof pixi.MaskFilter) {
|
|
13247
|
+
const source = filter.resources.filterUniforms;
|
|
13248
|
+
let entry = maskGroups.get(filter);
|
|
13249
|
+
if (!entry) {
|
|
13250
|
+
const original = maskUniformOrigins.get(source) ?? source;
|
|
13251
|
+
const owned = new pixi.UniformGroup(original.uniformStructures, {
|
|
13252
|
+
isStatic: original.isStatic,
|
|
13253
|
+
ubo: original.ubo,
|
|
13254
|
+
});
|
|
13255
|
+
maskUniformOrigins.set(owned, original);
|
|
13256
|
+
entry = { original, owned };
|
|
13257
|
+
maskGroups.set(filter, entry);
|
|
13258
|
+
}
|
|
13259
|
+
// Pixi pools MaskFilters across renderers. Their uniform buffers need
|
|
13260
|
+
// one GPU upload owner, even when a second scene borrows the same filter.
|
|
13261
|
+
if (source !== entry.owned) {
|
|
13262
|
+
Object.assign(entry.owned.uniforms, source.uniforms);
|
|
13263
|
+
filter.resources.filterUniforms = entry.owned;
|
|
13264
|
+
if (entry.owned.isStatic)
|
|
13265
|
+
entry.owned.update();
|
|
13266
|
+
}
|
|
13267
|
+
}
|
|
13268
|
+
originalApplyFilter.call(system, filter, input, output, clear);
|
|
13269
|
+
// Pixi replaces group 0 with its renderer-owned group when applying an
|
|
13270
|
+
// enabled filter. Disabled filters use a separate passthrough shader.
|
|
13271
|
+
if (filter.enabled)
|
|
13272
|
+
globalGroup = filter.groups[0];
|
|
13273
|
+
};
|
|
13274
|
+
system.applyFilter = applyFilter;
|
|
13275
|
+
return () => {
|
|
13276
|
+
releaseMaskTarget();
|
|
13277
|
+
if (system.applyFilter === applyFilter) {
|
|
13278
|
+
system.applyFilter = originalApplyFilter;
|
|
13279
|
+
}
|
|
13280
|
+
globalGroup?.destroy();
|
|
13281
|
+
globalGroup = undefined;
|
|
13282
|
+
for (const [filter, { original, owned }] of maskGroups) {
|
|
13283
|
+
if (filter.resources?.filterUniforms === owned) {
|
|
13284
|
+
Object.assign(original.uniforms, owned.uniforms);
|
|
13285
|
+
filter.resources.filterUniforms = original;
|
|
13286
|
+
}
|
|
13287
|
+
const buffer = owned.buffer;
|
|
13288
|
+
owned.buffer = undefined;
|
|
13289
|
+
buffer?.destroy();
|
|
13290
|
+
}
|
|
13291
|
+
maskGroups.clear();
|
|
13292
|
+
};
|
|
13293
|
+
}
|
|
13294
|
+
function installMaskTargetBindings(renderer) {
|
|
13295
|
+
const pipe = renderer.renderPipes?.alphaMask;
|
|
13296
|
+
if (!pipe)
|
|
13297
|
+
return () => undefined;
|
|
13298
|
+
const targets = renderer.renderTarget;
|
|
13299
|
+
const originalExecute = pipe.execute;
|
|
13300
|
+
const captures = [];
|
|
13301
|
+
const execute = (instruction) => {
|
|
13302
|
+
if (!instruction.mask.renderMaskToTexture) {
|
|
13303
|
+
originalExecute.call(pipe, instruction);
|
|
13304
|
+
return;
|
|
13305
|
+
}
|
|
13306
|
+
if (instruction.action === "pushMaskBegin") {
|
|
13307
|
+
captures.push(targets.getBindState());
|
|
13308
|
+
}
|
|
13309
|
+
if (instruction.action !== "pushMaskEnd") {
|
|
13310
|
+
originalExecute.call(pipe, instruction);
|
|
13311
|
+
return;
|
|
13312
|
+
}
|
|
13313
|
+
const binding = captures.pop();
|
|
13314
|
+
const originalPop = targets.pop;
|
|
13315
|
+
// Filters bind their captures without pushing them onto the target stack.
|
|
13316
|
+
// Returning from an alpha-mask capture must restore that bound surface.
|
|
13317
|
+
targets.pop = () => {
|
|
13318
|
+
const target = originalPop.call(targets);
|
|
13319
|
+
return binding ? targets.bind(binding) : target;
|
|
13320
|
+
};
|
|
13321
|
+
try {
|
|
13322
|
+
originalExecute.call(pipe, instruction);
|
|
13323
|
+
}
|
|
13324
|
+
finally {
|
|
13325
|
+
targets.pop = originalPop;
|
|
13326
|
+
}
|
|
13327
|
+
};
|
|
13328
|
+
pipe.execute = execute;
|
|
13329
|
+
return () => {
|
|
13330
|
+
if (pipe.execute === execute)
|
|
13331
|
+
pipe.execute = originalExecute;
|
|
13332
|
+
captures.length = 0;
|
|
13333
|
+
};
|
|
13334
|
+
}
|
|
13335
|
+
|
|
13336
|
+
/** Follows visible heatmap samples while preserving faint anomaly interiors. */
|
|
13337
|
+
function createPixiFocusHeatmapCutout(options) {
|
|
13338
|
+
const geometry = new options.MeshGeometry({
|
|
13339
|
+
indices: new Uint32Array([0, 1, 2, 0, 2, 3]),
|
|
13340
|
+
positions: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]),
|
|
13341
|
+
shrinkBuffersToFit: true,
|
|
13342
|
+
topology: "triangle-list",
|
|
13343
|
+
uvs: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]),
|
|
13344
|
+
});
|
|
13345
|
+
const shader = options.Shader.from({
|
|
13346
|
+
gl: { vertex: tintedMaskVertexGlsl, fragment: heatmapCutoutGlsl },
|
|
13347
|
+
gpu: {
|
|
13348
|
+
vertex: { source: untintedMaskVertexWgsl, entryPoint: "mainVertex" },
|
|
13349
|
+
fragment: { source: heatmapCutoutWgsl, entryPoint: "mainFragment" },
|
|
13350
|
+
},
|
|
13351
|
+
resources: {
|
|
13352
|
+
uTexture: options.artifact.texture.source,
|
|
13353
|
+
uSampler: options.artifact.texture.source.style,
|
|
13354
|
+
heatmapCoverageUniforms: {
|
|
13355
|
+
uAntialiasing: { value: 0, type: "f32" },
|
|
13356
|
+
},
|
|
13357
|
+
},
|
|
13358
|
+
});
|
|
13359
|
+
const display = new options.Mesh({ geometry, shader });
|
|
13360
|
+
const uniforms = shader.resources.heatmapCoverageUniforms;
|
|
13361
|
+
return {
|
|
13362
|
+
display,
|
|
13363
|
+
render(artifact, antialiasing = false) {
|
|
13364
|
+
uniforms.uniforms.uAntialiasing = antialiasing ? 1 : 0;
|
|
13365
|
+
const { bounds } = artifact;
|
|
13366
|
+
display.position.set(bounds.x - bounds.width / 2, bounds.y - bounds.height / 2);
|
|
13367
|
+
display.scale.set(bounds.width, bounds.height);
|
|
13368
|
+
display.visible = true;
|
|
13369
|
+
},
|
|
13370
|
+
destroy() {
|
|
13371
|
+
display.removeFromParent();
|
|
13372
|
+
display.destroy();
|
|
13373
|
+
destroyShaderKeepingProgram(shader);
|
|
13374
|
+
const buffer = uniforms.buffer;
|
|
13375
|
+
uniforms.buffer = undefined;
|
|
13376
|
+
buffer?.destroy();
|
|
13377
|
+
geometry.destroy();
|
|
13378
|
+
},
|
|
13379
|
+
};
|
|
13380
|
+
}
|
|
13381
|
+
const heatmapCutoutGlsl = `#version 300 es
|
|
13382
|
+
precision highp float;
|
|
13383
|
+
|
|
13384
|
+
in vec2 vUV;
|
|
13385
|
+
uniform sampler2D uTexture;
|
|
13386
|
+
uniform float uAntialiasing;
|
|
13387
|
+
out vec4 finalColor;
|
|
13388
|
+
|
|
13389
|
+
float visibleSample(ivec2 position, ivec2 dimensions) {
|
|
13390
|
+
return texelFetch(uTexture, clamp(position, ivec2(0), dimensions - ivec2(1)), 0).a > 0.0 ? 1.0 : 0.0;
|
|
13391
|
+
}
|
|
13392
|
+
|
|
13393
|
+
void main(void) {
|
|
13394
|
+
float coverage;
|
|
13395
|
+
if (uAntialiasing > 0.0) {
|
|
13396
|
+
ivec2 dimensions = textureSize(uTexture, 0);
|
|
13397
|
+
vec2 position = vUV * vec2(dimensions) - 0.5;
|
|
13398
|
+
ivec2 base = ivec2(floor(position));
|
|
13399
|
+
vec2 weight = fract(position);
|
|
13400
|
+
coverage = mix(
|
|
13401
|
+
mix(visibleSample(base, dimensions), visibleSample(base + ivec2(1, 0), dimensions), weight.x),
|
|
13402
|
+
mix(visibleSample(base + ivec2(0, 1), dimensions), visibleSample(base + ivec2(1, 1), dimensions), weight.x),
|
|
13403
|
+
weight.y
|
|
13404
|
+
);
|
|
13405
|
+
} else {
|
|
13406
|
+
coverage = texture(uTexture, vUV).a > 0.0 ? 1.0 : 0.0;
|
|
13407
|
+
}
|
|
13408
|
+
finalColor = vec4(coverage);
|
|
13409
|
+
}
|
|
13410
|
+
`;
|
|
13411
|
+
const heatmapCutoutWgsl = `
|
|
13412
|
+
@group(2) @binding(0) var uTexture: texture_2d<f32>;
|
|
13413
|
+
@group(2) @binding(1) var uSampler: sampler;
|
|
13414
|
+
struct HeatmapCoverageUniforms {
|
|
13415
|
+
uAntialiasing: f32,
|
|
13416
|
+
};
|
|
13417
|
+
@group(2) @binding(2) var<uniform> heatmapCoverageUniforms: HeatmapCoverageUniforms;
|
|
13418
|
+
|
|
13419
|
+
fn visibleSample(position: vec2<i32>, dimensions: vec2<i32>) -> f32 {
|
|
13420
|
+
let alpha = textureLoad(uTexture, clamp(position, vec2<i32>(0), dimensions - vec2<i32>(1)), 0).a;
|
|
13421
|
+
return select(0.0, 1.0, alpha > 0.0);
|
|
13422
|
+
}
|
|
13423
|
+
|
|
13424
|
+
@fragment
|
|
13425
|
+
fn mainFragment(@location(0) vUV: vec2<f32>) -> @location(0) vec4<f32> {
|
|
13426
|
+
var coverage: f32;
|
|
13427
|
+
if (heatmapCoverageUniforms.uAntialiasing > 0.0) {
|
|
13428
|
+
let dimensions = vec2<i32>(textureDimensions(uTexture));
|
|
13429
|
+
let position = vUV * vec2<f32>(dimensions) - 0.5;
|
|
13430
|
+
let base = vec2<i32>(floor(position));
|
|
13431
|
+
let weight = fract(position);
|
|
13432
|
+
coverage = mix(
|
|
13433
|
+
mix(visibleSample(base, dimensions), visibleSample(base + vec2<i32>(1, 0), dimensions), weight.x),
|
|
13434
|
+
mix(visibleSample(base + vec2<i32>(0, 1), dimensions), visibleSample(base + vec2<i32>(1, 1), dimensions), weight.x),
|
|
13435
|
+
weight.y
|
|
13436
|
+
);
|
|
13437
|
+
} else {
|
|
13438
|
+
let alpha = textureSampleLevel(uTexture, uSampler, vUV, 0.0).a;
|
|
13439
|
+
coverage = select(0.0, 1.0, alpha > 0.0);
|
|
13440
|
+
}
|
|
13441
|
+
return vec4<f32>(coverage);
|
|
13442
|
+
}
|
|
13443
|
+
`;
|
|
13444
|
+
|
|
13445
|
+
function setPixiViewportBounds(display, bounds, viewport, Rectangle) {
|
|
13446
|
+
if (!viewport) {
|
|
13447
|
+
display.boundsArea = undefined;
|
|
13448
|
+
return;
|
|
13449
|
+
}
|
|
13450
|
+
const left = Math.max(bounds.x, viewport.x);
|
|
13451
|
+
const top = Math.max(bounds.y, viewport.y);
|
|
13452
|
+
const right = Math.min(bounds.x + bounds.width, viewport.x + viewport.width);
|
|
13453
|
+
const bottom = Math.min(bounds.y + bounds.height, viewport.y + viewport.height);
|
|
13454
|
+
const clipped = (display.boundsArea ??= new Rectangle());
|
|
13455
|
+
clipped.x = left;
|
|
13456
|
+
clipped.y = top;
|
|
13457
|
+
clipped.width = Math.max(0, right - left);
|
|
13458
|
+
clipped.height = Math.max(0, bottom - top);
|
|
13459
|
+
}
|
|
13460
|
+
/**
|
|
13461
|
+
* AlphaMask allocates from its mask's full bounds, including geometry outside
|
|
13462
|
+
* the canvas. A neutral parent clips that allocation without changing a
|
|
13463
|
+
* rotated or mirrored child's geometry or texture coordinates.
|
|
13464
|
+
*/
|
|
13465
|
+
function syncPixiMaskViewportBounds(container, viewport, Rectangle) {
|
|
13466
|
+
if (!viewport) {
|
|
13467
|
+
container.boundsArea = undefined;
|
|
13468
|
+
return;
|
|
13469
|
+
}
|
|
13470
|
+
let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
|
|
13471
|
+
for (const child of container.children) {
|
|
13472
|
+
if (!child.visible)
|
|
13473
|
+
continue;
|
|
13474
|
+
const bounds = child.getLocalBounds();
|
|
13475
|
+
if (bounds.width <= 0 || bounds.height <= 0)
|
|
13476
|
+
continue;
|
|
13477
|
+
child.updateLocalTransform();
|
|
13478
|
+
const { a, b, c, d, tx, ty } = child.localTransform;
|
|
13479
|
+
left = Math.min(left, tx +
|
|
13480
|
+
Math.min(a * bounds.minX, a * bounds.maxX) +
|
|
13481
|
+
Math.min(c * bounds.minY, c * bounds.maxY));
|
|
13482
|
+
top = Math.min(top, ty +
|
|
13483
|
+
Math.min(b * bounds.minX, b * bounds.maxX) +
|
|
13484
|
+
Math.min(d * bounds.minY, d * bounds.maxY));
|
|
13485
|
+
right = Math.max(right, tx +
|
|
13486
|
+
Math.max(a * bounds.minX, a * bounds.maxX) +
|
|
13487
|
+
Math.max(c * bounds.minY, c * bounds.maxY));
|
|
13488
|
+
bottom = Math.max(bottom, ty +
|
|
13489
|
+
Math.max(b * bounds.minX, b * bounds.maxX) +
|
|
13490
|
+
Math.max(d * bounds.minY, d * bounds.maxY));
|
|
13491
|
+
}
|
|
13492
|
+
setPixiViewportBounds(container, left === Infinity
|
|
13493
|
+
? { x: viewport.x, y: viewport.y, width: 0, height: 0 }
|
|
13494
|
+
: { x: left, y: top, width: right - left, height: bottom - top }, viewport, Rectangle);
|
|
13495
|
+
}
|
|
13496
|
+
|
|
12820
13497
|
const MAX_FOCUS_MASK_IDS = 16;
|
|
12821
13498
|
/**
|
|
12822
13499
|
* How far the media may travel before a cutout drawn for an earlier frame stops
|
|
@@ -12839,6 +13516,11 @@ function createPixiFocusLayer(options) {
|
|
|
12839
13516
|
: (options.focusStyle ?? new BaseFocusStyle());
|
|
12840
13517
|
let focusGraphics;
|
|
12841
13518
|
let focusMaskGraphics;
|
|
13519
|
+
let heatmapMaskContainer;
|
|
13520
|
+
let heatmapVectorGraphics;
|
|
13521
|
+
let usesHeatmapAlphaMask = false;
|
|
13522
|
+
let viewportBounds;
|
|
13523
|
+
const heatmapCutouts = new Map();
|
|
12842
13524
|
let idMaskRenderer;
|
|
12843
13525
|
let focusDisplay;
|
|
12844
13526
|
let currentAlpha = 0;
|
|
@@ -12865,6 +13547,7 @@ function createPixiFocusLayer(options) {
|
|
|
12865
13547
|
resetHeldFocus();
|
|
12866
13548
|
focusGraphics = new options.Graphics();
|
|
12867
13549
|
focusGraphics.visible = false;
|
|
13550
|
+
syncViewportBounds(true);
|
|
12868
13551
|
idMaskRenderer = createIdMaskRenderer();
|
|
12869
13552
|
if (!options.Container || !focusGraphics.setMask) {
|
|
12870
13553
|
focusDisplay = focusGraphics;
|
|
@@ -12893,12 +13576,27 @@ function createPixiFocusLayer(options) {
|
|
|
12893
13576
|
hide();
|
|
12894
13577
|
idMaskRenderer?.destroy();
|
|
12895
13578
|
idMaskRenderer = undefined;
|
|
13579
|
+
activateVectorMask();
|
|
13580
|
+
heatmapMaskContainer?.destroy({ children: true });
|
|
13581
|
+
heatmapMaskContainer = undefined;
|
|
12896
13582
|
},
|
|
12897
13583
|
releaseMaskTexture(texture) {
|
|
12898
|
-
if (!isDestroyed)
|
|
13584
|
+
if (!isDestroyed) {
|
|
12899
13585
|
idMaskRenderer?.releaseTexture(texture.source);
|
|
13586
|
+
const cutouts = heatmapCutouts.get(texture);
|
|
13587
|
+
if (cutouts) {
|
|
13588
|
+
for (const cutout of cutouts.values())
|
|
13589
|
+
cutout.destroy();
|
|
13590
|
+
heatmapCutouts.delete(texture);
|
|
13591
|
+
vectorFocusSignature = null;
|
|
13592
|
+
if (heatmapCutouts.size === 0 && usesHeatmapAlphaMask) {
|
|
13593
|
+
activateVectorMask()?.clear();
|
|
13594
|
+
}
|
|
13595
|
+
}
|
|
13596
|
+
}
|
|
12900
13597
|
},
|
|
12901
13598
|
drawFrame(context) {
|
|
13599
|
+
syncViewportBounds();
|
|
12902
13600
|
if (isDestroyed || !focusStyle || mediaWidth <= 0 || mediaHeight <= 0) {
|
|
12903
13601
|
hide();
|
|
12904
13602
|
return;
|
|
@@ -12919,11 +13617,15 @@ function createPixiFocusLayer(options) {
|
|
|
12919
13617
|
const instruction = resolvedInstruction
|
|
12920
13618
|
? {
|
|
12921
13619
|
...resolvedInstruction,
|
|
12922
|
-
targets: resolvedInstruction.targets.filter(({ detection }) =>
|
|
13620
|
+
targets: resolvedInstruction.targets.filter(({ detection, frame }) => frame === context.frame &&
|
|
13621
|
+
(options.isDetectionVisible?.(detection) ?? true)),
|
|
12923
13622
|
}
|
|
12924
13623
|
: undefined;
|
|
12925
13624
|
endHold();
|
|
12926
13625
|
if (!instruction || instruction.targets.length === 0) {
|
|
13626
|
+
if (usesHeatmapAlphaMask && heldFill) {
|
|
13627
|
+
drawOverlayWithoutCutout(heldFill);
|
|
13628
|
+
}
|
|
12927
13629
|
resetHeldFocus();
|
|
12928
13630
|
transitionToHidden();
|
|
12929
13631
|
return;
|
|
@@ -12933,12 +13635,13 @@ function createPixiFocusLayer(options) {
|
|
|
12933
13635
|
if (focusDisplay)
|
|
12934
13636
|
focusDisplay.visible = true;
|
|
12935
13637
|
if (drawIdMaskFocus(context.idMaskArtifact, instruction)) {
|
|
13638
|
+
activateVectorMask();
|
|
12936
13639
|
hideVectorFocus();
|
|
12937
13640
|
markCutoutDrawn(context.mediaTime, context.frame.mediaTime);
|
|
12938
13641
|
return;
|
|
12939
13642
|
}
|
|
12940
13643
|
idMaskRenderer?.hide();
|
|
12941
|
-
if (drawVectorFocus(instruction, context
|
|
13644
|
+
if (drawVectorFocus(instruction, context)) {
|
|
12942
13645
|
markCutoutDrawn(context.mediaTime, context.frame.mediaTime);
|
|
12943
13646
|
}
|
|
12944
13647
|
},
|
|
@@ -12966,7 +13669,29 @@ function createPixiFocusLayer(options) {
|
|
|
12966
13669
|
}
|
|
12967
13670
|
focusStyle = nextFocusStyle;
|
|
12968
13671
|
},
|
|
13672
|
+
syncViewportBounds,
|
|
12969
13673
|
};
|
|
13674
|
+
function syncViewportBounds(force = false) {
|
|
13675
|
+
if (!options.Rectangle)
|
|
13676
|
+
return;
|
|
13677
|
+
const next = options.getViewportBounds?.();
|
|
13678
|
+
if (!force &&
|
|
13679
|
+
next?.x === viewportBounds?.x &&
|
|
13680
|
+
next?.y === viewportBounds?.y &&
|
|
13681
|
+
next?.width === viewportBounds?.width &&
|
|
13682
|
+
next?.height === viewportBounds?.height)
|
|
13683
|
+
return;
|
|
13684
|
+
viewportBounds = next ? { ...next } : undefined;
|
|
13685
|
+
if (focusGraphics) {
|
|
13686
|
+
setPixiViewportBounds(focusGraphics, { x: 0, y: 0, width: mediaWidth, height: mediaHeight }, viewportBounds, options.Rectangle);
|
|
13687
|
+
}
|
|
13688
|
+
syncHeatmapViewportBounds();
|
|
13689
|
+
}
|
|
13690
|
+
function syncHeatmapViewportBounds() {
|
|
13691
|
+
if (heatmapMaskContainer && options.Rectangle) {
|
|
13692
|
+
syncPixiMaskViewportBounds(heatmapMaskContainer, viewportBounds, options.Rectangle);
|
|
13693
|
+
}
|
|
13694
|
+
}
|
|
12970
13695
|
function drawIdMaskFocus(artifact, instruction) {
|
|
12971
13696
|
if (!idMaskRenderer ||
|
|
12972
13697
|
!artifact ||
|
|
@@ -12975,17 +13700,19 @@ function createPixiFocusLayer(options) {
|
|
|
12975
13700
|
}
|
|
12976
13701
|
const maskIds = getTargetMaskIds(instruction.targets);
|
|
12977
13702
|
if (maskIds.length === 0 ||
|
|
13703
|
+
instruction.targets.some((target) => !target.detection.mask) ||
|
|
12978
13704
|
(!instruction.ambient && maskIds.length !== instruction.targets.length)) {
|
|
12979
13705
|
return false;
|
|
12980
13706
|
}
|
|
12981
13707
|
idMaskRenderer.render(artifact.frame, artifact.texture, maskIds, instruction.fill, instruction.ambient === true);
|
|
12982
13708
|
return true;
|
|
12983
13709
|
}
|
|
12984
|
-
function drawVectorFocus(instruction,
|
|
13710
|
+
function drawVectorFocus(instruction, context) {
|
|
12985
13711
|
if (!focusGraphics) {
|
|
12986
13712
|
return false;
|
|
12987
13713
|
}
|
|
12988
13714
|
const targetsWithGeometry = instruction.targets.filter((target) => target.detection.mask ||
|
|
13715
|
+
target.detection.heatmap ||
|
|
12989
13716
|
target.detection.rect ||
|
|
12990
13717
|
target.detection.polygon);
|
|
12991
13718
|
if (targetsWithGeometry.length === 0) {
|
|
@@ -12993,37 +13720,55 @@ function createPixiFocusLayer(options) {
|
|
|
12993
13720
|
return false;
|
|
12994
13721
|
}
|
|
12995
13722
|
const signature = {
|
|
13723
|
+
annotationAntialiasing: options.getAnnotationAntialiasing?.() ?? false,
|
|
12996
13724
|
cornerRadius: instruction.fallback?.cornerRadius,
|
|
12997
13725
|
detections: targetsWithGeometry.map((target) => target.detection),
|
|
12998
13726
|
fillAlpha: instruction.fill.alpha,
|
|
12999
13727
|
fillColor: instruction.fill.color,
|
|
13000
|
-
frame,
|
|
13728
|
+
frame: context.frame,
|
|
13729
|
+
heatmapsEnabled: (options.getHeatmapRenderers?.().length ?? 0) > 0,
|
|
13730
|
+
heatmapArtifacts: context.heatmapArtifacts ?? [],
|
|
13001
13731
|
mediaHeight,
|
|
13002
13732
|
mediaWidth,
|
|
13003
|
-
shape: instruction.fallback?.shape,
|
|
13733
|
+
shape: instruction.fallback === null ? null : instruction.fallback?.shape,
|
|
13004
13734
|
};
|
|
13005
13735
|
focusGraphics.visible = true;
|
|
13006
13736
|
if (focusMaskGraphics)
|
|
13007
|
-
focusMaskGraphics.visible =
|
|
13737
|
+
focusMaskGraphics.visible = !usesHeatmapAlphaMask;
|
|
13008
13738
|
// Tessellating thousands of mask runs on every draw is what makes this path
|
|
13009
13739
|
// expensive, and nothing it draws moves while its inputs hold still.
|
|
13010
13740
|
if (isSameVectorFocus(vectorFocusSignature, signature)) {
|
|
13011
13741
|
return true;
|
|
13012
13742
|
}
|
|
13013
13743
|
vectorFocusSignature = signature;
|
|
13744
|
+
const gpuHeatmapTargets = context.heatmapArtifacts !== undefined &&
|
|
13745
|
+
signature.heatmapsEnabled &&
|
|
13746
|
+
options.AlphaMask &&
|
|
13747
|
+
options.Container &&
|
|
13748
|
+
options.Mesh &&
|
|
13749
|
+
options.MeshGeometry &&
|
|
13750
|
+
options.Shader &&
|
|
13751
|
+
focusMaskGraphics &&
|
|
13752
|
+
focusGraphics.setMask &&
|
|
13753
|
+
targetsWithGeometry.some(({ detection }) => detection.heatmap && !detection.mask);
|
|
13754
|
+
const cutoutGraphics = gpuHeatmapTargets
|
|
13755
|
+
? activateHeatmapMask(instruction.targets, signature.annotationAntialiasing, signature.heatmapArtifacts)
|
|
13756
|
+
: activateVectorMask();
|
|
13014
13757
|
focusGraphics.clear();
|
|
13015
13758
|
focusGraphics.rect(0, 0, mediaWidth, mediaHeight);
|
|
13016
13759
|
focusGraphics.fill(instruction.fill);
|
|
13017
|
-
|
|
13018
|
-
|
|
13019
|
-
|
|
13020
|
-
|
|
13021
|
-
|
|
13760
|
+
const vectorTargets = targetsWithGeometry.filter(({ detection }) => !signature.heatmapsEnabled || !detection.heatmap || detection.mask);
|
|
13761
|
+
if (cutoutGraphics) {
|
|
13762
|
+
cutoutGraphics.clear();
|
|
13763
|
+
for (const target of vectorTargets) {
|
|
13764
|
+
if (drawCutoutShape(cutoutGraphics, target, instruction) === "drawn") {
|
|
13765
|
+
cutoutGraphics.fill({ alpha: 1, color: 0xffffff });
|
|
13022
13766
|
}
|
|
13023
13767
|
}
|
|
13768
|
+
syncHeatmapViewportBounds();
|
|
13024
13769
|
return true;
|
|
13025
13770
|
}
|
|
13026
|
-
for (const target of
|
|
13771
|
+
for (const target of vectorTargets) {
|
|
13027
13772
|
if (drawCutoutShape(focusGraphics, target, instruction) === "drawn") {
|
|
13028
13773
|
focusGraphics.cut();
|
|
13029
13774
|
}
|
|
@@ -13039,7 +13784,7 @@ function createPixiFocusLayer(options) {
|
|
|
13039
13784
|
graphics.poly(target.detection.polygon.points.flatMap(({ x, y }) => [x, y]), true);
|
|
13040
13785
|
return "drawn";
|
|
13041
13786
|
}
|
|
13042
|
-
if (!target.detection.rect) {
|
|
13787
|
+
if (!target.detection.rect || instruction.fallback === null) {
|
|
13043
13788
|
return "empty";
|
|
13044
13789
|
}
|
|
13045
13790
|
const { rect } = target.detection;
|
|
@@ -13053,6 +13798,93 @@ function createPixiFocusLayer(options) {
|
|
|
13053
13798
|
}
|
|
13054
13799
|
return "drawn";
|
|
13055
13800
|
}
|
|
13801
|
+
function activateVectorMask() {
|
|
13802
|
+
clearHeatmapCutouts();
|
|
13803
|
+
if (heatmapMaskContainer)
|
|
13804
|
+
heatmapMaskContainer.visible = false;
|
|
13805
|
+
if (usesHeatmapAlphaMask && focusMaskGraphics) {
|
|
13806
|
+
focusGraphics?.setMask?.({
|
|
13807
|
+
inverse: true,
|
|
13808
|
+
mask: focusMaskGraphics,
|
|
13809
|
+
});
|
|
13810
|
+
usesHeatmapAlphaMask = false;
|
|
13811
|
+
}
|
|
13812
|
+
if (focusMaskGraphics)
|
|
13813
|
+
focusMaskGraphics.visible = true;
|
|
13814
|
+
return focusMaskGraphics;
|
|
13815
|
+
}
|
|
13816
|
+
function activateHeatmapMask(targets, annotationAntialiasing, heatmapArtifacts) {
|
|
13817
|
+
const indexes = new Set(targets
|
|
13818
|
+
.filter(({ detection }) => !detection.mask)
|
|
13819
|
+
.map(({ detectionIndex }) => detectionIndex));
|
|
13820
|
+
const artifacts = heatmapArtifacts.filter((artifact) => indexes.has(artifact.detectionIndex));
|
|
13821
|
+
if (artifacts.length === 0)
|
|
13822
|
+
return activateVectorMask();
|
|
13823
|
+
if (!heatmapMaskContainer) {
|
|
13824
|
+
heatmapMaskContainer = new options.Container();
|
|
13825
|
+
heatmapVectorGraphics = new options.Graphics();
|
|
13826
|
+
heatmapMaskContainer.addChild(heatmapVectorGraphics);
|
|
13827
|
+
focusDisplay.addChild(heatmapMaskContainer);
|
|
13828
|
+
}
|
|
13829
|
+
heatmapMaskContainer.visible = true;
|
|
13830
|
+
if (!usesHeatmapAlphaMask) {
|
|
13831
|
+
focusGraphics?.setMask?.({
|
|
13832
|
+
inverse: true,
|
|
13833
|
+
channel: "alpha",
|
|
13834
|
+
mask: new options.AlphaMask({
|
|
13835
|
+
mask: heatmapMaskContainer,
|
|
13836
|
+
}),
|
|
13837
|
+
});
|
|
13838
|
+
usesHeatmapAlphaMask = true;
|
|
13839
|
+
}
|
|
13840
|
+
if (focusMaskGraphics)
|
|
13841
|
+
focusMaskGraphics.visible = false;
|
|
13842
|
+
const activeTextures = new Map();
|
|
13843
|
+
for (const artifact of artifacts) {
|
|
13844
|
+
let indexes = activeTextures.get(artifact.texture);
|
|
13845
|
+
if (!indexes) {
|
|
13846
|
+
indexes = new Set();
|
|
13847
|
+
activeTextures.set(artifact.texture, indexes);
|
|
13848
|
+
}
|
|
13849
|
+
indexes.add(artifact.detectionIndex);
|
|
13850
|
+
let textureCutouts = heatmapCutouts.get(artifact.texture);
|
|
13851
|
+
if (!textureCutouts) {
|
|
13852
|
+
textureCutouts = new Map();
|
|
13853
|
+
heatmapCutouts.set(artifact.texture, textureCutouts);
|
|
13854
|
+
}
|
|
13855
|
+
let cutout = textureCutouts.get(artifact.detectionIndex);
|
|
13856
|
+
if (!cutout) {
|
|
13857
|
+
cutout = createPixiFocusHeatmapCutout({
|
|
13858
|
+
artifact,
|
|
13859
|
+
Mesh: options.Mesh,
|
|
13860
|
+
MeshGeometry: options.MeshGeometry,
|
|
13861
|
+
Shader: options.Shader,
|
|
13862
|
+
});
|
|
13863
|
+
textureCutouts.set(artifact.detectionIndex, cutout);
|
|
13864
|
+
heatmapMaskContainer.addChild(cutout.display);
|
|
13865
|
+
}
|
|
13866
|
+
cutout.render(artifact, annotationAntialiasing);
|
|
13867
|
+
}
|
|
13868
|
+
for (const [texture, cutouts] of heatmapCutouts) {
|
|
13869
|
+
const activeIndexes = activeTextures.get(texture);
|
|
13870
|
+
for (const [index, cutout] of cutouts) {
|
|
13871
|
+
if (activeIndexes?.has(index))
|
|
13872
|
+
continue;
|
|
13873
|
+
cutout.destroy();
|
|
13874
|
+
cutouts.delete(index);
|
|
13875
|
+
}
|
|
13876
|
+
if (cutouts.size === 0)
|
|
13877
|
+
heatmapCutouts.delete(texture);
|
|
13878
|
+
}
|
|
13879
|
+
return heatmapVectorGraphics;
|
|
13880
|
+
}
|
|
13881
|
+
function clearHeatmapCutouts() {
|
|
13882
|
+
for (const cutouts of heatmapCutouts.values()) {
|
|
13883
|
+
for (const cutout of cutouts.values())
|
|
13884
|
+
cutout.destroy();
|
|
13885
|
+
}
|
|
13886
|
+
heatmapCutouts.clear();
|
|
13887
|
+
}
|
|
13056
13888
|
function drawMaskCutout(graphics, target) {
|
|
13057
13889
|
const mask = target.detection.mask;
|
|
13058
13890
|
if (!mask) {
|
|
@@ -13114,7 +13946,8 @@ function createPixiFocusLayer(options) {
|
|
|
13114
13946
|
targetAlpha = 1;
|
|
13115
13947
|
if (focusDisplay)
|
|
13116
13948
|
focusDisplay.visible = true;
|
|
13117
|
-
if (
|
|
13949
|
+
if (!usesHeatmapAlphaMask &&
|
|
13950
|
+
isDrawnCutoutStillCurrent(mediaTime, heldMaskFrameTime)) {
|
|
13118
13951
|
return;
|
|
13119
13952
|
}
|
|
13120
13953
|
if (isMaskArtifactOwed) {
|
|
@@ -13197,6 +14030,7 @@ function createPixiFocusLayer(options) {
|
|
|
13197
14030
|
cutoutMediaTime = null;
|
|
13198
14031
|
cutoutFrameTime = null;
|
|
13199
14032
|
drawnOverlayWithoutCutout = fill;
|
|
14033
|
+
activateVectorMask();
|
|
13200
14034
|
if (idMaskRenderer) {
|
|
13201
14035
|
hideVectorFocus();
|
|
13202
14036
|
idMaskRenderer.renderWithoutCutout(fill);
|
|
@@ -13256,7 +14090,10 @@ function createPixiFocusLayer(options) {
|
|
|
13256
14090
|
}
|
|
13257
14091
|
function isSameVectorFocus(previous, next) {
|
|
13258
14092
|
return (previous !== null &&
|
|
14093
|
+
previous.annotationAntialiasing === next.annotationAntialiasing &&
|
|
13259
14094
|
previous.frame === next.frame &&
|
|
14095
|
+
previous.heatmapsEnabled === next.heatmapsEnabled &&
|
|
14096
|
+
sameHeatmapArtifacts(previous.heatmapArtifacts, next.heatmapArtifacts) &&
|
|
13260
14097
|
previous.fillAlpha === next.fillAlpha &&
|
|
13261
14098
|
previous.fillColor === next.fillColor &&
|
|
13262
14099
|
previous.cornerRadius === next.cornerRadius &&
|
|
@@ -13266,6 +14103,18 @@ function isSameVectorFocus(previous, next) {
|
|
|
13266
14103
|
previous.detections.length === next.detections.length &&
|
|
13267
14104
|
previous.detections.every((detection, index) => detection === next.detections[index]));
|
|
13268
14105
|
}
|
|
14106
|
+
function sameHeatmapArtifacts(left, right) {
|
|
14107
|
+
return (left.length === right.length &&
|
|
14108
|
+
left.every((artifact, index) => {
|
|
14109
|
+
const next = right[index];
|
|
14110
|
+
return (artifact.texture === next.texture &&
|
|
14111
|
+
artifact.detectionIndex === next.detectionIndex &&
|
|
14112
|
+
artifact.bounds.x === next.bounds.x &&
|
|
14113
|
+
artifact.bounds.y === next.bounds.y &&
|
|
14114
|
+
artifact.bounds.width === next.bounds.width &&
|
|
14115
|
+
artifact.bounds.height === next.bounds.height);
|
|
14116
|
+
}));
|
|
14117
|
+
}
|
|
13269
14118
|
function getTargetMaskIds(targets) {
|
|
13270
14119
|
return targets
|
|
13271
14120
|
.filter((target) => target.detection.mask)
|
|
@@ -13550,6 +14399,416 @@ fn mainFragment(
|
|
|
13550
14399
|
}
|
|
13551
14400
|
`;
|
|
13552
14401
|
|
|
14402
|
+
/*!
|
|
14403
|
+
* The FXAA algorithm is adapted from PixiJS v7.4.3's official shader:
|
|
14404
|
+
* https://github.com/pixijs/pixijs/blob/v7.4.3/packages/filter-fxaa/src/fxaa.frag
|
|
14405
|
+
* It includes alpha in contrast, filters premultiplied RGBA, clamps samples,
|
|
14406
|
+
* limits the span to four pixels and mixes the center back for thin strokes.
|
|
14407
|
+
*
|
|
14408
|
+
* From: https://github.com/mitsuhiko/webgl-meincraft
|
|
14409
|
+
* Copyright (c) 2011 by Armin Ronacher. Some rights reserved.
|
|
14410
|
+
*
|
|
14411
|
+
* Redistribution and use in source and binary forms, with or without
|
|
14412
|
+
* modification, are permitted provided that the following conditions are met:
|
|
14413
|
+
*
|
|
14414
|
+
* Redistributions of source code must retain the above copyright notice,
|
|
14415
|
+
* this list of conditions and the following disclaimer.
|
|
14416
|
+
*
|
|
14417
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
14418
|
+
* this list of conditions and the following disclaimer in the documentation
|
|
14419
|
+
* and/or other materials provided with the distribution.
|
|
14420
|
+
*
|
|
14421
|
+
* The names of the contributors may not be used to endorse or promote
|
|
14422
|
+
* products derived from this software without specific prior written permission.
|
|
14423
|
+
*
|
|
14424
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
14425
|
+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
14426
|
+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
14427
|
+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
14428
|
+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
14429
|
+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
14430
|
+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
14431
|
+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
14432
|
+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
14433
|
+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
14434
|
+
* POSSIBILITY OF SUCH DAMAGE.
|
|
14435
|
+
*/
|
|
14436
|
+
const annotationFxaaGl = `
|
|
14437
|
+
vec4 applyAnnotationFxaa(vec2 uv) {
|
|
14438
|
+
vec2 pixel = uInputPixel.zw;
|
|
14439
|
+
vec4 center = readColor(uv);
|
|
14440
|
+
float nw = contrastValue(readColor(uv + vec2(-1.0, -1.0) * pixel));
|
|
14441
|
+
float ne = contrastValue(readColor(uv + vec2( 1.0, -1.0) * pixel));
|
|
14442
|
+
float sw = contrastValue(readColor(uv + vec2(-1.0, 1.0) * pixel));
|
|
14443
|
+
float se = contrastValue(readColor(uv + vec2( 1.0, 1.0) * pixel));
|
|
14444
|
+
float middle = contrastValue(center);
|
|
14445
|
+
float low = min(middle, min(min(nw, ne), min(sw, se)));
|
|
14446
|
+
float high = max(middle, max(max(nw, ne), max(sw, se)));
|
|
14447
|
+
if (high - low < max(0.03125, high * 0.0625)) {
|
|
14448
|
+
return center;
|
|
14449
|
+
}
|
|
14450
|
+
vec2 direction = vec2(-((nw + ne) - (sw + se)), (nw + sw) - (ne + se));
|
|
14451
|
+
float reduction = max((nw + ne + sw + se) * 0.03125, 0.0078125);
|
|
14452
|
+
direction *= 1.0 / (min(abs(direction.x), abs(direction.y)) + reduction);
|
|
14453
|
+
direction = clamp(direction, vec2(-4.0), vec2(4.0)) * pixel;
|
|
14454
|
+
vec4 narrow = 0.5 * (
|
|
14455
|
+
readColor(uv - direction / 6.0) +
|
|
14456
|
+
readColor(uv + direction / 6.0)
|
|
14457
|
+
);
|
|
14458
|
+
vec4 wide = 0.5 * narrow + 0.25 * (
|
|
14459
|
+
readColor(uv - direction * 0.5) +
|
|
14460
|
+
readColor(uv + direction * 0.5)
|
|
14461
|
+
);
|
|
14462
|
+
float value = contrastValue(wide);
|
|
14463
|
+
vec4 filtered = value < low || value > high ? narrow : wide;
|
|
14464
|
+
return mix(center, filtered, 0.75);
|
|
14465
|
+
}
|
|
14466
|
+
`;
|
|
14467
|
+
const annotationFxaaWgsl = `
|
|
14468
|
+
fn applyAnnotationFxaa(uv: vec2<f32>) -> vec4<f32> {
|
|
14469
|
+
let pixel = gfu.uInputPixel.zw;
|
|
14470
|
+
let center = readColor(uv);
|
|
14471
|
+
let nw = contrastValue(readColor(uv + vec2<f32>(-1.0, -1.0) * pixel));
|
|
14472
|
+
let ne = contrastValue(readColor(uv + vec2<f32>( 1.0, -1.0) * pixel));
|
|
14473
|
+
let sw = contrastValue(readColor(uv + vec2<f32>(-1.0, 1.0) * pixel));
|
|
14474
|
+
let se = contrastValue(readColor(uv + vec2<f32>( 1.0, 1.0) * pixel));
|
|
14475
|
+
let middle = contrastValue(center);
|
|
14476
|
+
let low = min(middle, min(min(nw, ne), min(sw, se)));
|
|
14477
|
+
let high = max(middle, max(max(nw, ne), max(sw, se)));
|
|
14478
|
+
if (high - low < max(0.03125, high * 0.0625)) { return center; }
|
|
14479
|
+
var direction = vec2<f32>(-((nw + ne) - (sw + se)), (nw + sw) - (ne + se));
|
|
14480
|
+
let reduction = max((nw + ne + sw + se) * 0.03125, 0.0078125);
|
|
14481
|
+
direction *= 1.0 / (min(abs(direction.x), abs(direction.y)) + reduction);
|
|
14482
|
+
direction = clamp(direction, vec2<f32>(-4.0), vec2<f32>(4.0)) * pixel;
|
|
14483
|
+
let narrow = 0.5 * (readColor(uv - direction / 6.0) + readColor(uv + direction / 6.0));
|
|
14484
|
+
let wide = 0.5 * narrow + 0.25 * (readColor(uv - direction * 0.5) + readColor(uv + direction * 0.5));
|
|
14485
|
+
let value = contrastValue(wide);
|
|
14486
|
+
let filtered = select(wide, narrow, value < low || value > high);
|
|
14487
|
+
return mix(center, filtered, 0.75);
|
|
14488
|
+
}
|
|
14489
|
+
`;
|
|
14490
|
+
|
|
14491
|
+
/** Smooths a captured annotation group without reading the media texture. */
|
|
14492
|
+
function createPixiAnnotationAntialiasFilter(options) {
|
|
14493
|
+
const source = createGpuSource(options.maskCoverage ?? false);
|
|
14494
|
+
const filter = options.Filter.from({
|
|
14495
|
+
antialias: false,
|
|
14496
|
+
resolution: options.resolution ?? 1,
|
|
14497
|
+
padding: 2,
|
|
14498
|
+
resources: options.maskCoverage
|
|
14499
|
+
? {
|
|
14500
|
+
coverageAaUniforms: {
|
|
14501
|
+
uOutputOffset: {
|
|
14502
|
+
type: "vec4<f32>",
|
|
14503
|
+
value: new Float32Array([0, 0, 1, 0]),
|
|
14504
|
+
},
|
|
14505
|
+
},
|
|
14506
|
+
}
|
|
14507
|
+
: {},
|
|
14508
|
+
gl: {
|
|
14509
|
+
vertex: options.maskCoverage
|
|
14510
|
+
? coverageFilterVert
|
|
14511
|
+
: options.defaultFilterVert,
|
|
14512
|
+
fragment: `#version 300 es
|
|
14513
|
+
precision highp float;
|
|
14514
|
+
in vec2 vTextureCoord;
|
|
14515
|
+
uniform sampler2D uTexture;
|
|
14516
|
+
uniform vec4 uInputClamp;
|
|
14517
|
+
uniform vec4 uInputPixel;
|
|
14518
|
+
uniform vec4 uInputSize;
|
|
14519
|
+
uniform vec4 uOutputFrame;
|
|
14520
|
+
uniform vec4 uOutputTexture;
|
|
14521
|
+
out vec4 finalColor;
|
|
14522
|
+
|
|
14523
|
+
vec4 readColor(vec2 uv) {
|
|
14524
|
+
${options.maskCoverage
|
|
14525
|
+
? "return texture(uTexture, clamp(uv, uInputClamp.xy, uInputClamp.zw));"
|
|
14526
|
+
: `vec2 lower = max(uInputClamp.xy, -uOutputFrame.xy * uInputSize.zw + 0.5 * uInputPixel.zw);
|
|
14527
|
+
vec2 upper = min(uInputClamp.zw, (uOutputTexture.xy - uOutputFrame.xy) * uInputSize.zw - 0.5 * uInputPixel.zw);
|
|
14528
|
+
return texture(uTexture, clamp(uv, lower, max(lower, upper)));`}
|
|
14529
|
+
}
|
|
14530
|
+
|
|
14531
|
+
float contrastValue(vec4 color) {
|
|
14532
|
+
return dot(color.rgb, vec3(0.299, 0.587, 0.114)) + color.a;
|
|
14533
|
+
}
|
|
14534
|
+
${annotationFxaaGl}
|
|
14535
|
+
|
|
14536
|
+
void main(void) {
|
|
14537
|
+
finalColor = applyAnnotationFxaa(vTextureCoord);
|
|
14538
|
+
}
|
|
14539
|
+
|
|
14540
|
+
`,
|
|
14541
|
+
},
|
|
14542
|
+
gpu: {
|
|
14543
|
+
vertex: { entryPoint: "mainVertex", source },
|
|
14544
|
+
fragment: { entryPoint: "mainFragment", source },
|
|
14545
|
+
},
|
|
14546
|
+
});
|
|
14547
|
+
if (options.maskCoverage) {
|
|
14548
|
+
filter.apply = (manager, input, output, clear) => {
|
|
14549
|
+
const uniforms = filter.resources.coverageAaUniforms;
|
|
14550
|
+
const offset = uniforms.uniforms.uOutputOffset;
|
|
14551
|
+
const renderer = manager.renderer;
|
|
14552
|
+
const target = renderer.renderTarget.getRenderTarget(output);
|
|
14553
|
+
const global = renderer.globalUniforms.globalUniformData;
|
|
14554
|
+
// A mask target has its own origin outside Pixi's filter stack. The
|
|
14555
|
+
// global uniforms identify that target after the capture offset pops.
|
|
14556
|
+
offset[0] = global.offset.x;
|
|
14557
|
+
offset[1] = global.offset.y;
|
|
14558
|
+
offset[2] = target.resolution;
|
|
14559
|
+
offset[3] = global.resolution === target.size ? 1 : 0;
|
|
14560
|
+
uniforms.update();
|
|
14561
|
+
manager.applyFilter(filter, input, output, clear);
|
|
14562
|
+
};
|
|
14563
|
+
}
|
|
14564
|
+
return filter;
|
|
14565
|
+
}
|
|
14566
|
+
/** Keeps Pixi's power-of-two capture textures within the backend's limit. */
|
|
14567
|
+
function resolvePixiAnnotationAntialiasResolution(outputResolution, viewport, maxTextureSize, captureScale = 1) {
|
|
14568
|
+
const pooledLimit = 2 ** Math.floor(Math.log2(maxTextureSize));
|
|
14569
|
+
// Nested masks round logical bounds; FXAA adds two padding pixels per edge.
|
|
14570
|
+
const roundedSide = Math.max(1, Math.ceil(viewport.width) + 6, Math.ceil(viewport.height) + 6);
|
|
14571
|
+
return Math.min(outputResolution * captureScale, Math.max(1, pooledLimit - 2) / roundedSide);
|
|
14572
|
+
}
|
|
14573
|
+
const coverageFilterVert = `#version 300 es
|
|
14574
|
+
precision highp float;
|
|
14575
|
+
in vec2 aPosition;
|
|
14576
|
+
out vec2 vTextureCoord;
|
|
14577
|
+
uniform vec4 uOutputFrame;
|
|
14578
|
+
uniform vec4 uOutputTexture;
|
|
14579
|
+
uniform vec4 uInputSize;
|
|
14580
|
+
uniform vec4 uGlobalFrame;
|
|
14581
|
+
uniform vec4 uOutputOffset;
|
|
14582
|
+
|
|
14583
|
+
void main(void) {
|
|
14584
|
+
vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
|
|
14585
|
+
if (uOutputOffset.w > 0.0) {
|
|
14586
|
+
position -= uOutputOffset.xy - uGlobalFrame.xy / uOutputOffset.z;
|
|
14587
|
+
}
|
|
14588
|
+
gl_Position = vec4(
|
|
14589
|
+
position.x * (2.0 / uOutputTexture.x) - 1.0,
|
|
14590
|
+
position.y * (2.0 * uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z,
|
|
14591
|
+
0.0, 1.0
|
|
14592
|
+
);
|
|
14593
|
+
vTextureCoord = aPosition * (uOutputFrame.zw * uInputSize.zw);
|
|
14594
|
+
}
|
|
14595
|
+
`;
|
|
14596
|
+
function createGpuSource(maskCoverage) {
|
|
14597
|
+
return `
|
|
14598
|
+
struct GlobalFilterUniforms {
|
|
14599
|
+
uInputSize: vec4<f32>,
|
|
14600
|
+
uInputPixel: vec4<f32>,
|
|
14601
|
+
uInputClamp: vec4<f32>,
|
|
14602
|
+
uOutputFrame: vec4<f32>,
|
|
14603
|
+
uGlobalFrame: vec4<f32>,
|
|
14604
|
+
uOutputTexture: vec4<f32>,
|
|
14605
|
+
}
|
|
14606
|
+
@group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;
|
|
14607
|
+
@group(0) @binding(1) var uTexture: texture_2d<f32>;
|
|
14608
|
+
@group(0) @binding(2) var uSampler: sampler;
|
|
14609
|
+
${maskCoverage
|
|
14610
|
+
? `struct CoverageAaUniforms { uOutputOffset: vec4<f32>, }
|
|
14611
|
+
@group(1) @binding(0) var<uniform> coverageAaUniforms: CoverageAaUniforms;`
|
|
14612
|
+
: ""}
|
|
14613
|
+
|
|
14614
|
+
struct FilterOutput {
|
|
14615
|
+
@builtin(position) position: vec4<f32>,
|
|
14616
|
+
@location(0) uv: vec2<f32>,
|
|
14617
|
+
}
|
|
14618
|
+
|
|
14619
|
+
@vertex
|
|
14620
|
+
fn mainVertex(@location(0) aPosition: vec2<f32>) -> FilterOutput {
|
|
14621
|
+
var out: FilterOutput;
|
|
14622
|
+
var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;
|
|
14623
|
+
${maskCoverage
|
|
14624
|
+
? `let offset = coverageAaUniforms.uOutputOffset;
|
|
14625
|
+
if (offset.w > 0.0) {
|
|
14626
|
+
position -= offset.xy - gfu.uGlobalFrame.xy / offset.z;
|
|
14627
|
+
}`
|
|
14628
|
+
: ""}
|
|
14629
|
+
out.position = vec4<f32>(
|
|
14630
|
+
position.x * (2.0 / gfu.uOutputTexture.x) - 1.0,
|
|
14631
|
+
position.y * (2.0 * gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z,
|
|
14632
|
+
0.0, 1.0
|
|
14633
|
+
);
|
|
14634
|
+
out.uv = aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw);
|
|
14635
|
+
return out;
|
|
14636
|
+
}
|
|
14637
|
+
|
|
14638
|
+
fn readColor(uv: vec2<f32>) -> vec4<f32> {
|
|
14639
|
+
${maskCoverage
|
|
14640
|
+
? "return textureSampleLevel(uTexture, uSampler, clamp(uv, gfu.uInputClamp.xy, gfu.uInputClamp.zw), 0.0);"
|
|
14641
|
+
: `let lower = max(gfu.uInputClamp.xy, -gfu.uOutputFrame.xy * gfu.uInputSize.zw + 0.5 * gfu.uInputPixel.zw);
|
|
14642
|
+
let upper = min(gfu.uInputClamp.zw, (gfu.uOutputTexture.xy - gfu.uOutputFrame.xy) * gfu.uInputSize.zw - 0.5 * gfu.uInputPixel.zw);
|
|
14643
|
+
return textureSampleLevel(uTexture, uSampler, clamp(uv, lower, max(lower, upper)), 0.0);`}
|
|
14644
|
+
}
|
|
14645
|
+
|
|
14646
|
+
fn contrastValue(color: vec4<f32>) -> f32 {
|
|
14647
|
+
return dot(color.rgb, vec3<f32>(0.299, 0.587, 0.114)) + color.a;
|
|
14648
|
+
}
|
|
14649
|
+
${annotationFxaaWgsl}
|
|
14650
|
+
|
|
14651
|
+
@fragment
|
|
14652
|
+
fn mainFragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
|
14653
|
+
return applyAnnotationFxaa(uv);
|
|
14654
|
+
}
|
|
14655
|
+
`;
|
|
14656
|
+
}
|
|
14657
|
+
|
|
14658
|
+
var PixiSceneLayerKind;
|
|
14659
|
+
(function (PixiSceneLayerKind) {
|
|
14660
|
+
PixiSceneLayerKind["Media"] = "media";
|
|
14661
|
+
PixiSceneLayerKind["Depth"] = "depth";
|
|
14662
|
+
PixiSceneLayerKind["Heatmap"] = "heatmap";
|
|
14663
|
+
PixiSceneLayerKind["Mask"] = "mask";
|
|
14664
|
+
PixiSceneLayerKind["Box"] = "box";
|
|
14665
|
+
PixiSceneLayerKind["Vector"] = "vector";
|
|
14666
|
+
PixiSceneLayerKind["Region"] = "region";
|
|
14667
|
+
PixiSceneLayerKind["Focus"] = "focus";
|
|
14668
|
+
PixiSceneLayerKind["Preview"] = "preview";
|
|
14669
|
+
PixiSceneLayerKind["Guide"] = "guide";
|
|
14670
|
+
PixiSceneLayerKind["Handle"] = "handle";
|
|
14671
|
+
PixiSceneLayerKind["Interaction"] = "interaction";
|
|
14672
|
+
PixiSceneLayerKind["Label"] = "label";
|
|
14673
|
+
})(PixiSceneLayerKind || (PixiSceneLayerKind = {}));
|
|
14674
|
+
function createPixiSceneLayerSlot(kind, order = defaultLayerOrder(kind)) {
|
|
14675
|
+
let display;
|
|
14676
|
+
return {
|
|
14677
|
+
getDisplay() {
|
|
14678
|
+
return display;
|
|
14679
|
+
},
|
|
14680
|
+
kind,
|
|
14681
|
+
order,
|
|
14682
|
+
setDisplay(nextDisplay) {
|
|
14683
|
+
display = nextDisplay;
|
|
14684
|
+
},
|
|
14685
|
+
};
|
|
14686
|
+
}
|
|
14687
|
+
function syncPixiSceneLayerChildren(scene, slots) {
|
|
14688
|
+
if (!scene) {
|
|
14689
|
+
return;
|
|
14690
|
+
}
|
|
14691
|
+
const displays = [...slots]
|
|
14692
|
+
.sort((left, right) => left.order - right.order)
|
|
14693
|
+
.map((slot) => slot.getDisplay())
|
|
14694
|
+
.filter((display) => display !== undefined);
|
|
14695
|
+
clearSceneChildren(scene);
|
|
14696
|
+
if (displays.length === 0) {
|
|
14697
|
+
return;
|
|
14698
|
+
}
|
|
14699
|
+
scene.addChild(...displays);
|
|
14700
|
+
}
|
|
14701
|
+
function defaultLayerOrder(kind) {
|
|
14702
|
+
switch (kind) {
|
|
14703
|
+
case PixiSceneLayerKind.Media:
|
|
14704
|
+
return 0;
|
|
14705
|
+
case PixiSceneLayerKind.Focus:
|
|
14706
|
+
return 10;
|
|
14707
|
+
case PixiSceneLayerKind.Depth:
|
|
14708
|
+
return 25;
|
|
14709
|
+
case PixiSceneLayerKind.Heatmap:
|
|
14710
|
+
return 50;
|
|
14711
|
+
case PixiSceneLayerKind.Mask:
|
|
14712
|
+
return 100;
|
|
14713
|
+
case PixiSceneLayerKind.Box:
|
|
14714
|
+
return 200;
|
|
14715
|
+
case PixiSceneLayerKind.Vector:
|
|
14716
|
+
return 250;
|
|
14717
|
+
case PixiSceneLayerKind.Region:
|
|
14718
|
+
return 275;
|
|
14719
|
+
case PixiSceneLayerKind.Preview:
|
|
14720
|
+
return 400;
|
|
14721
|
+
case PixiSceneLayerKind.Guide:
|
|
14722
|
+
return 500;
|
|
14723
|
+
case PixiSceneLayerKind.Handle:
|
|
14724
|
+
return 750;
|
|
14725
|
+
case PixiSceneLayerKind.Interaction:
|
|
14726
|
+
return 700;
|
|
14727
|
+
case PixiSceneLayerKind.Label:
|
|
14728
|
+
return 800;
|
|
14729
|
+
default:
|
|
14730
|
+
return 350;
|
|
14731
|
+
}
|
|
14732
|
+
}
|
|
14733
|
+
function clearSceneChildren(scene) {
|
|
14734
|
+
if (typeof scene.removeChildren === "function") {
|
|
14735
|
+
scene.removeChildren();
|
|
14736
|
+
return;
|
|
14737
|
+
}
|
|
14738
|
+
const fakeScene = scene;
|
|
14739
|
+
if (Array.isArray(fakeScene.children)) {
|
|
14740
|
+
fakeScene.children.splice(0);
|
|
14741
|
+
}
|
|
14742
|
+
}
|
|
14743
|
+
|
|
14744
|
+
/** Keeps media pixels outside AA while retaining the scene's layer order. */
|
|
14745
|
+
function createPixiAnnotationAntialiasLayer(options) {
|
|
14746
|
+
const groups = [];
|
|
14747
|
+
let filter;
|
|
14748
|
+
let captureResolution = 1;
|
|
14749
|
+
return {
|
|
14750
|
+
getFxaaFilter() {
|
|
14751
|
+
return options.getEnabled() ? ensureFilter() : null;
|
|
14752
|
+
},
|
|
14753
|
+
setResolution(resolution) {
|
|
14754
|
+
captureResolution = resolution;
|
|
14755
|
+
if (filter)
|
|
14756
|
+
filter.resolution = resolution;
|
|
14757
|
+
},
|
|
14758
|
+
sync(scene, slots) {
|
|
14759
|
+
if (!scene)
|
|
14760
|
+
return;
|
|
14761
|
+
for (const group of groups) {
|
|
14762
|
+
group.removeChildren();
|
|
14763
|
+
group.filters = null;
|
|
14764
|
+
}
|
|
14765
|
+
if (!options.getEnabled()) {
|
|
14766
|
+
syncPixiSceneLayerChildren(scene, slots);
|
|
14767
|
+
return;
|
|
14768
|
+
}
|
|
14769
|
+
scene.removeChildren();
|
|
14770
|
+
let index = 0;
|
|
14771
|
+
let group;
|
|
14772
|
+
for (const slot of [...slots].sort((left, right) => left.order - right.order)) {
|
|
14773
|
+
const display = slot.getDisplay();
|
|
14774
|
+
if (!display)
|
|
14775
|
+
continue;
|
|
14776
|
+
if (slot.kind === PixiSceneLayerKind.Media ||
|
|
14777
|
+
slot.kind === PixiSceneLayerKind.Region ||
|
|
14778
|
+
slot.kind === PixiSceneLayerKind.Label) {
|
|
14779
|
+
// Regions smooth coverage separately from their copied video.
|
|
14780
|
+
// Labels capture backgrounds separately so text stays sharp.
|
|
14781
|
+
group = undefined;
|
|
14782
|
+
scene.addChild(display);
|
|
14783
|
+
continue;
|
|
14784
|
+
}
|
|
14785
|
+
if (!group) {
|
|
14786
|
+
const sharedFilter = ensureFilter();
|
|
14787
|
+
group = groups[index] ??= new options.Container();
|
|
14788
|
+
index += 1;
|
|
14789
|
+
group.filters = [sharedFilter];
|
|
14790
|
+
scene.addChild(group);
|
|
14791
|
+
}
|
|
14792
|
+
group.addChild(display);
|
|
14793
|
+
}
|
|
14794
|
+
},
|
|
14795
|
+
destroy() {
|
|
14796
|
+
for (const group of groups) {
|
|
14797
|
+
group.filters = null;
|
|
14798
|
+
if (!group.parent)
|
|
14799
|
+
group.destroy({ children: false });
|
|
14800
|
+
}
|
|
14801
|
+
filter?.destroy();
|
|
14802
|
+
},
|
|
14803
|
+
};
|
|
14804
|
+
function ensureFilter() {
|
|
14805
|
+
return (filter ??= createPixiAnnotationAntialiasFilter({
|
|
14806
|
+
...options,
|
|
14807
|
+
resolution: captureResolution,
|
|
14808
|
+
}));
|
|
14809
|
+
}
|
|
14810
|
+
}
|
|
14811
|
+
|
|
13553
14812
|
const DEFAULT_PICK_PADDING = 4;
|
|
13554
14813
|
const POINT_PRECISE_PICK_TARGETS = new Set([
|
|
13555
14814
|
DetectionPickTarget.Keypoint,
|
|
@@ -14119,7 +15378,7 @@ function createPixiMaskScreenStrokeRenderer(options) {
|
|
|
14119
15378
|
let strokes = frame.screenStrokes ?? [];
|
|
14120
15379
|
if (overrides) {
|
|
14121
15380
|
strokes = overrides.flatMap(({ detectionIndex, stroke }) => {
|
|
14122
|
-
if (stroke?.widthUnit
|
|
15381
|
+
if (stroke?.widthUnit === undefined ||
|
|
14123
15382
|
stroke.width <= 0 ||
|
|
14124
15383
|
stroke.alpha <= 0)
|
|
14125
15384
|
return [];
|
|
@@ -14141,21 +15400,36 @@ function createPixiMaskScreenStrokeRenderer(options) {
|
|
|
14141
15400
|
hide();
|
|
14142
15401
|
return;
|
|
14143
15402
|
}
|
|
14144
|
-
const signature = `${viewportScale}:${strokes.map(({ detectionIndex, width, color, alpha }) => `${detectionIndex}:${width}:${color}:${alpha}`).join(";")}`;
|
|
15403
|
+
const signature = `${viewportScale}:${strokes.map(({ detectionIndex, width, widthUnit, alignment, color, alpha }) => `${detectionIndex}:${width}:${widthUnit}:${alignment}:${color}:${alpha}`).join(";")}`;
|
|
14145
15404
|
if (lastFrame !== frame || lastSignature !== signature) {
|
|
14146
15405
|
display.clear();
|
|
14147
15406
|
const scaleX = options.mediaWidth / frame.width;
|
|
14148
15407
|
const scaleY = options.mediaHeight / frame.height;
|
|
14149
15408
|
for (const stroke of strokes) {
|
|
14150
|
-
for (const
|
|
14151
|
-
|
|
14152
|
-
|
|
14153
|
-
|
|
14154
|
-
|
|
14155
|
-
|
|
14156
|
-
|
|
15409
|
+
for (const { paths, alignment } of resolveContourGroups(stroke)) {
|
|
15410
|
+
for (const path of paths) {
|
|
15411
|
+
if (path.length < 6)
|
|
15412
|
+
continue;
|
|
15413
|
+
display.moveTo(path[0] * scaleX, path[1] * scaleY);
|
|
15414
|
+
for (let index = 2; index < path.length; index += 2)
|
|
15415
|
+
display.lineTo(path[index] * scaleX, path[index + 1] * scaleY);
|
|
15416
|
+
display.closePath();
|
|
15417
|
+
}
|
|
15418
|
+
display.stroke(resolvePixiStroke({
|
|
15419
|
+
...stroke,
|
|
15420
|
+
alignment,
|
|
15421
|
+
width: stroke.widthUnit === "mask"
|
|
15422
|
+
? (2 *
|
|
15423
|
+
stroke.width *
|
|
15424
|
+
options.mediaWidth *
|
|
15425
|
+
viewportScale) /
|
|
15426
|
+
(frame.kind === PreparedMaskFrameKind.IdMask
|
|
15427
|
+
? frame.sourceWidth
|
|
15428
|
+
: frame.width)
|
|
15429
|
+
: stroke.width,
|
|
15430
|
+
join: "round",
|
|
15431
|
+
}, viewportScale));
|
|
14157
15432
|
}
|
|
14158
|
-
display.stroke(resolvePixiStroke({ ...stroke, join: "round" }, viewportScale));
|
|
14159
15433
|
}
|
|
14160
15434
|
lastFrame = frame;
|
|
14161
15435
|
lastSignature = signature;
|
|
@@ -14164,22 +15438,56 @@ function createPixiMaskScreenStrokeRenderer(options) {
|
|
|
14164
15438
|
},
|
|
14165
15439
|
};
|
|
14166
15440
|
}
|
|
15441
|
+
function resolveContourGroups(stroke) {
|
|
15442
|
+
if (stroke.alignment === undefined ||
|
|
15443
|
+
stroke.alignment === StrokeAlignment.Center) {
|
|
15444
|
+
return [{ paths: stroke.paths, alignment: stroke.alignment }];
|
|
15445
|
+
}
|
|
15446
|
+
const outer = [];
|
|
15447
|
+
const holes = [];
|
|
15448
|
+
// Pixi aligns each contour to its enclosed area. Hole alignment must reverse
|
|
15449
|
+
// so Inside and Outside remain relative to the mask's foreground pixels.
|
|
15450
|
+
for (const path of stroke.paths) {
|
|
15451
|
+
let area = 0;
|
|
15452
|
+
for (let index = 0, previous = path.length - 2; index < path.length; index += 2) {
|
|
15453
|
+
area +=
|
|
15454
|
+
path[previous] * path[index + 1] - path[index] * path[previous + 1];
|
|
15455
|
+
previous = index;
|
|
15456
|
+
}
|
|
15457
|
+
(area < 0 ? holes : outer).push(path);
|
|
15458
|
+
}
|
|
15459
|
+
return [
|
|
15460
|
+
{ paths: outer, alignment: stroke.alignment },
|
|
15461
|
+
{
|
|
15462
|
+
paths: holes,
|
|
15463
|
+
alignment: stroke.alignment === StrokeAlignment.Outside
|
|
15464
|
+
? StrokeAlignment.Inside
|
|
15465
|
+
: StrokeAlignment.Outside,
|
|
15466
|
+
},
|
|
15467
|
+
].filter(({ paths }) => paths.length > 0);
|
|
15468
|
+
}
|
|
14167
15469
|
|
|
14168
15470
|
const coverageBody = `
|
|
14169
|
-
return clamp(0.5 + (width - distance) / pixelWidth, 0.0, 1.0)
|
|
14170
|
-
- clamp(0.5 - (width + distance) / pixelWidth, 0.0, 1.0);
|
|
15471
|
+
return clamp(0.5 + (width * (1.0 - alignment) - distance) / pixelWidth, 0.0, 1.0)
|
|
15472
|
+
- clamp(0.5 - (width * alignment + distance) / pixelWidth, 0.0, 1.0);
|
|
14171
15473
|
`;
|
|
14172
15474
|
const distanceBody = `
|
|
14173
15475
|
return max(max(offset - position, position - offset - 1.0), 0.0);
|
|
14174
15476
|
`;
|
|
14175
15477
|
const radiusBody = `
|
|
14176
|
-
if (
|
|
14177
|
-
return
|
|
15478
|
+
if (maxWidth < 0.0) {
|
|
15479
|
+
return 0.0;
|
|
14178
15480
|
}
|
|
14179
|
-
return min(
|
|
15481
|
+
return min(ceil(maxWidth + pixelWidth * 0.5), ${MAX_ID_MASK_STROKE_WIDTH + 1}.0);
|
|
14180
15482
|
`;
|
|
14181
15483
|
const idMaskStrokeCoverageGlsl = `
|
|
14182
|
-
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) {
|
|
14183
15491
|
${coverageBody}
|
|
14184
15492
|
}
|
|
14185
15493
|
|
|
@@ -14187,42 +15495,55 @@ float neighborCellDistance(float offset, float position) {
|
|
|
14187
15495
|
${distanceBody}
|
|
14188
15496
|
}
|
|
14189
15497
|
|
|
14190
|
-
float strokeScanRadius(float maxWidth, float
|
|
15498
|
+
float strokeScanRadius(float maxWidth, float pixelWidth) {
|
|
14191
15499
|
${radiusBody}
|
|
14192
15500
|
}
|
|
14193
15501
|
|
|
14194
|
-
|
|
14195
|
-
|
|
14196
|
-
|
|
14197
|
-
|
|
14198
|
-
differs(sampleMaskId(vUV + vec2(0.0, texel.y)), centerId) ||
|
|
14199
|
-
differs(sampleMaskId(vUV + vec2(0.0, -texel.y)), centerId);
|
|
15502
|
+
float strokeWidthInTexels(float maskId, float pixelWidth) {
|
|
15503
|
+
float alignment = readStrokeAlignment(maskId);
|
|
15504
|
+
float sideFraction = max(alignment, 1.0 - alignment);
|
|
15505
|
+
return min(readStrokeWidth(maskId) * pixelWidth * uStrokePixelRatio, ${MAX_ID_MASK_STROKE_WIDTH}.0 / sideFraction);
|
|
14200
15506
|
}
|
|
14201
15507
|
|
|
14202
|
-
float innerStrokeCoverage(float centerId,
|
|
14203
|
-
if (width
|
|
14204
|
-
|
|
14205
|
-
|
|
15508
|
+
float innerStrokeCoverage(float centerId, ivec2 sourceCell, vec2 cell, float width, float pixelWidth) {
|
|
15509
|
+
if (width <= 0.0) return 0.0;
|
|
15510
|
+
float alignment = readStrokeAlignment(centerId);
|
|
15511
|
+
int radius = int(strokeScanRadius(width * alignment, pixelWidth));
|
|
15512
|
+
float nearestDistance = width * alignment + pixelWidth * 0.5;
|
|
15513
|
+
float minimumDistance = min(min(cell.x, 1.0 - cell.x), min(cell.y, 1.0 - cell.y));
|
|
14206
15514
|
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
|
|
14210
|
-
|
|
14211
|
-
|
|
14212
|
-
|
|
14213
|
-
|
|
14214
|
-
|
|
14215
|
-
|
|
14216
|
-
|
|
14217
|
-
|
|
14218
|
-
|
|
15515
|
+
for (int offsetY = radius; offsetY >= -radius; offsetY -= 1) {
|
|
15516
|
+
for (int offsetX = radius; offsetX >= -radius; offsetX -= 1) {
|
|
15517
|
+
if (offsetX == 0 && offsetY == 0) {
|
|
15518
|
+
continue;
|
|
15519
|
+
}
|
|
15520
|
+
float distance = max(
|
|
15521
|
+
neighborCellDistance(float(offsetX), cell.x),
|
|
15522
|
+
neighborCellDistance(float(offsetY), cell.y)
|
|
15523
|
+
);
|
|
15524
|
+
if (distance >= nearestDistance) {
|
|
15525
|
+
continue;
|
|
15526
|
+
}
|
|
15527
|
+
float maskId = sampleMaskIdCell(sourceCell + ivec2(offsetX, offsetY));
|
|
15528
|
+
if (differs(maskId, centerId)) {
|
|
15529
|
+
nearestDistance = distance;
|
|
15530
|
+
if (distance <= minimumDistance) {
|
|
15531
|
+
return subtexelStrokeCoverage(-distance, width, alignment, pixelWidth);
|
|
15532
|
+
}
|
|
15533
|
+
}
|
|
15534
|
+
}
|
|
14219
15535
|
}
|
|
14220
|
-
return
|
|
15536
|
+
return subtexelStrokeCoverage(-nearestDistance, width, alignment, pixelWidth);
|
|
14221
15537
|
}
|
|
14222
15538
|
|
|
14223
15539
|
// Descending offsets give the outer border its palette priority.
|
|
14224
|
-
vec2 findNeighborStroke(float centerId,
|
|
14225
|
-
|
|
15540
|
+
vec2 findNeighborStroke(float centerId, ivec2 sourceCell, vec2 cell, float pixelWidth) {
|
|
15541
|
+
float maxWidth = min(uMaxStrokeWidth * pixelWidth * uStrokePixelRatio, ${MAX_ID_MASK_STROKE_WIDTH}.0);
|
|
15542
|
+
int radius = int(strokeScanRadius(maxWidth, pixelWidth));
|
|
15543
|
+
vec2 stroke = vec2(0.0);
|
|
15544
|
+
float nearestDistance = maxWidth + pixelWidth * 0.5;
|
|
15545
|
+
float minimumDistance = min(min(cell.x, 1.0 - cell.x), min(cell.y, 1.0 - cell.y));
|
|
15546
|
+
if (uBorderEnabled <= 0.5) return stroke;
|
|
14226
15547
|
|
|
14227
15548
|
for (int offsetY = radius; offsetY >= -radius; offsetY -= 1) {
|
|
14228
15549
|
for (int offsetX = radius; offsetX >= -radius; offsetX -= 1) {
|
|
@@ -14230,35 +15551,44 @@ vec2 findNeighborStroke(float centerId, vec2 texel, vec2 cell, float pixelWidth)
|
|
|
14230
15551
|
continue;
|
|
14231
15552
|
}
|
|
14232
15553
|
|
|
14233
|
-
float
|
|
14234
|
-
|
|
15554
|
+
float distance = max(
|
|
15555
|
+
neighborCellDistance(float(offsetX), cell.x),
|
|
15556
|
+
neighborCellDistance(float(offsetY), cell.y)
|
|
15557
|
+
);
|
|
15558
|
+
if (distance >= nearestDistance) {
|
|
15559
|
+
continue;
|
|
15560
|
+
}
|
|
15561
|
+
float maskId = sampleMaskIdCell(sourceCell + ivec2(offsetX, offsetY));
|
|
15562
|
+
if (maskId < 0.5 || !differs(maskId, centerId) ||
|
|
15563
|
+
(stroke.x > 0.5 && differs(maskId, stroke.x))) {
|
|
14235
15564
|
continue;
|
|
14236
15565
|
}
|
|
14237
15566
|
|
|
14238
|
-
float width =
|
|
14239
|
-
if (width
|
|
14240
|
-
float
|
|
14241
|
-
if (width >= distance && readStroke(maskId).a > 0.0) {
|
|
14242
|
-
return vec2(maskId, 1.0);
|
|
14243
|
-
}
|
|
14244
|
-
} else if (width > 0.0 && readStroke(maskId).a > 0.0) {
|
|
14245
|
-
float distance = max(
|
|
14246
|
-
neighborCellDistance(float(offsetX), cell.x),
|
|
14247
|
-
neighborCellDistance(float(offsetY), cell.y)
|
|
14248
|
-
);
|
|
14249
|
-
float coverage = subtexelStrokeCoverage(distance, width, pixelWidth);
|
|
15567
|
+
float width = strokeWidthInTexels(maskId, pixelWidth);
|
|
15568
|
+
if (width > 0.0 && readStroke(maskId).a > 0.0) {
|
|
15569
|
+
float coverage = subtexelStrokeCoverage(distance, width, readStrokeAlignment(maskId), pixelWidth);
|
|
14250
15570
|
if (coverage > 0.0) {
|
|
14251
|
-
|
|
15571
|
+
stroke = vec2(maskId, coverage);
|
|
15572
|
+
nearestDistance = distance;
|
|
15573
|
+
if (distance <= minimumDistance) {
|
|
15574
|
+
return stroke;
|
|
15575
|
+
}
|
|
14252
15576
|
}
|
|
14253
15577
|
}
|
|
14254
15578
|
}
|
|
14255
15579
|
}
|
|
14256
15580
|
|
|
14257
|
-
return
|
|
15581
|
+
return stroke;
|
|
14258
15582
|
}
|
|
14259
15583
|
`;
|
|
14260
15584
|
const idMaskStrokeCoverageWgsl = `
|
|
14261
|
-
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 {
|
|
14262
15592
|
${coverageBody}
|
|
14263
15593
|
}
|
|
14264
15594
|
|
|
@@ -14266,45 +15596,55 @@ fn neighborCellDistance(offset: f32, position: f32) -> f32 {
|
|
|
14266
15596
|
${distanceBody}
|
|
14267
15597
|
}
|
|
14268
15598
|
|
|
14269
|
-
fn strokeScanRadius(maxWidth: f32,
|
|
15599
|
+
fn strokeScanRadius(maxWidth: f32, pixelWidth: f32) -> f32 {
|
|
14270
15600
|
${radiusBody}
|
|
14271
15601
|
}
|
|
14272
15602
|
|
|
14273
|
-
fn
|
|
14274
|
-
|
|
14275
|
-
|
|
14276
|
-
|
|
14277
|
-
differs(sampleMaskId(uv + vec2<f32>(0.0, texel.y)), centerId) ||
|
|
14278
|
-
differs(sampleMaskId(uv + vec2<f32>(0.0, -texel.y)), centerId);
|
|
15603
|
+
fn strokeWidthInTexels(maskId: f32, pixelWidth: f32) -> f32 {
|
|
15604
|
+
let alignment = readStrokeAlignment(maskId);
|
|
15605
|
+
let sideFraction = max(alignment, 1.0 - alignment);
|
|
15606
|
+
return min(readStrokeWidth(maskId) * pixelWidth * maskUniforms.uStrokePixelRatio, ${MAX_ID_MASK_STROKE_WIDTH}.0 / sideFraction);
|
|
14279
15607
|
}
|
|
14280
15608
|
|
|
14281
|
-
fn innerStrokeCoverage(
|
|
14282
|
-
if (width
|
|
14283
|
-
|
|
14284
|
-
|
|
14285
|
-
|
|
14286
|
-
|
|
14287
|
-
}
|
|
15609
|
+
fn innerStrokeCoverage(centerId: f32, sourceCell: vec2<i32>, cell: vec2<f32>, width: f32, pixelWidth: f32) -> f32 {
|
|
15610
|
+
if (width <= 0.0) { return 0.0; }
|
|
15611
|
+
let alignment = readStrokeAlignment(centerId);
|
|
15612
|
+
let radius = i32(strokeScanRadius(width * alignment, pixelWidth));
|
|
15613
|
+
var nearestDistance = width * alignment + pixelWidth * 0.5;
|
|
15614
|
+
let minimumDistance = min(min(cell.x, 1.0 - cell.x), min(cell.y, 1.0 - cell.y));
|
|
14288
15615
|
|
|
14289
|
-
var
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14297
|
-
|
|
14298
|
-
|
|
14299
|
-
|
|
14300
|
-
|
|
15616
|
+
for (var offsetY = radius; offsetY >= -radius; offsetY -= 1) {
|
|
15617
|
+
for (var offsetX = radius; offsetX >= -radius; offsetX -= 1) {
|
|
15618
|
+
if (offsetX == 0 && offsetY == 0) {
|
|
15619
|
+
continue;
|
|
15620
|
+
}
|
|
15621
|
+
let distance = max(
|
|
15622
|
+
neighborCellDistance(f32(offsetX), cell.x),
|
|
15623
|
+
neighborCellDistance(f32(offsetY), cell.y)
|
|
15624
|
+
);
|
|
15625
|
+
if (distance >= nearestDistance) {
|
|
15626
|
+
continue;
|
|
15627
|
+
}
|
|
15628
|
+
let maskId = sampleMaskIdCell(sourceCell + vec2<i32>(offsetX, offsetY));
|
|
15629
|
+
if (differs(maskId, centerId)) {
|
|
15630
|
+
nearestDistance = distance;
|
|
15631
|
+
if (distance <= minimumDistance) {
|
|
15632
|
+
return subtexelStrokeCoverage(-distance, width, alignment, pixelWidth);
|
|
15633
|
+
}
|
|
15634
|
+
}
|
|
15635
|
+
}
|
|
14301
15636
|
}
|
|
14302
|
-
return
|
|
15637
|
+
return subtexelStrokeCoverage(-nearestDistance, width, alignment, pixelWidth);
|
|
14303
15638
|
}
|
|
14304
15639
|
|
|
14305
15640
|
// Descending offsets give the outer border its palette priority.
|
|
14306
|
-
fn findNeighborStroke(
|
|
14307
|
-
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; }
|
|
14308
15648
|
|
|
14309
15649
|
for (var offsetY = radius; offsetY >= -radius; offsetY -= 1) {
|
|
14310
15650
|
for (var offsetX = radius; offsetX >= -radius; offsetX -= 1) {
|
|
@@ -14312,45 +15652,44 @@ fn findNeighborStroke(uv: vec2<f32>, centerId: f32, texel: vec2<f32>, cell: vec2
|
|
|
14312
15652
|
continue;
|
|
14313
15653
|
}
|
|
14314
15654
|
|
|
14315
|
-
let
|
|
14316
|
-
|
|
15655
|
+
let distance = max(
|
|
15656
|
+
neighborCellDistance(f32(offsetX), cell.x),
|
|
15657
|
+
neighborCellDistance(f32(offsetY), cell.y)
|
|
15658
|
+
);
|
|
15659
|
+
if (distance >= nearestDistance) {
|
|
15660
|
+
continue;
|
|
15661
|
+
}
|
|
15662
|
+
let maskId = sampleMaskIdCell(sourceCell + vec2<i32>(offsetX, offsetY));
|
|
15663
|
+
if (maskId < 0.5 || !differs(maskId, centerId) ||
|
|
15664
|
+
(stroke.x > 0.5 && differs(maskId, stroke.x))) {
|
|
14317
15665
|
continue;
|
|
14318
15666
|
}
|
|
14319
15667
|
|
|
14320
|
-
let width =
|
|
14321
|
-
if (width
|
|
14322
|
-
let
|
|
14323
|
-
if (width >= distance && readStroke(maskId).a > 0.0) {
|
|
14324
|
-
return vec2<f32>(maskId, 1.0);
|
|
14325
|
-
}
|
|
14326
|
-
} else if (width > 0.0 && readStroke(maskId).a > 0.0) {
|
|
14327
|
-
let distance = max(
|
|
14328
|
-
neighborCellDistance(f32(offsetX), cell.x),
|
|
14329
|
-
neighborCellDistance(f32(offsetY), cell.y)
|
|
14330
|
-
);
|
|
14331
|
-
let coverage = subtexelStrokeCoverage(distance, width, pixelWidth);
|
|
15668
|
+
let width = strokeWidthInTexels(maskId, pixelWidth);
|
|
15669
|
+
if (width > 0.0 && readStroke(maskId).a > 0.0) {
|
|
15670
|
+
let coverage = subtexelStrokeCoverage(distance, width, readStrokeAlignment(maskId), pixelWidth);
|
|
14332
15671
|
if (coverage > 0.0) {
|
|
14333
|
-
|
|
15672
|
+
stroke = vec2<f32>(maskId, coverage);
|
|
15673
|
+
nearestDistance = distance;
|
|
15674
|
+
if (distance <= minimumDistance) {
|
|
15675
|
+
return stroke;
|
|
15676
|
+
}
|
|
14334
15677
|
}
|
|
14335
15678
|
}
|
|
14336
15679
|
}
|
|
14337
15680
|
}
|
|
14338
15681
|
|
|
14339
|
-
return
|
|
15682
|
+
return stroke;
|
|
14340
15683
|
}
|
|
14341
15684
|
`;
|
|
14342
15685
|
|
|
14343
|
-
/**
|
|
14344
|
-
* GLSL charges a uniform array of scalars one uniform vector per element, the
|
|
14345
|
-
* same as an array of vec4, so the stroke widths ride four to a vector and an
|
|
14346
|
-
* entry costs 2.25 vectors instead of 3. The lanes only line up while the entry
|
|
14347
|
-
* count stays a multiple of four.
|
|
14348
|
-
*/
|
|
15686
|
+
/** Packs width and alignment scalars within the fragment uniform budget. */
|
|
14349
15687
|
const ID_MASK_STROKE_WIDTH_LANES = MAX_ID_MASK_PALETTE_ENTRIES / 4;
|
|
14350
15688
|
const idMaskPaletteGlsl = `
|
|
14351
15689
|
uniform vec4 uFillPalette[${MAX_ID_MASK_PALETTE_ENTRIES}];
|
|
14352
15690
|
uniform vec4 uStrokePalette[${MAX_ID_MASK_PALETTE_ENTRIES}];
|
|
14353
15691
|
uniform vec4 uStrokeWidths[${ID_MASK_STROKE_WIDTH_LANES}];
|
|
15692
|
+
uniform vec4 uStrokeAlignments[${ID_MASK_STROKE_WIDTH_LANES}];
|
|
14354
15693
|
|
|
14355
15694
|
int paletteIndex(float maskId) {
|
|
14356
15695
|
return int(clamp(maskId, 0.0, float(${MAX_ID_MASK_PALETTE_ENTRIES - 1})));
|
|
@@ -14369,10 +15708,16 @@ float readStrokeWidth(float maskId) {
|
|
|
14369
15708
|
|
|
14370
15709
|
return uStrokeWidths[index / 4][index % 4];
|
|
14371
15710
|
}
|
|
15711
|
+
|
|
15712
|
+
float readStrokeAlignment(float maskId) {
|
|
15713
|
+
int index = paletteIndex(maskId);
|
|
15714
|
+
return uStrokeAlignments[index / 4][index % 4];
|
|
15715
|
+
}
|
|
14372
15716
|
`;
|
|
14373
15717
|
const idMaskFillPaletteWgslField = `uFillPalette: array<vec4<f32>, ${MAX_ID_MASK_PALETTE_ENTRIES}>,`;
|
|
14374
15718
|
const idMaskStrokePaletteWgslField = `uStrokePalette: array<vec4<f32>, ${MAX_ID_MASK_PALETTE_ENTRIES}>,`;
|
|
14375
15719
|
const idMaskStrokeWidthsWgslField = `uStrokeWidths: array<vec4<f32>, ${ID_MASK_STROKE_WIDTH_LANES}>,`;
|
|
15720
|
+
const idMaskStrokeAlignmentsWgslField = `uStrokeAlignments: array<vec4<f32>, ${ID_MASK_STROKE_WIDTH_LANES}>,`;
|
|
14376
15721
|
/** Reads the uniform group a fragment stage must bind under the name maskUniforms. */
|
|
14377
15722
|
const idMaskPaletteWgsl = `
|
|
14378
15723
|
fn paletteIndex(maskId: f32) -> i32 {
|
|
@@ -14392,20 +15737,31 @@ fn readStrokeWidth(maskId: f32) -> f32 {
|
|
|
14392
15737
|
|
|
14393
15738
|
return maskUniforms.uStrokeWidths[index / 4][index % 4];
|
|
14394
15739
|
}
|
|
15740
|
+
|
|
15741
|
+
fn readStrokeAlignment(maskId: f32) -> f32 {
|
|
15742
|
+
let index = paletteIndex(maskId);
|
|
15743
|
+
return maskUniforms.uStrokeAlignments[index / 4][index % 4];
|
|
15744
|
+
}
|
|
14395
15745
|
`;
|
|
14396
15746
|
|
|
14397
|
-
function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, labelStyle, resolveContextState, }) {
|
|
15747
|
+
function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, labelStyle, getRenderedBoxes, resolveContextState, }) {
|
|
14398
15748
|
const entries = [];
|
|
14399
15749
|
let container;
|
|
14400
15750
|
let currentLabelStyle = labelStyle;
|
|
14401
15751
|
// A versioned source can replace a frame without changing its media time or
|
|
14402
15752
|
// frame index. Buffered frames are immutable snapshots, so retain by object.
|
|
14403
15753
|
let lastFrame;
|
|
15754
|
+
let lastBoxFrame;
|
|
14404
15755
|
let styleVersion = 0;
|
|
14405
15756
|
let drawnStyleVersion = -1;
|
|
14406
15757
|
let lastViewportScale = 0;
|
|
14407
15758
|
let hitRects = [];
|
|
14408
15759
|
let previewEntry;
|
|
15760
|
+
let previewIndex = 0;
|
|
15761
|
+
let backgroundFilter = null;
|
|
15762
|
+
let backgroundFilters = null;
|
|
15763
|
+
let backgroundsGrouped = false;
|
|
15764
|
+
const backgroundGroups = [];
|
|
14409
15765
|
const entriesByDetectionKey = new Map();
|
|
14410
15766
|
const boundsByDetectionKey = new Map();
|
|
14411
15767
|
const clearLayout = () => {
|
|
@@ -14417,6 +15773,7 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14417
15773
|
for (let index = startIndex; index < entries.length; index += 1) {
|
|
14418
15774
|
entries[index].background.visible = false;
|
|
14419
15775
|
entries[index].label.visible = false;
|
|
15776
|
+
entries[index].instruction = undefined;
|
|
14420
15777
|
}
|
|
14421
15778
|
};
|
|
14422
15779
|
const createEntry = () => {
|
|
@@ -14433,6 +15790,9 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14433
15790
|
labelBaseY: 0,
|
|
14434
15791
|
text: null,
|
|
14435
15792
|
textStyleKey: null,
|
|
15793
|
+
textHeight: 0,
|
|
15794
|
+
textWidth: 0,
|
|
15795
|
+
instruction: undefined,
|
|
14436
15796
|
};
|
|
14437
15797
|
container?.addChild(entry.background, entry.label);
|
|
14438
15798
|
return entry;
|
|
@@ -14450,8 +15810,69 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14450
15810
|
return;
|
|
14451
15811
|
previewEntry.background.visible = false;
|
|
14452
15812
|
previewEntry.label.visible = false;
|
|
15813
|
+
previewEntry.instruction = undefined;
|
|
15814
|
+
};
|
|
15815
|
+
const syncBackgroundCaptures = () => {
|
|
15816
|
+
if (!container || (!backgroundFilter && !backgroundsGrouped))
|
|
15817
|
+
return;
|
|
15818
|
+
for (const group of backgroundGroups) {
|
|
15819
|
+
group.removeChildren();
|
|
15820
|
+
group.filters = null;
|
|
15821
|
+
}
|
|
15822
|
+
container.removeChildren();
|
|
15823
|
+
backgroundsGrouped = backgroundFilter !== null;
|
|
15824
|
+
const orderedEntries = previewEntry
|
|
15825
|
+
? [
|
|
15826
|
+
...entries.slice(0, previewIndex),
|
|
15827
|
+
previewEntry,
|
|
15828
|
+
...entries.slice(previewIndex),
|
|
15829
|
+
]
|
|
15830
|
+
: entries;
|
|
15831
|
+
if (!backgroundFilter) {
|
|
15832
|
+
for (const entry of orderedEntries) {
|
|
15833
|
+
container.addChild(entry.background, entry.label);
|
|
15834
|
+
}
|
|
15835
|
+
return;
|
|
15836
|
+
}
|
|
15837
|
+
let groupIndex = 0;
|
|
15838
|
+
const run = [];
|
|
15839
|
+
let runBounds;
|
|
15840
|
+
const padding = backgroundFilter.padding / Math.max(lastViewportScale, 1e-6);
|
|
15841
|
+
const flushRun = () => {
|
|
15842
|
+
if (run.length === 0)
|
|
15843
|
+
return;
|
|
15844
|
+
if (run.some((entry) => entry.background.visible)) {
|
|
15845
|
+
const group = (backgroundGroups[groupIndex] ??= new Container());
|
|
15846
|
+
groupIndex += 1;
|
|
15847
|
+
group.label = "label-background-aa";
|
|
15848
|
+
group.filters = backgroundFilters;
|
|
15849
|
+
container.addChild(group);
|
|
15850
|
+
for (const entry of run)
|
|
15851
|
+
group.addChild(entry.background);
|
|
15852
|
+
}
|
|
15853
|
+
else {
|
|
15854
|
+
for (const entry of run)
|
|
15855
|
+
container.addChild(entry.background);
|
|
15856
|
+
}
|
|
15857
|
+
for (const entry of run)
|
|
15858
|
+
container.addChild(entry.label);
|
|
15859
|
+
run.length = 0;
|
|
15860
|
+
runBounds = undefined;
|
|
15861
|
+
};
|
|
15862
|
+
for (const entry of orderedEntries) {
|
|
15863
|
+
if (entry.background.visible || entry.label.visible) {
|
|
15864
|
+
const padded = resolveCaptureBounds(entry, padding);
|
|
15865
|
+
// Reordering a disjoint run leaves translucent overlapping chips and
|
|
15866
|
+
// their text in the same paint order as the original label pairs.
|
|
15867
|
+
if (runBounds && rectanglesOverlap(runBounds, padded))
|
|
15868
|
+
flushRun();
|
|
15869
|
+
runBounds = runBounds ? unionCaptureBounds(runBounds, padded) : padded;
|
|
15870
|
+
}
|
|
15871
|
+
run.push(entry);
|
|
15872
|
+
}
|
|
15873
|
+
flushRun();
|
|
14453
15874
|
};
|
|
14454
|
-
const redrawFrame = (frame, mediaTime, viewportScale) => {
|
|
15875
|
+
const redrawFrame = (frame, mediaTime, viewportScale, renderedBoxes) => {
|
|
14455
15876
|
let drawnCount = 0;
|
|
14456
15877
|
clearLayout();
|
|
14457
15878
|
if (!currentLabelStyle) {
|
|
@@ -14474,7 +15895,7 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14474
15895
|
continue;
|
|
14475
15896
|
}
|
|
14476
15897
|
const entry = ensureEntry(drawnCount);
|
|
14477
|
-
const hitRect = drawInstruction(entry, instruction, viewportScale);
|
|
15898
|
+
const hitRect = drawInstruction(entry, instruction, viewportScale, renderedBoxes?.get(detectionIndex));
|
|
14478
15899
|
const key = detectionKey$1(detection, detectionIndex);
|
|
14479
15900
|
entriesByDetectionKey.set(key, entry);
|
|
14480
15901
|
boundsByDetectionKey.set(key, {
|
|
@@ -14497,24 +15918,31 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14497
15918
|
drawFrame(mediaTime, viewportScale) {
|
|
14498
15919
|
const resolvedViewportScale = viewportScale ?? 1;
|
|
14499
15920
|
const frame = detectionTimeline.selectFrame(mediaTime);
|
|
15921
|
+
const boxFrame = getRenderedBoxes?.();
|
|
15922
|
+
const matchingBoxFrame = boxFrame?.frame === frame ? boxFrame : undefined;
|
|
14500
15923
|
if (frame === lastFrame &&
|
|
15924
|
+
matchingBoxFrame === lastBoxFrame &&
|
|
14501
15925
|
drawnStyleVersion === styleVersion &&
|
|
14502
15926
|
resolvedViewportScale === lastViewportScale) {
|
|
14503
15927
|
return;
|
|
14504
15928
|
}
|
|
14505
15929
|
lastFrame = frame;
|
|
15930
|
+
lastBoxFrame = matchingBoxFrame;
|
|
14506
15931
|
drawnStyleVersion = styleVersion;
|
|
14507
15932
|
lastViewportScale = resolvedViewportScale;
|
|
14508
15933
|
if (!frame) {
|
|
14509
15934
|
clearLayout();
|
|
14510
15935
|
hideEntriesFrom(0);
|
|
15936
|
+
syncBackgroundCaptures();
|
|
14511
15937
|
return;
|
|
14512
15938
|
}
|
|
14513
|
-
redrawFrame(frame, mediaTime, resolvedViewportScale);
|
|
15939
|
+
redrawFrame(frame, mediaTime, resolvedViewportScale, matchingBoxFrame?.rects);
|
|
15940
|
+
syncBackgroundCaptures();
|
|
14514
15941
|
},
|
|
14515
|
-
drawCreationPreview(detection, mediaTime, viewportScale) {
|
|
15942
|
+
drawCreationPreview(detection, mediaTime, viewportScale, renderedBox) {
|
|
14516
15943
|
if (!detection || !currentLabelStyle) {
|
|
14517
15944
|
hideCreationPreview();
|
|
15945
|
+
syncBackgroundCaptures();
|
|
14518
15946
|
return;
|
|
14519
15947
|
}
|
|
14520
15948
|
const resolvedViewportScale = viewportScale ?? 1;
|
|
@@ -14530,15 +15958,39 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14530
15958
|
});
|
|
14531
15959
|
if (!instruction) {
|
|
14532
15960
|
hideCreationPreview();
|
|
15961
|
+
syncBackgroundCaptures();
|
|
14533
15962
|
return;
|
|
14534
15963
|
}
|
|
14535
|
-
|
|
14536
|
-
|
|
15964
|
+
if (!previewEntry) {
|
|
15965
|
+
previewIndex = entries.length;
|
|
15966
|
+
previewEntry = createEntry();
|
|
15967
|
+
}
|
|
15968
|
+
drawInstruction(previewEntry, instruction, resolvedViewportScale, renderedBox);
|
|
15969
|
+
syncBackgroundCaptures();
|
|
15970
|
+
},
|
|
15971
|
+
updateEditingBox(id, renderedBox) {
|
|
15972
|
+
const key = `id:${String(id)}`;
|
|
15973
|
+
const entry = entriesByDetectionKey.get(key);
|
|
15974
|
+
const bounds = boundsByDetectionKey.get(key);
|
|
15975
|
+
const instruction = entry?.instruction;
|
|
15976
|
+
if (!entry || !bounds || !instruction?.background)
|
|
15977
|
+
return;
|
|
15978
|
+
const corners = resolveRoundedCorners(instruction.background, instruction.placement ?? LabelPlacement.Top, bounds, renderedBox, lastViewportScale);
|
|
15979
|
+
drawBackground(entry, instruction.background, bounds.baseX, bounds.baseY, bounds.width, bounds.height, lastViewportScale, corners);
|
|
15980
|
+
entry.background.x = bounds.x;
|
|
15981
|
+
entry.background.y = bounds.y;
|
|
14537
15982
|
},
|
|
14538
15983
|
setLabelStyle(nextLabelStyle) {
|
|
14539
15984
|
currentLabelStyle = nextLabelStyle ?? undefined;
|
|
14540
15985
|
styleVersion += 1;
|
|
14541
15986
|
},
|
|
15987
|
+
setBackgroundAntialiasFilter(filter) {
|
|
15988
|
+
if (filter === backgroundFilter)
|
|
15989
|
+
return;
|
|
15990
|
+
backgroundFilter = filter;
|
|
15991
|
+
backgroundFilters = filter ? [filter] : null;
|
|
15992
|
+
syncBackgroundCaptures();
|
|
15993
|
+
},
|
|
14542
15994
|
translateDetection(id, x, y) {
|
|
14543
15995
|
const key = `id:${String(id)}`;
|
|
14544
15996
|
const entry = entriesByDetectionKey.get(key);
|
|
@@ -14556,6 +16008,7 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14556
16008
|
y: bounds.baseY + y,
|
|
14557
16009
|
});
|
|
14558
16010
|
}
|
|
16011
|
+
syncBackgroundCaptures();
|
|
14559
16012
|
return true;
|
|
14560
16013
|
},
|
|
14561
16014
|
getDetectionLabelBounds(id) {
|
|
@@ -14592,6 +16045,13 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14592
16045
|
: null;
|
|
14593
16046
|
},
|
|
14594
16047
|
destroy() {
|
|
16048
|
+
for (const group of backgroundGroups) {
|
|
16049
|
+
group.filters = null;
|
|
16050
|
+
if (!group.parent)
|
|
16051
|
+
group.destroy({ children: false });
|
|
16052
|
+
}
|
|
16053
|
+
backgroundFilter = null;
|
|
16054
|
+
backgroundFilters = null;
|
|
14595
16055
|
hideEntriesFrom(0);
|
|
14596
16056
|
hideCreationPreview();
|
|
14597
16057
|
entries.length = 0;
|
|
@@ -14601,7 +16061,8 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
|
|
|
14601
16061
|
},
|
|
14602
16062
|
};
|
|
14603
16063
|
}
|
|
14604
|
-
function drawInstruction(entry, instruction, viewportScale) {
|
|
16064
|
+
function drawInstruction(entry, instruction, viewportScale, renderedBox) {
|
|
16065
|
+
entry.instruction = instruction;
|
|
14605
16066
|
const textStyle = resolveTextStyle(instruction.textStyle, viewportScale);
|
|
14606
16067
|
const textStyleKey = createTextStyleKey(textStyle);
|
|
14607
16068
|
const textAlpha = instruction.textStyle?.alpha ?? 1;
|
|
@@ -14621,8 +16082,10 @@ function drawInstruction(entry, instruction, viewportScale) {
|
|
|
14621
16082
|
const background = instruction.background;
|
|
14622
16083
|
const paddingX = (background?.paddingX ?? 0) / viewportScale;
|
|
14623
16084
|
const paddingY = (background?.paddingY ?? 0) / viewportScale;
|
|
14624
|
-
|
|
14625
|
-
|
|
16085
|
+
entry.textWidth = entry.label.width;
|
|
16086
|
+
entry.textHeight = entry.label.height;
|
|
16087
|
+
const width = entry.textWidth + paddingX * 2;
|
|
16088
|
+
const height = entry.textHeight + paddingY * 2;
|
|
14626
16089
|
const { x, y } = resolveLabelPosition(instruction, width, height);
|
|
14627
16090
|
entry.label.x = x + paddingX;
|
|
14628
16091
|
entry.label.y = y + paddingY;
|
|
@@ -14633,9 +16096,41 @@ function drawInstruction(entry, instruction, viewportScale) {
|
|
|
14633
16096
|
entry.backgroundKey = null;
|
|
14634
16097
|
return { height, width, x, y };
|
|
14635
16098
|
}
|
|
14636
|
-
|
|
16099
|
+
const corners = resolveRoundedCorners(background, instruction.placement ?? LabelPlacement.Top, { x, y, width, height }, renderedBox, viewportScale);
|
|
16100
|
+
drawBackground(entry, background, x, y, width, height, viewportScale, corners);
|
|
14637
16101
|
return { height, width, x, y };
|
|
14638
16102
|
}
|
|
16103
|
+
function resolveCaptureBounds(entry, padding) {
|
|
16104
|
+
const { background, label } = entry;
|
|
16105
|
+
const x = Math.min(background.visible ? background.x : Infinity, label.visible ? label.x : Infinity);
|
|
16106
|
+
const y = Math.min(background.visible ? background.y : Infinity, label.visible ? label.y : Infinity);
|
|
16107
|
+
return {
|
|
16108
|
+
height: Math.max(background.visible ? background.y + entry.backgroundHeight : -Infinity, label.visible ? label.y + entry.textHeight : -Infinity) -
|
|
16109
|
+
y +
|
|
16110
|
+
padding * 2,
|
|
16111
|
+
width: Math.max(background.visible ? background.x + entry.backgroundWidth : -Infinity, label.visible ? label.x + entry.textWidth : -Infinity) -
|
|
16112
|
+
x +
|
|
16113
|
+
padding * 2,
|
|
16114
|
+
x: x - padding,
|
|
16115
|
+
y: y - padding,
|
|
16116
|
+
};
|
|
16117
|
+
}
|
|
16118
|
+
function rectanglesOverlap(left, right) {
|
|
16119
|
+
return (left.x < right.x + right.width &&
|
|
16120
|
+
right.x < left.x + left.width &&
|
|
16121
|
+
left.y < right.y + right.height &&
|
|
16122
|
+
right.y < left.y + left.height);
|
|
16123
|
+
}
|
|
16124
|
+
function unionCaptureBounds(left, right) {
|
|
16125
|
+
const x = Math.min(left.x, right.x);
|
|
16126
|
+
const y = Math.min(left.y, right.y);
|
|
16127
|
+
return {
|
|
16128
|
+
height: Math.max(left.y + left.height, right.y + right.height) - y,
|
|
16129
|
+
width: Math.max(left.x + left.width, right.x + right.width) - x,
|
|
16130
|
+
x,
|
|
16131
|
+
y,
|
|
16132
|
+
};
|
|
16133
|
+
}
|
|
14639
16134
|
function resolveLabelPosition(instruction, width, height) {
|
|
14640
16135
|
const { rect } = instruction;
|
|
14641
16136
|
const offsetX = instruction.offsetX ?? 0;
|
|
@@ -14669,9 +16164,9 @@ function resolveLabelPosition(instruction, width, height) {
|
|
|
14669
16164
|
};
|
|
14670
16165
|
}
|
|
14671
16166
|
}
|
|
14672
|
-
function drawBackground(entry, background, x, y, width, height, viewportScale) {
|
|
16167
|
+
function drawBackground(entry, background, x, y, width, height, viewportScale, corners) {
|
|
14673
16168
|
const graphics = entry.background;
|
|
14674
|
-
const backgroundKey = createBackgroundKey(background);
|
|
16169
|
+
const backgroundKey = createBackgroundKey(background, corners);
|
|
14675
16170
|
graphics.visible = true;
|
|
14676
16171
|
graphics.x = x;
|
|
14677
16172
|
graphics.y = y;
|
|
@@ -14686,8 +16181,8 @@ function drawBackground(entry, background, x, y, width, height, viewportScale) {
|
|
|
14686
16181
|
entry.backgroundWidth = width;
|
|
14687
16182
|
entry.backgroundHeight = height;
|
|
14688
16183
|
graphics.clear();
|
|
14689
|
-
const radius = (background.cornerRadius ?? 0) / viewportScale;
|
|
14690
|
-
if (
|
|
16184
|
+
const radius = Math.min((background.cornerRadius ?? 0) / viewportScale, width / 2, height / 2);
|
|
16185
|
+
if (corners === "top" && radius > 0) {
|
|
14691
16186
|
graphics
|
|
14692
16187
|
.moveTo(0, height)
|
|
14693
16188
|
.lineTo(0, radius)
|
|
@@ -14697,6 +16192,16 @@ function drawBackground(entry, background, x, y, width, height, viewportScale) {
|
|
|
14697
16192
|
.lineTo(width, height)
|
|
14698
16193
|
.closePath();
|
|
14699
16194
|
}
|
|
16195
|
+
else if (corners === "bottom" && radius > 0) {
|
|
16196
|
+
graphics
|
|
16197
|
+
.moveTo(0, 0)
|
|
16198
|
+
.lineTo(width, 0)
|
|
16199
|
+
.lineTo(width, height - radius)
|
|
16200
|
+
.quadraticCurveTo(width, height, width - radius, height)
|
|
16201
|
+
.lineTo(radius, height)
|
|
16202
|
+
.quadraticCurveTo(0, height, 0, height - radius)
|
|
16203
|
+
.closePath();
|
|
16204
|
+
}
|
|
14700
16205
|
else {
|
|
14701
16206
|
graphics.roundRect(0, 0, width, height, radius);
|
|
14702
16207
|
}
|
|
@@ -14721,14 +16226,46 @@ function createTextStyleKey(textStyle) {
|
|
|
14721
16226
|
textStyle.fontWeight,
|
|
14722
16227
|
].join(":");
|
|
14723
16228
|
}
|
|
14724
|
-
function
|
|
16229
|
+
function resolveRoundedCorners(background, placement, label, renderedBox, viewportScale) {
|
|
16230
|
+
if (background.topCornersOnly !== undefined) {
|
|
16231
|
+
return background.topCornersOnly ? "top" : "all";
|
|
16232
|
+
}
|
|
16233
|
+
if (!renderedBox)
|
|
16234
|
+
return "all";
|
|
16235
|
+
const box = centerRectToTopLeftRect(renderedBox);
|
|
16236
|
+
const epsilon = 1e-6 / viewportScale;
|
|
16237
|
+
if (Math.min(label.x + label.width, box.x + box.width) -
|
|
16238
|
+
Math.max(label.x, box.x) <=
|
|
16239
|
+
epsilon) {
|
|
16240
|
+
return "all";
|
|
16241
|
+
}
|
|
16242
|
+
switch (placement) {
|
|
16243
|
+
case LabelPlacement.Top:
|
|
16244
|
+
return Math.abs(label.y + label.height - box.y) <= epsilon
|
|
16245
|
+
? "top"
|
|
16246
|
+
: "all";
|
|
16247
|
+
case LabelPlacement.Bottom:
|
|
16248
|
+
return Math.abs(label.y - box.y - box.height) <= epsilon
|
|
16249
|
+
? "bottom"
|
|
16250
|
+
: "all";
|
|
16251
|
+
case LabelPlacement.InsideTop:
|
|
16252
|
+
return Math.abs(label.y - box.y) <= epsilon ? "bottom" : "all";
|
|
16253
|
+
case LabelPlacement.InsideBottom:
|
|
16254
|
+
return Math.abs(label.y + label.height - box.y - box.height) <= epsilon
|
|
16255
|
+
? "top"
|
|
16256
|
+
: "all";
|
|
16257
|
+
case LabelPlacement.Center:
|
|
16258
|
+
return "all";
|
|
16259
|
+
}
|
|
16260
|
+
}
|
|
16261
|
+
function createBackgroundKey(background, corners) {
|
|
14725
16262
|
return [
|
|
14726
16263
|
background.alpha,
|
|
14727
16264
|
background.color,
|
|
14728
16265
|
background.cornerRadius ?? 0,
|
|
14729
16266
|
background.paddingX ?? 0,
|
|
14730
16267
|
background.paddingY ?? 0,
|
|
14731
|
-
|
|
16268
|
+
corners,
|
|
14732
16269
|
].join(":");
|
|
14733
16270
|
}
|
|
14734
16271
|
function detectionKey$1(detection, detectionIndex) {
|
|
@@ -15031,7 +16568,9 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
15031
16568
|
let activePicks = [];
|
|
15032
16569
|
let currentMediaTime = 0;
|
|
15033
16570
|
let viewportScale = 1;
|
|
16571
|
+
let strokePixelRatio = 1;
|
|
15034
16572
|
let maskRenderer;
|
|
16573
|
+
let labelDisplay = null;
|
|
15035
16574
|
let screenStrokeRenderer;
|
|
15036
16575
|
let isDestroyed = false;
|
|
15037
16576
|
const syntheticTimeline = createSyntheticTimeline(() => syntheticFrame);
|
|
@@ -15065,17 +16604,20 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
15065
16604
|
mediaHeight = height;
|
|
15066
16605
|
const container = new options.Container();
|
|
15067
16606
|
const boxGraphics = new options.Graphics();
|
|
15068
|
-
|
|
16607
|
+
labelDisplay = labelLayer.createContainer();
|
|
15069
16608
|
const vectors = vectorLayer.createContainer();
|
|
15070
16609
|
boxLayer.attachGraphics(boxGraphics);
|
|
15071
16610
|
maskRenderer = createMaskRenderer();
|
|
15072
16611
|
if (maskRenderer) {
|
|
15073
16612
|
container.addChild(maskRenderer.mesh);
|
|
15074
16613
|
}
|
|
15075
|
-
container.addChild(boxGraphics, vectors
|
|
16614
|
+
container.addChild(boxGraphics, vectors);
|
|
15076
16615
|
displayContainer = container;
|
|
15077
16616
|
return container;
|
|
15078
16617
|
},
|
|
16618
|
+
getLabelDisplay() {
|
|
16619
|
+
return labelDisplay;
|
|
16620
|
+
},
|
|
15079
16621
|
destroy() {
|
|
15080
16622
|
if (isDestroyed) {
|
|
15081
16623
|
return;
|
|
@@ -15086,6 +16628,7 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
15086
16628
|
maskRenderer?.destroy();
|
|
15087
16629
|
screenStrokeRenderer?.destroy();
|
|
15088
16630
|
labelLayer.destroy();
|
|
16631
|
+
labelDisplay = null;
|
|
15089
16632
|
vectorLayer.destroy();
|
|
15090
16633
|
},
|
|
15091
16634
|
releaseMaskTexture(texture) {
|
|
@@ -15095,6 +16638,7 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
15095
16638
|
drawFrame(context) {
|
|
15096
16639
|
currentMediaTime = context.mediaTime;
|
|
15097
16640
|
viewportScale = context.viewportScale ?? 1;
|
|
16641
|
+
strokePixelRatio = context.strokePixelRatio ?? 1;
|
|
15098
16642
|
if (isDestroyed || !interactionStyle || !context.frame) {
|
|
15099
16643
|
clear();
|
|
15100
16644
|
return;
|
|
@@ -15128,6 +16672,9 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
15128
16672
|
labelLayer.setLabelStyle(labelStyle);
|
|
15129
16673
|
vectorLayer.setStyles({ polygonStyle, polylineStyle, keypointStyle });
|
|
15130
16674
|
},
|
|
16675
|
+
setBackgroundAntialiasFilter(filter) {
|
|
16676
|
+
labelLayer.setBackgroundAntialiasFilter(filter);
|
|
16677
|
+
},
|
|
15131
16678
|
};
|
|
15132
16679
|
function getActivePicks({ frame, hoveredPick, mediaTime, selectedPick, selectedPicks, }) {
|
|
15133
16680
|
if (!frame || !interactionStyle) {
|
|
@@ -15239,9 +16786,9 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
15239
16786
|
screenStrokeRenderer?.hide();
|
|
15240
16787
|
return;
|
|
15241
16788
|
}
|
|
15242
|
-
maskRenderer.render(artifact.frame, artifact.texture, instructions);
|
|
16789
|
+
maskRenderer.render(artifact.frame, artifact.texture, instructions, strokePixelRatio);
|
|
15243
16790
|
if (!screenStrokeRenderer &&
|
|
15244
|
-
instructions.some(({ instruction }) => instruction.stroke?.widthUnit
|
|
16791
|
+
instructions.some(({ instruction }) => instruction.stroke?.widthUnit !== undefined)) {
|
|
15245
16792
|
screenStrokeRenderer = createPixiMaskScreenStrokeRenderer({
|
|
15246
16793
|
Graphics: options.Graphics,
|
|
15247
16794
|
mediaWidth,
|
|
@@ -15408,6 +16955,7 @@ function createInteractionMaskRenderer(options) {
|
|
|
15408
16955
|
const fillPalette = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4);
|
|
15409
16956
|
const strokePalette = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4);
|
|
15410
16957
|
const strokeWidths = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES);
|
|
16958
|
+
const strokeAlignments = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES);
|
|
15411
16959
|
const uniforms = new options.UniformGroup({
|
|
15412
16960
|
uFillPalette: {
|
|
15413
16961
|
size: MAX_ID_MASK_PALETTE_ENTRIES,
|
|
@@ -15415,7 +16963,8 @@ function createInteractionMaskRenderer(options) {
|
|
|
15415
16963
|
value: fillPalette,
|
|
15416
16964
|
},
|
|
15417
16965
|
uMaxStrokeWidth: { type: "f32", value: 0 },
|
|
15418
|
-
|
|
16966
|
+
uBorderEnabled: { type: "f32", value: 0 },
|
|
16967
|
+
uStrokePixelRatio: { type: "f32", value: 1 },
|
|
15419
16968
|
uStrokePalette: {
|
|
15420
16969
|
size: MAX_ID_MASK_PALETTE_ENTRIES,
|
|
15421
16970
|
type: "vec4<f32>",
|
|
@@ -15426,6 +16975,11 @@ function createInteractionMaskRenderer(options) {
|
|
|
15426
16975
|
type: "vec4<f32>",
|
|
15427
16976
|
value: strokeWidths,
|
|
15428
16977
|
},
|
|
16978
|
+
uStrokeAlignments: {
|
|
16979
|
+
size: ID_MASK_STROKE_WIDTH_LANES,
|
|
16980
|
+
type: "vec4<f32>",
|
|
16981
|
+
value: strokeAlignments,
|
|
16982
|
+
},
|
|
15429
16983
|
uTextureSize: {
|
|
15430
16984
|
type: "vec2<f32>",
|
|
15431
16985
|
value: new Float32Array([options.mediaWidth, options.mediaHeight]),
|
|
@@ -15475,7 +17029,7 @@ function createInteractionMaskRenderer(options) {
|
|
|
15475
17029
|
mesh.visible = false;
|
|
15476
17030
|
},
|
|
15477
17031
|
mesh,
|
|
15478
|
-
render(frame, texture, instructions) {
|
|
17032
|
+
render(frame, texture, instructions, strokePixelRatio) {
|
|
15479
17033
|
if (frame.kind !== PreparedMaskFrameKind.IdMask) {
|
|
15480
17034
|
mesh.visible = false;
|
|
15481
17035
|
return;
|
|
@@ -15484,8 +17038,9 @@ function createInteractionMaskRenderer(options) {
|
|
|
15484
17038
|
fillPalette.fill(0);
|
|
15485
17039
|
strokePalette.fill(0);
|
|
15486
17040
|
strokeWidths.fill(0);
|
|
17041
|
+
strokeAlignments.fill(0);
|
|
15487
17042
|
let maxStrokeWidth = 0;
|
|
15488
|
-
let
|
|
17043
|
+
let hasStroke = false;
|
|
15489
17044
|
for (const { detectionIndex, instruction } of instructions) {
|
|
15490
17045
|
const maskId = resolveIdMaskPaletteId(detectionIndex);
|
|
15491
17046
|
// A raster naming this detection is one the palette accepted, so an id
|
|
@@ -15495,26 +17050,31 @@ function createInteractionMaskRenderer(options) {
|
|
|
15495
17050
|
}
|
|
15496
17051
|
writeIdMaskPaletteEntry(fillPalette, maskId, instruction.color, instruction.alpha);
|
|
15497
17052
|
if (instruction.stroke &&
|
|
15498
|
-
instruction.stroke.widthUnit
|
|
17053
|
+
instruction.stroke.widthUnit === undefined &&
|
|
15499
17054
|
instruction.stroke.width > 0) {
|
|
15500
17055
|
writeIdMaskPaletteEntry(strokePalette, maskId, instruction.stroke.color, instruction.stroke.alpha);
|
|
15501
|
-
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
|
|
15505
|
-
|
|
15506
|
-
|
|
17056
|
+
const alignment = instruction.stroke.alignment === StrokeAlignment.Inside
|
|
17057
|
+
? 1
|
|
17058
|
+
: instruction.stroke.alignment === StrokeAlignment.Center
|
|
17059
|
+
? 0.5
|
|
17060
|
+
: 0;
|
|
17061
|
+
strokeWidths[maskId] = instruction.stroke.width;
|
|
17062
|
+
strokeAlignments[maskId] = alignment;
|
|
17063
|
+
hasStroke = true;
|
|
17064
|
+
maxStrokeWidth = Math.max(maxStrokeWidth, instruction.stroke.width * (1 - alignment));
|
|
15507
17065
|
}
|
|
15508
17066
|
}
|
|
15509
17067
|
uniforms.uniforms.uFillPalette = fillPalette;
|
|
15510
17068
|
uniforms.uniforms.uStrokePalette = strokePalette;
|
|
15511
17069
|
uniforms.uniforms.uStrokeWidths = strokeWidths;
|
|
17070
|
+
uniforms.uniforms.uStrokeAlignments = strokeAlignments;
|
|
17071
|
+
uniforms.uniforms.uBorderEnabled = hasStroke ? 1 : 0;
|
|
17072
|
+
uniforms.uniforms.uStrokePixelRatio = strokePixelRatio;
|
|
15512
17073
|
uniforms.uniforms.uTextureSize = new Float32Array([
|
|
15513
17074
|
frame.width,
|
|
15514
17075
|
frame.height,
|
|
15515
17076
|
]);
|
|
15516
17077
|
uniforms.uniforms.uMaxStrokeWidth = maxStrokeWidth;
|
|
15517
|
-
uniforms.uniforms.uMaxFractionalStrokeWidth = maxFractionalStrokeWidth;
|
|
15518
17078
|
uniforms.update();
|
|
15519
17079
|
mesh.visible = true;
|
|
15520
17080
|
},
|
|
@@ -15569,14 +17129,11 @@ in vec4 vColor;
|
|
|
15569
17129
|
uniform sampler2D uTexture;
|
|
15570
17130
|
uniform vec2 uTextureSize;
|
|
15571
17131
|
uniform float uMaxStrokeWidth;
|
|
15572
|
-
uniform float
|
|
17132
|
+
uniform float uBorderEnabled;
|
|
17133
|
+
uniform float uStrokePixelRatio;
|
|
15573
17134
|
${idMaskPaletteGlsl}
|
|
15574
17135
|
out vec4 finalColor;
|
|
15575
17136
|
|
|
15576
|
-
float sampleMaskId(vec2 uv) {
|
|
15577
|
-
return floor(texture(uTexture, uv).r * 255.0 + 0.5);
|
|
15578
|
-
}
|
|
15579
|
-
|
|
15580
17137
|
vec4 premultiplyAlpha(vec4 color) {
|
|
15581
17138
|
return vec4(color.rgb * color.a, color.a);
|
|
15582
17139
|
}
|
|
@@ -15588,19 +17145,20 @@ bool differs(float left, float right) {
|
|
|
15588
17145
|
${idMaskStrokeCoverageGlsl}
|
|
15589
17146
|
|
|
15590
17147
|
void main(void) {
|
|
15591
|
-
|
|
15592
|
-
|
|
15593
|
-
|
|
15594
|
-
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);
|
|
15595
17153
|
float pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
|
|
15596
17154
|
|
|
15597
17155
|
if (centerId > 0.5) {
|
|
15598
17156
|
vec4 fill = readFill(centerId);
|
|
15599
17157
|
vec4 stroke = readStroke(centerId);
|
|
15600
17158
|
|
|
15601
|
-
float width =
|
|
15602
|
-
if (
|
|
15603
|
-
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);
|
|
15604
17162
|
if (coverage >= 1.0) {
|
|
15605
17163
|
finalColor = premultiplyAlpha(stroke * vColor);
|
|
15606
17164
|
return;
|
|
@@ -15615,8 +17173,8 @@ void main(void) {
|
|
|
15615
17173
|
return;
|
|
15616
17174
|
}
|
|
15617
17175
|
|
|
15618
|
-
if (
|
|
15619
|
-
vec2 border = findNeighborStroke(centerId,
|
|
17176
|
+
if (uBorderEnabled > 0.5) {
|
|
17177
|
+
vec2 border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
|
|
15620
17178
|
|
|
15621
17179
|
if (border.x > 0.5) {
|
|
15622
17180
|
finalColor = premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
|
|
@@ -15631,9 +17189,11 @@ const interactionMaskFragmentWgsl = `
|
|
|
15631
17189
|
struct InteractionMaskUniforms {
|
|
15632
17190
|
${idMaskFillPaletteWgslField}
|
|
15633
17191
|
uMaxStrokeWidth: f32,
|
|
15634
|
-
|
|
17192
|
+
uBorderEnabled: f32,
|
|
17193
|
+
uStrokePixelRatio: f32,
|
|
15635
17194
|
${idMaskStrokePaletteWgslField}
|
|
15636
17195
|
${idMaskStrokeWidthsWgslField}
|
|
17196
|
+
${idMaskStrokeAlignmentsWgslField}
|
|
15637
17197
|
uTextureSize: vec2<f32>,
|
|
15638
17198
|
}
|
|
15639
17199
|
|
|
@@ -15641,9 +17201,6 @@ struct InteractionMaskUniforms {
|
|
|
15641
17201
|
@group(2) @binding(1) var uTexture: texture_2d<f32>;
|
|
15642
17202
|
@group(2) @binding(2) var uSampler: sampler;
|
|
15643
17203
|
|
|
15644
|
-
fn sampleMaskId(uv: vec2<f32>) -> f32 {
|
|
15645
|
-
return floor(textureSampleLevel(uTexture, uSampler, uv, 0.0).r * 255.0 + 0.5);
|
|
15646
|
-
}
|
|
15647
17204
|
${idMaskPaletteWgsl}
|
|
15648
17205
|
fn premultiplyAlpha(color: vec4<f32>) -> vec4<f32> {
|
|
15649
17206
|
return vec4<f32>(color.rgb * color.a, color.a);
|
|
@@ -15660,19 +17217,20 @@ fn mainFragment(
|
|
|
15660
17217
|
@location(0) vUV: vec2<f32>,
|
|
15661
17218
|
@location(1) vColor: vec4<f32>,
|
|
15662
17219
|
) -> @location(0) vec4<f32> {
|
|
15663
|
-
let
|
|
15664
|
-
let
|
|
15665
|
-
let
|
|
15666
|
-
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);
|
|
15667
17225
|
let pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
|
|
15668
17226
|
|
|
15669
17227
|
if (centerId > 0.5) {
|
|
15670
17228
|
let fill = readFill(centerId);
|
|
15671
17229
|
let stroke = readStroke(centerId);
|
|
15672
17230
|
|
|
15673
|
-
let width =
|
|
15674
|
-
if (maskUniforms.
|
|
15675
|
-
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);
|
|
15676
17234
|
if (coverage >= 1.0) {
|
|
15677
17235
|
return premultiplyAlpha(stroke * vColor);
|
|
15678
17236
|
}
|
|
@@ -15684,8 +17242,8 @@ fn mainFragment(
|
|
|
15684
17242
|
return premultiplyAlpha(fill * vColor);
|
|
15685
17243
|
}
|
|
15686
17244
|
|
|
15687
|
-
if (maskUniforms.
|
|
15688
|
-
let border = findNeighborStroke(
|
|
17245
|
+
if (maskUniforms.uBorderEnabled > 0.5) {
|
|
17246
|
+
let border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
|
|
15689
17247
|
|
|
15690
17248
|
if (border.x > 0.5) {
|
|
15691
17249
|
return premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
|
|
@@ -15697,6 +17255,7 @@ fn mainFragment(
|
|
|
15697
17255
|
`;
|
|
15698
17256
|
|
|
15699
17257
|
function createPixiIdMaskShaderRenderer(options) {
|
|
17258
|
+
const defaultStrokeAlignments = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES);
|
|
15700
17259
|
const uniforms = new options.UniformGroup({
|
|
15701
17260
|
uBorderEnabled: { type: "f32", value: 0 },
|
|
15702
17261
|
uFillPalette: {
|
|
@@ -15705,7 +17264,7 @@ function createPixiIdMaskShaderRenderer(options) {
|
|
|
15705
17264
|
value: new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4),
|
|
15706
17265
|
},
|
|
15707
17266
|
uMaxStrokeWidth: { type: "f32", value: 0 },
|
|
15708
|
-
|
|
17267
|
+
uStrokePixelRatio: { type: "f32", value: 1 },
|
|
15709
17268
|
uStrokePalette: {
|
|
15710
17269
|
size: MAX_ID_MASK_PALETTE_ENTRIES,
|
|
15711
17270
|
type: "vec4<f32>",
|
|
@@ -15716,6 +17275,11 @@ function createPixiIdMaskShaderRenderer(options) {
|
|
|
15716
17275
|
type: "vec4<f32>",
|
|
15717
17276
|
value: new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES),
|
|
15718
17277
|
},
|
|
17278
|
+
uStrokeAlignments: {
|
|
17279
|
+
size: ID_MASK_STROKE_WIDTH_LANES,
|
|
17280
|
+
type: "vec4<f32>",
|
|
17281
|
+
value: defaultStrokeAlignments,
|
|
17282
|
+
},
|
|
15719
17283
|
uTextureSize: {
|
|
15720
17284
|
type: "vec2<f32>",
|
|
15721
17285
|
value: new Float32Array([options.mediaWidth, options.mediaHeight]),
|
|
@@ -15768,24 +17332,30 @@ function createPixiIdMaskShaderRenderer(options) {
|
|
|
15768
17332
|
mesh.visible = false;
|
|
15769
17333
|
},
|
|
15770
17334
|
mesh,
|
|
15771
|
-
render(frame, texture) {
|
|
17335
|
+
render(frame, texture, strokePixelRatio = 1) {
|
|
15772
17336
|
bindTexture(texture.source);
|
|
15773
17337
|
uniforms.uniforms.uFillPalette = frame.fillPalette;
|
|
15774
17338
|
uniforms.uniforms.uStrokePalette = frame.strokePalette;
|
|
15775
17339
|
uniforms.uniforms.uStrokeWidths = frame.strokeWidths;
|
|
17340
|
+
uniforms.uniforms.uStrokeAlignments =
|
|
17341
|
+
frame.strokeAlignments ?? defaultStrokeAlignments;
|
|
17342
|
+
uniforms.uniforms.uStrokePixelRatio = strokePixelRatio;
|
|
15776
17343
|
uniforms.uniforms.uTextureSize = new Float32Array([
|
|
15777
17344
|
frame.width,
|
|
15778
17345
|
frame.height,
|
|
15779
17346
|
]);
|
|
15780
17347
|
uniforms.uniforms.uBorderEnabled = frame.hasStroke ? 1 : 0;
|
|
15781
|
-
uniforms.uniforms.uMaxStrokeWidth =
|
|
15782
|
-
uniforms.uniforms.uMaxFractionalStrokeWidth = frame.strokeWidths.reduce((maximum, width) => width > 0 && width < 1 ? Math.max(maximum, width) : maximum, 0);
|
|
17348
|
+
uniforms.uniforms.uMaxStrokeWidth = frame.maxStrokeWidth;
|
|
15783
17349
|
uniforms.update();
|
|
15784
17350
|
mesh.visible = true;
|
|
15785
17351
|
},
|
|
15786
17352
|
setOpacity(opacity) {
|
|
15787
17353
|
mesh.alpha = opacity;
|
|
15788
17354
|
},
|
|
17355
|
+
setStrokePixelRatio(pixelRatio) {
|
|
17356
|
+
uniforms.uniforms.uStrokePixelRatio = pixelRatio;
|
|
17357
|
+
uniforms.update();
|
|
17358
|
+
},
|
|
15789
17359
|
};
|
|
15790
17360
|
function bindTexture(source) {
|
|
15791
17361
|
try {
|
|
@@ -15838,14 +17408,10 @@ uniform sampler2D uTexture;
|
|
|
15838
17408
|
uniform vec2 uTextureSize;
|
|
15839
17409
|
uniform float uBorderEnabled;
|
|
15840
17410
|
uniform float uMaxStrokeWidth;
|
|
15841
|
-
uniform float
|
|
17411
|
+
uniform float uStrokePixelRatio;
|
|
15842
17412
|
${idMaskPaletteGlsl}
|
|
15843
17413
|
out vec4 finalColor;
|
|
15844
17414
|
|
|
15845
|
-
float sampleMaskId(vec2 uv) {
|
|
15846
|
-
return floor(texture(uTexture, uv).r * 255.0 + 0.5);
|
|
15847
|
-
}
|
|
15848
|
-
|
|
15849
17415
|
vec4 premultiplyAlpha(vec4 color) {
|
|
15850
17416
|
return vec4(color.rgb * color.a, color.a);
|
|
15851
17417
|
}
|
|
@@ -15857,15 +17423,16 @@ bool differs(float left, float right) {
|
|
|
15857
17423
|
${idMaskStrokeCoverageGlsl}
|
|
15858
17424
|
|
|
15859
17425
|
void main(void) {
|
|
15860
|
-
|
|
15861
|
-
|
|
15862
|
-
|
|
15863
|
-
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);
|
|
15864
17431
|
float pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
|
|
15865
17432
|
|
|
15866
17433
|
if (centerId < 0.5) {
|
|
15867
|
-
if (uBorderEnabled > 0.5
|
|
15868
|
-
vec2 border = findNeighborStroke(centerId,
|
|
17434
|
+
if (uBorderEnabled > 0.5) {
|
|
17435
|
+
vec2 border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
|
|
15869
17436
|
|
|
15870
17437
|
if (border.x > 0.5) {
|
|
15871
17438
|
finalColor = premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
|
|
@@ -15878,9 +17445,9 @@ void main(void) {
|
|
|
15878
17445
|
}
|
|
15879
17446
|
|
|
15880
17447
|
if (uBorderEnabled > 0.5) {
|
|
15881
|
-
float width =
|
|
17448
|
+
float width = strokeWidthInTexels(centerId, pixelWidth);
|
|
15882
17449
|
if (width > 0.0 && readStroke(centerId).a > 0.0) {
|
|
15883
|
-
float coverage = innerStrokeCoverage(centerId,
|
|
17450
|
+
float coverage = innerStrokeCoverage(centerId, sourceCell, cell, width, pixelWidth);
|
|
15884
17451
|
if (coverage >= 1.0) {
|
|
15885
17452
|
finalColor = premultiplyAlpha(readStroke(centerId) * vColor);
|
|
15886
17453
|
return;
|
|
@@ -15904,9 +17471,10 @@ struct MaskUniforms {
|
|
|
15904
17471
|
uBorderEnabled: f32,
|
|
15905
17472
|
${idMaskFillPaletteWgslField}
|
|
15906
17473
|
uMaxStrokeWidth: f32,
|
|
15907
|
-
|
|
17474
|
+
uStrokePixelRatio: f32,
|
|
15908
17475
|
${idMaskStrokePaletteWgslField}
|
|
15909
17476
|
${idMaskStrokeWidthsWgslField}
|
|
17477
|
+
${idMaskStrokeAlignmentsWgslField}
|
|
15910
17478
|
uTextureSize: vec2<f32>,
|
|
15911
17479
|
}
|
|
15912
17480
|
|
|
@@ -15914,9 +17482,6 @@ struct MaskUniforms {
|
|
|
15914
17482
|
@group(2) @binding(1) var uTexture: texture_2d<f32>;
|
|
15915
17483
|
@group(2) @binding(2) var uSampler: sampler;
|
|
15916
17484
|
|
|
15917
|
-
fn sampleMaskId(uv: vec2<f32>) -> f32 {
|
|
15918
|
-
return floor(textureSampleLevel(uTexture, uSampler, uv, 0.0).r * 255.0 + 0.5);
|
|
15919
|
-
}
|
|
15920
17485
|
${idMaskPaletteWgsl}
|
|
15921
17486
|
fn premultiplyAlpha(color: vec4<f32>) -> vec4<f32> {
|
|
15922
17487
|
return vec4<f32>(color.rgb * color.a, color.a);
|
|
@@ -15933,15 +17498,16 @@ fn mainFragment(
|
|
|
15933
17498
|
@location(0) vUV: vec2<f32>,
|
|
15934
17499
|
@location(1) vColor: vec4<f32>,
|
|
15935
17500
|
) -> @location(0) vec4<f32> {
|
|
15936
|
-
let
|
|
15937
|
-
let
|
|
15938
|
-
let
|
|
15939
|
-
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);
|
|
15940
17506
|
let pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
|
|
15941
17507
|
|
|
15942
17508
|
if (centerId < 0.5) {
|
|
15943
|
-
if (maskUniforms.uBorderEnabled > 0.5
|
|
15944
|
-
let border = findNeighborStroke(
|
|
17509
|
+
if (maskUniforms.uBorderEnabled > 0.5) {
|
|
17510
|
+
let border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
|
|
15945
17511
|
|
|
15946
17512
|
if (border.x > 0.5) {
|
|
15947
17513
|
return premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
|
|
@@ -15952,9 +17518,9 @@ fn mainFragment(
|
|
|
15952
17518
|
}
|
|
15953
17519
|
|
|
15954
17520
|
if (maskUniforms.uBorderEnabled > 0.5) {
|
|
15955
|
-
let width =
|
|
17521
|
+
let width = strokeWidthInTexels(centerId, pixelWidth);
|
|
15956
17522
|
if (width > 0.0 && readStroke(centerId).a > 0.0) {
|
|
15957
|
-
let coverage = innerStrokeCoverage(
|
|
17523
|
+
let coverage = innerStrokeCoverage(centerId, sourceCell, cell, width, pixelWidth);
|
|
15958
17524
|
if (coverage >= 1.0) {
|
|
15959
17525
|
return premultiplyAlpha(readStroke(centerId) * vColor);
|
|
15960
17526
|
}
|
|
@@ -16020,7 +17586,10 @@ function createPixiMaskLayer(options) {
|
|
|
16020
17586
|
const haloTextures = new Map();
|
|
16021
17587
|
const regionMaskTextures = new Map();
|
|
16022
17588
|
const maskFrameOptions = options.renderPreparation?.maskFrame;
|
|
17589
|
+
const capsRasterToDisplay = maskFrameOptions?.display !== undefined;
|
|
16023
17590
|
let rasterDisplay = maskFrameOptions?.display;
|
|
17591
|
+
let rasterResolutionScale = 1;
|
|
17592
|
+
let strokeViewportScale;
|
|
16024
17593
|
const preparedRenderWindow = createPreparedRenderWindow({
|
|
16025
17594
|
artifactKind: options.artifactKind,
|
|
16026
17595
|
detectionTimeline: options.detectionTimeline,
|
|
@@ -16044,6 +17613,7 @@ function createPixiMaskLayer(options) {
|
|
|
16044
17613
|
resolveInstructions: options.resolveInstructions,
|
|
16045
17614
|
resolveMaxRasterWidth,
|
|
16046
17615
|
resolveMaskFrameBackingBytes,
|
|
17616
|
+
resolveMaskDisplayWidth,
|
|
16047
17617
|
});
|
|
16048
17618
|
return {
|
|
16049
17619
|
createSprite({ width, height }) {
|
|
@@ -16162,16 +17732,24 @@ function createPixiMaskLayer(options) {
|
|
|
16162
17732
|
setPlaybackActive(active) {
|
|
16163
17733
|
preparedRenderWindow.setPlaybackActive(active);
|
|
16164
17734
|
},
|
|
16165
|
-
setRasterDisplay(display) {
|
|
16166
|
-
if (
|
|
17735
|
+
setRasterDisplay(display, resolutionScale = 1) {
|
|
17736
|
+
if (isDestroyed)
|
|
16167
17737
|
return;
|
|
16168
17738
|
const previousWidth = resolveMaxRasterWidth();
|
|
16169
17739
|
rasterDisplay = display;
|
|
17740
|
+
rasterResolutionScale = resolutionScale;
|
|
17741
|
+
idMaskRenderer?.setStrokePixelRatio(resolveDisplayPixelRatio(display) * resolutionScale);
|
|
16170
17742
|
const nextWidth = resolveMaxRasterWidth();
|
|
16171
17743
|
if (mediaWidth > 0 && previousWidth !== nextWidth) {
|
|
16172
17744
|
preparedRenderWindow.invalidateRasterSize();
|
|
16173
17745
|
}
|
|
16174
17746
|
},
|
|
17747
|
+
setViewportScale(scale) {
|
|
17748
|
+
if (isDestroyed || scale <= 0 || scale === strokeViewportScale)
|
|
17749
|
+
return;
|
|
17750
|
+
strokeViewportScale = scale;
|
|
17751
|
+
preparedRenderWindow.invalidateMaskDisplayWidth();
|
|
17752
|
+
},
|
|
16175
17753
|
setTimelineContext(context) {
|
|
16176
17754
|
preparedRenderWindow.setTimelineContext(context);
|
|
16177
17755
|
},
|
|
@@ -16261,14 +17839,22 @@ function createPixiMaskLayer(options) {
|
|
|
16261
17839
|
maskTextures.set(maskFrame.key, texture);
|
|
16262
17840
|
return texture;
|
|
16263
17841
|
}
|
|
16264
|
-
|
|
16265
|
-
|
|
16266
|
-
|
|
16267
|
-
|
|
17842
|
+
function resolveMaskDisplayWidth() {
|
|
17843
|
+
if (strokeViewportScale !== undefined && mediaWidth > 0) {
|
|
17844
|
+
return mediaWidth * strokeViewportScale;
|
|
17845
|
+
}
|
|
17846
|
+
const display = rasterDisplay;
|
|
17847
|
+
if (!display || mediaWidth <= 0 || mediaHeight <= 0)
|
|
17848
|
+
return undefined;
|
|
17849
|
+
const fit = Math.min(display.boxWidth / mediaWidth, display.boxHeight / mediaHeight);
|
|
17850
|
+
return fit > 0 ? mediaWidth * fit : undefined;
|
|
17851
|
+
}
|
|
17852
|
+
/** Mask detail follows the annotation capture density. */
|
|
16268
17853
|
function resolveMaxRasterWidth() {
|
|
16269
17854
|
const display = rasterDisplay;
|
|
16270
17855
|
const artifactKind = options.artifactKind ?? RenderPreparationArtifactKind.MaskFrame;
|
|
16271
17856
|
if (!display ||
|
|
17857
|
+
!capsRasterToDisplay ||
|
|
16272
17858
|
artifactKind !== RenderPreparationArtifactKind.MaskFrame ||
|
|
16273
17859
|
mediaWidth <= 0 ||
|
|
16274
17860
|
mediaHeight <= 0) {
|
|
@@ -16277,7 +17863,7 @@ function createPixiMaskLayer(options) {
|
|
|
16277
17863
|
const fit = Math.min(display.boxWidth / mediaWidth, display.boxHeight / mediaHeight);
|
|
16278
17864
|
const pixelRatio = resolveDisplayPixelRatio(display);
|
|
16279
17865
|
return fit > 0 && pixelRatio > 0
|
|
16280
|
-
? alignRasterWidth(Math.ceil(mediaWidth * fit * pixelRatio))
|
|
17866
|
+
? alignRasterWidth(Math.ceil(mediaWidth * fit * pixelRatio * rasterResolutionScale))
|
|
16281
17867
|
: undefined;
|
|
16282
17868
|
}
|
|
16283
17869
|
/**
|
|
@@ -16358,7 +17944,9 @@ function createPixiMaskLayer(options) {
|
|
|
16358
17944
|
return;
|
|
16359
17945
|
}
|
|
16360
17946
|
maskSprite.visible = false;
|
|
16361
|
-
idMaskRenderer.render(maskFrame, texture
|
|
17947
|
+
idMaskRenderer.render(maskFrame, texture, rasterDisplay
|
|
17948
|
+
? resolveDisplayPixelRatio(rasterDisplay) * rasterResolutionScale
|
|
17949
|
+
: 1);
|
|
16362
17950
|
}
|
|
16363
17951
|
function hideFill() {
|
|
16364
17952
|
if (maskSprite) {
|
|
@@ -16797,11 +18385,15 @@ function createPixiPolygonLayer(options) {
|
|
|
16797
18385
|
rasterLayer.setMaskStyle(nextPolygonStyle ? createArtifactStyle() : nextPolygonStyle);
|
|
16798
18386
|
},
|
|
16799
18387
|
needsRenderPreparationWait: rasterLayer.needsRenderPreparationWait,
|
|
18388
|
+
setPlaybackActive: rasterLayer.setPlaybackActive,
|
|
18389
|
+
setRasterDisplay: rasterLayer.setRasterDisplay,
|
|
16800
18390
|
setTimelineContext: rasterLayer.setTimelineContext,
|
|
18391
|
+
setViewportScale: adoptViewportScale,
|
|
16801
18392
|
waitForRenderPreparation: rasterLayer.waitForRenderPreparation,
|
|
16802
18393
|
};
|
|
16803
18394
|
function adoptViewportScale(nextViewportScale) {
|
|
16804
18395
|
const normalizedViewportScale = Math.max(nextViewportScale, Number.EPSILON);
|
|
18396
|
+
rasterLayer.setViewportScale(normalizedViewportScale);
|
|
16805
18397
|
if (normalizedViewportScale === viewportScale) {
|
|
16806
18398
|
return;
|
|
16807
18399
|
}
|
|
@@ -16865,9 +18457,10 @@ function resolvePreparedPolygonInstructions(options) {
|
|
|
16865
18457
|
},
|
|
16866
18458
|
stroke: instruction.stroke
|
|
16867
18459
|
? {
|
|
18460
|
+
alignment: BoxStrokeAlignment.Center,
|
|
16868
18461
|
alpha: instruction.stroke.alpha,
|
|
16869
18462
|
color: instruction.stroke.color,
|
|
16870
|
-
width:
|
|
18463
|
+
width: instruction.stroke.width,
|
|
16871
18464
|
}
|
|
16872
18465
|
: undefined,
|
|
16873
18466
|
});
|
|
@@ -17012,17 +18605,30 @@ function createPixiRegionCoverageMask(options) {
|
|
|
17012
18605
|
});
|
|
17013
18606
|
let shader = createShader();
|
|
17014
18607
|
const display = new options.Mesh({ geometry, shader });
|
|
17015
|
-
const
|
|
18608
|
+
const container = new options.Container();
|
|
18609
|
+
container.addChild(display);
|
|
18610
|
+
const effect = new options.AlphaMask({ mask: container });
|
|
18611
|
+
let antialiasFilter;
|
|
17016
18612
|
return {
|
|
17017
18613
|
destroy() {
|
|
18614
|
+
if (antialiasFilter && display.filters?.includes(antialiasFilter)) {
|
|
18615
|
+
display.filters = null;
|
|
18616
|
+
}
|
|
18617
|
+
antialiasFilter?.destroy();
|
|
17018
18618
|
effect.destroy();
|
|
17019
18619
|
display.destroy();
|
|
18620
|
+
container.destroy();
|
|
17020
18621
|
destroyShaderKeepingProgram(shader);
|
|
17021
18622
|
geometry.destroy();
|
|
17022
18623
|
placeholderSource.destroy();
|
|
17023
18624
|
},
|
|
18625
|
+
container,
|
|
17024
18626
|
display,
|
|
17025
18627
|
effect,
|
|
18628
|
+
setAntialiasResolution(resolution) {
|
|
18629
|
+
if (antialiasFilter)
|
|
18630
|
+
antialiasFilter.resolution = resolution;
|
|
18631
|
+
},
|
|
17026
18632
|
releaseTexture(source) {
|
|
17027
18633
|
if (shader.resources.uTexture !== source)
|
|
17028
18634
|
return;
|
|
@@ -17031,6 +18637,7 @@ function createPixiRegionCoverageMask(options) {
|
|
|
17031
18637
|
},
|
|
17032
18638
|
render(renderOptions) {
|
|
17033
18639
|
bindTexture(renderOptions.artifact.texture.source);
|
|
18640
|
+
const alphaCoverage = updateAntialiasing();
|
|
17034
18641
|
display.visible = true;
|
|
17035
18642
|
uniforms.uniforms.uCrop = new Float32Array([
|
|
17036
18643
|
renderOptions.crop.x,
|
|
@@ -17048,8 +18655,34 @@ function createPixiRegionCoverageMask(options) {
|
|
|
17048
18655
|
display.position.set(renderOptions.x, renderOptions.y);
|
|
17049
18656
|
display.scale.set(renderOptions.width * (renderOptions.flipHorizontal ? -1 : 1), renderOptions.height * (renderOptions.flipVertical ? -1 : 1));
|
|
17050
18657
|
display.rotation = renderOptions.rotation;
|
|
18658
|
+
syncViewportBounds();
|
|
18659
|
+
return alphaCoverage;
|
|
17051
18660
|
},
|
|
18661
|
+
syncViewportBounds,
|
|
17052
18662
|
};
|
|
18663
|
+
function syncViewportBounds() {
|
|
18664
|
+
syncPixiMaskViewportBounds(container, options.getViewportBounds?.(), options.Rectangle);
|
|
18665
|
+
}
|
|
18666
|
+
function updateAntialiasing() {
|
|
18667
|
+
if (options.getAnnotationAntialiasing?.() &&
|
|
18668
|
+
options.Filter &&
|
|
18669
|
+
options.defaultFilterVert) {
|
|
18670
|
+
antialiasFilter ??= createPixiAnnotationAntialiasFilter({
|
|
18671
|
+
maskCoverage: true,
|
|
18672
|
+
Filter: options.Filter,
|
|
18673
|
+
defaultFilterVert: options.defaultFilterVert,
|
|
18674
|
+
resolution: options.getAntialiasResolution?.(),
|
|
18675
|
+
});
|
|
18676
|
+
if (display.filters?.[0] !== antialiasFilter) {
|
|
18677
|
+
display.filters = [antialiasFilter];
|
|
18678
|
+
}
|
|
18679
|
+
return true;
|
|
18680
|
+
}
|
|
18681
|
+
if (antialiasFilter && display.filters?.includes(antialiasFilter)) {
|
|
18682
|
+
display.filters = null;
|
|
18683
|
+
}
|
|
18684
|
+
return false;
|
|
18685
|
+
}
|
|
17053
18686
|
function bindTexture(source) {
|
|
17054
18687
|
try {
|
|
17055
18688
|
shader.resources.uTexture = source;
|
|
@@ -17351,6 +18984,7 @@ function createPixiRegionLayer(options) {
|
|
|
17351
18984
|
let currentMediaTime = 0;
|
|
17352
18985
|
let currentViewportScale = 1;
|
|
17353
18986
|
let destroyed = false;
|
|
18987
|
+
let viewportBounds;
|
|
17354
18988
|
let renderers = [...options.regionRenderers];
|
|
17355
18989
|
const assets = new Map();
|
|
17356
18990
|
const entries = new Map();
|
|
@@ -17431,6 +19065,7 @@ function createPixiRegionLayer(options) {
|
|
|
17431
19065
|
}
|
|
17432
19066
|
const position = positionSprite(entry.display, renderer, region, sourceSize, viewportScale);
|
|
17433
19067
|
updateEffect(entry, renderer, viewportScale);
|
|
19068
|
+
updateAssetAntialiasing(entry, renderer);
|
|
17434
19069
|
if (renderer.source.kind === RegionRendererSourceKind.Media &&
|
|
17435
19070
|
mediaCrop &&
|
|
17436
19071
|
!updateMediaCoverage(entry, renderer, detection, detectionIndex, mediaCrop)) {
|
|
@@ -17482,6 +19117,15 @@ function createPixiRegionLayer(options) {
|
|
|
17482
19117
|
}
|
|
17483
19118
|
}
|
|
17484
19119
|
},
|
|
19120
|
+
setAntialiasResolution(resolution) {
|
|
19121
|
+
for (const entry of entries.values()) {
|
|
19122
|
+
updateEntryResolution(entry, resolution);
|
|
19123
|
+
}
|
|
19124
|
+
for (const pool of pools.values()) {
|
|
19125
|
+
for (const entry of pool)
|
|
19126
|
+
updateEntryResolution(entry, resolution);
|
|
19127
|
+
}
|
|
19128
|
+
},
|
|
17485
19129
|
setRenderers(nextRenderers) {
|
|
17486
19130
|
renderers = [...nextRenderers];
|
|
17487
19131
|
syncSources();
|
|
@@ -17495,10 +19139,24 @@ function createPixiRegionLayer(options) {
|
|
|
17495
19139
|
entry.display.position.set(entry.baseX + x, entry.baseY + y);
|
|
17496
19140
|
entry.coverageMask?.position.set(entry.baseX + x, entry.baseY + y);
|
|
17497
19141
|
entry.exactCoverageMask?.display.position.set(entry.baseX + x, entry.baseY + y);
|
|
19142
|
+
syncEntryViewportBounds(entry);
|
|
17498
19143
|
translated = true;
|
|
17499
19144
|
}
|
|
17500
19145
|
return translated;
|
|
17501
19146
|
},
|
|
19147
|
+
syncViewportBounds() {
|
|
19148
|
+
const next = options.getViewportBounds?.();
|
|
19149
|
+
if (next?.x === viewportBounds?.x &&
|
|
19150
|
+
next?.y === viewportBounds?.y &&
|
|
19151
|
+
next?.width === viewportBounds?.width &&
|
|
19152
|
+
next?.height === viewportBounds?.height)
|
|
19153
|
+
return;
|
|
19154
|
+
viewportBounds = next ? { ...next } : undefined;
|
|
19155
|
+
for (const entry of entries.values()) {
|
|
19156
|
+
if (entry.active)
|
|
19157
|
+
syncEntryViewportBounds(entry);
|
|
19158
|
+
}
|
|
19159
|
+
},
|
|
17502
19160
|
destroy() {
|
|
17503
19161
|
destroyed = true;
|
|
17504
19162
|
destroyAllDisplays();
|
|
@@ -17620,11 +19278,18 @@ function createPixiRegionLayer(options) {
|
|
|
17620
19278
|
return entry;
|
|
17621
19279
|
}
|
|
17622
19280
|
function updateMediaCoverage(entry, renderer, detection, detectionIndex, crop) {
|
|
17623
|
-
if (renderer.source.kind !== RegionRendererSourceKind.Media
|
|
17624
|
-
renderer.source.coverage === undefined) {
|
|
19281
|
+
if (renderer.source.kind !== RegionRendererSourceKind.Media) {
|
|
17625
19282
|
removeCoverageMask(entry);
|
|
17626
19283
|
return true;
|
|
17627
19284
|
}
|
|
19285
|
+
if (renderer.source.coverage === undefined) {
|
|
19286
|
+
removeExactCoverageMask(entry);
|
|
19287
|
+
if (!annotationAntialiasingEnabled()) {
|
|
19288
|
+
removePolygonCoverageMask(entry);
|
|
19289
|
+
return true;
|
|
19290
|
+
}
|
|
19291
|
+
return updateGraphicsCoverage(entry, renderer, crop);
|
|
19292
|
+
}
|
|
17628
19293
|
if (renderer.source.coverage.kind === RegionRendererCoverageKind.Mask) {
|
|
17629
19294
|
removePolygonCoverageMask(entry);
|
|
17630
19295
|
const artifact = options.getActiveRegionMaskCoverage(currentFrame?.mediaTime ?? null);
|
|
@@ -17642,7 +19307,14 @@ function createPixiRegionLayer(options) {
|
|
|
17642
19307
|
}
|
|
17643
19308
|
const mask = entry.exactCoverageMask ??
|
|
17644
19309
|
createPixiRegionCoverageMask({
|
|
19310
|
+
getAnnotationAntialiasing: options.getAnnotationAntialiasing,
|
|
19311
|
+
getAntialiasResolution: options.getAntialiasResolution,
|
|
19312
|
+
getViewportBounds: options.getViewportBounds,
|
|
19313
|
+
defaultFilterVert: options.defaultFilterVert,
|
|
19314
|
+
Filter: options.Filter,
|
|
17645
19315
|
AlphaMask: options.AlphaMask,
|
|
19316
|
+
Container: options.Container,
|
|
19317
|
+
Rectangle: options.Rectangle,
|
|
17646
19318
|
ImageSource: options.ImageSource,
|
|
17647
19319
|
Mesh: options.Mesh,
|
|
17648
19320
|
MeshGeometry: options.MeshGeometry,
|
|
@@ -17651,7 +19323,7 @@ function createPixiRegionLayer(options) {
|
|
|
17651
19323
|
});
|
|
17652
19324
|
if (!entry.exactCoverageMask) {
|
|
17653
19325
|
entry.exactCoverageMask = mask;
|
|
17654
|
-
container?.addChild(mask.
|
|
19326
|
+
container?.addChild(mask.container);
|
|
17655
19327
|
// A Mesh assigned through `display.mask` becomes a stencil mask in
|
|
17656
19328
|
// Pixi v8, which clips by the quad geometry and ignores shader alpha.
|
|
17657
19329
|
// AlphaMask renders the id-sampling mesh to a GPU texture first, so
|
|
@@ -17663,7 +19335,7 @@ function createPixiRegionLayer(options) {
|
|
|
17663
19335
|
removeExactCoverageMask(entry);
|
|
17664
19336
|
return false;
|
|
17665
19337
|
}
|
|
17666
|
-
mask.render({
|
|
19338
|
+
const alphaCoverage = mask.render({
|
|
17667
19339
|
artifact: { texture: coverageTexture },
|
|
17668
19340
|
coverage: coverageEntry,
|
|
17669
19341
|
crop,
|
|
@@ -17675,6 +19347,7 @@ function createPixiRegionLayer(options) {
|
|
|
17675
19347
|
x: entry.display.position.x,
|
|
17676
19348
|
y: entry.display.position.y,
|
|
17677
19349
|
});
|
|
19350
|
+
setCoverageChannel(entry, alphaCoverage);
|
|
17678
19351
|
return true;
|
|
17679
19352
|
}
|
|
17680
19353
|
if (renderer.source.coverage.kind !== RegionRendererCoverageKind.Polygon ||
|
|
@@ -17684,26 +19357,95 @@ function createPixiRegionLayer(options) {
|
|
|
17684
19357
|
return false;
|
|
17685
19358
|
}
|
|
17686
19359
|
removeExactCoverageMask(entry);
|
|
19360
|
+
return updateGraphicsCoverage(entry, renderer, crop, detection.polygon.points);
|
|
19361
|
+
}
|
|
19362
|
+
function updateGraphicsCoverage(entry, renderer, crop, points) {
|
|
17687
19363
|
const mask = entry.coverageMask ?? new options.Graphics();
|
|
17688
19364
|
if (!entry.coverageMask) {
|
|
17689
19365
|
entry.coverageMask = mask;
|
|
17690
|
-
|
|
17691
|
-
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);
|
|
17692
19376
|
}
|
|
17693
|
-
|
|
17694
|
-
.
|
|
17695
|
-
|
|
17696
|
-
|
|
17697
|
-
y - crop.y - crop.height / 2,
|
|
17698
|
-
]), true)
|
|
17699
|
-
.fill({ alpha: 1, color: 0xffffff });
|
|
19377
|
+
else {
|
|
19378
|
+
mask.rect(-crop.width / 2, -crop.height / 2, crop.width, crop.height);
|
|
19379
|
+
}
|
|
19380
|
+
mask.fill({ alpha: 1, color: 0xffffff });
|
|
17700
19381
|
mask.position.set(entry.display.position.x, entry.display.position.y);
|
|
17701
19382
|
mask.scale.set((entry.display.width / crop.width) *
|
|
17702
19383
|
(renderer.transform?.flip?.horizontal ? -1 : 1), (entry.display.height / crop.height) *
|
|
17703
19384
|
(renderer.transform?.flip?.vertical ? -1 : 1));
|
|
17704
19385
|
mask.rotation = entry.display.rotation;
|
|
19386
|
+
if (annotationAntialiasingEnabled()) {
|
|
19387
|
+
entry.display.mask = null;
|
|
19388
|
+
const effect = entry.coverageMaskEffect ??
|
|
19389
|
+
new options.AlphaMask({ mask: entry.coverageMaskContainer });
|
|
19390
|
+
if (!entry.coverageMaskEffect)
|
|
19391
|
+
entry.coverageMaskEffect = effect;
|
|
19392
|
+
if (!entry.display.effects?.includes(effect)) {
|
|
19393
|
+
effect.init(entry.coverageMaskContainer);
|
|
19394
|
+
entry.display.addEffect(effect);
|
|
19395
|
+
}
|
|
19396
|
+
const filter = ensureAntialiasFilter(entry, true);
|
|
19397
|
+
if (filter && mask.filters?.[0] !== filter)
|
|
19398
|
+
mask.filters = [filter];
|
|
19399
|
+
syncEntryViewportBounds(entry);
|
|
19400
|
+
setCoverageChannel(entry, true);
|
|
19401
|
+
}
|
|
19402
|
+
else {
|
|
19403
|
+
if (entry.coverageMaskEffect) {
|
|
19404
|
+
entry.display.removeEffect(entry.coverageMaskEffect);
|
|
19405
|
+
entry.coverageMaskEffect.reset();
|
|
19406
|
+
}
|
|
19407
|
+
if (mask.filters?.length)
|
|
19408
|
+
mask.filters = null;
|
|
19409
|
+
setCoverageChannel(entry, false);
|
|
19410
|
+
entry.display.mask = mask;
|
|
19411
|
+
}
|
|
17705
19412
|
return true;
|
|
17706
19413
|
}
|
|
19414
|
+
function syncEntryViewportBounds(entry) {
|
|
19415
|
+
entry.exactCoverageMask?.syncViewportBounds();
|
|
19416
|
+
if (entry.coverageMaskContainer) {
|
|
19417
|
+
syncPixiMaskViewportBounds(entry.coverageMaskContainer, options.getViewportBounds?.(), options.Rectangle);
|
|
19418
|
+
}
|
|
19419
|
+
}
|
|
19420
|
+
function annotationAntialiasingEnabled() {
|
|
19421
|
+
return Boolean(options.getAnnotationAntialiasing?.() &&
|
|
19422
|
+
options.Filter &&
|
|
19423
|
+
options.defaultFilterVert);
|
|
19424
|
+
}
|
|
19425
|
+
function ensureAntialiasFilter(entry, maskCoverage = false) {
|
|
19426
|
+
if (!options.Filter || !options.defaultFilterVert)
|
|
19427
|
+
return undefined;
|
|
19428
|
+
entry.antialiasFilter ??= createPixiAnnotationAntialiasFilter({
|
|
19429
|
+
maskCoverage,
|
|
19430
|
+
Filter: options.Filter,
|
|
19431
|
+
defaultFilterVert: options.defaultFilterVert,
|
|
19432
|
+
resolution: options.getAntialiasResolution?.(),
|
|
19433
|
+
});
|
|
19434
|
+
return entry.antialiasFilter;
|
|
19435
|
+
}
|
|
19436
|
+
function updateAssetAntialiasing(entry, renderer) {
|
|
19437
|
+
if (renderer.source.kind !== RegionRendererSourceKind.Asset)
|
|
19438
|
+
return;
|
|
19439
|
+
if (annotationAntialiasingEnabled()) {
|
|
19440
|
+
const filter = ensureAntialiasFilter(entry);
|
|
19441
|
+
if (filter && entry.display.filters?.[0] !== filter) {
|
|
19442
|
+
entry.display.filters = [filter];
|
|
19443
|
+
}
|
|
19444
|
+
}
|
|
19445
|
+
else if (entry.antialiasFilter && entry.display.filters?.length) {
|
|
19446
|
+
entry.display.filters = null;
|
|
19447
|
+
}
|
|
19448
|
+
}
|
|
17707
19449
|
function updateEffect(entry, renderer, viewportScale) {
|
|
17708
19450
|
const effect = renderer.source.kind === RegionRendererSourceKind.Media
|
|
17709
19451
|
? renderer.source.effect
|
|
@@ -17723,26 +19465,6 @@ function createPixiRegionLayer(options) {
|
|
|
17723
19465
|
}
|
|
17724
19466
|
entry.effect?.apply(entry.display, viewportScale);
|
|
17725
19467
|
}
|
|
17726
|
-
function removeCoverageMask(entry) {
|
|
17727
|
-
removePolygonCoverageMask(entry);
|
|
17728
|
-
removeExactCoverageMask(entry);
|
|
17729
|
-
}
|
|
17730
|
-
function removePolygonCoverageMask(entry) {
|
|
17731
|
-
if (!entry.coverageMask)
|
|
17732
|
-
return;
|
|
17733
|
-
entry.display.mask = null;
|
|
17734
|
-
entry.coverageMask.removeFromParent();
|
|
17735
|
-
entry.coverageMask.destroy();
|
|
17736
|
-
entry.coverageMask = undefined;
|
|
17737
|
-
}
|
|
17738
|
-
function removeExactCoverageMask(entry) {
|
|
17739
|
-
if (!entry.exactCoverageMask)
|
|
17740
|
-
return;
|
|
17741
|
-
entry.display.removeEffect(entry.exactCoverageMask.effect);
|
|
17742
|
-
entry.exactCoverageMask.display.removeFromParent();
|
|
17743
|
-
entry.exactCoverageMask.destroy();
|
|
17744
|
-
entry.exactCoverageMask = undefined;
|
|
17745
|
-
}
|
|
17746
19468
|
function destroyRendererDisplays(rendererId) {
|
|
17747
19469
|
for (const [key, entry] of entries) {
|
|
17748
19470
|
if (entry.rendererId !== rendererId)
|
|
@@ -17774,17 +19496,53 @@ function createPixiRegionLayer(options) {
|
|
|
17774
19496
|
pools.clear();
|
|
17775
19497
|
}
|
|
17776
19498
|
}
|
|
19499
|
+
function setCoverageChannel(entry, alpha) {
|
|
19500
|
+
if ((entry.alphaCoverage ?? false) === alpha)
|
|
19501
|
+
return;
|
|
19502
|
+
entry.display.setMask({ channel: alpha ? "alpha" : "red" });
|
|
19503
|
+
entry.alphaCoverage = alpha;
|
|
19504
|
+
}
|
|
19505
|
+
function removeCoverageMask(entry) {
|
|
19506
|
+
removePolygonCoverageMask(entry);
|
|
19507
|
+
removeExactCoverageMask(entry);
|
|
19508
|
+
}
|
|
19509
|
+
function removePolygonCoverageMask(entry) {
|
|
19510
|
+
if (!entry.coverageMask)
|
|
19511
|
+
return;
|
|
19512
|
+
entry.display.mask = null;
|
|
19513
|
+
if (entry.coverageMaskEffect) {
|
|
19514
|
+
entry.display.removeEffect(entry.coverageMaskEffect);
|
|
19515
|
+
entry.coverageMaskEffect.destroy();
|
|
19516
|
+
entry.coverageMaskEffect = undefined;
|
|
19517
|
+
}
|
|
19518
|
+
destroyEntryAntialiasFilter(entry);
|
|
19519
|
+
entry.coverageMask.removeFromParent();
|
|
19520
|
+
entry.coverageMask.destroy();
|
|
19521
|
+
entry.coverageMask = undefined;
|
|
19522
|
+
entry.coverageMaskContainer?.removeFromParent();
|
|
19523
|
+
entry.coverageMaskContainer?.destroy();
|
|
19524
|
+
entry.coverageMaskContainer = undefined;
|
|
19525
|
+
setCoverageChannel(entry, false);
|
|
19526
|
+
}
|
|
19527
|
+
function removeExactCoverageMask(entry) {
|
|
19528
|
+
if (!entry.exactCoverageMask)
|
|
19529
|
+
return;
|
|
19530
|
+
entry.display.removeEffect(entry.exactCoverageMask.effect);
|
|
19531
|
+
entry.exactCoverageMask.container.removeFromParent();
|
|
19532
|
+
entry.exactCoverageMask.destroy();
|
|
19533
|
+
entry.exactCoverageMask = undefined;
|
|
19534
|
+
setCoverageChannel(entry, false);
|
|
19535
|
+
}
|
|
19536
|
+
function updateEntryResolution(entry, resolution) {
|
|
19537
|
+
if (entry.antialiasFilter)
|
|
19538
|
+
entry.antialiasFilter.resolution = resolution;
|
|
19539
|
+
entry.exactCoverageMask?.setAntialiasResolution(resolution);
|
|
19540
|
+
}
|
|
17777
19541
|
function destroyEntry(entry) {
|
|
17778
19542
|
entry.effect?.destroy();
|
|
17779
19543
|
entry.effect = undefined;
|
|
17780
|
-
entry
|
|
17781
|
-
entry
|
|
17782
|
-
entry.coverageMask?.destroy();
|
|
17783
|
-
if (entry.exactCoverageMask) {
|
|
17784
|
-
entry.display.removeEffect(entry.exactCoverageMask.effect);
|
|
17785
|
-
entry.exactCoverageMask.display.removeFromParent();
|
|
17786
|
-
entry.exactCoverageMask.destroy();
|
|
17787
|
-
}
|
|
19544
|
+
removeCoverageMask(entry);
|
|
19545
|
+
destroyEntryAntialiasFilter(entry);
|
|
17788
19546
|
entry.display.removeFromParent?.();
|
|
17789
19547
|
// GifSprite sources are shared and released through Assets.unload().
|
|
17790
19548
|
entry.display.destroy();
|
|
@@ -17792,6 +19550,18 @@ function destroyEntry(entry) {
|
|
|
17792
19550
|
// lightweight crop texture; the scene destroys the shared source.
|
|
17793
19551
|
entry.ownedTexture?.destroy(false);
|
|
17794
19552
|
}
|
|
19553
|
+
function destroyEntryAntialiasFilter(entry) {
|
|
19554
|
+
if (!entry.antialiasFilter)
|
|
19555
|
+
return;
|
|
19556
|
+
if (entry.display.filters?.includes(entry.antialiasFilter)) {
|
|
19557
|
+
entry.display.filters = null;
|
|
19558
|
+
}
|
|
19559
|
+
if (entry.coverageMask?.filters?.includes(entry.antialiasFilter)) {
|
|
19560
|
+
entry.coverageMask.filters = null;
|
|
19561
|
+
}
|
|
19562
|
+
entry.antialiasFilter.destroy();
|
|
19563
|
+
entry.antialiasFilter = undefined;
|
|
19564
|
+
}
|
|
17795
19565
|
function isAnimatedDisplay(display) {
|
|
17796
19566
|
return "play" in display && typeof display.play === "function";
|
|
17797
19567
|
}
|
|
@@ -17987,6 +19757,7 @@ function acquireAsset(loader, src) {
|
|
|
17987
19757
|
|
|
17988
19758
|
function createPixiAnnotationOverlayLayer(editingEngine, initialStyle, initialKeypointStyle) {
|
|
17989
19759
|
let graphics;
|
|
19760
|
+
let renderedEditingBox;
|
|
17990
19761
|
let style = resolveAnnotationOverlayStyle(initialStyle);
|
|
17991
19762
|
let keypointStyle = initialKeypointStyle === undefined
|
|
17992
19763
|
? new BaseKeypointStyle()
|
|
@@ -18003,12 +19774,16 @@ function createPixiAnnotationOverlayLayer(editingEngine, initialStyle, initialKe
|
|
|
18003
19774
|
if (next !== undefined)
|
|
18004
19775
|
keypointStyle = next;
|
|
18005
19776
|
},
|
|
19777
|
+
getRenderedEditingBox() {
|
|
19778
|
+
return renderedEditingBox;
|
|
19779
|
+
},
|
|
18006
19780
|
draw(context) {
|
|
19781
|
+
renderedEditingBox = undefined;
|
|
18007
19782
|
if (!graphics)
|
|
18008
19783
|
return;
|
|
18009
19784
|
graphics.clear();
|
|
18010
19785
|
drawExternalPreview(graphics, context.previewOverlay, context.viewportScale, style);
|
|
18011
|
-
drawEditingPreview(graphics, editingEngine, context.frame, context.viewportScale, style, keypointStyle);
|
|
19786
|
+
renderedEditingBox = drawEditingPreview(graphics, editingEngine, context.frame, context.viewportScale, style, keypointStyle);
|
|
18012
19787
|
drawSelectionHandles(graphics, context, style, editingEngine);
|
|
18013
19788
|
drawMarquee(graphics, context.marquee, style);
|
|
18014
19789
|
drawGuides(graphics, editingEngine, context, style);
|
|
@@ -18021,6 +19796,7 @@ function drawEditingPreview(graphics, engine, frame, viewportScale, style, keypo
|
|
|
18021
19796
|
const detection = state?.preview;
|
|
18022
19797
|
if (!detection)
|
|
18023
19798
|
return;
|
|
19799
|
+
let renderedBox;
|
|
18024
19800
|
const styleContext = {
|
|
18025
19801
|
gestureKind: state.kind,
|
|
18026
19802
|
viewportScale,
|
|
@@ -18047,11 +19823,16 @@ function drawEditingPreview(graphics, engine, frame, viewportScale, style, keypo
|
|
|
18047
19823
|
{ x: x + width / 2, y: y + height / 2 },
|
|
18048
19824
|
{ x: x - width / 2, y: y + height / 2 },
|
|
18049
19825
|
], true, stroke, viewportScale);
|
|
19826
|
+
if (width > 0 &&
|
|
19827
|
+
height > 0 &&
|
|
19828
|
+
(boxFill.alpha > 0 || (stroke.alpha > 0 && stroke.width > 0))) {
|
|
19829
|
+
renderedBox = { detection, rect: detection.rect };
|
|
19830
|
+
}
|
|
18050
19831
|
}
|
|
18051
19832
|
if (detection.polygon) {
|
|
18052
19833
|
if (state.kind === AnnotationGestureStateKind.Creating) {
|
|
18053
19834
|
drawCreatingPolygon(graphics, detection.polygon.points, viewportScale, stroke, closeZoneStroke);
|
|
18054
|
-
return;
|
|
19835
|
+
return renderedBox;
|
|
18055
19836
|
}
|
|
18056
19837
|
graphics
|
|
18057
19838
|
.poly(detection.polygon.points.flatMap(({ x, y }) => [x, y]), true)
|
|
@@ -18083,6 +19864,7 @@ function drawEditingPreview(graphics, engine, frame, viewportScale, style, keypo
|
|
|
18083
19864
|
if (instruction)
|
|
18084
19865
|
drawPixiKeypointInstruction(graphics, instruction, viewportScale);
|
|
18085
19866
|
}
|
|
19867
|
+
return renderedBox;
|
|
18086
19868
|
}
|
|
18087
19869
|
const CREATION_VERTEX_RADIUS = 6;
|
|
18088
19870
|
const CREATION_VERTEX_STROKE_WIDTH = 1.5;
|
|
@@ -18335,92 +20117,6 @@ function resolveAnnotationOverlayStyle(style) {
|
|
|
18335
20117
|
};
|
|
18336
20118
|
}
|
|
18337
20119
|
|
|
18338
|
-
var PixiSceneLayerKind;
|
|
18339
|
-
(function (PixiSceneLayerKind) {
|
|
18340
|
-
PixiSceneLayerKind["Media"] = "media";
|
|
18341
|
-
PixiSceneLayerKind["Depth"] = "depth";
|
|
18342
|
-
PixiSceneLayerKind["Heatmap"] = "heatmap";
|
|
18343
|
-
PixiSceneLayerKind["Mask"] = "mask";
|
|
18344
|
-
PixiSceneLayerKind["Box"] = "box";
|
|
18345
|
-
PixiSceneLayerKind["Vector"] = "vector";
|
|
18346
|
-
PixiSceneLayerKind["Region"] = "region";
|
|
18347
|
-
PixiSceneLayerKind["Focus"] = "focus";
|
|
18348
|
-
PixiSceneLayerKind["Preview"] = "preview";
|
|
18349
|
-
PixiSceneLayerKind["Guide"] = "guide";
|
|
18350
|
-
PixiSceneLayerKind["Handle"] = "handle";
|
|
18351
|
-
PixiSceneLayerKind["Interaction"] = "interaction";
|
|
18352
|
-
PixiSceneLayerKind["Label"] = "label";
|
|
18353
|
-
})(PixiSceneLayerKind || (PixiSceneLayerKind = {}));
|
|
18354
|
-
function createPixiSceneLayerSlot(kind, order = defaultLayerOrder(kind)) {
|
|
18355
|
-
let display;
|
|
18356
|
-
return {
|
|
18357
|
-
getDisplay() {
|
|
18358
|
-
return display;
|
|
18359
|
-
},
|
|
18360
|
-
kind,
|
|
18361
|
-
order,
|
|
18362
|
-
setDisplay(nextDisplay) {
|
|
18363
|
-
display = nextDisplay;
|
|
18364
|
-
},
|
|
18365
|
-
};
|
|
18366
|
-
}
|
|
18367
|
-
function syncPixiSceneLayerChildren(scene, slots) {
|
|
18368
|
-
if (!scene) {
|
|
18369
|
-
return;
|
|
18370
|
-
}
|
|
18371
|
-
const displays = [...slots]
|
|
18372
|
-
.sort((left, right) => left.order - right.order)
|
|
18373
|
-
.map((slot) => slot.getDisplay())
|
|
18374
|
-
.filter((display) => display !== undefined);
|
|
18375
|
-
clearSceneChildren(scene);
|
|
18376
|
-
if (displays.length === 0) {
|
|
18377
|
-
return;
|
|
18378
|
-
}
|
|
18379
|
-
scene.addChild(...displays);
|
|
18380
|
-
}
|
|
18381
|
-
function defaultLayerOrder(kind) {
|
|
18382
|
-
switch (kind) {
|
|
18383
|
-
case PixiSceneLayerKind.Media:
|
|
18384
|
-
return 0;
|
|
18385
|
-
case PixiSceneLayerKind.Depth:
|
|
18386
|
-
return 25;
|
|
18387
|
-
case PixiSceneLayerKind.Heatmap:
|
|
18388
|
-
return 50;
|
|
18389
|
-
case PixiSceneLayerKind.Mask:
|
|
18390
|
-
return 100;
|
|
18391
|
-
case PixiSceneLayerKind.Box:
|
|
18392
|
-
return 200;
|
|
18393
|
-
case PixiSceneLayerKind.Vector:
|
|
18394
|
-
return 250;
|
|
18395
|
-
case PixiSceneLayerKind.Region:
|
|
18396
|
-
return 275;
|
|
18397
|
-
case PixiSceneLayerKind.Focus:
|
|
18398
|
-
return 300;
|
|
18399
|
-
case PixiSceneLayerKind.Preview:
|
|
18400
|
-
return 400;
|
|
18401
|
-
case PixiSceneLayerKind.Guide:
|
|
18402
|
-
return 500;
|
|
18403
|
-
case PixiSceneLayerKind.Handle:
|
|
18404
|
-
return 750;
|
|
18405
|
-
case PixiSceneLayerKind.Interaction:
|
|
18406
|
-
return 700;
|
|
18407
|
-
case PixiSceneLayerKind.Label:
|
|
18408
|
-
return 800;
|
|
18409
|
-
default:
|
|
18410
|
-
return 350;
|
|
18411
|
-
}
|
|
18412
|
-
}
|
|
18413
|
-
function clearSceneChildren(scene) {
|
|
18414
|
-
if (typeof scene.removeChildren === "function") {
|
|
18415
|
-
scene.removeChildren();
|
|
18416
|
-
return;
|
|
18417
|
-
}
|
|
18418
|
-
const fakeScene = scene;
|
|
18419
|
-
if (Array.isArray(fakeScene.children)) {
|
|
18420
|
-
fakeScene.children.splice(0);
|
|
18421
|
-
}
|
|
18422
|
-
}
|
|
18423
|
-
|
|
18424
20120
|
function calculatePixiSceneFit(options) {
|
|
18425
20121
|
const { fit, mediaHeight, mediaWidth, screenHeight, screenWidth } = options;
|
|
18426
20122
|
if (mediaWidth <= 0 ||
|
|
@@ -18456,6 +20152,7 @@ const REGION_COVERAGE_ONLY_MASK_STYLE = {
|
|
|
18456
20152
|
const STATIC_FOCUS_SETTLE_MS = 10_000;
|
|
18457
20153
|
/** The presentation fields a render answers to that no renderer kind owns. */
|
|
18458
20154
|
const RENDERED_PRESENTATION_TAIL = [
|
|
20155
|
+
"annotationAntialiasing",
|
|
18459
20156
|
"annotationOverlayStyle",
|
|
18460
20157
|
"backgroundColor",
|
|
18461
20158
|
"focusStyle",
|
|
@@ -18551,6 +20248,14 @@ async function createPixiMediaScene(options) {
|
|
|
18551
20248
|
let currentFocusStyle = options.focusStyle ?? null;
|
|
18552
20249
|
let currentLabelStyle = options.labelStyle ?? null;
|
|
18553
20250
|
let currentMaskStyle = options.maskStyle ?? null;
|
|
20251
|
+
let annotationAntialiasing = options.annotationAntialiasing ?? false;
|
|
20252
|
+
let annotationAntialiasResolution = 1;
|
|
20253
|
+
const annotationAntialiasLayer = createPixiAnnotationAntialiasLayer({
|
|
20254
|
+
Container,
|
|
20255
|
+
Filter,
|
|
20256
|
+
defaultFilterVert,
|
|
20257
|
+
getEnabled: () => Boolean(annotationAntialiasing),
|
|
20258
|
+
});
|
|
18554
20259
|
let currentMaskHaloStyle = options.maskHaloStyle ?? null;
|
|
18555
20260
|
let currentBoxCornerStyle = options.boxCornerStyle ?? null;
|
|
18556
20261
|
let currentEllipseStyle = options.ellipseStyle ?? null;
|
|
@@ -18572,6 +20277,9 @@ async function createPixiMediaScene(options) {
|
|
|
18572
20277
|
let isScrubbing = false;
|
|
18573
20278
|
let displayBrightness = 1;
|
|
18574
20279
|
let displayContrast = 1;
|
|
20280
|
+
let displayGamma = 1;
|
|
20281
|
+
let displayAutoContrast = false;
|
|
20282
|
+
let inspectionFilter;
|
|
18575
20283
|
let viewportScale = 1;
|
|
18576
20284
|
let hasPresentedSample = false;
|
|
18577
20285
|
let mediaHeight = 0;
|
|
@@ -18642,6 +20350,7 @@ async function createPixiMediaScene(options) {
|
|
|
18642
20350
|
isVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
|
|
18643
20351
|
// A partial heatmap changes pixels before the all-ready token changes.
|
|
18644
20352
|
onPreparedWindowChange: () => redrawAnnotationsNow(true),
|
|
20353
|
+
onTextureRelease: (texture) => focusLayer?.releaseMaskTexture(texture),
|
|
18645
20354
|
prepareTexture: (texture) => app.renderer.prepare.upload(texture),
|
|
18646
20355
|
renderPreparation: options.renderPreparation,
|
|
18647
20356
|
renderers: currentHeatmapRenderers,
|
|
@@ -18703,6 +20412,7 @@ async function createPixiMediaScene(options) {
|
|
|
18703
20412
|
resolveContextState,
|
|
18704
20413
|
})
|
|
18705
20414
|
: undefined;
|
|
20415
|
+
polygonLayer?.setPlaybackActive(isPlaybackActive);
|
|
18706
20416
|
const vectorLayer = createPixiVectorLayer({
|
|
18707
20417
|
container: retainedGeometry,
|
|
18708
20418
|
Container,
|
|
@@ -18732,6 +20442,9 @@ async function createPixiMediaScene(options) {
|
|
|
18732
20442
|
Texture,
|
|
18733
20443
|
UniformGroup,
|
|
18734
20444
|
detectionTimeline: annotationDetectionTimeline,
|
|
20445
|
+
getAnnotationAntialiasing: () => Boolean(annotationAntialiasing),
|
|
20446
|
+
getAntialiasResolution: () => annotationAntialiasResolution,
|
|
20447
|
+
getViewportBounds: resolveViewportBounds,
|
|
18735
20448
|
getActiveRegionMaskCoverage: (detectionFrameTime) => maskLayer?.getActiveRegionMaskCoverage(detectionFrameTime) ?? null,
|
|
18736
20449
|
// Under GPU compositing the decoded frame lands in a texture the
|
|
18737
20450
|
// compositor swaps onto the media sprite, leaving the canvas that
|
|
@@ -18783,6 +20496,7 @@ async function createPixiMediaScene(options) {
|
|
|
18783
20496
|
Text,
|
|
18784
20497
|
detectionTimeline: annotationDetectionTimeline,
|
|
18785
20498
|
labelStyle: options.labelStyle,
|
|
20499
|
+
getRenderedBoxes: () => boxLayer.getRenderedBoxes(),
|
|
18786
20500
|
resolveContextState: resolveLabelContextState,
|
|
18787
20501
|
})
|
|
18788
20502
|
: undefined;
|
|
@@ -18796,6 +20510,7 @@ async function createPixiMediaScene(options) {
|
|
|
18796
20510
|
resolution: resolvePixiResolution(options.maxDevicePixelRatio),
|
|
18797
20511
|
});
|
|
18798
20512
|
const destroyBatchTextureBindings = installPixiBatchTextureBindings(pixi, app.renderer);
|
|
20513
|
+
const destroyFilterBindings = installPixiFilterBindings(pixi, app.renderer);
|
|
18799
20514
|
const rendererCanvas = app.canvas;
|
|
18800
20515
|
rendererCanvas.style.display = "block";
|
|
18801
20516
|
rendererCanvas.style.position = "absolute";
|
|
@@ -18841,6 +20556,7 @@ async function createPixiMediaScene(options) {
|
|
|
18841
20556
|
const focusSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Focus);
|
|
18842
20557
|
const previewSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Preview);
|
|
18843
20558
|
const interactionSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Interaction);
|
|
20559
|
+
const interactionLabelSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Label, interactionSlot.order);
|
|
18844
20560
|
const handleSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Handle);
|
|
18845
20561
|
const labelSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Label);
|
|
18846
20562
|
const layerSlots = [
|
|
@@ -18855,6 +20571,7 @@ async function createPixiMediaScene(options) {
|
|
|
18855
20571
|
previewSlot,
|
|
18856
20572
|
handleSlot,
|
|
18857
20573
|
interactionSlot,
|
|
20574
|
+
interactionLabelSlot,
|
|
18858
20575
|
labelSlot,
|
|
18859
20576
|
];
|
|
18860
20577
|
const viewport = createViewportController({ scale: 1 });
|
|
@@ -19026,13 +20743,18 @@ async function createPixiMediaScene(options) {
|
|
|
19026
20743
|
}
|
|
19027
20744
|
let focusLayer = options.focusStyle
|
|
19028
20745
|
? createPixiFocusLayer({
|
|
20746
|
+
AlphaMask,
|
|
19029
20747
|
Container,
|
|
19030
20748
|
Graphics,
|
|
19031
20749
|
ImageSource,
|
|
19032
20750
|
Mesh,
|
|
19033
20751
|
MeshGeometry,
|
|
20752
|
+
Rectangle,
|
|
19034
20753
|
Shader,
|
|
19035
20754
|
UniformGroup,
|
|
20755
|
+
getHeatmapRenderers: () => currentHeatmapRenderers,
|
|
20756
|
+
getAnnotationAntialiasing: () => Boolean(annotationAntialiasing),
|
|
20757
|
+
getViewportBounds: resolveViewportBounds,
|
|
19036
20758
|
focusStyle: options.focusStyle,
|
|
19037
20759
|
isDetectionVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
|
|
19038
20760
|
})
|
|
@@ -19042,7 +20764,9 @@ async function createPixiMediaScene(options) {
|
|
|
19042
20764
|
let stagingTextureSource;
|
|
19043
20765
|
const collectFrameTimings = options.diagnostics?.frameTimings === true;
|
|
19044
20766
|
const syncSceneChildren = () => {
|
|
19045
|
-
|
|
20767
|
+
labelLayer?.setBackgroundAntialiasFilter(annotationAntialiasLayer.getFxaaFilter());
|
|
20768
|
+
interactionPresentationLayer?.setBackgroundAntialiasFilter(annotationAntialiasLayer.getFxaaFilter());
|
|
20769
|
+
annotationAntialiasLayer.sync(mediaScene, layerSlots);
|
|
19046
20770
|
};
|
|
19047
20771
|
const updateMediaSceneFit = () => {
|
|
19048
20772
|
if (!mediaScene || mediaWidth <= 0 || mediaHeight <= 0) {
|
|
@@ -19108,8 +20832,24 @@ async function createPixiMediaScene(options) {
|
|
|
19108
20832
|
rendererCanvas.style.left = `${next.left}px`;
|
|
19109
20833
|
rendererCanvas.style.top = `${next.top}px`;
|
|
19110
20834
|
app.renderer.resize(next.width, next.height, presentationResolution);
|
|
20835
|
+
syncAnnotationAntialiasResolution();
|
|
19111
20836
|
return presentationBox;
|
|
19112
20837
|
};
|
|
20838
|
+
const syncAnnotationAntialiasResolution = () => {
|
|
20839
|
+
annotationAntialiasResolution = resolvePixiAnnotationAntialiasResolution(presentationResolution, containerSize, maxTextureSize(), annotationAntialiasing === 2 ? 2 : 1);
|
|
20840
|
+
annotationAntialiasLayer.setResolution(annotationAntialiasResolution);
|
|
20841
|
+
regionLayer.setAntialiasResolution(annotationAntialiasResolution);
|
|
20842
|
+
syncMaskRasterDisplay();
|
|
20843
|
+
};
|
|
20844
|
+
function syncMaskRasterDisplay() {
|
|
20845
|
+
if (!rasterDisplay)
|
|
20846
|
+
return;
|
|
20847
|
+
const scale = annotationAntialiasing
|
|
20848
|
+
? annotationAntialiasResolution / presentationResolution
|
|
20849
|
+
: 1;
|
|
20850
|
+
maskLayer?.setRasterDisplay(rasterDisplay, scale);
|
|
20851
|
+
polygonLayer?.setRasterDisplay(rasterDisplay, scale);
|
|
20852
|
+
}
|
|
19113
20853
|
const syncDisplaySizing = () => {
|
|
19114
20854
|
if (!baseFit || mediaWidth <= 0 || mediaHeight <= 0)
|
|
19115
20855
|
return;
|
|
@@ -19126,7 +20866,7 @@ async function createPixiMediaScene(options) {
|
|
|
19126
20866
|
return;
|
|
19127
20867
|
}
|
|
19128
20868
|
rasterDisplay = display;
|
|
19129
|
-
|
|
20869
|
+
syncMaskRasterDisplay();
|
|
19130
20870
|
options.onDisplayChange?.(display);
|
|
19131
20871
|
};
|
|
19132
20872
|
const updateMediaSampling = () => {
|
|
@@ -19147,12 +20887,26 @@ async function createPixiMediaScene(options) {
|
|
|
19147
20887
|
return;
|
|
19148
20888
|
const transform = viewport.getTransform();
|
|
19149
20889
|
viewportScale = baseFit.scale * transform.scale;
|
|
20890
|
+
maskLayer?.setViewportScale(viewportScale);
|
|
20891
|
+
polygonLayer?.setViewportScale(viewportScale);
|
|
19150
20892
|
updateMediaSampling();
|
|
19151
20893
|
mediaScene.scale.set(viewportScale);
|
|
19152
20894
|
const box = syncPresentationBox();
|
|
19153
20895
|
mediaScene.position.set(baseFit.x + transform.x - box.left, baseFit.y + transform.y - box.top);
|
|
20896
|
+
focusLayer?.syncViewportBounds();
|
|
20897
|
+
regionLayer.syncViewportBounds();
|
|
19154
20898
|
maskBrushPreview?.setViewportScale(viewportScale);
|
|
19155
20899
|
};
|
|
20900
|
+
function resolveViewportBounds() {
|
|
20901
|
+
if (!mediaScene || viewportScale <= 0)
|
|
20902
|
+
return undefined;
|
|
20903
|
+
return {
|
|
20904
|
+
x: -mediaScene.x / viewportScale,
|
|
20905
|
+
y: -mediaScene.y / viewportScale,
|
|
20906
|
+
width: app.renderer.screen.width / viewportScale,
|
|
20907
|
+
height: app.renderer.screen.height / viewportScale,
|
|
20908
|
+
};
|
|
20909
|
+
}
|
|
19156
20910
|
const mediaPointToScreen = (point) => {
|
|
19157
20911
|
const fit = baseFit ?? { scale: 1, x: 0, y: 0 };
|
|
19158
20912
|
const screenPoint = viewport.mediaToScreen({
|
|
@@ -19174,6 +20928,7 @@ async function createPixiMediaScene(options) {
|
|
|
19174
20928
|
syncPresentationBox();
|
|
19175
20929
|
updateMediaSceneFit();
|
|
19176
20930
|
syncDisplaySizing();
|
|
20931
|
+
drawInteractionPresentationLayer(currentMediaTime);
|
|
19177
20932
|
renderOnChange();
|
|
19178
20933
|
});
|
|
19179
20934
|
measureContainer();
|
|
@@ -19389,6 +21144,7 @@ async function createPixiMediaScene(options) {
|
|
|
19389
21144
|
interactionSlot.setDisplay(interactionPresentationDisplay || interactionHitDisplay
|
|
19390
21145
|
? interactionDisplay
|
|
19391
21146
|
: undefined);
|
|
21147
|
+
interactionLabelSlot.setDisplay(interactionPresentationLayer?.getLabelDisplay() ?? undefined);
|
|
19392
21148
|
attachFocusLayerDisplay();
|
|
19393
21149
|
attachMaskLayerDisplay();
|
|
19394
21150
|
attachLabelLayerDisplay();
|
|
@@ -19405,6 +21161,7 @@ async function createPixiMediaScene(options) {
|
|
|
19405
21161
|
setPlaybackActive(active) {
|
|
19406
21162
|
isPlaybackActive = active;
|
|
19407
21163
|
maskLayer?.setPlaybackActive(active);
|
|
21164
|
+
polygonLayer?.setPlaybackActive(active);
|
|
19408
21165
|
depthSource?.setPlaybackActive?.(active);
|
|
19409
21166
|
},
|
|
19410
21167
|
setScrubbing(scrubbing) {
|
|
@@ -19429,6 +21186,7 @@ async function createPixiMediaScene(options) {
|
|
|
19429
21186
|
if (!collectFrameTimings) {
|
|
19430
21187
|
sample.draw(stagingContext, 0, 0, mediaWidth, mediaHeight);
|
|
19431
21188
|
presentedSampleTimestamp = sample.timestamp;
|
|
21189
|
+
refreshInspectionImage();
|
|
19432
21190
|
stagingTextureSource?.update();
|
|
19433
21191
|
stagingTexture?.update();
|
|
19434
21192
|
const { boxState, regionState } = drawFramePresentLayers(annotationDrawLayers, sample.timestamp);
|
|
@@ -19444,6 +21202,7 @@ async function createPixiMediaScene(options) {
|
|
|
19444
21202
|
const mediaUploadMs = measure(() => {
|
|
19445
21203
|
sample.draw(stagingContext, 0, 0, mediaWidth, mediaHeight);
|
|
19446
21204
|
presentedSampleTimestamp = sample.timestamp;
|
|
21205
|
+
refreshInspectionImage();
|
|
19447
21206
|
stagingTextureSource?.update();
|
|
19448
21207
|
stagingTexture?.update();
|
|
19449
21208
|
});
|
|
@@ -19503,19 +21262,32 @@ async function createPixiMediaScene(options) {
|
|
|
19503
21262
|
syncPresentationBox();
|
|
19504
21263
|
updateMediaSceneFit();
|
|
19505
21264
|
syncDisplaySizing();
|
|
21265
|
+
drawInteractionPresentationLayer(currentMediaTime);
|
|
19506
21266
|
renderOnChange();
|
|
19507
21267
|
},
|
|
19508
21268
|
setDisplayAdjustments(adjustments) {
|
|
19509
21269
|
void import('pixi.js').then(({ ColorMatrixFilter }) => {
|
|
21270
|
+
if (isDestroyed)
|
|
21271
|
+
return;
|
|
19510
21272
|
const brightness = adjustments.brightness ?? 1;
|
|
19511
21273
|
const contrast = adjustments.contrast ?? 1;
|
|
19512
21274
|
const filter = new ColorMatrixFilter();
|
|
19513
21275
|
// Preserve the renderer's pixel grid through the adjustment pass.
|
|
19514
21276
|
filter.resolution = "inherit";
|
|
19515
21277
|
filter.brightness(brightness, false);
|
|
19516
|
-
|
|
19517
|
-
|
|
19518
|
-
|
|
21278
|
+
displayGamma = adjustments.gamma ?? 1;
|
|
21279
|
+
displayAutoContrast = stillFrame && adjustments.autoContrast === true;
|
|
21280
|
+
filter.contrast(displayAutoContrast ? 0 : contrast - 1, true);
|
|
21281
|
+
if (mediaSprite) {
|
|
21282
|
+
if (displayGamma !== 1 || displayAutoContrast) {
|
|
21283
|
+
inspectionFilter ??= createPixiMediaInspectionFilter(mediaSprite, pixi);
|
|
21284
|
+
inspectionFilter.update(displayGamma, displayAutoContrast, hasPresentedSample ? stagingCanvas : null);
|
|
21285
|
+
mediaSprite.filters = [inspectionFilter.filter, filter];
|
|
21286
|
+
}
|
|
21287
|
+
else {
|
|
21288
|
+
mediaSprite.filters = [filter];
|
|
21289
|
+
}
|
|
21290
|
+
}
|
|
19519
21291
|
displayBrightness = brightness;
|
|
19520
21292
|
displayContrast = contrast;
|
|
19521
21293
|
renderOnChange();
|
|
@@ -19620,6 +21392,12 @@ async function createPixiMediaScene(options) {
|
|
|
19620
21392
|
setPresentation(presentation, mediaTime) {
|
|
19621
21393
|
appliedPresentation = presentation;
|
|
19622
21394
|
currentMediaTime = mediaTime;
|
|
21395
|
+
if (annotationAntialiasing !==
|
|
21396
|
+
(presentation.annotationAntialiasing ?? false)) {
|
|
21397
|
+
annotationAntialiasing = presentation.annotationAntialiasing ?? false;
|
|
21398
|
+
syncAnnotationAntialiasResolution();
|
|
21399
|
+
syncSceneChildren();
|
|
21400
|
+
}
|
|
19623
21401
|
if (presentation.backgroundColor !== undefined) {
|
|
19624
21402
|
app.renderer.background.color = presentation.backgroundColor;
|
|
19625
21403
|
applyBackdropColor(backdrop, presentation.backgroundColor);
|
|
@@ -19822,6 +21600,11 @@ async function createPixiMediaScene(options) {
|
|
|
19822
21600
|
// a decoder buffer in a producer that outlives the scene.
|
|
19823
21601
|
frameChannel?.onPresentedFrame((presented) => presented.frame.close());
|
|
19824
21602
|
destroyBatchTextureBindings();
|
|
21603
|
+
destroyFilterBindings();
|
|
21604
|
+
labelLayer?.setBackgroundAntialiasFilter(null);
|
|
21605
|
+
interactionPresentationLayer?.setBackgroundAntialiasFilter(null);
|
|
21606
|
+
annotationAntialiasLayer.destroy();
|
|
21607
|
+
inspectionFilter?.destroy();
|
|
19825
21608
|
mediaCompositor?.destroy();
|
|
19826
21609
|
interactionLayer?.destroy();
|
|
19827
21610
|
interactionPresentationLayer?.destroy();
|
|
@@ -19929,8 +21712,8 @@ async function createPixiMediaScene(options) {
|
|
|
19929
21712
|
renderPreparation: options.renderPreparation,
|
|
19930
21713
|
resolveInstructions: resolveArtifactMaskInstructions,
|
|
19931
21714
|
});
|
|
19932
|
-
|
|
19933
|
-
|
|
21715
|
+
syncMaskRasterDisplay();
|
|
21716
|
+
maskLayer.setViewportScale(viewportScale);
|
|
19934
21717
|
maskLayer.setPlaybackActive(isPlaybackActive);
|
|
19935
21718
|
if (timelineContext) {
|
|
19936
21719
|
maskLayer.setTimelineContext(timelineContext);
|
|
@@ -20184,6 +21967,9 @@ async function createPixiMediaScene(options) {
|
|
|
20184
21967
|
renderPreparation: options.renderPreparation,
|
|
20185
21968
|
resolveContextState,
|
|
20186
21969
|
});
|
|
21970
|
+
polygonLayer.setPlaybackActive(isPlaybackActive);
|
|
21971
|
+
syncMaskRasterDisplay();
|
|
21972
|
+
polygonLayer.setViewportScale(viewportScale);
|
|
20187
21973
|
if (timelineContext) {
|
|
20188
21974
|
polygonLayer.setTimelineContext(timelineContext);
|
|
20189
21975
|
}
|
|
@@ -20224,14 +22010,19 @@ async function createPixiMediaScene(options) {
|
|
|
20224
22010
|
function ensureFocusLayer(focusStyle) {
|
|
20225
22011
|
if (!focusLayer) {
|
|
20226
22012
|
focusLayer = createPixiFocusLayer({
|
|
22013
|
+
AlphaMask,
|
|
20227
22014
|
Container,
|
|
20228
22015
|
Graphics,
|
|
20229
22016
|
ImageSource,
|
|
20230
22017
|
Mesh,
|
|
20231
22018
|
MeshGeometry,
|
|
22019
|
+
Rectangle,
|
|
20232
22020
|
Shader,
|
|
20233
22021
|
UniformGroup,
|
|
20234
22022
|
focusStyle,
|
|
22023
|
+
getHeatmapRenderers: () => currentHeatmapRenderers,
|
|
22024
|
+
getAnnotationAntialiasing: () => Boolean(annotationAntialiasing),
|
|
22025
|
+
getViewportBounds: resolveViewportBounds,
|
|
20235
22026
|
isDetectionVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
|
|
20236
22027
|
});
|
|
20237
22028
|
}
|
|
@@ -20272,6 +22063,7 @@ async function createPixiMediaScene(options) {
|
|
|
20272
22063
|
const interactionState = interactionLayer?.getState();
|
|
20273
22064
|
focusLayer.drawFrame({
|
|
20274
22065
|
frame,
|
|
22066
|
+
heatmapArtifacts: heatmapLayer?.getActiveFocusArtifacts(frame) ?? [],
|
|
20275
22067
|
hoveredPick: withEditingPreview(filterVisiblePick(interactionState?.hoveredPick ?? null)),
|
|
20276
22068
|
idMaskArtifact: maskLayer?.getActiveIdMaskFrameTexture(frame?.mediaTime ?? null) ??
|
|
20277
22069
|
null,
|
|
@@ -20304,6 +22096,9 @@ async function createPixiMediaScene(options) {
|
|
|
20304
22096
|
mediaTime,
|
|
20305
22097
|
selectedPick: interactionState?.selectedPick ?? null,
|
|
20306
22098
|
selectedPicks: interactionState?.selectedPicks,
|
|
22099
|
+
strokePixelRatio: annotationAntialiasing
|
|
22100
|
+
? annotationAntialiasResolution
|
|
22101
|
+
: presentationResolution,
|
|
20307
22102
|
viewportScale,
|
|
20308
22103
|
});
|
|
20309
22104
|
}
|
|
@@ -20382,9 +22177,6 @@ async function createPixiMediaScene(options) {
|
|
|
20382
22177
|
const frame = annotationDetectionTimeline.selectFrame(mediaTime);
|
|
20383
22178
|
const interactionState = interactionLayer?.getState();
|
|
20384
22179
|
const editingState = options.editingEngine?.getState();
|
|
20385
|
-
labelLayer?.drawCreationPreview(editingState?.kind === AnnotationGestureStateKind.Creating
|
|
20386
|
-
? editingState.preview
|
|
20387
|
-
: null, mediaTime, viewportScale);
|
|
20388
22180
|
const previewOverlay = options.previewOverlay?.() ?? null;
|
|
20389
22181
|
drawnEditingState = editingState;
|
|
20390
22182
|
// Only a scene that renders on change compares signatures.
|
|
@@ -20404,6 +22196,18 @@ async function createPixiMediaScene(options) {
|
|
|
20404
22196
|
viewportScale,
|
|
20405
22197
|
visibility: currentVisibility,
|
|
20406
22198
|
});
|
|
22199
|
+
const editingBox = annotationOverlayLayer.getRenderedEditingBox();
|
|
22200
|
+
const creationPreview = editingState?.kind === AnnotationGestureStateKind.Creating
|
|
22201
|
+
? editingState.preview
|
|
22202
|
+
: null;
|
|
22203
|
+
labelLayer?.drawCreationPreview(creationPreview, mediaTime, viewportScale, editingBox && editingBox.detection === creationPreview
|
|
22204
|
+
? editingBox.rect
|
|
22205
|
+
: undefined);
|
|
22206
|
+
if (editingState?.activeDetectionId != null) {
|
|
22207
|
+
labelLayer?.updateEditingBox(editingState.activeDetectionId, editingBox?.detection.id === editingState.activeDetectionId
|
|
22208
|
+
? editingBox.rect
|
|
22209
|
+
: undefined);
|
|
22210
|
+
}
|
|
20407
22211
|
}
|
|
20408
22212
|
/**
|
|
20409
22213
|
* Milliseconds for animations that are not annotation geometry. A pull scene
|
|
@@ -20532,6 +22336,12 @@ async function createPixiMediaScene(options) {
|
|
|
20532
22336
|
app.ticker.remove(drawAnnotationOverlayNow);
|
|
20533
22337
|
}
|
|
20534
22338
|
}
|
|
22339
|
+
function refreshInspectionImage() {
|
|
22340
|
+
if (!stillFrame || !inspectionFilter)
|
|
22341
|
+
return;
|
|
22342
|
+
inspectionFilter.invalidateImage();
|
|
22343
|
+
inspectionFilter.update(displayGamma, displayAutoContrast, stagingCanvas);
|
|
22344
|
+
}
|
|
20535
22345
|
function hasLoadingDetections() {
|
|
20536
22346
|
const loading = currentVisibility?.loadingDetectionIds;
|
|
20537
22347
|
if (!loading)
|
|
@@ -20563,6 +22373,8 @@ async function createPixiMediaScene(options) {
|
|
|
20563
22373
|
app.renderer.resolution,
|
|
20564
22374
|
displayBrightness,
|
|
20565
22375
|
displayContrast,
|
|
22376
|
+
displayGamma,
|
|
22377
|
+
displayAutoContrast,
|
|
20566
22378
|
...RENDERED_PRESENTATION_FIELDS.map((field) => appliedPresentation?.[field]),
|
|
20567
22379
|
// Styles above compare by identity; mask opacity is applied after
|
|
20568
22380
|
// preparation, so a host can move it inside a style object it keeps.
|
|
@@ -20663,6 +22475,7 @@ async function createPixiMediaScene(options) {
|
|
|
20663
22475
|
Text,
|
|
20664
22476
|
detectionTimeline: annotationDetectionTimeline,
|
|
20665
22477
|
labelStyle,
|
|
22478
|
+
getRenderedBoxes: () => boxLayer.getRenderedBoxes(),
|
|
20666
22479
|
resolveContextState: resolveLabelContextState,
|
|
20667
22480
|
});
|
|
20668
22481
|
}
|
|
@@ -21879,6 +23692,7 @@ async function createMediaSession(options) {
|
|
|
21879
23692
|
...options.renderer,
|
|
21880
23693
|
...sessionMedia.rendererSourceOption,
|
|
21881
23694
|
backgroundColor: initialPresentation.backgroundColor,
|
|
23695
|
+
annotationAntialiasing: initialPresentation.annotationAntialiasing,
|
|
21882
23696
|
annotationOverlayStyle: initialPresentation.annotationOverlayStyle,
|
|
21883
23697
|
boxStyle: initialPresentation.boxStyle,
|
|
21884
23698
|
boxCornerStyle: initialPresentation.boxCornerStyle,
|