supervision 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/index.d.ts +11 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1232 -107
  4. package/dist/index.js.map +1 -1
  5. package/dist/mask-preparation.worker.js +43 -9
  6. package/dist/mask-preparation.worker.js.map +1 -1
  7. package/dist/render-preparation/mask-frame-artifact.d.ts +6 -0
  8. package/dist/render-preparation/mask-frame-artifact.d.ts.map +1 -1
  9. package/dist/render-preparation/mask-frame-compositor.d.ts +1 -0
  10. package/dist/render-preparation/mask-frame-compositor.d.ts.map +1 -1
  11. package/dist/render-preparation/mask-frame-preparer.d.ts.map +1 -1
  12. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts +1 -0
  13. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts.map +1 -1
  14. package/dist/renderers/annotation-shape-styles.d.ts +17 -0
  15. package/dist/renderers/annotation-shape-styles.d.ts.map +1 -0
  16. package/dist/renderers/media-renderer-core.d.ts.map +1 -1
  17. package/dist/renderers/media-renderer-scene.d.ts +17 -2
  18. package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
  19. package/dist/renderers/pixi-annotation-overlay-layer.d.ts.map +1 -1
  20. package/dist/renderers/pixi-focus-layer.d.ts +1 -0
  21. package/dist/renderers/pixi-focus-layer.d.ts.map +1 -1
  22. package/dist/renderers/pixi-interaction-layer.d.ts +2 -0
  23. package/dist/renderers/pixi-interaction-layer.d.ts.map +1 -1
  24. package/dist/renderers/pixi-interaction-presentation-layer.d.ts +2 -1
  25. package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -1
  26. package/dist/renderers/pixi-mask-halo.d.ts +94 -0
  27. package/dist/renderers/pixi-mask-halo.d.ts.map +1 -0
  28. package/dist/renderers/pixi-mask-layer.d.ts +10 -1
  29. package/dist/renderers/pixi-mask-layer.d.ts.map +1 -1
  30. package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
  31. package/dist/renderers/pixi-path.d.ts +13 -7
  32. package/dist/renderers/pixi-path.d.ts.map +1 -1
  33. package/dist/renderers/pixi-polygon-layer.d.ts.map +1 -1
  34. package/dist/renderers/pixi-region-layer.d.ts +42 -0
  35. package/dist/renderers/pixi-region-layer.d.ts.map +1 -0
  36. package/dist/renderers/pixi-scene-layer-slot.d.ts +1 -0
  37. package/dist/renderers/pixi-scene-layer-slot.d.ts.map +1 -1
  38. package/dist/renderers/pixi-vector-layer.d.ts +3 -1
  39. package/dist/renderers/pixi-vector-layer.d.ts.map +1 -1
  40. package/dist/sessions/media-session.d.ts.map +1 -1
  41. package/dist/types/media-renderer.d.ts +1 -1
  42. package/dist/types/media-renderer.d.ts.map +1 -1
  43. package/node_modules/supervision-js-core/dist/index.d.ts +19 -3
  44. package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
  45. package/node_modules/supervision-js-core/dist/index.js +944 -157
  46. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  47. package/node_modules/supervision-js-core/dist/interactions/detection-picker.d.ts +18 -0
  48. package/node_modules/supervision-js-core/dist/interactions/detection-picker.d.ts.map +1 -1
  49. package/node_modules/supervision-js-core/dist/styles/annotation-renderer-presentation.d.ts +9 -0
  50. package/node_modules/supervision-js-core/dist/styles/annotation-renderer-presentation.d.ts.map +1 -0
  51. package/node_modules/supervision-js-core/dist/styles/annotation-renderer-registry.d.ts +47 -0
  52. package/node_modules/supervision-js-core/dist/styles/annotation-renderer-registry.d.ts.map +1 -0
  53. package/node_modules/supervision-js-core/dist/styles/box-corner-style.d.ts +14 -0
  54. package/node_modules/supervision-js-core/dist/styles/box-corner-style.d.ts.map +1 -0
  55. package/node_modules/supervision-js-core/dist/styles/box-style.d.ts.map +1 -1
  56. package/node_modules/supervision-js-core/dist/styles/default-annotation-presentation.d.ts +38 -0
  57. package/node_modules/supervision-js-core/dist/styles/default-annotation-presentation.d.ts.map +1 -1
  58. package/node_modules/supervision-js-core/dist/styles/interaction-style.d.ts.map +1 -1
  59. package/node_modules/supervision-js-core/dist/styles/keypoint-style.d.ts.map +1 -1
  60. package/node_modules/supervision-js-core/dist/styles/marker-style.d.ts +14 -0
  61. package/node_modules/supervision-js-core/dist/styles/marker-style.d.ts.map +1 -0
  62. package/node_modules/supervision-js-core/dist/styles/polygon-style.d.ts.map +1 -1
  63. package/node_modules/supervision-js-core/dist/styles/polyline-style.d.ts.map +1 -1
  64. package/node_modules/supervision-js-core/dist/styles/source-presentation.d.ts +18 -2
  65. package/node_modules/supervision-js-core/dist/styles/source-presentation.d.ts.map +1 -1
  66. package/node_modules/supervision-js-core/dist/styles/stroke-style.d.ts +12 -0
  67. package/node_modules/supervision-js-core/dist/styles/stroke-style.d.ts.map +1 -0
  68. package/node_modules/supervision-js-core/dist/types/annotation-renderer.d.ts +197 -0
  69. package/node_modules/supervision-js-core/dist/types/annotation-renderer.d.ts.map +1 -0
  70. package/node_modules/supervision-js-core/dist/types/box-corner-style.d.ts +31 -0
  71. package/node_modules/supervision-js-core/dist/types/box-corner-style.d.ts.map +1 -0
  72. package/node_modules/supervision-js-core/dist/types/box-style.d.ts +4 -17
  73. package/node_modules/supervision-js-core/dist/types/box-style.d.ts.map +1 -1
  74. package/node_modules/supervision-js-core/dist/types/ellipse-style.d.ts +30 -0
  75. package/node_modules/supervision-js-core/dist/types/ellipse-style.d.ts.map +1 -0
  76. package/node_modules/supervision-js-core/dist/types/interaction.d.ts +12 -0
  77. package/node_modules/supervision-js-core/dist/types/interaction.d.ts.map +1 -1
  78. package/node_modules/supervision-js-core/dist/types/marker-style.d.ts +46 -0
  79. package/node_modules/supervision-js-core/dist/types/marker-style.d.ts.map +1 -0
  80. package/node_modules/supervision-js-core/dist/types/mask-halo-style.d.ts +29 -0
  81. package/node_modules/supervision-js-core/dist/types/mask-halo-style.d.ts.map +1 -0
  82. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +26 -1
  83. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
  84. package/node_modules/supervision-js-core/dist/types/paint-style.d.ts +36 -0
  85. package/node_modules/supervision-js-core/dist/types/paint-style.d.ts.map +1 -0
  86. package/node_modules/supervision-js-core/dist/types/shape-style.d.ts +112 -0
  87. package/node_modules/supervision-js-core/dist/types/shape-style.d.ts.map +1 -0
  88. package/node_modules/supervision-js-core/dist/utils/shape-geometry.d.ts +50 -0
  89. package/node_modules/supervision-js-core/dist/utils/shape-geometry.d.ts.map +1 -0
  90. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { copySortedDetectionFrames, detectionFrameOverlapsRange, includeDefined, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createIdleDetectionBufferState, MediaInteractionMode, BoxStrokeAlignment, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, BaseFocusStyle, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget, createDetectionPickKey, rebaseDetectionPickToFrame, pickDetectionAtPoint, pickAnnotationHandle, getAnnotationHandles, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, decodeCompressedRleMask, LabelPlacement, BasePolygonStyle, BasePolylineStyle, BaseKeypointStyle, KeypointMarkerShape, BaseInteractionStyle, DetectionInteractionState, MAX_ID_MASK_PALETTE_ENTRIES, DetectionBufferStatus, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MAX_ID_MASK_STROKE_WIDTH, resolveMaskStyleOpacity, pickDetectionByMaskId, resolveStyleValue, lightenColor, createViewportController, resolveAnnotationStyleState, MediaSessionMode, DetectionFrameRetentionMode, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, createCompositeDetectionFrameSource, MediaSessionActivityStatus, MediaSessionActivityKind, MediaSessionStatus, createSourceAwarePresentation } from 'supervision-js-core';
2
- export { BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMaskStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, BoxStrokeAlignment, DEFAULT_DETECTION_CLASS_STYLES, DEFAULT_DETECTION_COLOR_SEQUENCE, DetectionBufferStatus, DetectionFrameRetentionMode, DetectionFrameSelectionMode, DetectionInteractionState, DetectionMaskEncoding, DetectionPickTarget, FocusTargetMode, KeypointMarkerShape, KeypointVisibility, LabelPlacement, LabelVisibilityMode, MaskRenderMode, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, SUPERVISION_ROBOFLOW_COLOR, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, normalizeDetectionClassName, pickDetectionAtPoint, resolveDetectionClassColorStyle } from 'supervision-js-core';
1
+ import { copySortedDetectionFrames, detectionFrameOverlapsRange, includeDefined, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createArrayDetectionFrameSource, createBufferedDetectionTimeline, resolveAnnotationRendererPresentation, createIdleDetectionBufferState, MediaInteractionMode, StrokeAlignment, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, BaseFocusStyle, decodeCompressedRleMask, extractMaskRectRuns, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget, createDetectionPickKey, rebaseDetectionPickToFrame, pickDetectionAtPoint, followDetectionPickAcrossFrames, haveSameDetectionPickIdentities, pickAnnotationHandle, getAnnotationHandles, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, LabelPlacement, BasePolygonStyle, BasePolylineStyle, BaseKeypointStyle, KeypointMarkerShape, MarkerSizeSpace, MarkerShape, ShapeInstructionKind, sampleEllipseArc, resolveEllipseSegmentCount, resolveMarkerGeometry, BaseInteractionStyle, DetectionInteractionState, MAX_ID_MASK_PALETTE_ENTRIES, DetectionBufferStatus, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MAX_ID_MASK_STROKE_WIDTH, resolveMaskStyleOpacity, pickDetectionByMaskId, BoxStrokeAlignment, resolveStyleValue, resolveAnnotationStyleState, lightenColor, createViewportController, MediaSessionMode, DetectionFrameRetentionMode, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, createCompositeDetectionFrameSource, MediaSessionActivityStatus, MediaSessionActivityKind, MediaSessionStatus, createSourceAwarePresentation } from 'supervision-js-core';
2
+ export { BaseBoxCornerStyle, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMarkerStyle, BaseMaskStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, BoxStrokeAlignment, DEFAULT_DETECTION_CLASS_STYLES, DEFAULT_DETECTION_COLOR_SEQUENCE, DetectionBufferStatus, DetectionFrameRetentionMode, DetectionFrameSelectionMode, DetectionInteractionState, DetectionMaskEncoding, DetectionPickTarget, FocusTargetMode, KeypointMarkerShape, KeypointVisibility, LabelPlacement, LabelVisibilityMode, MarkerShape, MarkerSizeSpace, MaskRenderMode, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, RegionRendererComposeMode, RegionRendererRegionKind, RegionRendererSourceKind, SUPERVISION_ROBOFLOW_COLOR, annotationRendererKinds, annotationRenderers, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, normalizeDetectionClassName, pickDetectionAtPoint, resolveDetectionClassColorStyle } from 'supervision-js-core';
3
3
 
4
4
  const DEFAULT_DATABASE_NAME = "supervision-js-detection-frames";
5
5
  const DEFAULT_CHUNK_DURATION_SECONDS = 1;
@@ -2231,7 +2231,7 @@ async function createMediaRendererCore(options, providers) {
2231
2231
  throw new RangeError("playbackRate must be a finite number greater than zero.");
2232
2232
  }
2233
2233
  const defaultPresentation = createDefaultAnnotationPresentation();
2234
- const resolvePresentation = (presentation) => ({
2234
+ const resolvePresentation = (presentation) => resolveAnnotationRendererPresentation({
2235
2235
  ...defaultPresentation,
2236
2236
  ...presentation,
2237
2237
  });
@@ -2239,13 +2239,18 @@ async function createMediaRendererCore(options, providers) {
2239
2239
  annotationOverlayStyle: options.annotationOverlayStyle,
2240
2240
  backgroundColor: options.backgroundColor,
2241
2241
  boxStyle: options.boxStyle,
2242
+ boxCornerStyle: options.boxCornerStyle,
2243
+ ellipseStyle: options.ellipseStyle,
2242
2244
  focusStyle: options.focusStyle,
2243
2245
  interactionStyle: options.interactionStyle,
2244
2246
  keypointStyle: options.keypointStyle,
2245
2247
  labelStyle: options.labelStyle,
2248
+ maskHaloStyle: options.maskHaloStyle,
2246
2249
  maskStyle: options.maskStyle,
2250
+ markerStyle: options.markerStyle,
2247
2251
  polygonStyle: options.polygonStyle,
2248
2252
  polylineStyle: options.polylineStyle,
2253
+ renderers: options.renderers,
2249
2254
  visibility: options.visibility,
2250
2255
  });
2251
2256
  let detectionTimeline;
@@ -2537,6 +2542,8 @@ async function createMediaRendererCore(options, providers) {
2537
2542
  annotationOverlayStyle: currentPresentation.annotationOverlayStyle,
2538
2543
  backgroundColor: currentPresentation.backgroundColor,
2539
2544
  boxStyle: currentPresentation.boxStyle,
2545
+ boxCornerStyle: currentPresentation.boxCornerStyle,
2546
+ ellipseStyle: currentPresentation.ellipseStyle,
2540
2547
  canInteract: () => canInteract(options, runtimeState.isPlaybackActive()),
2541
2548
  container: options.container,
2542
2549
  detectionTimeline,
@@ -2549,10 +2556,18 @@ async function createMediaRendererCore(options, providers) {
2549
2556
  keypointStyle: currentPresentation.keypointStyle,
2550
2557
  labelStyle: currentPresentation.labelStyle,
2551
2558
  maskBrush: options.createMaskBrush?.(mediaDimensions) ?? options.maskBrush,
2559
+ maskHaloStyle: currentPresentation.maskHaloStyle,
2552
2560
  maskStyle: currentPresentation.maskStyle,
2561
+ markerStyle: currentPresentation.markerStyle,
2553
2562
  maxDevicePixelRatio: options.maxDevicePixelRatio,
2563
+ onPresentationUpdate(presentedSample) {
2564
+ if (!runtimeState.isDestroyed()) {
2565
+ runtimeState.recordPresentationUpdate(presentedSample);
2566
+ }
2567
+ },
2554
2568
  polygonStyle: currentPresentation.polygonStyle,
2555
2569
  polylineStyle: currentPresentation.polylineStyle,
2570
+ regionRenderers: resolveRegionRenderers(currentPresentation),
2556
2571
  previewOverlay: options.previewOverlay,
2557
2572
  renderPreparation: options.renderPreparation
2558
2573
  ? {
@@ -2698,6 +2713,10 @@ async function createMediaRendererCore(options, providers) {
2698
2713
  }
2699
2714
  }
2700
2715
  }
2716
+ function resolveRegionRenderers(presentation) {
2717
+ return (presentation.renderers?.filter((renderer) => renderer.kind === "region") ??
2718
+ []);
2719
+ }
2701
2720
  async function openRendererMediaSource(options, providers) {
2702
2721
  if (options.src !== undefined && options.source !== undefined) {
2703
2722
  throw new Error("Provide either src or source, not both.");
@@ -2791,10 +2810,10 @@ function drawPixiPath(graphics, points, closed, stroke, viewportScale) {
2791
2810
  if (points.length < 2) {
2792
2811
  return;
2793
2812
  }
2794
- if (stroke.dash?.length) {
2795
- appendDashedPath(graphics, points, closed, stroke.dash, viewportScale);
2796
- }
2797
- else {
2813
+ const appendedDash = stroke.dash?.length
2814
+ ? appendDashedPath(graphics, points, closed, stroke.dash, viewportScale)
2815
+ : false;
2816
+ if (!appendedDash) {
2798
2817
  graphics.moveTo(points[0].x, points[0].y);
2799
2818
  for (let index = 1; index < points.length; index += 1) {
2800
2819
  graphics.lineTo(points[index].x, points[index].y);
@@ -2808,16 +2827,21 @@ function drawPixiPath(graphics, points, closed, stroke, viewportScale) {
2808
2827
  function resolvePixiStroke(stroke, viewportScale) {
2809
2828
  const pixiStroke = {
2810
2829
  alpha: stroke.alpha,
2830
+ ...(stroke.cap === undefined ? {} : { cap: stroke.cap }),
2811
2831
  color: stroke.color,
2832
+ ...(stroke.join === undefined ? {} : { join: stroke.join }),
2833
+ ...(stroke.miterLimit === undefined
2834
+ ? {}
2835
+ : { miterLimit: stroke.miterLimit }),
2812
2836
  width: resolveScreenLength(stroke.width, viewportScale),
2813
2837
  };
2814
2838
  if (stroke.alignment === undefined)
2815
2839
  return pixiStroke;
2816
2840
  return {
2817
2841
  ...pixiStroke,
2818
- alignment: stroke.alignment === BoxStrokeAlignment.Inside
2842
+ alignment: stroke.alignment === StrokeAlignment.Inside
2819
2843
  ? 1
2820
- : stroke.alignment === BoxStrokeAlignment.Outside
2844
+ : stroke.alignment === StrokeAlignment.Outside
2821
2845
  ? 0
2822
2846
  : 0.5,
2823
2847
  };
@@ -2827,7 +2851,7 @@ function appendDashedPath(graphics, points, closed, dashPattern, viewportScale)
2827
2851
  .filter((value) => Number.isFinite(value) && value > 0)
2828
2852
  .map((value) => resolveScreenLength(value, viewportScale));
2829
2853
  if (pattern.length === 0) {
2830
- return;
2854
+ return false;
2831
2855
  }
2832
2856
  if (pattern.length % 2 === 1) {
2833
2857
  pattern.push(...pattern);
@@ -2835,6 +2859,7 @@ function appendDashedPath(graphics, points, closed, dashPattern, viewportScale)
2835
2859
  let patternIndex = 0;
2836
2860
  let patternRemaining = pattern[0];
2837
2861
  let drawing = true;
2862
+ let penDown = false;
2838
2863
  const segmentCount = closed ? points.length : points.length - 1;
2839
2864
  for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex += 1) {
2840
2865
  const start = points[segmentIndex];
@@ -2855,8 +2880,14 @@ function appendDashedPath(graphics, points, closed, dashPattern, viewportScale)
2855
2880
  const x2 = start.x + dx * endRatio;
2856
2881
  const y2 = start.y + dy * endRatio;
2857
2882
  if (drawing) {
2858
- graphics.moveTo(x1, y1);
2883
+ if (!penDown) {
2884
+ graphics.moveTo(x1, y1);
2885
+ }
2859
2886
  graphics.lineTo(x2, y2);
2887
+ penDown = true;
2888
+ }
2889
+ else {
2890
+ penDown = false;
2860
2891
  }
2861
2892
  offset += step;
2862
2893
  patternRemaining -= step;
@@ -2864,9 +2895,13 @@ function appendDashedPath(graphics, points, closed, dashPattern, viewportScale)
2864
2895
  patternIndex = (patternIndex + 1) % pattern.length;
2865
2896
  patternRemaining = pattern[patternIndex];
2866
2897
  drawing = !drawing;
2898
+ if (!drawing) {
2899
+ penDown = false;
2900
+ }
2867
2901
  }
2868
2902
  }
2869
2903
  }
2904
+ return true;
2870
2905
  }
2871
2906
 
2872
2907
  function createPixiBoxLayer(options) {
@@ -3055,6 +3090,10 @@ function createPixiFocusLayer(options) {
3055
3090
  let targetAlpha = 0;
3056
3091
  let lastTick = null;
3057
3092
  let isDestroyed = false;
3093
+ // A frame can fall back to a composited RGBA texture when its colored ID-mask
3094
+ // palette is exhausted. Keep decoded row runs by the immutable mask payload so
3095
+ // that fallback focus still cuts out the actual mask, not its bounding rect.
3096
+ const maskCutoutRuns = new WeakMap();
3058
3097
  return {
3059
3098
  createDisplay({ width, height }) {
3060
3099
  mediaWidth = width;
@@ -3099,13 +3138,19 @@ function createPixiFocusLayer(options) {
3099
3138
  hide();
3100
3139
  return;
3101
3140
  }
3102
- const instruction = focusStyle.resolve({
3141
+ const resolvedInstruction = focusStyle.resolve({
3103
3142
  frame: context.frame,
3104
3143
  hoveredPick: context.hoveredPick,
3105
3144
  mediaTime: context.mediaTime,
3106
3145
  selectedPick: context.selectedPick,
3107
3146
  viewportScale: context.viewportScale,
3108
3147
  });
3148
+ const instruction = resolvedInstruction
3149
+ ? {
3150
+ ...resolvedInstruction,
3151
+ targets: resolvedInstruction.targets.filter(({ detection }) => options.isDetectionVisible?.(detection) ?? true),
3152
+ }
3153
+ : undefined;
3109
3154
  if (!instruction || instruction.targets.length === 0) {
3110
3155
  transitionToHidden();
3111
3156
  return;
@@ -3163,8 +3208,10 @@ function createPixiFocusLayer(options) {
3163
3208
  if (!focusGraphics) {
3164
3209
  return;
3165
3210
  }
3166
- const targetsWithRects = instruction.targets.filter((target) => target.detection.rect || target.detection.polygon);
3167
- if (targetsWithRects.length === 0) {
3211
+ const targetsWithGeometry = instruction.targets.filter((target) => target.detection.mask ||
3212
+ target.detection.rect ||
3213
+ target.detection.polygon);
3214
+ if (targetsWithGeometry.length === 0) {
3168
3215
  hideVectorFocus();
3169
3216
  return;
3170
3217
  }
@@ -3175,24 +3222,30 @@ function createPixiFocusLayer(options) {
3175
3222
  if (focusMaskGraphics) {
3176
3223
  focusMaskGraphics.visible = true;
3177
3224
  focusMaskGraphics.clear();
3178
- for (const target of targetsWithRects) {
3179
- drawCutoutShape(focusMaskGraphics, target, instruction);
3180
- focusMaskGraphics.fill({ alpha: 1, color: 0xffffff });
3225
+ for (const target of targetsWithGeometry) {
3226
+ if (drawCutoutShape(focusMaskGraphics, target, instruction) === "drawn") {
3227
+ focusMaskGraphics.fill({ alpha: 1, color: 0xffffff });
3228
+ }
3181
3229
  }
3182
3230
  return;
3183
3231
  }
3184
- for (const target of targetsWithRects) {
3185
- drawCutoutShape(focusGraphics, target, instruction);
3186
- focusGraphics.cut();
3232
+ for (const target of targetsWithGeometry) {
3233
+ if (drawCutoutShape(focusGraphics, target, instruction) === "drawn") {
3234
+ focusGraphics.cut();
3235
+ }
3187
3236
  }
3188
3237
  }
3189
3238
  function drawCutoutShape(graphics, target, instruction) {
3239
+ const maskCutout = drawMaskCutout(graphics, target);
3240
+ if (maskCutout) {
3241
+ return maskCutout;
3242
+ }
3190
3243
  if (target.detection.polygon?.points.length && graphics.poly) {
3191
3244
  graphics.poly(target.detection.polygon.points.flatMap(({ x, y }) => [x, y]), true);
3192
- return;
3245
+ return "drawn";
3193
3246
  }
3194
3247
  if (!target.detection.rect) {
3195
- return;
3248
+ return "empty";
3196
3249
  }
3197
3250
  const { rect } = target.detection;
3198
3251
  const fallback = instruction.fallback;
@@ -3203,6 +3256,44 @@ function createPixiFocusLayer(options) {
3203
3256
  else {
3204
3257
  graphics.rect(left, top, rect.width, rect.height);
3205
3258
  }
3259
+ return "drawn";
3260
+ }
3261
+ function drawMaskCutout(graphics, target) {
3262
+ const mask = target.detection.mask;
3263
+ if (!mask) {
3264
+ return undefined;
3265
+ }
3266
+ let runs;
3267
+ if (!maskCutoutRuns.has(mask)) {
3268
+ try {
3269
+ const decoded = decodeCompressedRleMask(mask);
3270
+ runs = {
3271
+ height: decoded.height,
3272
+ runs: extractMaskRectRuns(decoded.data, decoded.width, decoded.height) ??
3273
+ [],
3274
+ width: decoded.width,
3275
+ };
3276
+ }
3277
+ catch {
3278
+ // Preserve the documented rectangle fallback for malformed masks. A
3279
+ // valid mask must never lose its shape merely because ID-mask rendering
3280
+ // is unavailable, but an unreadable payload still has only bounds.
3281
+ runs = null;
3282
+ }
3283
+ maskCutoutRuns.set(mask, runs);
3284
+ }
3285
+ else {
3286
+ runs = maskCutoutRuns.get(mask);
3287
+ }
3288
+ if (runs === null) {
3289
+ return undefined;
3290
+ }
3291
+ const horizontalScale = mediaWidth / runs.width;
3292
+ const verticalScale = mediaHeight / runs.height;
3293
+ for (const run of runs.runs) {
3294
+ graphics.rect(run.x * horizontalScale, run.y * verticalScale, run.width * horizontalScale, run.height * verticalScale);
3295
+ }
3296
+ return runs.runs.length === 0 ? "empty" : "drawn";
3206
3297
  }
3207
3298
  function hide() {
3208
3299
  hideVectorFocus();
@@ -3271,7 +3362,7 @@ function createFocusIdMaskRenderer(options) {
3271
3362
  autoGenerateMipmaps: false,
3272
3363
  dynamic: false,
3273
3364
  height: 1,
3274
- resource: createPlaceholderCanvas$2(),
3365
+ resource: createPlaceholderCanvas$3(),
3275
3366
  scaleMode: "nearest",
3276
3367
  width: 1,
3277
3368
  });
@@ -3363,7 +3454,7 @@ function createPremultipliedColor(fill) {
3363
3454
  alpha,
3364
3455
  ]);
3365
3456
  }
3366
- function createPlaceholderCanvas$2() {
3457
+ function createPlaceholderCanvas$3() {
3367
3458
  const canvas = document.createElement("canvas");
3368
3459
  canvas.height = 1;
3369
3460
  canvas.width = 1;
@@ -3485,15 +3576,21 @@ function createPixiInteractionLayer(options) {
3485
3576
  drawFrame(mediaTime) {
3486
3577
  currentMediaTime = mediaTime;
3487
3578
  activeFrame = options.detectionTimeline.selectFrame(mediaTime);
3488
- if (!canHandleInteraction()) {
3489
- pointerPoint = null;
3490
- marqueeRect = null;
3491
- setHoveredPick(null);
3492
- setSelectedPick(null);
3493
- }
3494
- else {
3579
+ if (canHandleInteraction()) {
3495
3580
  clearStalePicks(activeFrame);
3581
+ return;
3496
3582
  }
3583
+ pointerPoint = null;
3584
+ marqueeRect = null;
3585
+ setHoveredPick(null);
3586
+ if (isDestroyed || mode === MediaInteractionMode.Disabled) {
3587
+ setSelectedPick(null);
3588
+ return;
3589
+ }
3590
+ // Paused-only interaction gates new picks during playback, but an
3591
+ // existing selection keeps following its detection so selection-driven
3592
+ // presentation such as focus survives frame advances.
3593
+ followSelectedPicks(activeFrame);
3497
3594
  },
3498
3595
  setSelectedDetection(selection) {
3499
3596
  if (selection === null) {
@@ -3512,14 +3609,15 @@ function createPixiInteractionLayer(options) {
3512
3609
  return createState();
3513
3610
  },
3514
3611
  cycleSelection(direction = 1) {
3515
- if (!activeFrame?.detections.length)
3612
+ const visibleDetections = activeFrame?.detections.flatMap((detection, detectionIndex) => canPickDetection(detection) ? [{ detection, detectionIndex }] : []);
3613
+ if (!activeFrame || !visibleDetections?.length)
3516
3614
  return null;
3517
- const currentIndex = selectedPick?.detectionIndex ?? (direction > 0 ? -1 : 0);
3518
- const nextIndex = (currentIndex + direction + activeFrame.detections.length) %
3519
- activeFrame.detections.length;
3520
- const detection = activeFrame.detections[nextIndex];
3615
+ const currentVisibleIndex = visibleDetections.findIndex(({ detectionIndex }) => detectionIndex === selectedPick?.detectionIndex);
3616
+ const nextVisibleIndex = (currentVisibleIndex + direction + visibleDetections.length) %
3617
+ visibleDetections.length;
3618
+ const { detection, detectionIndex } = visibleDetections[nextVisibleIndex];
3521
3619
  const next = createPickFromSelection({
3522
- detectionIndex: nextIndex,
3620
+ detectionIndex,
3523
3621
  point: getDetectionCenter(detection),
3524
3622
  });
3525
3623
  setSelectedPick(next);
@@ -3648,6 +3746,8 @@ function createPixiInteractionLayer(options) {
3648
3746
  return;
3649
3747
  if (didDragMarquee && marqueeRect && activeFrame) {
3650
3748
  selectedPicks = activeFrame.detections.flatMap((detection, detectionIndex) => {
3749
+ if (!canPickDetection(detection))
3750
+ return [];
3651
3751
  const rect = getDetectionRect(detection);
3652
3752
  if (!rect || !intersects(rect, marqueeRect))
3653
3753
  return [];
@@ -3684,10 +3784,13 @@ function createPixiInteractionLayer(options) {
3684
3784
  const pickPoint = { x: point.x, y: point.y };
3685
3785
  const labelPick = options.pickLabelDetectionAtPoint?.(pickPoint, currentMediaTime);
3686
3786
  const activeLabelPick = rebaseDetectionPickToFrame(labelPick ?? null, activeFrame);
3687
- if (activeLabelPick)
3787
+ if (activeLabelPick && canPickDetection(activeLabelPick.detection)) {
3688
3788
  return activeLabelPick;
3789
+ }
3689
3790
  const geometryPick = pickDetectionAtPoint(activeFrame, pickPoint, {
3690
3791
  ...options.interaction,
3792
+ filter: (detection, detectionIndex) => canPickDetection(detection) &&
3793
+ (options.interaction.filter?.(detection, detectionIndex) ?? true),
3691
3794
  includeMasks: options.pickMaskDetectionAtPoint === undefined,
3692
3795
  maskMediaDimensions: options.getMediaDimensions?.(),
3693
3796
  padding: pickPadding,
@@ -3697,7 +3800,9 @@ function createPixiInteractionLayer(options) {
3697
3800
  }
3698
3801
  const maskPick = options.pickMaskDetectionAtPoint?.(pickPoint, currentMediaTime);
3699
3802
  const activeMaskPick = rebaseDetectionPickToFrame(maskPick ?? null, activeFrame);
3700
- return activeMaskPick ?? geometryPick;
3803
+ return activeMaskPick && canPickDetection(activeMaskPick.detection)
3804
+ ? activeMaskPick
3805
+ : geometryPick;
3701
3806
  }
3702
3807
  function createPickFromSelection(selection) {
3703
3808
  const frame = selection.mediaTime === undefined
@@ -3709,7 +3814,10 @@ function createPixiInteractionLayer(options) {
3709
3814
  const detection = detectionIndex === undefined || detectionIndex < 0
3710
3815
  ? undefined
3711
3816
  : frame?.detections[detectionIndex];
3712
- if (!frame || detectionIndex === undefined || !detection) {
3817
+ if (!frame ||
3818
+ detectionIndex === undefined ||
3819
+ !detection ||
3820
+ !canPickDetection(detection)) {
3713
3821
  return null;
3714
3822
  }
3715
3823
  return {
@@ -3723,19 +3831,44 @@ function createPixiInteractionLayer(options) {
3723
3831
  };
3724
3832
  }
3725
3833
  function clearStalePicks(frame) {
3726
- const nextHoveredPick = rebaseDetectionPickToFrame(hoveredPick, frame);
3727
- const nextSelectedPick = rebaseDetectionPickToFrame(selectedPick, frame);
3834
+ // Hover stays bound to the pointer, so it keeps a per-frame lifetime.
3835
+ const nextHoveredPick = filterPick(rebaseDetectionPickToFrame(hoveredPick, frame));
3728
3836
  if (hoveredPick && !nextHoveredPick) {
3729
3837
  setHoveredPick(null);
3730
3838
  }
3731
3839
  else if (nextHoveredPick && nextHoveredPick !== hoveredPick) {
3732
- setHoveredPick(nextHoveredPick);
3840
+ // Hover remains pointer-owned. Refresh its frame snapshot without
3841
+ // emitting a second event-driven redraw from the normal frame path.
3842
+ hoveredPick = nextHoveredPick;
3843
+ hoveredPickKey = createDetectionPickKey(nextHoveredPick);
3733
3844
  }
3734
- if (selectedPick && !nextSelectedPick) {
3735
- setSelectedPick(null);
3845
+ followSelectedPicks(frame);
3846
+ }
3847
+ function followSelectedPicks(frame) {
3848
+ if (selectedPicks.length === 0) {
3849
+ return;
3850
+ }
3851
+ const nextSelectedPicks = selectedPicks.flatMap((pick) => {
3852
+ const nextPick = filterPick(followDetectionPickAcrossFrames(pick, frame));
3853
+ return nextPick ? [nextPick] : [];
3854
+ });
3855
+ const currentKeys = selectedPicks.map(createDetectionPickKey).join("|");
3856
+ const nextKeys = nextSelectedPicks.map(createDetectionPickKey).join("|");
3857
+ const identityChanged = !haveSameDetectionPickIdentities(selectedPicks, nextSelectedPicks);
3858
+ selectedPicks = nextSelectedPicks;
3859
+ selectedPick = nextSelectedPicks.at(-1) ?? null;
3860
+ if (nextKeys === currentKeys) {
3861
+ return;
3736
3862
  }
3737
- else if (nextSelectedPick && nextSelectedPick !== selectedPick) {
3738
- setSelectedPick(nextSelectedPick);
3863
+ selectedPickKey = createDetectionPickKey(selectedPick);
3864
+ // A followed pick re-bases onto every new frame, so its full pick key
3865
+ // changes at playback rate. The scene's normal frame path consumes this
3866
+ // refreshed state immediately; emitting onStateChange here would redraw
3867
+ // focus and interaction a second time. Public callbacks only report
3868
+ // identity or membership changes.
3869
+ if (identityChanged) {
3870
+ options.interaction.onSelect?.(selectedPick);
3871
+ options.interaction.onSelectionChange?.(selectedPicks);
3739
3872
  }
3740
3873
  }
3741
3874
  function setHoveredPick(nextPick) {
@@ -3844,6 +3977,12 @@ function createPixiInteractionLayer(options) {
3844
3977
  }
3845
3978
  return options.canInteract();
3846
3979
  }
3980
+ function canPickDetection(detection) {
3981
+ return options.canPickDetection?.(detection) ?? true;
3982
+ }
3983
+ function filterPick(pick) {
3984
+ return pick && canPickDetection(pick.detection) ? pick : null;
3985
+ }
3847
3986
  }
3848
3987
  function getDetectionCenter(detection) {
3849
3988
  const rect = getDetectionRect(detection);
@@ -3889,8 +4028,11 @@ function compositeMaskFrame(instructions) {
3889
4028
  }
3890
4029
  return { data, height, width };
3891
4030
  }
4031
+ function createMaskIdFrame(instructions) {
4032
+ return createIdMaskFrame(materializeMaskInstructions(instructions));
4033
+ }
3892
4034
  async function createPngIdMaskFrame(instructions) {
3893
- const frame = createIdMaskFrame(materializeMaskInstructions(instructions));
4035
+ const frame = createMaskIdFrame(instructions);
3894
4036
  if (!frame) {
3895
4037
  return undefined;
3896
4038
  }
@@ -4429,6 +4571,9 @@ function createPixiVectorLayer(options) {
4429
4571
  let keypointStyle = options.keypointStyle === undefined
4430
4572
  ? new BaseKeypointStyle()
4431
4573
  : options.keypointStyle;
4574
+ // Shape decorations are opt-in; no default style exists so configuring
4575
+ // nothing keeps the semantic-geometry skip untouched.
4576
+ let shapeStyle = options.shapeStyle ?? null;
4432
4577
  let styleVersion = 0;
4433
4578
  let drawnStyleVersion = -1;
4434
4579
  // A versioned source can replace a frame without changing its timeline key.
@@ -4465,7 +4610,8 @@ function createPixiVectorLayer(options) {
4465
4610
  const resolvedDetections = frame.detections.flatMap((detection, detectionIndex) => {
4466
4611
  if (!detection.polygon &&
4467
4612
  !detection.polyline &&
4468
- !detection.keypoints) {
4613
+ !detection.keypoints &&
4614
+ !shapeStyle) {
4469
4615
  return [];
4470
4616
  }
4471
4617
  const resolved = resolveDetection(detection, detectionIndex, frame, mediaTime, viewportScale);
@@ -4500,6 +4646,8 @@ function createPixiVectorLayer(options) {
4500
4646
  polylineStyle = styles.polylineStyle;
4501
4647
  if (styles.keypointStyle !== undefined)
4502
4648
  keypointStyle = styles.keypointStyle;
4649
+ if (styles.shapeStyle !== undefined)
4650
+ shapeStyle = styles.shapeStyle;
4503
4651
  styleVersion += 1;
4504
4652
  },
4505
4653
  destroy() {
@@ -4544,13 +4692,16 @@ function createPixiVectorLayer(options) {
4544
4692
  const polygon = polygonStyle?.resolve(detection, context);
4545
4693
  const polyline = polylineStyle?.resolve(detection, context);
4546
4694
  const keypoints = keypointStyle?.resolve(detection, context);
4547
- if (!polygon && !polyline && !keypoints)
4695
+ const shapes = shapeStyle?.resolve(detection, context);
4696
+ if (!polygon && !polyline && !keypoints && !shapes?.length) {
4548
4697
  return undefined;
4698
+ }
4549
4699
  return {
4550
4700
  key: detectionKey(detection, detectionIndex),
4551
4701
  keypoints,
4552
4702
  polygon,
4553
4703
  polyline,
4704
+ shapes,
4554
4705
  zIndex: detection.zIndex ?? detectionIndex,
4555
4706
  };
4556
4707
  }
@@ -4559,7 +4710,10 @@ function createPixiVectorLayer(options) {
4559
4710
  if (!entry.cleared)
4560
4711
  graphics.clear();
4561
4712
  entry.cleared = false;
4562
- const { keypoints, polygon, polyline } = detection;
4713
+ const { keypoints, polygon, polyline, shapes } = detection;
4714
+ for (const shape of shapes ?? []) {
4715
+ drawShapeInstruction(graphics, shape, viewportScale);
4716
+ }
4563
4717
  if (polygon) {
4564
4718
  graphics.poly(polygon.points.flatMap(({ x, y }) => [x, y]), true);
4565
4719
  if (polygon.fill)
@@ -4577,31 +4731,102 @@ function createPixiVectorLayer(options) {
4577
4731
  drawPixiPath(graphics, [edge.from, edge.to], false, edge.stroke, viewportScale);
4578
4732
  }
4579
4733
  for (const marker of keypoints.markers) {
4580
- const radius = resolveScreenLength(marker.radius, viewportScale);
4581
4734
  if (marker.shape === KeypointMarkerShape.Cross) {
4582
- drawPixiPath(graphics, [
4583
- { x: marker.point.x - radius, y: marker.point.y - radius },
4584
- { x: marker.point.x + radius, y: marker.point.y + radius },
4585
- ], false, marker.stroke ?? { alpha: 1, color: 0xffffff, width: 2 }, viewportScale);
4586
- drawPixiPath(graphics, [
4587
- { x: marker.point.x + radius, y: marker.point.y - radius },
4588
- { x: marker.point.x - radius, y: marker.point.y + radius },
4589
- ], false, marker.stroke ?? { alpha: 1, color: 0xffffff, width: 2 }, viewportScale);
4735
+ drawMarkerInstruction(graphics, {
4736
+ center: marker.point,
4737
+ kind: ShapeInstructionKind.Marker,
4738
+ shape: MarkerShape.Cross,
4739
+ size: marker.radius * 2,
4740
+ sizeSpace: MarkerSizeSpace.Screen,
4741
+ stroke: asOpenStroke(marker.stroke ?? { alpha: 1, color: 0xffffff, width: 2 }),
4742
+ }, viewportScale);
4590
4743
  }
4591
4744
  else {
4592
- graphics.circle(marker.point.x, marker.point.y, radius);
4593
- if (marker.fill)
4594
- graphics.fill(marker.fill);
4595
- if (marker.stroke)
4596
- graphics.stroke({
4597
- alpha: marker.stroke.alpha,
4598
- color: marker.stroke.color,
4599
- width: resolveScreenLength(marker.stroke.width, viewportScale),
4600
- });
4745
+ drawMarkerInstruction(graphics, {
4746
+ center: marker.point,
4747
+ fill: marker.fill,
4748
+ kind: ShapeInstructionKind.Marker,
4749
+ shape: MarkerShape.Circle,
4750
+ size: marker.radius * 2,
4751
+ sizeSpace: MarkerSizeSpace.Screen,
4752
+ stroke: marker.stroke,
4753
+ }, viewportScale);
4601
4754
  }
4602
4755
  }
4603
4756
  }
4604
4757
  }
4758
+ function drawShapeInstruction(graphics, instruction, viewportScale) {
4759
+ if (instruction.kind === ShapeInstructionKind.Ellipse) {
4760
+ const { closed, points } = sampleEllipseArc(instruction, resolveEllipseSegmentCount(instruction, viewportScale));
4761
+ if (closed && instruction.fill) {
4762
+ graphics.poly(points.flatMap(({ x, y }) => [x, y]), true);
4763
+ graphics.fill(instruction.fill);
4764
+ }
4765
+ if (instruction.stroke) {
4766
+ drawPixiPath(graphics, points, closed, instruction.stroke, viewportScale);
4767
+ }
4768
+ return;
4769
+ }
4770
+ if (instruction.kind === ShapeInstructionKind.Marker) {
4771
+ drawMarkerInstruction(graphics, instruction, viewportScale);
4772
+ return;
4773
+ }
4774
+ for (const segment of instruction.segments) {
4775
+ if (instruction.closed && instruction.fill) {
4776
+ graphics.poly(segment.flatMap(({ x, y }) => [x, y]), true);
4777
+ graphics.fill(instruction.fill);
4778
+ }
4779
+ drawPixiPath(graphics, segment, instruction.closed, instruction.stroke, viewportScale);
4780
+ }
4781
+ }
4782
+ function drawMarkerInstruction(graphics, instruction, viewportScale) {
4783
+ const geometry = resolveMarkerGeometry(instruction, viewportScale);
4784
+ if (geometry.kind === "circle") {
4785
+ const dashed = Boolean(instruction.stroke?.dash?.length);
4786
+ if (instruction.fill || !dashed) {
4787
+ graphics.circle(geometry.center.x, geometry.center.y, geometry.radius);
4788
+ if (instruction.fill)
4789
+ graphics.fill(instruction.fill);
4790
+ }
4791
+ if (instruction.stroke) {
4792
+ if (dashed) {
4793
+ const ellipse = {
4794
+ center: geometry.center,
4795
+ radiusX: geometry.radius,
4796
+ radiusY: geometry.radius,
4797
+ };
4798
+ const { points } = sampleEllipseArc(ellipse, resolveEllipseSegmentCount(ellipse, viewportScale));
4799
+ drawPixiPath(graphics, points, true, instruction.stroke, viewportScale);
4800
+ }
4801
+ else {
4802
+ graphics.stroke(resolvePixiStroke(instruction.stroke, viewportScale));
4803
+ }
4804
+ }
4805
+ return;
4806
+ }
4807
+ for (const subpath of geometry.subpaths) {
4808
+ if (geometry.closed && instruction.fill) {
4809
+ graphics.poly(subpath.flatMap(({ x, y }) => [x, y]), true);
4810
+ graphics.fill(instruction.fill);
4811
+ }
4812
+ if (instruction.stroke) {
4813
+ drawPixiPath(graphics, subpath, geometry.closed, instruction.stroke, viewportScale);
4814
+ }
4815
+ }
4816
+ }
4817
+ function asOpenStroke(stroke) {
4818
+ if (!stroke)
4819
+ return undefined;
4820
+ return {
4821
+ alpha: stroke.alpha,
4822
+ cap: stroke.cap,
4823
+ color: stroke.color,
4824
+ dash: stroke.dash,
4825
+ join: stroke.join,
4826
+ miterLimit: stroke.miterLimit,
4827
+ width: stroke.width,
4828
+ };
4829
+ }
4605
4830
  function detectionKey(detection, detectionIndex) {
4606
4831
  return detection.id === undefined
4607
4832
  ? `index:${detectionIndex}`
@@ -4731,7 +4956,8 @@ function createPixiInteractionPresentationLayer(options) {
4731
4956
  }
4732
4957
  function createActivePick(pick, frame, mediaTime, state) {
4733
4958
  const activePick = rebaseDetectionPickToFrame(pick, frame);
4734
- if (!activePick) {
4959
+ if (!activePick ||
4960
+ options.isDetectionVisible?.(activePick.detection) === false) {
4735
4961
  return null;
4736
4962
  }
4737
4963
  const context = {
@@ -4988,7 +5214,7 @@ function createInteractionMaskRenderer(options) {
4988
5214
  autoGenerateMipmaps: false,
4989
5215
  dynamic: false,
4990
5216
  height: 1,
4991
- resource: createPlaceholderCanvas$1(),
5217
+ resource: createPlaceholderCanvas$2(),
4992
5218
  scaleMode: "nearest",
4993
5219
  width: 1,
4994
5220
  });
@@ -5099,7 +5325,7 @@ function writePaletteEntry(palette, maskId, color, alpha) {
5099
5325
  palette[offset + 2] = (color & 0xff) / 255;
5100
5326
  palette[offset + 3] = clampedAlpha;
5101
5327
  }
5102
- function createPlaceholderCanvas$1() {
5328
+ function createPlaceholderCanvas$2() {
5103
5329
  const canvas = document.createElement("canvas");
5104
5330
  canvas.height = 1;
5105
5331
  canvas.width = 1;
@@ -5247,7 +5473,7 @@ void main(void) {
5247
5473
  }
5248
5474
  `;
5249
5475
 
5250
- const EMBEDDED_MASK_PREPARATION_WORKER_SOURCE = "(function () {\n 'use strict';\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 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 DetectionFrameSelectionMode[\"Interval\"] = \"interval\";\n /**\n * Select from a known inference frame grid using `frameIndex`, `frameRate`,\n * and optional `frameIndexOriginTime`. This is useful when inference was run\n * on normalized frames and playback should snap detections to that grid.\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 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[\"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 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 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 var BoxShape;\n (function (BoxShape) {\n BoxShape[\"Rect\"] = \"rect\";\n BoxShape[\"RoundedRect\"] = \"roundedRect\";\n })(BoxShape || (BoxShape = {}));\n var BoxStrokeAlignment;\n (function (BoxStrokeAlignment) {\n BoxStrokeAlignment[\"Inside\"] = \"inside\";\n BoxStrokeAlignment[\"Center\"] = \"center\";\n BoxStrokeAlignment[\"Outside\"] = \"outside\";\n })(BoxStrokeAlignment || (BoxStrokeAlignment = {}));\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 var KeypointMarkerShape;\n (function (KeypointMarkerShape) {\n KeypointMarkerShape[\"Circle\"] = \"circle\";\n KeypointMarkerShape[\"Cross\"] = \"cross\";\n })(KeypointMarkerShape || (KeypointMarkerShape = {}));\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 assertMaskDimensions(data, 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 if (data.length !== width * height) {\n throw new Error(\"Mask data length must equal width * height.\");\n }\n }\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.ceil(intersections[index]));\n const right = Math.min(dimensions.width - 1, Math.floor(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 const MAX_ID_MASK_PALETTE_ENTRIES = 64;\n const MAX_ID_MASK_STROKE_WIDTH = 16;\n function createIdMaskFrame(instructions) {\n if (instructions.length === 0) {\n return undefined;\n }\n const width = Math.max(...instructions.map(({ mask }) => mask.width));\n const height = Math.max(...instructions.map(({ mask }) => mask.height));\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 = instruction.detectionIndex + 1;\n if (detectionMaskId <= 0 ||\n detectionMaskId >= MAX_ID_MASK_PALETTE_ENTRIES) {\n return undefined;\n }\n writePaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);\n if (instruction.stroke && instruction.stroke.width > 0) {\n const strokeWidth = Math.min(Math.max(0, instruction.stroke.width), MAX_ID_MASK_STROKE_WIDTH);\n hasStroke = true;\n strokeWidths[detectionMaskId] = strokeWidth;\n maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth);\n writePaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);\n }\n const decodedMask = decodeCompressedRleMask(instruction.mask);\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n const maskOffset = y * decodedMask.width + x;\n if (decodedMask.data[maskOffset]) {\n data[y * width + x] = detectionMaskId;\n }\n }\n }\n }\n return {\n data,\n fillPalette,\n hasStroke,\n height,\n maxStrokeWidth,\n strokePalette,\n strokeWidths,\n width,\n };\n }\n function writePaletteEntry(palette, id, color, alpha) {\n const offset = id * 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\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 * 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 /**\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 MediaSessionActivityKind[\"DetectionsBuffering\"] = \"detectionsBuffering\";\n MediaSessionActivityKind[\"DetectionsLoading\"] = \"detectionsLoading\";\n MediaSessionActivityKind[\"Error\"] = \"error\";\n MediaSessionActivityKind[\"MediaNormalizing\"] = \"mediaNormalizing\";\n MediaSessionActivityKind[\"MediaOpening\"] = \"mediaOpening\";\n MediaSessionActivityKind[\"PlaybackBuffering\"] = \"playbackBuffering\";\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 const PNG_SIGNATURE = new Uint8Array([\n 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,\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 async function createPngIdMaskFrame(instructions) {\n const frame = createIdMaskFrame(materializeMaskInstructions(instructions));\n if (!frame) {\n return undefined;\n }\n return {\n ...frame,\n png: await encodeGrayscalePng({\n height: frame.height,\n pixels: frame.data,\n width: frame.width,\n }),\n };\n }\n function compositeInstruction(rgba, canvasWidth, instruction) {\n const decodedMask = decodeCompressedRleMask(instruction.mask);\n const fill = resolveRgbaColor(instruction.color, instruction.alpha);\n compositeMaskFill(rgba, canvasWidth, decodedMask, fill);\n if (instruction.stroke) {\n compositeMaskStroke(rgba, canvasWidth, decodedMask, instruction.stroke);\n }\n }\n function materializeMaskInstructions(instructions) {\n return instructions.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 function compositeMaskFill(rgba, canvasWidth, decodedMask, fill) {\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n const maskOffset = y * decodedMask.width + x;\n if (!decodedMask.data[maskOffset]) {\n continue;\n }\n writePixel(rgba, canvasWidth, x, y, fill);\n }\n }\n }\n function compositeMaskStroke(rgba, canvasWidth, decodedMask, 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 = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; 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 async function encodeGrayscalePng(options) {\n if (typeof CompressionStream === \"undefined\") {\n throw new Error(\"CompressionStream is required to encode PNG ID masks.\");\n }\n const rawScanlines = createFilterlessPngScanlines(options);\n const ihdr = new Uint8Array(13);\n const ihdrView = new DataView(ihdr.buffer);\n ihdrView.setUint32(0, options.width);\n ihdrView.setUint32(4, options.height);\n ihdr[8] = 8;\n ihdr[9] = 0;\n ihdr[10] = 0;\n ihdr[11] = 0;\n ihdr[12] = 0;\n const compressed = new Uint8Array(await new Response(new Blob([rawScanlines])\n .stream()\n .pipeThrough(new CompressionStream(\"deflate\"))).arrayBuffer());\n return concatUint8Arrays([\n PNG_SIGNATURE,\n createPngChunk(\"IHDR\", ihdr),\n createPngChunk(\"IDAT\", compressed),\n createPngChunk(\"IEND\", new Uint8Array(0)),\n ]);\n }\n function createFilterlessPngScanlines(options) {\n const rowStride = options.width + 1;\n const scanlines = new Uint8Array(rowStride * options.height);\n for (let y = 0; y < options.height; y += 1) {\n const sourceOffset = y * options.width;\n const targetOffset = y * rowStride;\n scanlines[targetOffset] = 0;\n scanlines.set(options.pixels.subarray(sourceOffset, sourceOffset + options.width), targetOffset + 1);\n }\n return scanlines;\n }\n function createPngChunk(type, data) {\n const typeBytes = new TextEncoder().encode(type);\n const chunk = new Uint8Array(12 + data.length);\n const view = new DataView(chunk.buffer);\n view.setUint32(0, data.length);\n chunk.set(typeBytes, 4);\n chunk.set(data, 8);\n view.setUint32(8 + data.length, crc32(concatUint8Arrays([typeBytes, data])));\n return chunk;\n }\n const crc32Table = createCrc32Table();\n function createCrc32Table() {\n const table = new Uint32Array(256);\n for (let index = 0; index < table.length; index += 1) {\n let value = index;\n for (let bit = 0; bit < 8; bit += 1) {\n value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;\n }\n table[index] = value >>> 0;\n }\n return table;\n }\n function crc32(bytes) {\n let crc = 0xffffffff;\n for (const byte of bytes) {\n crc = crc32Table[(crc ^ byte) & 0xff] ^ (crc >>> 8);\n }\n return (crc ^ 0xffffffff) >>> 0;\n }\n function concatUint8Arrays(chunks) {\n const totalLength = chunks.reduce((total, chunk) => total + chunk.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n return result;\n }\n\n var PreparedMaskFrameKind;\n (function (PreparedMaskFrameKind) {\n PreparedMaskFrameKind[\"PngIdMask\"] = \"pngIdMask\";\n PreparedMaskFrameKind[\"RgbaImage\"] = \"rgbaImage\";\n })(PreparedMaskFrameKind || (PreparedMaskFrameKind = {}));\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 const workerScope = globalThis;\n workerScope.addEventListener(\"message\", (event) => {\n const message = event.data;\n if (message.type !== MaskPreparationWorkerMessageType.Prepare) {\n return;\n }\n void prepareMaskFrame(message);\n });\n async function prepareMaskFrame(message) {\n try {\n const pngIdMaskFrame = await createPngIdMaskWorkerResponse(message);\n if (pngIdMaskFrame) {\n workerScope.postMessage(pngIdMaskFrame, [\n pngIdMaskFrame.imageBitmap,\n pngIdMaskFrame.png.buffer,\n pngIdMaskFrame.fillPalette.buffer,\n pngIdMaskFrame.strokePalette.buffer,\n pngIdMaskFrame.strokeWidths.buffer,\n ]);\n return;\n }\n const compositedFrame = compositeMaskFrame(message.job.instructions);\n if (!compositedFrame) {\n workerScope.postMessage({\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Empty,\n });\n return;\n }\n const imageData = new ImageData(compositedFrame.data, compositedFrame.width, compositedFrame.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n if (imageBitmap) {\n workerScope.postMessage({\n imageBitmap,\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [imageBitmap]);\n return;\n }\n workerScope.postMessage({\n imageData,\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [imageData.data.buffer]);\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 async function createPngIdMaskWorkerResponse(message) {\n if (typeof Blob === \"undefined\" ||\n typeof createImageBitmap === \"undefined\" ||\n typeof CompressionStream === \"undefined\") {\n return undefined;\n }\n let frame;\n try {\n frame = await createPngIdMaskFrame(message.job.instructions);\n }\n catch {\n return undefined;\n }\n if (!frame) {\n return undefined;\n }\n let imageBitmap;\n try {\n imageBitmap = await createImageBitmap(new Blob([frame.png], { type: \"image/png\" }));\n }\n catch {\n return undefined;\n }\n return {\n artifactKind: PreparedMaskFrameKind.PngIdMask,\n fillPalette: frame.fillPalette,\n hasStroke: frame.hasStroke,\n imageBitmap,\n key: message.job.key,\n maxStrokeWidth: frame.maxStrokeWidth,\n png: frame.png,\n requestId: message.requestId,\n strokePalette: frame.strokePalette,\n strokeWidths: frame.strokeWidths,\n type: MaskPreparationWorkerMessageType.Complete,\n };\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})();";
5476
+ const EMBEDDED_MASK_PREPARATION_WORKER_SOURCE = "(function () {\n 'use strict';\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 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 DetectionFrameSelectionMode[\"Interval\"] = \"interval\";\n /**\n * Select from a known inference frame grid using `frameIndex`, `frameRate`,\n * and optional `frameIndexOriginTime`. This is useful when inference was run\n * on normalized frames and playback should snap detections to that grid.\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 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[\"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 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 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 KeypointMarkerShape;\n (function (KeypointMarkerShape) {\n KeypointMarkerShape[\"Circle\"] = \"circle\";\n KeypointMarkerShape[\"Cross\"] = \"cross\";\n })(KeypointMarkerShape || (KeypointMarkerShape = {}));\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 assertMaskDimensions(data, 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 if (data.length !== width * height) {\n throw new Error(\"Mask data length must equal width * height.\");\n }\n }\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.ceil(intersections[index]));\n const right = Math.min(dimensions.width - 1, Math.floor(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 const MAX_ID_MASK_PALETTE_ENTRIES = 64;\n const MAX_ID_MASK_STROKE_WIDTH = 16;\n function createIdMaskFrame(instructions) {\n if (instructions.length === 0) {\n return undefined;\n }\n const width = Math.max(...instructions.map(({ mask }) => mask.width));\n const height = Math.max(...instructions.map(({ mask }) => mask.height));\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 = instruction.detectionIndex + 1;\n if (detectionMaskId <= 0 ||\n detectionMaskId >= MAX_ID_MASK_PALETTE_ENTRIES) {\n return undefined;\n }\n writePaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);\n if (instruction.stroke && instruction.stroke.width > 0) {\n const strokeWidth = Math.min(Math.max(0, instruction.stroke.width), MAX_ID_MASK_STROKE_WIDTH);\n hasStroke = true;\n strokeWidths[detectionMaskId] = strokeWidth;\n maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth);\n writePaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);\n }\n const decodedMask = decodeCompressedRleMask(instruction.mask);\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n const maskOffset = y * decodedMask.width + x;\n if (decodedMask.data[maskOffset]) {\n data[y * width + x] = detectionMaskId;\n }\n }\n }\n }\n return {\n data,\n fillPalette,\n hasStroke,\n height,\n maxStrokeWidth,\n strokePalette,\n strokeWidths,\n width,\n };\n }\n function writePaletteEntry(palette, id, color, alpha) {\n const offset = id * 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\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 * 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 /**\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 MediaSessionActivityKind[\"DetectionsBuffering\"] = \"detectionsBuffering\";\n MediaSessionActivityKind[\"DetectionsLoading\"] = \"detectionsLoading\";\n MediaSessionActivityKind[\"Error\"] = \"error\";\n MediaSessionActivityKind[\"MediaNormalizing\"] = \"mediaNormalizing\";\n MediaSessionActivityKind[\"MediaOpening\"] = \"mediaOpening\";\n MediaSessionActivityKind[\"PlaybackBuffering\"] = \"playbackBuffering\";\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 const PNG_SIGNATURE = new Uint8Array([\n 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,\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 createMaskIdFrame(instructions) {\n return createIdMaskFrame(materializeMaskInstructions(instructions));\n }\n async function createPngIdMaskFrame(instructions) {\n const frame = createMaskIdFrame(instructions);\n if (!frame) {\n return undefined;\n }\n return {\n ...frame,\n png: await encodeGrayscalePng({\n height: frame.height,\n pixels: frame.data,\n width: frame.width,\n }),\n };\n }\n function compositeInstruction(rgba, canvasWidth, instruction) {\n const decodedMask = decodeCompressedRleMask(instruction.mask);\n const fill = resolveRgbaColor(instruction.color, instruction.alpha);\n compositeMaskFill(rgba, canvasWidth, decodedMask, fill);\n if (instruction.stroke) {\n compositeMaskStroke(rgba, canvasWidth, decodedMask, instruction.stroke);\n }\n }\n function materializeMaskInstructions(instructions) {\n return instructions.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 function compositeMaskFill(rgba, canvasWidth, decodedMask, fill) {\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n const maskOffset = y * decodedMask.width + x;\n if (!decodedMask.data[maskOffset]) {\n continue;\n }\n writePixel(rgba, canvasWidth, x, y, fill);\n }\n }\n }\n function compositeMaskStroke(rgba, canvasWidth, decodedMask, 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 = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; 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 async function encodeGrayscalePng(options) {\n if (typeof CompressionStream === \"undefined\") {\n throw new Error(\"CompressionStream is required to encode PNG ID masks.\");\n }\n const rawScanlines = createFilterlessPngScanlines(options);\n const ihdr = new Uint8Array(13);\n const ihdrView = new DataView(ihdr.buffer);\n ihdrView.setUint32(0, options.width);\n ihdrView.setUint32(4, options.height);\n ihdr[8] = 8;\n ihdr[9] = 0;\n ihdr[10] = 0;\n ihdr[11] = 0;\n ihdr[12] = 0;\n const compressed = new Uint8Array(await new Response(new Blob([rawScanlines])\n .stream()\n .pipeThrough(new CompressionStream(\"deflate\"))).arrayBuffer());\n return concatUint8Arrays([\n PNG_SIGNATURE,\n createPngChunk(\"IHDR\", ihdr),\n createPngChunk(\"IDAT\", compressed),\n createPngChunk(\"IEND\", new Uint8Array(0)),\n ]);\n }\n function createFilterlessPngScanlines(options) {\n const rowStride = options.width + 1;\n const scanlines = new Uint8Array(rowStride * options.height);\n for (let y = 0; y < options.height; y += 1) {\n const sourceOffset = y * options.width;\n const targetOffset = y * rowStride;\n scanlines[targetOffset] = 0;\n scanlines.set(options.pixels.subarray(sourceOffset, sourceOffset + options.width), targetOffset + 1);\n }\n return scanlines;\n }\n function createPngChunk(type, data) {\n const typeBytes = new TextEncoder().encode(type);\n const chunk = new Uint8Array(12 + data.length);\n const view = new DataView(chunk.buffer);\n view.setUint32(0, data.length);\n chunk.set(typeBytes, 4);\n chunk.set(data, 8);\n view.setUint32(8 + data.length, crc32(concatUint8Arrays([typeBytes, data])));\n return chunk;\n }\n const crc32Table = createCrc32Table();\n function createCrc32Table() {\n const table = new Uint32Array(256);\n for (let index = 0; index < table.length; index += 1) {\n let value = index;\n for (let bit = 0; bit < 8; bit += 1) {\n value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;\n }\n table[index] = value >>> 0;\n }\n return table;\n }\n function crc32(bytes) {\n let crc = 0xffffffff;\n for (const byte of bytes) {\n crc = crc32Table[(crc ^ byte) & 0xff] ^ (crc >>> 8);\n }\n return (crc ^ 0xffffffff) >>> 0;\n }\n function concatUint8Arrays(chunks) {\n const totalLength = chunks.reduce((total, chunk) => total + chunk.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n return result;\n }\n\n var PreparedMaskFrameKind;\n (function (PreparedMaskFrameKind) {\n PreparedMaskFrameKind[\"PngIdMask\"] = \"pngIdMask\";\n PreparedMaskFrameKind[\"RgbaImage\"] = \"rgbaImage\";\n })(PreparedMaskFrameKind || (PreparedMaskFrameKind = {}));\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 const workerScope = globalThis;\n workerScope.addEventListener(\"message\", (event) => {\n const message = event.data;\n if (message.type !== MaskPreparationWorkerMessageType.Prepare) {\n return;\n }\n void prepareMaskFrame(message);\n });\n async function prepareMaskFrame(message) {\n try {\n const pngIdMaskFrame = await createPngIdMaskWorkerResponse(message);\n if (pngIdMaskFrame) {\n workerScope.postMessage(pngIdMaskFrame, [\n pngIdMaskFrame.imageBitmap,\n pngIdMaskFrame.png.buffer,\n pngIdMaskFrame.fillPalette.buffer,\n pngIdMaskFrame.strokePalette.buffer,\n pngIdMaskFrame.strokeWidths.buffer,\n ]);\n return;\n }\n const compositedFrame = compositeMaskFrame(message.job.instructions);\n if (!compositedFrame) {\n workerScope.postMessage({\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Empty,\n });\n return;\n }\n const imageData = new ImageData(compositedFrame.data, compositedFrame.width, compositedFrame.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n const idMaskData = createMaskIdFrame(message.job.instructions)?.data;\n if (imageBitmap) {\n workerScope.postMessage({\n imageBitmap,\n idMaskData,\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [...(idMaskData ? [idMaskData.buffer] : []), imageBitmap]);\n return;\n }\n workerScope.postMessage({\n idMaskData,\n imageData,\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [...(idMaskData ? [idMaskData.buffer] : []), imageData.data.buffer]);\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 async function createPngIdMaskWorkerResponse(message) {\n if (typeof Blob === \"undefined\" ||\n typeof createImageBitmap === \"undefined\" ||\n typeof CompressionStream === \"undefined\") {\n return undefined;\n }\n let frame;\n try {\n frame = await createPngIdMaskFrame(message.job.instructions);\n }\n catch {\n return undefined;\n }\n if (!frame) {\n return undefined;\n }\n let imageBitmap;\n try {\n imageBitmap = await createImageBitmap(new Blob([frame.png], { type: \"image/png\" }));\n }\n catch {\n return undefined;\n }\n return {\n artifactKind: PreparedMaskFrameKind.PngIdMask,\n fillPalette: frame.fillPalette,\n hasStroke: frame.hasStroke,\n imageBitmap,\n key: message.job.key,\n maxStrokeWidth: frame.maxStrokeWidth,\n png: frame.png,\n requestId: message.requestId,\n strokePalette: frame.strokePalette,\n strokeWidths: frame.strokeWidths,\n type: MaskPreparationWorkerMessageType.Complete,\n };\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})();";
5251
5477
 
5252
5478
  const DEFAULT_WORKER_NAME = "supervision-js-render-preparation";
5253
5479
  let defaultWorkerUrl;
@@ -5522,6 +5748,7 @@ function createMainThreadMaskFramePreparer(workerStatus, message = null) {
5522
5748
  canvas.height = 0;
5523
5749
  },
5524
5750
  height: compositedFrame.height,
5751
+ idMaskData: createMaskIdFrame(job.instructions)?.data,
5525
5752
  key: job.key,
5526
5753
  kind: PreparedMaskFrameKind.RgbaImage,
5527
5754
  source: canvas,
@@ -5696,6 +5923,7 @@ function createPreparedFrameFromWorkerResponse(message) {
5696
5923
  message.imageBitmap?.close();
5697
5924
  },
5698
5925
  height: message.imageBitmap.height,
5926
+ idMaskData: message.idMaskData,
5699
5927
  key: message.key,
5700
5928
  kind: PreparedMaskFrameKind.RgbaImage,
5701
5929
  source: message.imageBitmap,
@@ -5719,6 +5947,7 @@ function createPreparedFrameFromWorkerResponse(message) {
5719
5947
  canvas.height = 0;
5720
5948
  },
5721
5949
  height: message.imageData.height,
5950
+ idMaskData: message.idMaskData,
5722
5951
  key: message.key,
5723
5952
  kind: PreparedMaskFrameKind.RgbaImage,
5724
5953
  source: canvas,
@@ -6587,7 +6816,7 @@ function createPixiIdMaskShaderRenderer(options) {
6587
6816
  autoGenerateMipmaps: false,
6588
6817
  dynamic: false,
6589
6818
  height: 1,
6590
- resource: createPlaceholderCanvas(),
6819
+ resource: createPlaceholderCanvas$1(),
6591
6820
  scaleMode: "nearest",
6592
6821
  width: 1,
6593
6822
  });
@@ -6677,7 +6906,7 @@ function createPixiIdMaskShaderRenderer(options) {
6677
6906
  mesh.shader = shader;
6678
6907
  }
6679
6908
  }
6680
- function createPlaceholderCanvas() {
6909
+ function createPlaceholderCanvas$1() {
6681
6910
  const canvas = document.createElement("canvas");
6682
6911
  canvas.height = 1;
6683
6912
  canvas.width = 1;
@@ -6827,15 +7056,219 @@ void main(void) {
6827
7056
  }
6828
7057
  `;
6829
7058
 
7059
+ /**
7060
+ * Renders blurred, per-detection-colored copies of the prepared id mask
7061
+ * beneath the mask layer. Each pass maps mask ids through a halo palette and
7062
+ * a GPU blur filter turns that coverage into a smooth glow following the
7063
+ * exact mask silhouette, reusing the already-prepared mask texture. Passes
7064
+ * are pooled and one runs per distinct spread.
7065
+ */
7066
+ function createPixiMaskHaloRenderer(options) {
7067
+ const display = new options.Container();
7068
+ const placeholderSource = new options.ImageSource({
7069
+ autoGenerateMipmaps: false,
7070
+ dynamic: false,
7071
+ height: 1,
7072
+ resource: createPlaceholderCanvas(),
7073
+ scaleMode: "nearest",
7074
+ width: 1,
7075
+ });
7076
+ const geometry = new options.MeshGeometry({
7077
+ indices: new Uint32Array([0, 1, 2, 0, 2, 3]),
7078
+ positions: new Float32Array([
7079
+ 0,
7080
+ 0,
7081
+ options.mediaWidth,
7082
+ 0,
7083
+ options.mediaWidth,
7084
+ options.mediaHeight,
7085
+ 0,
7086
+ options.mediaHeight,
7087
+ ]),
7088
+ shrinkBuffersToFit: true,
7089
+ topology: "triangle-list",
7090
+ uvs: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]),
7091
+ });
7092
+ const passes = [];
7093
+ display.visible = false;
7094
+ return {
7095
+ destroy() {
7096
+ for (const pass of passes) {
7097
+ pass.mesh.destroy();
7098
+ pass.shader.destroy(true);
7099
+ }
7100
+ passes.length = 0;
7101
+ display.destroy();
7102
+ geometry.destroy();
7103
+ placeholderSource.destroy();
7104
+ },
7105
+ display,
7106
+ hide() {
7107
+ display.visible = false;
7108
+ },
7109
+ render(frame, texture, groups) {
7110
+ if (groups.length === 0) {
7111
+ display.visible = false;
7112
+ return;
7113
+ }
7114
+ for (const [index, group] of groups.entries()) {
7115
+ const pass = ensurePass(index);
7116
+ bindTexture(pass, texture.source);
7117
+ pass.uniforms.uniforms.uHaloPalette = group.palette;
7118
+ pass.uniforms.update();
7119
+ pass.blurFilter.strength = Math.max(group.spread, 0);
7120
+ pass.mesh.visible = true;
7121
+ }
7122
+ for (const pass of passes.slice(groups.length)) {
7123
+ pass.mesh.visible = false;
7124
+ }
7125
+ display.visible = true;
7126
+ },
7127
+ setOpacity(opacity) {
7128
+ display.alpha = opacity;
7129
+ },
7130
+ };
7131
+ function ensurePass(index) {
7132
+ const existing = passes[index];
7133
+ if (existing) {
7134
+ return existing;
7135
+ }
7136
+ const uniforms = new options.UniformGroup({
7137
+ uHaloPalette: {
7138
+ size: MAX_ID_MASK_PALETTE_ENTRIES,
7139
+ type: "vec4<f32>",
7140
+ value: new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4),
7141
+ },
7142
+ });
7143
+ const shader = createShader(uniforms);
7144
+ const mesh = new options.Mesh({ geometry, shader });
7145
+ const blurFilter = new options.BlurFilter({ quality: 4, strength: 8 });
7146
+ // Filters size their working texture from measured bounds, and a
7147
+ // custom-geometry mesh measures empty; an explicit bounds area keeps the
7148
+ // blur pass covering the full media rect.
7149
+ mesh.boundsArea = new options.Rectangle(0, 0, options.mediaWidth, options.mediaHeight);
7150
+ mesh.filters = [blurFilter];
7151
+ display.addChild(mesh);
7152
+ const pass = { blurFilter, mesh, shader, uniforms };
7153
+ passes.push(pass);
7154
+ return pass;
7155
+ }
7156
+ function bindTexture(pass, source) {
7157
+ try {
7158
+ pass.shader.resources.uTexture = source;
7159
+ pass.shader.resources.uSampler = source.style;
7160
+ }
7161
+ catch {
7162
+ try {
7163
+ pass.shader.destroy(true);
7164
+ }
7165
+ catch {
7166
+ // Pixi has already invalidated this shader's resource group.
7167
+ }
7168
+ pass.shader = createShader(pass.uniforms);
7169
+ pass.mesh.shader = pass.shader;
7170
+ pass.shader.resources.uTexture = source;
7171
+ pass.shader.resources.uSampler = source.style;
7172
+ }
7173
+ }
7174
+ function createShader(uniforms) {
7175
+ return options.Shader.from({
7176
+ gl: {
7177
+ fragment: maskHaloFragmentShader,
7178
+ vertex: maskHaloVertexShader,
7179
+ },
7180
+ resources: {
7181
+ haloUniforms: uniforms,
7182
+ uSampler: placeholderSource.style,
7183
+ uTexture: placeholderSource,
7184
+ },
7185
+ });
7186
+ }
7187
+ }
7188
+ /**
7189
+ * Builds one halo palette uniform: premultiplied halo color and alpha per
7190
+ * mask id, transparent for ids outside the group.
7191
+ */
7192
+ function buildMaskHaloPalette(halos) {
7193
+ const palette = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4);
7194
+ for (const [maskId, halo] of halos) {
7195
+ if (maskId <= 0 || maskId >= MAX_ID_MASK_PALETTE_ENTRIES) {
7196
+ continue;
7197
+ }
7198
+ const alpha = Math.max(0, Math.min(halo.alpha, 1));
7199
+ const offset = maskId * 4;
7200
+ palette[offset] = (((halo.color >> 16) & 0xff) / 255) * alpha;
7201
+ palette[offset + 1] = (((halo.color >> 8) & 0xff) / 255) * alpha;
7202
+ palette[offset + 2] = ((halo.color & 0xff) / 255) * alpha;
7203
+ palette[offset + 3] = alpha;
7204
+ }
7205
+ return palette;
7206
+ }
7207
+ function createPlaceholderCanvas() {
7208
+ const canvas = document.createElement("canvas");
7209
+ canvas.height = 1;
7210
+ canvas.width = 1;
7211
+ return canvas;
7212
+ }
7213
+ const maskHaloVertexShader = `#version 300 es
7214
+ precision highp float;
7215
+
7216
+ in vec2 aPosition;
7217
+ in vec2 aUV;
7218
+
7219
+ uniform mat3 uProjectionMatrix;
7220
+ uniform mat3 uWorldTransformMatrix;
7221
+ uniform vec4 uWorldColorAlpha;
7222
+ uniform mat3 uTransformMatrix;
7223
+ uniform vec4 uColor;
7224
+
7225
+ out vec2 vUV;
7226
+ out vec4 vColor;
7227
+
7228
+ void main(void) {
7229
+ mat3 modelViewProjectionMatrix =
7230
+ uProjectionMatrix * uWorldTransformMatrix * uTransformMatrix;
7231
+
7232
+ gl_Position =
7233
+ vec4((modelViewProjectionMatrix * vec3(aPosition, 1.0)).xy, 0.0, 1.0);
7234
+ vUV = aUV;
7235
+ vColor = uWorldColorAlpha * uColor;
7236
+ }
7237
+ `;
7238
+ const maskHaloFragmentShader = `#version 300 es
7239
+ precision highp float;
7240
+ precision highp int;
7241
+
7242
+ in vec2 vUV;
7243
+ in vec4 vColor;
7244
+
7245
+ uniform sampler2D uTexture;
7246
+ uniform vec4 uHaloPalette[${MAX_ID_MASK_PALETTE_ENTRIES}];
7247
+
7248
+ out vec4 finalColor;
7249
+
7250
+ void main(void) {
7251
+ float maskId = floor(texture(uTexture, vUV).r * 255.0 + 0.5);
7252
+ int paletteIndex =
7253
+ int(clamp(maskId, 0.0, float(${MAX_ID_MASK_PALETTE_ENTRIES - 1})));
7254
+
7255
+ finalColor = uHaloPalette[paletteIndex] * vColor;
7256
+ }
7257
+ `;
7258
+
6830
7259
  const MAX_PENDING_MASK_HOLD_SECONDS = 0.05;
6831
7260
  function createPixiMaskLayer(options) {
6832
7261
  let mediaHeight = 0;
6833
7262
  let mediaWidth = 0;
6834
7263
  let idMaskRenderer;
7264
+ let haloRenderer;
7265
+ let currentMaskHaloStyle = options.maskHaloStyle ?? null;
7266
+ options.maskStyle;
6835
7267
  let maskSprite;
6836
7268
  let activeFrameKey = null;
6837
7269
  let activeFrameMediaTime = null;
6838
7270
  let activeIdMaskFrame = null;
7271
+ let activeRgbaMaskFrame = null;
6839
7272
  let maskOpacity = resolveMaskStyleOpacity(options.maskStyle);
6840
7273
  let maskPickCanvas;
6841
7274
  let maskPickContext;
@@ -6843,6 +7276,7 @@ function createPixiMaskLayer(options) {
6843
7276
  let visibleMaskMediaTime = null;
6844
7277
  let isDestroyed = false;
6845
7278
  const maskTextures = new Map();
7279
+ const haloTextures = new Map();
6846
7280
  const preparedRenderWindow = createPreparedRenderWindow({
6847
7281
  artifactKind: options.artifactKind,
6848
7282
  detectionTimeline: options.detectionTimeline,
@@ -6888,10 +7322,19 @@ function createPixiMaskLayer(options) {
6888
7322
  maskSprite.height = mediaHeight;
6889
7323
  idMaskRenderer = createIdMaskRenderer();
6890
7324
  idMaskRenderer?.setOpacity(maskOpacity);
7325
+ haloRenderer = createHaloRenderer();
7326
+ // Halo alpha is resolved from MaskHaloStyle per detection. It must not
7327
+ // inherit the independent mask renderer's global opacity.
7328
+ haloRenderer?.setOpacity(1);
6891
7329
  if (!idMaskRenderer || !options.Container) {
6892
7330
  return maskSprite;
6893
7331
  }
6894
7332
  const maskContainer = new options.Container();
7333
+ // The halo draws beneath the mask so the glow bleeds outward from the
7334
+ // silhouette while fills and borders stay crisp on top.
7335
+ if (haloRenderer) {
7336
+ maskContainer.addChild(haloRenderer.display);
7337
+ }
6895
7338
  maskContainer.addChild(maskSprite, idMaskRenderer.mesh);
6896
7339
  return maskContainer;
6897
7340
  },
@@ -6901,6 +7344,7 @@ function createPixiMaskLayer(options) {
6901
7344
  activeFrameKey = preparedFrame?.key ?? null;
6902
7345
  activeFrameMediaTime = preparedFrame?.detectionFrame.mediaTime ?? null;
6903
7346
  activeIdMaskFrame = null;
7347
+ activeRgbaMaskFrame = null;
6904
7348
  hideSprite();
6905
7349
  return;
6906
7350
  }
@@ -6913,6 +7357,7 @@ function createPixiMaskLayer(options) {
6913
7357
  if (preparedFrame.maskStatus === PreparedRenderFrameMaskStatus.Empty ||
6914
7358
  preparedFrame.maskStatus === PreparedRenderFrameMaskStatus.Disabled) {
6915
7359
  activeIdMaskFrame = null;
7360
+ activeRgbaMaskFrame = null;
6916
7361
  hideSprite();
6917
7362
  return;
6918
7363
  }
@@ -6965,6 +7410,14 @@ function createPixiMaskLayer(options) {
6965
7410
  applyMaskOpacity();
6966
7411
  }
6967
7412
  preparedRenderWindow.setMaskStyle(nextMaskStyle);
7413
+ refreshHalo();
7414
+ },
7415
+ setMaskHaloStyle(nextMaskHaloStyle) {
7416
+ if (nextMaskHaloStyle === undefined) {
7417
+ return;
7418
+ }
7419
+ currentMaskHaloStyle = nextMaskHaloStyle;
7420
+ refreshHalo();
6968
7421
  },
6969
7422
  destroy() {
6970
7423
  if (isDestroyed) {
@@ -6975,17 +7428,22 @@ function createPixiMaskLayer(options) {
6975
7428
  destroyTextures();
6976
7429
  resetMaskPickCanvas();
6977
7430
  idMaskRenderer?.destroy();
7431
+ haloRenderer?.destroy();
6978
7432
  },
6979
7433
  };
6980
7434
  function showMaskFrame(maskFrame, mediaTime) {
6981
7435
  visibleMaskMediaTime = mediaTime;
6982
7436
  activeIdMaskFrame =
6983
7437
  maskFrame.kind === PreparedMaskFrameKind.PngIdMask ? maskFrame : null;
7438
+ activeRgbaMaskFrame =
7439
+ maskFrame.kind === PreparedMaskFrameKind.RgbaImage ? maskFrame : null;
6984
7440
  if (maskFrame.kind === PreparedMaskFrameKind.PngIdMask && idMaskRenderer) {
6985
7441
  showIdMaskFrame(maskFrame);
6986
7442
  return;
6987
7443
  }
6988
- showRgbaMaskFrame(maskFrame);
7444
+ if (maskFrame.kind === PreparedMaskFrameKind.RgbaImage) {
7445
+ showRgbaMaskFrame(maskFrame);
7446
+ }
6989
7447
  }
6990
7448
  function getTexture(maskFrame) {
6991
7449
  const existingTexture = maskTextures.get(maskFrame.key);
@@ -7009,6 +7467,40 @@ function createPixiMaskLayer(options) {
7009
7467
  maskTextures.set(maskFrame.key, texture);
7010
7468
  return texture;
7011
7469
  }
7470
+ function getHaloTexture(maskFrame) {
7471
+ const existingTexture = haloTextures.get(maskFrame.key);
7472
+ if (existingTexture) {
7473
+ return existingTexture;
7474
+ }
7475
+ if (!maskFrame.idMaskData || typeof document === "undefined") {
7476
+ return undefined;
7477
+ }
7478
+ const canvas = document.createElement("canvas");
7479
+ const context = canvas.getContext("2d");
7480
+ if (!context || typeof context.createImageData !== "function") {
7481
+ return undefined;
7482
+ }
7483
+ canvas.width = maskFrame.width;
7484
+ canvas.height = maskFrame.height;
7485
+ const imageData = context.createImageData(maskFrame.width, maskFrame.height);
7486
+ for (let index = 0; index < maskFrame.idMaskData.length; index += 1) {
7487
+ const offset = index * 4;
7488
+ imageData.data[offset] = maskFrame.idMaskData[index];
7489
+ imageData.data[offset + 3] = 255;
7490
+ }
7491
+ context.putImageData(imageData, 0, 0);
7492
+ const source = new options.ImageSource({
7493
+ autoGenerateMipmaps: false,
7494
+ dynamic: false,
7495
+ height: maskFrame.height,
7496
+ resource: canvas,
7497
+ scaleMode: "nearest",
7498
+ width: maskFrame.width,
7499
+ });
7500
+ const texture = new options.Texture({ dynamic: false, source });
7501
+ haloTextures.set(maskFrame.key, texture);
7502
+ return texture;
7503
+ }
7012
7504
  function showRgbaMaskFrame(maskFrame) {
7013
7505
  if (!maskSprite) {
7014
7506
  return;
@@ -7020,6 +7512,13 @@ function createPixiMaskLayer(options) {
7020
7512
  maskSprite.height = mediaHeight;
7021
7513
  maskSprite.visible = true;
7022
7514
  idMaskRenderer?.hide();
7515
+ const haloTexture = getHaloTexture(maskFrame);
7516
+ if (haloTexture) {
7517
+ renderHalo(maskFrame, haloTexture);
7518
+ }
7519
+ else {
7520
+ haloRenderer?.hide();
7521
+ }
7023
7522
  }
7024
7523
  function showIdMaskFrame(maskFrame) {
7025
7524
  if (!idMaskRenderer || !maskSprite) {
@@ -7027,14 +7526,82 @@ function createPixiMaskLayer(options) {
7027
7526
  }
7028
7527
  maskSprite.visible = false;
7029
7528
  idMaskRenderer.render(maskFrame, getTexture(maskFrame));
7529
+ renderHalo(maskFrame, getTexture(maskFrame));
7530
+ }
7531
+ function renderHalo(maskFrame, texture) {
7532
+ if (!haloRenderer) {
7533
+ return;
7534
+ }
7535
+ const groups = resolveFrameHaloGroups();
7536
+ if (groups.length === 0) {
7537
+ haloRenderer.hide();
7538
+ return;
7539
+ }
7540
+ haloRenderer.render(maskFrame, texture, groups);
7541
+ }
7542
+ function refreshHalo() {
7543
+ if (activeIdMaskFrame) {
7544
+ renderHalo(activeIdMaskFrame, getTexture(activeIdMaskFrame));
7545
+ return;
7546
+ }
7547
+ if (activeRgbaMaskFrame) {
7548
+ const texture = getHaloTexture(activeRgbaMaskFrame);
7549
+ if (texture) {
7550
+ renderHalo(activeRgbaMaskFrame, texture);
7551
+ }
7552
+ }
7553
+ }
7554
+ /**
7555
+ * Halo instructions resolve per frame from the live halo style rather than
7556
+ * being baked into the prepared artifact, so halo edits never invalidate
7557
+ * prepared mask pixels. Detections are grouped by their requested spread
7558
+ * and every group renders in its own blur pass, preserving each
7559
+ * detection's public spread contract.
7560
+ */
7561
+ function resolveFrameHaloGroups() {
7562
+ if (visibleMaskMediaTime === null || !currentMaskHaloStyle) {
7563
+ return [];
7564
+ }
7565
+ const frame = options.detectionTimeline.selectFrame(visibleMaskMediaTime);
7566
+ if (!frame) {
7567
+ return [];
7568
+ }
7569
+ const bySpread = new Map();
7570
+ for (const [detectionIndex, detection] of frame.detections.entries()) {
7571
+ if (!detection.mask) {
7572
+ continue;
7573
+ }
7574
+ const halo = currentMaskHaloStyle.resolve(detection, {
7575
+ detectionIndex,
7576
+ frame,
7577
+ mediaTime: visibleMaskMediaTime,
7578
+ });
7579
+ if (!halo || halo.alpha <= 0 || halo.spread <= 0) {
7580
+ continue;
7581
+ }
7582
+ let group = bySpread.get(halo.spread);
7583
+ if (!group) {
7584
+ group = new Map();
7585
+ bySpread.set(halo.spread, group);
7586
+ }
7587
+ group.set(detectionIndex + 1, { alpha: halo.alpha, color: halo.color });
7588
+ }
7589
+ return [...bySpread.entries()]
7590
+ .sort(([left], [right]) => left - right)
7591
+ .map(([spread, halos]) => ({
7592
+ palette: buildMaskHaloPalette(halos),
7593
+ spread,
7594
+ }));
7030
7595
  }
7031
7596
  function hideSprite() {
7032
7597
  visibleMaskMediaTime = null;
7033
7598
  activeIdMaskFrame = null;
7599
+ activeRgbaMaskFrame = null;
7034
7600
  if (maskSprite) {
7035
7601
  maskSprite.visible = false;
7036
7602
  }
7037
7603
  idMaskRenderer?.hide();
7604
+ haloRenderer?.hide();
7038
7605
  }
7039
7606
  function canHoldVisibleMaskFor(mediaTime) {
7040
7607
  return (visibleMaskMediaTime !== null &&
@@ -7050,11 +7617,21 @@ function createPixiMaskLayer(options) {
7050
7617
  function destroyTexture(key) {
7051
7618
  const texture = maskTextures.get(key);
7052
7619
  if (!texture) {
7620
+ destroyHaloTexture(key);
7053
7621
  return;
7054
7622
  }
7055
7623
  releaseTextureBindings(key, texture);
7056
7624
  maskTextures.delete(key);
7057
7625
  texture.destroy(true);
7626
+ destroyHaloTexture(key);
7627
+ }
7628
+ function destroyHaloTexture(key) {
7629
+ const texture = haloTextures.get(key);
7630
+ if (!texture) {
7631
+ return;
7632
+ }
7633
+ haloTextures.delete(key);
7634
+ texture.destroy(true);
7058
7635
  }
7059
7636
  function destroyTextures() {
7060
7637
  releaseTextureBindings();
@@ -7062,6 +7639,10 @@ function createPixiMaskLayer(options) {
7062
7639
  texture.destroy(true);
7063
7640
  }
7064
7641
  maskTextures.clear();
7642
+ for (const texture of haloTextures.values()) {
7643
+ texture.destroy(true);
7644
+ }
7645
+ haloTextures.clear();
7065
7646
  }
7066
7647
  function releaseTextureBindings(key, texture) {
7067
7648
  if (!texture || maskSprite?.texture === texture) {
@@ -7073,6 +7654,31 @@ function createPixiMaskLayer(options) {
7073
7654
  idMaskRenderer?.clearTexture();
7074
7655
  }
7075
7656
  }
7657
+ function createHaloRenderer() {
7658
+ if (!options.BlurFilter ||
7659
+ !options.Container ||
7660
+ !options.Rectangle ||
7661
+ !options.Mesh ||
7662
+ !options.MeshGeometry ||
7663
+ !options.Shader ||
7664
+ !options.UniformGroup ||
7665
+ mediaWidth <= 0 ||
7666
+ mediaHeight <= 0) {
7667
+ return undefined;
7668
+ }
7669
+ return createPixiMaskHaloRenderer({
7670
+ BlurFilter: options.BlurFilter,
7671
+ Container: options.Container,
7672
+ Rectangle: options.Rectangle,
7673
+ ImageSource: options.ImageSource,
7674
+ Mesh: options.Mesh,
7675
+ MeshGeometry: options.MeshGeometry,
7676
+ Shader: options.Shader,
7677
+ UniformGroup: options.UniformGroup,
7678
+ mediaHeight,
7679
+ mediaWidth,
7680
+ });
7681
+ }
7076
7682
  function createIdMaskRenderer() {
7077
7683
  if (!options.Mesh ||
7078
7684
  !options.MeshGeometry ||
@@ -7273,6 +7879,9 @@ function canPreparePolygonInstruction(instruction) {
7273
7879
  return (instruction.points.length >= 3 &&
7274
7880
  Boolean(instruction.fill || stroke) &&
7275
7881
  !stroke?.dash?.length &&
7882
+ stroke?.cap === undefined &&
7883
+ stroke?.join === undefined &&
7884
+ stroke?.miterLimit === undefined &&
7276
7885
  (stroke?.alignment === undefined ||
7277
7886
  stroke.alignment === BoxStrokeAlignment.Center));
7278
7887
  }
@@ -7317,6 +7926,367 @@ function resolvePreparedPolygonInstructions(options) {
7317
7926
  return instructions;
7318
7927
  }
7319
7928
 
7929
+ const sharedAssets = new WeakMap();
7930
+ /** Browser implementation for asset-backed region renderer descriptors. */
7931
+ function createPixiRegionLayer(options) {
7932
+ let container;
7933
+ let currentFrame;
7934
+ let currentMediaTime = 0;
7935
+ let currentViewportScale = 1;
7936
+ let destroyed = false;
7937
+ let renderers = [...options.regionRenderers];
7938
+ const assets = new Map();
7939
+ const entries = new Map();
7940
+ const pools = new Map();
7941
+ syncAssets();
7942
+ return {
7943
+ createContainer() {
7944
+ if (!container) {
7945
+ container = new options.Container();
7946
+ container.sortableChildren = true;
7947
+ }
7948
+ return container;
7949
+ },
7950
+ drawFrame(mediaTime, viewportScale = 1) {
7951
+ currentMediaTime = mediaTime;
7952
+ currentViewportScale = viewportScale;
7953
+ currentFrame = options.detectionTimeline.selectFrame(mediaTime);
7954
+ const activeKeys = new Set();
7955
+ const activeDetectionIndexes = new Set();
7956
+ for (const entry of entries.values())
7957
+ entry.active = false;
7958
+ if (currentFrame) {
7959
+ for (const [rendererIndex, renderer] of renderers.entries()) {
7960
+ const asset = assets.get(renderer.id);
7961
+ if (!asset?.asset)
7962
+ continue;
7963
+ for (const [detectionIndex, detection,] of currentFrame.detections.entries()) {
7964
+ const context = {
7965
+ detectionIndex,
7966
+ frame: currentFrame,
7967
+ mediaTime,
7968
+ viewportScale,
7969
+ ...options.resolveContextState?.(detection),
7970
+ };
7971
+ if (context.hidden ||
7972
+ !matchesTarget(renderer, detection, context)) {
7973
+ continue;
7974
+ }
7975
+ const region = resolveRegion(renderer, detection);
7976
+ if (!region)
7977
+ continue;
7978
+ const key = resolveSpriteKey(renderer.id, detection, detectionIndex);
7979
+ const entry = ensureEntry(key, renderer.id, asset.src, asset.asset, detection.id);
7980
+ const position = positionSprite(entry.display, renderer, region, asset.asset);
7981
+ entry.baseX = position.x;
7982
+ entry.baseY = position.y;
7983
+ entry.detectionId = detection.id;
7984
+ entry.display.zIndex =
7985
+ (renderer.compose?.zIndex ?? 0) * 1_000_000 +
7986
+ rendererIndex * 10_000 +
7987
+ (detection.zIndex ?? detectionIndex);
7988
+ entry.display.visible = true;
7989
+ entry.active = true;
7990
+ activeKeys.add(key);
7991
+ activeDetectionIndexes.add(detectionIndex);
7992
+ }
7993
+ }
7994
+ }
7995
+ for (const [key, entry] of entries) {
7996
+ if (activeKeys.has(key))
7997
+ continue;
7998
+ entry.display.visible = false;
7999
+ pauseAnimatedDisplay(entry.display);
8000
+ entries.delete(key);
8001
+ const poolKey = resolvePoolKey(entry.rendererId, entry.src);
8002
+ const pool = pools.get(poolKey) ?? [];
8003
+ pool.push(entry);
8004
+ pools.set(poolKey, pool);
8005
+ }
8006
+ return { activeDetectionIndexes: [...activeDetectionIndexes] };
8007
+ },
8008
+ setRenderers(nextRenderers) {
8009
+ renderers = [...nextRenderers];
8010
+ syncAssets();
8011
+ this.drawFrame(currentMediaTime, currentViewportScale);
8012
+ },
8013
+ translateDetection(id, x, y) {
8014
+ let translated = false;
8015
+ for (const entry of entries.values()) {
8016
+ if (!entry.active || entry.detectionId !== id)
8017
+ continue;
8018
+ entry.display.position.set(entry.baseX + x, entry.baseY + y);
8019
+ translated = true;
8020
+ }
8021
+ return translated;
8022
+ },
8023
+ destroy() {
8024
+ destroyed = true;
8025
+ destroyAllDisplays();
8026
+ for (const asset of assets.values())
8027
+ asset.lease.release();
8028
+ assets.clear();
8029
+ container = undefined;
8030
+ currentFrame = undefined;
8031
+ },
8032
+ };
8033
+ function syncAssets() {
8034
+ const desired = new Map(renderers.map((renderer) => [renderer.id, renderer.source.asset.src]));
8035
+ for (const [id, asset] of assets) {
8036
+ if (desired.get(id) === asset.src)
8037
+ continue;
8038
+ destroyRendererDisplays(id);
8039
+ asset.lease.release();
8040
+ assets.delete(id);
8041
+ }
8042
+ for (const [id, src] of desired) {
8043
+ if (assets.has(id))
8044
+ continue;
8045
+ const lease = acquireAsset(options.Assets, src);
8046
+ const asset = { lease, src };
8047
+ assets.set(id, asset);
8048
+ void lease.asset.then((loadedAsset) => {
8049
+ if (destroyed || assets.get(id) !== asset)
8050
+ return;
8051
+ asset.asset = loadedAsset;
8052
+ options.onInvalidate?.();
8053
+ }, (error) => {
8054
+ // A failed asset is omitted. Replacing the descriptor retries it.
8055
+ options.onAssetError?.({ error, rendererId: id, src });
8056
+ });
8057
+ }
8058
+ }
8059
+ function ensureEntry(key, rendererId, src, asset, detectionId) {
8060
+ let entry = entries.get(key);
8061
+ if (entry)
8062
+ return entry;
8063
+ const poolKey = resolvePoolKey(rendererId, src);
8064
+ const pool = pools.get(poolKey);
8065
+ entry = pool?.pop();
8066
+ if (pool?.length === 0)
8067
+ pools.delete(poolKey);
8068
+ if (entry) {
8069
+ entry.detectionId = detectionId;
8070
+ resumeAnimatedDisplay(entry.display);
8071
+ }
8072
+ else {
8073
+ const display = isGifSource(asset)
8074
+ ? new options.GifSprite({ autoPlay: true, loop: true, source: asset })
8075
+ : new options.Sprite({ texture: asset });
8076
+ display.anchor.set(0.5);
8077
+ container?.addChild(display);
8078
+ entry = {
8079
+ active: false,
8080
+ baseX: 0,
8081
+ baseY: 0,
8082
+ detectionId,
8083
+ display,
8084
+ rendererId,
8085
+ src,
8086
+ };
8087
+ }
8088
+ entries.set(key, entry);
8089
+ return entry;
8090
+ }
8091
+ function destroyRendererDisplays(rendererId) {
8092
+ for (const [key, entry] of entries) {
8093
+ if (entry.rendererId !== rendererId)
8094
+ continue;
8095
+ destroyDisplay(entry.display);
8096
+ entries.delete(key);
8097
+ }
8098
+ for (const [key, pool] of pools) {
8099
+ const retained = pool.filter((entry) => {
8100
+ if (entry.rendererId !== rendererId)
8101
+ return true;
8102
+ destroyDisplay(entry.display);
8103
+ return false;
8104
+ });
8105
+ if (retained.length > 0)
8106
+ pools.set(key, retained);
8107
+ else
8108
+ pools.delete(key);
8109
+ }
8110
+ }
8111
+ function destroyAllDisplays() {
8112
+ for (const entry of entries.values())
8113
+ destroyDisplay(entry.display);
8114
+ for (const pool of pools.values()) {
8115
+ for (const entry of pool)
8116
+ destroyDisplay(entry.display);
8117
+ }
8118
+ entries.clear();
8119
+ pools.clear();
8120
+ }
8121
+ }
8122
+ function destroyDisplay(display) {
8123
+ display.removeFromParent?.();
8124
+ // GifSprite sources are shared and released through Assets.unload().
8125
+ display.destroy();
8126
+ }
8127
+ function pauseAnimatedDisplay(display) {
8128
+ if ("stop" in display && typeof display.stop === "function")
8129
+ display.stop();
8130
+ }
8131
+ function resumeAnimatedDisplay(display) {
8132
+ if ("play" in display && typeof display.play === "function")
8133
+ display.play();
8134
+ }
8135
+ function matchesTarget(renderer, detection, context) {
8136
+ const { target } = renderer;
8137
+ return (matchesValue(target.id, detection.id) &&
8138
+ matchesValue(target.className, detection.className) &&
8139
+ matchesValue(target.sourceId, detection.sourceId) &&
8140
+ (target.resolve?.(detection, context) ?? true));
8141
+ }
8142
+ function matchesValue(configured, actual) {
8143
+ if (configured === undefined)
8144
+ return true;
8145
+ if (actual === undefined)
8146
+ return false;
8147
+ return Array.isArray(configured)
8148
+ ? configured.includes(actual)
8149
+ : configured === actual;
8150
+ }
8151
+ function resolveRegion(renderer, detection) {
8152
+ if (renderer.region.kind === "bounds")
8153
+ return detection.rect;
8154
+ const fallbackSize = detection.rect
8155
+ ? Math.max(1, detection.rect.width * 0.4)
8156
+ : undefined;
8157
+ if (renderer.region.anchor === "head") {
8158
+ const facePoints = detection.keypoints?.points
8159
+ .slice(0, 5)
8160
+ .filter((_, index) => (detection.keypoints?.visibility?.[index] ?? 2) > 0);
8161
+ if (facePoints && facePoints.length > 0) {
8162
+ const xs = facePoints.map(({ x }) => x);
8163
+ const ys = facePoints.map(({ y }) => y);
8164
+ const width = Math.max(fallbackSize ?? 1, (Math.max(...xs) - Math.min(...xs)) * 2);
8165
+ return {
8166
+ height: width,
8167
+ width,
8168
+ x: xs.reduce((sum, x) => sum + x, 0) / xs.length,
8169
+ y: ys.reduce((sum, y) => sum + y, 0) / ys.length,
8170
+ };
8171
+ }
8172
+ if (!detection.rect || fallbackSize === undefined)
8173
+ return undefined;
8174
+ return {
8175
+ height: fallbackSize,
8176
+ width: fallbackSize,
8177
+ x: detection.rect.x,
8178
+ y: detection.rect.y - detection.rect.height / 2,
8179
+ };
8180
+ }
8181
+ const point = detection.keypoints?.points[renderer.region.anchor];
8182
+ const visibility = detection.keypoints?.visibility?.[renderer.region.anchor] ?? 2;
8183
+ if (!point || visibility <= 0 || fallbackSize === undefined)
8184
+ return undefined;
8185
+ return { height: fallbackSize, width: fallbackSize, x: point.x, y: point.y };
8186
+ }
8187
+ function positionSprite(sprite, renderer, region, asset) {
8188
+ const scale = finiteOr(renderer.transform?.scale, 1);
8189
+ const opacity = Math.min(1, Math.max(0, finiteOr(renderer.transform?.opacity, 1)));
8190
+ const sourceWidth = Math.max(1, asset.width);
8191
+ const sourceHeight = Math.max(1, asset.height);
8192
+ const containScale = Math.min(region.width / sourceWidth, region.height / sourceHeight);
8193
+ const offset = renderer.transform?.offset;
8194
+ sprite.alpha = opacity;
8195
+ sprite.width = sourceWidth * containScale * scale;
8196
+ sprite.height = sourceHeight * containScale * scale;
8197
+ const position = {
8198
+ x: region.x + finiteOr(offset?.x, 0) * region.width,
8199
+ y: region.y + finiteOr(offset?.y, 0) * region.height,
8200
+ };
8201
+ sprite.position.set(position.x, position.y);
8202
+ sprite.rotation = finiteOr(renderer.transform?.rotation, 0);
8203
+ return position;
8204
+ }
8205
+ function isGifSource(asset) {
8206
+ return ("totalFrames" in asset && "frames" in asset && Array.isArray(asset.frames));
8207
+ }
8208
+ function resolvePoolKey(rendererId, src) {
8209
+ return JSON.stringify([rendererId, src]);
8210
+ }
8211
+ function resolveSpriteKey(rendererId, detection, detectionIndex) {
8212
+ return JSON.stringify([rendererId, detection.id ?? null, detectionIndex]);
8213
+ }
8214
+ function finiteOr(value, fallback) {
8215
+ return value !== undefined && Number.isFinite(value) ? value : fallback;
8216
+ }
8217
+ function acquireAsset(loader, src) {
8218
+ let cache = sharedAssets.get(loader);
8219
+ if (!cache) {
8220
+ cache = new Map();
8221
+ sharedAssets.set(loader, cache);
8222
+ }
8223
+ let entry = cache.get(src);
8224
+ if (!entry) {
8225
+ entry = { asset: loader.load(src), references: 0 };
8226
+ cache.set(src, entry);
8227
+ }
8228
+ entry.references += 1;
8229
+ let released = false;
8230
+ return {
8231
+ release() {
8232
+ if (released)
8233
+ return;
8234
+ released = true;
8235
+ entry.references -= 1;
8236
+ if (entry.references > 0)
8237
+ return;
8238
+ cache.delete(src);
8239
+ void entry.asset.then(() => loader.unload(src), () => undefined);
8240
+ },
8241
+ asset: entry.asset,
8242
+ };
8243
+ }
8244
+
8245
+ /**
8246
+ * Bridges public annotation renderer kinds onto the internal vector-layer
8247
+ * shape pipeline.
8248
+ *
8249
+ * Each shape-backed renderer kind resolves its own public instruction per
8250
+ * detection; this adapter lowers those instructions into the internal
8251
+ * renderer-neutral shape vocabulary and concatenates them into the single
8252
+ * shape style the vector layer consumes. Kinds stay independent: a kind that
8253
+ * resolves nothing for a detection simply contributes no instructions.
8254
+ */
8255
+ function resolveAnnotationShapeStyle(styles) {
8256
+ const boxCornerStyle = styles.boxCornerStyle ?? null;
8257
+ const ellipseStyle = styles.ellipseStyle ?? null;
8258
+ const markerStyle = styles.markerStyle ?? null;
8259
+ if (!boxCornerStyle && !ellipseStyle && !markerStyle) {
8260
+ return null;
8261
+ }
8262
+ return {
8263
+ resolve(detection, context) {
8264
+ const instructions = [];
8265
+ const boxCorners = boxCornerStyle?.resolve(detection, context);
8266
+ if (boxCorners) {
8267
+ instructions.push({
8268
+ closed: false,
8269
+ kind: ShapeInstructionKind.Path,
8270
+ segments: boxCorners.segments,
8271
+ stroke: boxCorners.stroke,
8272
+ });
8273
+ }
8274
+ const marker = markerStyle?.resolve(detection, context);
8275
+ if (marker) {
8276
+ instructions.push({ kind: ShapeInstructionKind.Marker, ...marker });
8277
+ }
8278
+ const ellipse = ellipseStyle?.resolve(detection, context);
8279
+ if (ellipse) {
8280
+ instructions.push(lowerEllipseInstruction(ellipse));
8281
+ }
8282
+ return instructions.length > 0 ? instructions : undefined;
8283
+ },
8284
+ };
8285
+ }
8286
+ function lowerEllipseInstruction(instruction) {
8287
+ return { ...instruction, kind: ShapeInstructionKind.Ellipse };
8288
+ }
8289
+
7320
8290
  function createPixiAnnotationOverlayLayer(editingEngine, initialStyle) {
7321
8291
  let graphics;
7322
8292
  let style = resolveAnnotationOverlayStyle(initialStyle);
@@ -7549,8 +8519,11 @@ function drawLoading(graphics, context, style) {
7549
8519
  const loading = values instanceof Set ? values : new Set(values);
7550
8520
  const angle = (context.now / 500) % (Math.PI * 2);
7551
8521
  for (const detection of context.frame?.detections ?? []) {
7552
- if (detection.id === undefined || !loading.has(detection.id))
8522
+ if (detection.id === undefined ||
8523
+ !loading.has(detection.id) ||
8524
+ resolveAnnotationStyleState(detection, context.visibility).hidden) {
7553
8525
  continue;
8526
+ }
7554
8527
  const rect = getDetectionRect(detection);
7555
8528
  if (!rect)
7556
8529
  continue;
@@ -7625,6 +8598,7 @@ var PixiSceneLayerKind;
7625
8598
  PixiSceneLayerKind["Mask"] = "mask";
7626
8599
  PixiSceneLayerKind["Box"] = "box";
7627
8600
  PixiSceneLayerKind["Vector"] = "vector";
8601
+ PixiSceneLayerKind["Region"] = "region";
7628
8602
  PixiSceneLayerKind["Focus"] = "focus";
7629
8603
  PixiSceneLayerKind["Preview"] = "preview";
7630
8604
  PixiSceneLayerKind["Guide"] = "guide";
@@ -7669,6 +8643,8 @@ function defaultLayerOrder(kind) {
7669
8643
  return 200;
7670
8644
  case PixiSceneLayerKind.Vector:
7671
8645
  return 250;
8646
+ case PixiSceneLayerKind.Region:
8647
+ return 275;
7672
8648
  case PixiSceneLayerKind.Focus:
7673
8649
  return 300;
7674
8650
  case PixiSceneLayerKind.Preview:
@@ -7724,14 +8700,42 @@ function observePixiContainerResize(container, resize) {
7724
8700
  return () => observer.disconnect();
7725
8701
  }
7726
8702
  async function createPixiMediaScene(options) {
7727
- const { Application, CanvasSource, Container, Graphics, ImageSource, Mesh, MeshGeometry, Rectangle, Sprite, Shader, Text, Texture, UniformGroup, } = await import('pixi.js');
8703
+ const { Application, Assets, BlurFilter, CanvasSource, Container, Graphics, ImageSource, Mesh, MeshGeometry, Rectangle, Sprite, Shader, Text, Texture, UniformGroup, } = await import('pixi.js');
8704
+ const { GifSprite } = await import('pixi.js/gif');
7728
8705
  const app = new Application();
8706
+ let currentEllipseStyle = options.ellipseStyle ?? null;
8707
+ let currentBoxCornerStyle = options.boxCornerStyle ?? null;
7729
8708
  let currentLabelStyle = options.labelStyle ?? null;
7730
8709
  let currentMaskStyle = options.maskStyle ?? null;
8710
+ let currentMarkerStyle = options.markerStyle ?? null;
8711
+ let currentMaskHaloStyle = options.maskHaloStyle ?? null;
8712
+ // Halo-only presentations still need prepared id-mask artifacts; this
8713
+ // invisible style drives preparation without drawing any fill or border.
8714
+ const haloOnlyMaskStyle = {
8715
+ artifactKey: "mask-halo-only:v1",
8716
+ resolve: (detection) => detection.mask
8717
+ ? { alpha: 0, color: 0x000000, mask: detection.mask }
8718
+ : undefined,
8719
+ };
8720
+ const createVisibilityMaskHaloStyle = (style) => ({
8721
+ resolve(detection, context) {
8722
+ const state = resolveContextState(detection);
8723
+ if (state.hidden) {
8724
+ return undefined;
8725
+ }
8726
+ return style.resolve(detection, { ...context, ...state });
8727
+ },
8728
+ });
8729
+ const resolveArtifactMaskStyle = () => currentMaskStyle ?? (currentMaskHaloStyle ? haloOnlyMaskStyle : null);
7731
8730
  let currentPolygonStyle = options.polygonStyle === undefined
7732
8731
  ? new BasePolygonStyle()
7733
8732
  : options.polygonStyle;
7734
8733
  let currentVisibility = options.visibility;
8734
+ let currentMediaTime = 0;
8735
+ let viewportScale = 1;
8736
+ let hasPresentedSample = false;
8737
+ let mediaHeight = 0;
8738
+ let mediaWidth = 0;
7735
8739
  let visibilityVersion = 0;
7736
8740
  let visibilityMaskStyleCache = null;
7737
8741
  const resolveContextState = (detection) => resolveAnnotationStyleState(detection, currentVisibility);
@@ -7740,6 +8744,28 @@ async function createPixiMediaScene(options) {
7740
8744
  hidden: currentVisibility?.labelsHidden === true ||
7741
8745
  resolveContextState(detection).hidden,
7742
8746
  });
8747
+ // The vector layer consumes one shape style; renderer kinds that lower to
8748
+ // shape instructions compose with the internal hook here.
8749
+ function resolveVectorShapeStyle() {
8750
+ const kindShapeStyle = resolveAnnotationShapeStyle({
8751
+ boxCornerStyle: currentBoxCornerStyle,
8752
+ ellipseStyle: currentEllipseStyle,
8753
+ markerStyle: currentMarkerStyle,
8754
+ });
8755
+ const baseShapeStyle = options.shapeStyle ?? null;
8756
+ if (!kindShapeStyle || !baseShapeStyle) {
8757
+ return kindShapeStyle ?? baseShapeStyle;
8758
+ }
8759
+ return {
8760
+ resolve(detection, context) {
8761
+ const combined = [
8762
+ ...(baseShapeStyle.resolve(detection, context) ?? []),
8763
+ ...(kindShapeStyle.resolve(detection, context) ?? []),
8764
+ ];
8765
+ return combined.length > 0 ? combined : undefined;
8766
+ },
8767
+ };
8768
+ }
7743
8769
  const boxLayer = createPixiBoxLayer({
7744
8770
  boxStyle: options.boxStyle,
7745
8771
  Container: options.editingEngine ? Container : undefined,
@@ -7772,6 +8798,24 @@ async function createPixiMediaScene(options) {
7772
8798
  polygonStyle: polygonLayer?.getVectorFallbackStyle() ?? currentPolygonStyle,
7773
8799
  polylineStyle: options.polylineStyle,
7774
8800
  keypointStyle: options.keypointStyle,
8801
+ shapeStyle: resolveVectorShapeStyle(),
8802
+ resolveContextState,
8803
+ });
8804
+ const regionLayer = createPixiRegionLayer({
8805
+ Assets,
8806
+ Container,
8807
+ GifSprite,
8808
+ Sprite,
8809
+ detectionTimeline: options.detectionTimeline,
8810
+ onInvalidate: () => {
8811
+ if (!hasPresentedSample || mediaWidth <= 0 || mediaHeight <= 0)
8812
+ return;
8813
+ const boxState = boxLayer.drawFrame(currentMediaTime, viewportScale);
8814
+ const regionState = regionLayer.drawFrame(currentMediaTime, viewportScale);
8815
+ options.onPresentationUpdate?.(createPresentedSampleState(currentMediaTime, boxState, regionState));
8816
+ },
8817
+ onAssetError: options.diagnostics?.onAssetError,
8818
+ regionRenderers: options.regionRenderers,
7775
8819
  resolveContextState,
7776
8820
  });
7777
8821
  const annotationOverlayLayer = createPixiAnnotationOverlayLayer(options.editingEngine, options.annotationOverlayStyle);
@@ -7785,9 +8829,11 @@ async function createPixiMediaScene(options) {
7785
8829
  preview: options.maskBrush,
7786
8830
  })
7787
8831
  : undefined;
7788
- let maskLayer = options.maskStyle
8832
+ let maskLayer = resolveArtifactMaskStyle()
7789
8833
  ? createPixiMaskLayer({
8834
+ BlurFilter,
7790
8835
  Container,
8836
+ Rectangle,
7791
8837
  ImageSource,
7792
8838
  Mesh,
7793
8839
  MeshGeometry,
@@ -7796,7 +8842,10 @@ async function createPixiMediaScene(options) {
7796
8842
  Texture,
7797
8843
  UniformGroup,
7798
8844
  detectionTimeline: options.detectionTimeline,
7799
- maskStyle: createVisibilityMaskStyle(options.maskStyle),
8845
+ maskHaloStyle: currentMaskHaloStyle
8846
+ ? createVisibilityMaskHaloStyle(currentMaskHaloStyle)
8847
+ : null,
8848
+ maskStyle: createVisibilityMaskStyle(resolveArtifactMaskStyle()),
7800
8849
  onActiveIdMaskFramePresented: handleActiveIdMaskFramePresented,
7801
8850
  renderPreparation: options.renderPreparation,
7802
8851
  })
@@ -7828,8 +8877,6 @@ async function createPixiMediaScene(options) {
7828
8877
  if (!stagingContext) {
7829
8878
  throw new Error("Unable to create staging canvas context.");
7830
8879
  }
7831
- let mediaHeight = 0;
7832
- let mediaWidth = 0;
7833
8880
  let mediaScene;
7834
8881
  let vectorDisplay;
7835
8882
  let polygonDisplay;
@@ -7838,6 +8885,7 @@ async function createPixiMediaScene(options) {
7838
8885
  const maskSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Mask);
7839
8886
  const boxSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Box);
7840
8887
  const vectorSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Vector);
8888
+ const regionSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Region);
7841
8889
  const focusSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Focus);
7842
8890
  const previewSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Preview);
7843
8891
  const interactionSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Interaction);
@@ -7848,16 +8896,14 @@ async function createPixiMediaScene(options) {
7848
8896
  maskSlot,
7849
8897
  boxSlot,
7850
8898
  vectorSlot,
8899
+ regionSlot,
7851
8900
  focusSlot,
7852
8901
  previewSlot,
7853
8902
  handleSlot,
7854
8903
  interactionSlot,
7855
8904
  labelSlot,
7856
8905
  ];
7857
- let currentMediaTime = 0;
7858
- let viewportScale = 1;
7859
8906
  const viewport = createViewportController({ scale: 1 });
7860
- let hasPresentedSample = false;
7861
8907
  /**
7862
8908
  * Timestamp of the sample whose pixels are on the staging canvas. Unlike
7863
8909
  * `currentMediaTime`, presentation and selection updates never move it.
@@ -7873,6 +8919,7 @@ async function createPixiMediaScene(options) {
7873
8919
  interaction: options.interaction ?? {
7874
8920
  mode: MediaInteractionMode.Always,
7875
8921
  },
8922
+ canPickDetection: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
7876
8923
  editingEngine: options.editingEngine,
7877
8924
  capturePointer: (pointerId) => {
7878
8925
  if (!rendererCanvas.hasPointerCapture?.(pointerId)) {
@@ -7909,6 +8956,7 @@ async function createPixiMediaScene(options) {
7909
8956
  Text,
7910
8957
  UniformGroup,
7911
8958
  interactionStyle: options.interactionStyle,
8959
+ isDetectionVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
7912
8960
  })
7913
8961
  : undefined;
7914
8962
  let fastTranslatedDetectionId = null;
@@ -7916,6 +8964,7 @@ async function createPixiMediaScene(options) {
7916
8964
  fastTranslatedDetectionId = id;
7917
8965
  boxLayer.translateDetection(id, dx, dy);
7918
8966
  vectorLayer.translateDetection(id, dx, dy);
8967
+ regionLayer.translateDetection(id, dx, dy);
7919
8968
  labelLayer?.translateDetection(id, dx, dy);
7920
8969
  });
7921
8970
  const unsubscribeEditingState = options.editingEngine?.subscribe((state) => {
@@ -7927,11 +8976,13 @@ async function createPixiMediaScene(options) {
7927
8976
  fastTranslatedDetectionId = null;
7928
8977
  boxLayer.translateDetection(id, 0, 0);
7929
8978
  vectorLayer.translateDetection(id, 0, 0);
8979
+ regionLayer.translateDetection(id, 0, 0);
7930
8980
  labelLayer?.translateDetection(id, 0, 0);
7931
8981
  boxLayer.invalidateDetection(id);
7932
8982
  vectorLayer.invalidateDetection(id);
7933
8983
  boxLayer.drawFrame(currentMediaTime, viewportScale);
7934
8984
  vectorLayer.drawFrame(currentMediaTime, viewportScale);
8985
+ regionLayer.drawFrame(currentMediaTime, viewportScale);
7935
8986
  labelLayer?.drawFrame(currentMediaTime, viewportScale);
7936
8987
  });
7937
8988
  const handleKeyDown = (event) => {
@@ -7966,6 +9017,7 @@ async function createPixiMediaScene(options) {
7966
9017
  Shader,
7967
9018
  UniformGroup,
7968
9019
  focusStyle: options.focusStyle,
9020
+ isDetectionVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
7969
9021
  })
7970
9022
  : undefined;
7971
9023
  let mediaSprite;
@@ -8075,6 +9127,7 @@ async function createPixiMediaScene(options) {
8075
9127
  attachPolygonLayerDisplay();
8076
9128
  vectorDisplay.addChild(vectorLayer.createContainer());
8077
9129
  vectorSlot.setDisplay(vectorDisplay);
9130
+ regionSlot.setDisplay(regionLayer.createContainer());
8078
9131
  previewSlot.setDisplay(maskBrushPreview?.display);
8079
9132
  if (annotationOverlay) {
8080
9133
  annotationOverlayLayer.attachGraphics(annotationOverlay);
@@ -8128,13 +9181,14 @@ async function createPixiMediaScene(options) {
8128
9181
  const boxState = boxLayer.drawFrame(sample.timestamp, viewportScale);
8129
9182
  polygonLayer?.drawFrame(sample.timestamp, viewportScale);
8130
9183
  vectorLayer.drawFrame(sample.timestamp, viewportScale);
9184
+ const regionState = regionLayer.drawFrame(sample.timestamp, viewportScale);
8131
9185
  interactionLayer?.drawFrame(sample.timestamp);
8132
9186
  drawFocusLayer(sample.timestamp);
8133
9187
  drawInteractionPresentationLayer(sample.timestamp);
8134
9188
  labelLayer?.drawFrame(sample.timestamp, viewportScale);
8135
9189
  updateMediaSceneFit();
8136
9190
  return {
8137
- ...createPresentedSampleState(sample.timestamp, boxState),
9191
+ ...createPresentedSampleState(sample.timestamp, boxState, regionState),
8138
9192
  duration: sample.duration,
8139
9193
  };
8140
9194
  }
@@ -8156,10 +9210,12 @@ async function createPixiMediaScene(options) {
8156
9210
  maskLayer?.drawFrame(sample.timestamp);
8157
9211
  });
8158
9212
  let boxState;
9213
+ let regionState;
8159
9214
  boxMs = measure(() => {
8160
9215
  boxState = boxLayer.drawFrame(sample.timestamp, viewportScale);
8161
9216
  polygonLayer?.drawFrame(sample.timestamp, viewportScale);
8162
9217
  vectorLayer.drawFrame(sample.timestamp, viewportScale);
9218
+ regionState = regionLayer.drawFrame(sample.timestamp, viewportScale);
8163
9219
  });
8164
9220
  interactionMs = measure(() => {
8165
9221
  interactionLayer?.drawFrame(sample.timestamp);
@@ -8186,7 +9242,7 @@ async function createPixiMediaScene(options) {
8186
9242
  totalMs: elapsedSince(totalStart),
8187
9243
  };
8188
9244
  return {
8189
- ...createPresentedSampleState(sample.timestamp, boxState),
9245
+ ...createPresentedSampleState(sample.timestamp, boxState, regionState),
8190
9246
  duration: sample.duration,
8191
9247
  renderTimings,
8192
9248
  };
@@ -8335,6 +9391,7 @@ async function createPixiMediaScene(options) {
8335
9391
  visibilityVersion += 1;
8336
9392
  maskLayer?.setMaskStyle(createVisibilityMaskStyle(currentMaskStyle));
8337
9393
  }
9394
+ interactionLayer?.drawFrame(mediaTime);
8338
9395
  }
8339
9396
  boxLayer.setBoxStyle(presentation.boxStyle);
8340
9397
  if (presentation.polygonStyle !== undefined) {
@@ -8355,15 +9412,38 @@ async function createPixiMediaScene(options) {
8355
9412
  polylineStyle: presentation.polylineStyle,
8356
9413
  keypointStyle: presentation.keypointStyle,
8357
9414
  });
8358
- if (presentation.maskStyle !== undefined) {
8359
- currentMaskStyle = presentation.maskStyle;
8360
- const nextMaskLayer = presentation.maskStyle
8361
- ? ensureMaskLayer(presentation.maskStyle)
9415
+ regionLayer.setRenderers(presentation.renderers?.filter((renderer) => renderer.kind === "region") ?? []);
9416
+ if (presentation.boxCornerStyle !== undefined ||
9417
+ presentation.ellipseStyle !== undefined ||
9418
+ presentation.markerStyle !== undefined) {
9419
+ if (presentation.boxCornerStyle !== undefined) {
9420
+ currentBoxCornerStyle = presentation.boxCornerStyle;
9421
+ }
9422
+ if (presentation.ellipseStyle !== undefined) {
9423
+ currentEllipseStyle = presentation.ellipseStyle;
9424
+ }
9425
+ if (presentation.markerStyle !== undefined) {
9426
+ currentMarkerStyle = presentation.markerStyle;
9427
+ }
9428
+ vectorLayer.setStyles({ shapeStyle: resolveVectorShapeStyle() });
9429
+ }
9430
+ if (presentation.maskStyle !== undefined ||
9431
+ presentation.maskHaloStyle !== undefined) {
9432
+ if (presentation.maskStyle !== undefined) {
9433
+ currentMaskStyle = presentation.maskStyle;
9434
+ }
9435
+ if (presentation.maskHaloStyle !== undefined) {
9436
+ currentMaskHaloStyle = presentation.maskHaloStyle;
9437
+ }
9438
+ const artifactStyle = resolveArtifactMaskStyle();
9439
+ const nextMaskLayer = artifactStyle
9440
+ ? ensureMaskLayer(artifactStyle)
8362
9441
  : maskLayer;
8363
- nextMaskLayer?.setMaskStyle(presentation.maskStyle
8364
- ? createVisibilityMaskStyle(presentation.maskStyle)
8365
- : presentation.maskStyle);
8366
- if (presentation.maskStyle) {
9442
+ nextMaskLayer?.setMaskStyle(artifactStyle ? createVisibilityMaskStyle(artifactStyle) : null);
9443
+ nextMaskLayer?.setMaskHaloStyle(currentMaskHaloStyle
9444
+ ? createVisibilityMaskHaloStyle(currentMaskHaloStyle)
9445
+ : null);
9446
+ if (artifactStyle) {
8367
9447
  attachMaskLayerDisplay();
8368
9448
  syncSceneChildren();
8369
9449
  }
@@ -8397,11 +9477,12 @@ async function createPixiMediaScene(options) {
8397
9477
  const boxState = boxLayer.drawFrame(mediaTime, viewportScale);
8398
9478
  polygonLayer?.drawFrame(mediaTime, viewportScale);
8399
9479
  vectorLayer.drawFrame(mediaTime, viewportScale);
9480
+ const regionState = regionLayer.drawFrame(mediaTime, viewportScale);
8400
9481
  interactionLayer?.drawFrame(mediaTime);
8401
9482
  drawFocusLayer(mediaTime);
8402
9483
  drawInteractionPresentationLayer(mediaTime);
8403
9484
  labelLayer?.drawFrame(mediaTime, viewportScale);
8404
- return createPresentedSampleState(mediaTime, boxState);
9485
+ return createPresentedSampleState(mediaTime, boxState, regionState);
8405
9486
  },
8406
9487
  setSelectedDetection(selection, mediaTime) {
8407
9488
  currentMediaTime = selection?.mediaTime ?? mediaTime;
@@ -8428,6 +9509,7 @@ async function createPixiMediaScene(options) {
8428
9509
  polygonLayer?.destroy();
8429
9510
  labelLayer?.destroy();
8430
9511
  vectorLayer.destroy();
9512
+ regionLayer.destroy();
8431
9513
  maskBrushPreview?.destroy();
8432
9514
  unsubscribeFastTranslate?.();
8433
9515
  unsubscribeEditingState?.();
@@ -8444,21 +9526,24 @@ async function createPixiMediaScene(options) {
8444
9526
  });
8445
9527
  },
8446
9528
  };
8447
- function createPresentedSampleState(mediaTime, boxState) {
9529
+ function createPresentedSampleState(mediaTime, boxState, regionState) {
8448
9530
  const detectionFrame = options.detectionTimeline.selectFrame(mediaTime);
8449
9531
  return {
8450
- activeDetectionCount: countPresentedDetections(detectionFrame, mediaTime, boxState),
9532
+ activeDetectionCount: countPresentedDetections(detectionFrame, mediaTime, boxState, regionState),
8451
9533
  activeDetectionFrameIndex: detectionFrame?.frameIndex ?? null,
8452
9534
  activeDetectionFrameTime: detectionFrame?.mediaTime ?? null,
8453
9535
  detectionBuffer: options.detectionTimeline.getState(),
8454
9536
  mediaTime,
8455
9537
  };
8456
9538
  }
8457
- function countPresentedDetections(frame, mediaTime, boxState) {
9539
+ function countPresentedDetections(frame, mediaTime, boxState, regionState) {
8458
9540
  if (!frame) {
8459
9541
  return 0;
8460
9542
  }
8461
9543
  const visibleDetectionIndexes = new Set(boxState.activeDetectionIndexes);
9544
+ for (const detectionIndex of regionState?.activeDetectionIndexes ?? []) {
9545
+ visibleDetectionIndexes.add(detectionIndex);
9546
+ }
8462
9547
  for (const [detectionIndex, detection] of frame.detections.entries()) {
8463
9548
  if (visibleDetectionIndexes.has(detectionIndex)) {
8464
9549
  continue;
@@ -8485,7 +9570,9 @@ async function createPixiMediaScene(options) {
8485
9570
  function ensureMaskLayer(maskStyle) {
8486
9571
  if (!maskLayer) {
8487
9572
  maskLayer = createPixiMaskLayer({
9573
+ BlurFilter,
8488
9574
  Container,
9575
+ Rectangle,
8489
9576
  ImageSource,
8490
9577
  Mesh,
8491
9578
  MeshGeometry,
@@ -8494,6 +9581,9 @@ async function createPixiMediaScene(options) {
8494
9581
  Texture,
8495
9582
  UniformGroup,
8496
9583
  detectionTimeline: options.detectionTimeline,
9584
+ maskHaloStyle: currentMaskHaloStyle
9585
+ ? createVisibilityMaskHaloStyle(currentMaskHaloStyle)
9586
+ : null,
8497
9587
  maskStyle: createVisibilityMaskStyle(maskStyle),
8498
9588
  onActiveIdMaskFramePresented: handleActiveIdMaskFramePresented,
8499
9589
  renderPreparation: options.renderPreparation,
@@ -8571,6 +9661,7 @@ async function createPixiMediaScene(options) {
8571
9661
  Shader,
8572
9662
  UniformGroup,
8573
9663
  focusStyle,
9664
+ isDetectionVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
8574
9665
  });
8575
9666
  }
8576
9667
  return focusLayer;
@@ -8606,10 +9697,10 @@ async function createPixiMediaScene(options) {
8606
9697
  const interactionState = interactionLayer?.getState();
8607
9698
  focusLayer.drawFrame({
8608
9699
  frame,
8609
- hoveredPick: interactionState?.hoveredPick ?? null,
9700
+ hoveredPick: filterVisiblePick(interactionState?.hoveredPick ?? null),
8610
9701
  idMaskArtifact: maskLayer?.getActiveIdMaskFrameTexture() ?? null,
8611
9702
  mediaTime,
8612
- selectedPick: interactionState?.selectedPick ?? null,
9703
+ selectedPick: filterVisiblePick(interactionState?.selectedPick ?? null),
8613
9704
  viewportScale,
8614
9705
  });
8615
9706
  }
@@ -8621,11 +9712,11 @@ async function createPixiMediaScene(options) {
8621
9712
  const interactionState = interactionLayer?.getState();
8622
9713
  interactionPresentationLayer.drawFrame({
8623
9714
  frame,
8624
- hoveredPick: interactionState?.hoveredPick ?? null,
9715
+ hoveredPick: filterVisiblePick(interactionState?.hoveredPick ?? null),
8625
9716
  idMaskArtifact: maskLayer?.getActiveIdMaskFrameTexture() ?? null,
8626
9717
  mediaTime,
8627
- selectedPick: interactionState?.selectedPick ?? null,
8628
- selectedPicks: interactionState?.selectedPicks,
9718
+ selectedPick: filterVisiblePick(interactionState?.selectedPick ?? null),
9719
+ selectedPicks: interactionState?.selectedPicks.filter(({ detection }) => !resolveAnnotationStyleState(detection, currentVisibility).hidden),
8629
9720
  viewportScale,
8630
9721
  });
8631
9722
  }
@@ -8696,6 +9787,12 @@ async function createPixiMediaScene(options) {
8696
9787
  };
8697
9788
  return visibilityMaskStyle;
8698
9789
  }
9790
+ function filterVisiblePick(pick) {
9791
+ return pick &&
9792
+ !resolveAnnotationStyleState(pick.detection, currentVisibility).hidden
9793
+ ? pick
9794
+ : null;
9795
+ }
8699
9796
  }
8700
9797
  function canReuseMaskVisibilityArtifacts(previous, next) {
8701
9798
  return ((previous?.annotationsHidden === true) ===
@@ -9332,14 +10429,39 @@ async function createMediaSession(options) {
9332
10429
  const sessionDetections = preparedDetections;
9333
10430
  const defaultPresentation = createDefaultAnnotationPresentation();
9334
10431
  let currentPresentation = options.presentation;
9335
- const resolvePresentation = (presentation) => createSourceAwarePresentation({ ...defaultPresentation, ...presentation }, sessionDetections.sourcePresentations);
9336
- const initialPresentation = resolvePresentation(currentPresentation);
10432
+ const resolvePresentation = (presentation) => {
10433
+ const renderers = presentation?.renderers;
10434
+ const resolvedPresentation = resolveAnnotationRendererPresentation({
10435
+ ...defaultPresentation,
10436
+ ...presentation,
10437
+ });
10438
+ return createSourceAwarePresentation(resolvedPresentation, sessionDetections.sourcePresentations, renderers === undefined
10439
+ ? undefined
10440
+ : { enabledRendererKinds: renderers.map(({ kind }) => kind) });
10441
+ };
10442
+ const resolveRendererPresentation = (presentation) => {
10443
+ const resolved = resolvePresentation(presentation);
10444
+ return {
10445
+ ...resolved,
10446
+ // Style-backed renderers have already been lowered into their
10447
+ // specialized fields, including source overrides. Keep their kinds in
10448
+ // the authoritative list so the renderer core does not clear those
10449
+ // fields when direct renderers are also present, but let the resolved
10450
+ // fields win over the descriptors' original global styles.
10451
+ renderers: resolved.renderers?.map((renderer) => renderer.kind === "region"
10452
+ ? renderer
10453
+ : { ...renderer, style: undefined }),
10454
+ };
10455
+ };
10456
+ const initialPresentation = resolveRendererPresentation(currentPresentation);
9337
10457
  const renderer = await createMediaRenderer({
9338
10458
  ...options.renderer,
9339
10459
  ...sessionMedia.rendererSourceOption,
9340
10460
  backgroundColor: initialPresentation.backgroundColor,
9341
10461
  annotationOverlayStyle: initialPresentation.annotationOverlayStyle,
9342
10462
  boxStyle: initialPresentation.boxStyle,
10463
+ boxCornerStyle: initialPresentation.boxCornerStyle,
10464
+ ellipseStyle: initialPresentation.ellipseStyle,
9343
10465
  container: options.container,
9344
10466
  detectionBuffer: sessionDefaults.detectionBuffer,
9345
10467
  detectionFrames: sessionDetections.detectionFrames,
@@ -9348,9 +10470,12 @@ async function createMediaSession(options) {
9348
10470
  focusStyle: initialPresentation.focusStyle,
9349
10471
  interactionStyle: initialPresentation.interactionStyle,
9350
10472
  labelStyle: initialPresentation.labelStyle,
10473
+ maskHaloStyle: initialPresentation.maskHaloStyle,
9351
10474
  maskStyle: initialPresentation.maskStyle,
10475
+ markerStyle: initialPresentation.markerStyle,
9352
10476
  polygonStyle: initialPresentation.polygonStyle,
9353
10477
  polylineStyle: initialPresentation.polylineStyle,
10478
+ renderers: initialPresentation.renderers,
9354
10479
  keypointStyle: initialPresentation.keypointStyle,
9355
10480
  visibility: initialPresentation.visibility,
9356
10481
  onState(state) {
@@ -9432,7 +10557,7 @@ async function createMediaSession(options) {
9432
10557
  },
9433
10558
  setPresentation(presentation) {
9434
10559
  currentPresentation = presentation;
9435
- renderer.setPresentation(resolvePresentation(currentPresentation));
10560
+ renderer.setPresentation(resolveRendererPresentation(currentPresentation));
9436
10561
  },
9437
10562
  setRenderQuality(quality) {
9438
10563
  renderer.setRenderQuality(quality);