supervision 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +486 -77
- package/dist/index.js.map +1 -1
- package/dist/renderers/media-clahe.d.ts +6 -0
- package/dist/renderers/media-clahe.d.ts.map +1 -0
- package/dist/renderers/media-renderer-core.d.ts.map +1 -1
- package/dist/renderers/media-renderer-scene.d.ts +6 -0
- package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
- package/dist/renderers/pixi-interaction-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-media-inspection-filter.d.ts +8 -0
- package/dist/renderers/pixi-media-inspection-filter.d.ts.map +1 -0
- package/dist/renderers/pixi-media-scene.d.ts +5 -0
- package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
- package/dist/renderers/pixi-region-layer.d.ts +2 -0
- package/dist/renderers/pixi-region-layer.d.ts.map +1 -1
- package/dist/types/media-renderer.d.ts +7 -6
- package/dist/types/media-renderer.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/interaction.d.ts +7 -0
- package/node_modules/supervision-js-core/dist/types/interaction.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +4 -0
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { copySortedDetectionFrames, detectionFrameOverlapsRange, sortedDetectionFrames, projectDetectionFrameForTracking, createSortTracker, createOCSortTracker, createCBIoUTracker, createByteTrackTracker, MediaErrorKind, includeDefined, createDepthColormapLut, parseDepthManifest, validateDepthMap, resolveDepthFrameFile, PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS, decodeCompressedRleMask, decodeCompressedRleCounts, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, resolveIdMaskPaletteId, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createProjectedDetectionFrameSource, createArrayDetectionFrameSource, createBufferedDetectionTimeline, resolveAnnotationRendererPresentation, PlaybackGateReach, createIdleDetectionBufferState, MediaInteractionMode, MAX_ID_MASK_PALETTE_ENTRIES, StrokeAlignment, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, depthPreviewTopCode, resolveDepthColorMapping, resolveDepthQuantity, computeDepthPercentileRange, BaseFocusStyle, extractDetectionMaskRectRuns, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget,
|
|
1
|
+
import { copySortedDetectionFrames, detectionFrameOverlapsRange, sortedDetectionFrames, projectDetectionFrameForTracking, createSortTracker, createOCSortTracker, createCBIoUTracker, createByteTrackTracker, MediaErrorKind, includeDefined, createDepthColormapLut, parseDepthManifest, validateDepthMap, resolveDepthFrameFile, PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS, decodeCompressedRleMask, decodeCompressedRleCounts, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, resolveIdMaskPaletteId, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createProjectedDetectionFrameSource, createArrayDetectionFrameSource, createBufferedDetectionTimeline, resolveAnnotationRendererPresentation, PlaybackGateReach, createIdleDetectionBufferState, MediaInteractionMode, MAX_ID_MASK_PALETTE_ENTRIES, StrokeAlignment, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, depthPreviewTopCode, resolveDepthColorMapping, resolveDepthQuantity, computeDepthPercentileRange, BaseFocusStyle, extractDetectionMaskRectRuns, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget, rebaseDetectionPickToFrame, pickDetectionAtPoint, createDetectionPickKey, followDetectionPickAcrossFrames, haveSameDetectionPickIdentities, pickAnnotationHandle, getAnnotationHandles, MAX_ID_MASK_STROKE_WIDTH, LabelPlacement, BasePolygonStyle, BasePolylineStyle, BaseKeypointStyle, ShapeInstructionKind, sampleEllipseArc, resolveEllipseSegmentCount, KeypointMarkerShape, MarkerSizeSpace, MarkerShape, resolveMarkerGeometry, BaseInteractionStyle, DetectionInteractionState, writeIdMaskPaletteEntry, resolveIdMaskStrokeTexels, DetectionBufferStatus, resolveMaskStyleOpacity, pickDetectionByMaskId, BoxStrokeAlignment, RegionRendererMediaEffectKind, RegionRendererSourceKind, RegionRendererCoverageKind, RegionRendererSizeSpace, resolveStyleValue, resolveAnnotationStyleState, lightenColor, resolveAnnotationRendererStyleFields, annotationRendererKinds, createViewportController, MediaSessionMode, DetectionFrameRetentionMode, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, createCompositeDetectionFrameSource, MediaSessionActivityStatus, MediaSessionActivityKind, MediaSessionStatus, createSourceAwarePresentation, projectDetectionFrames, DetectionFrameSelectionMode } from 'supervision-js-core';
|
|
2
2
|
export { BaseBoxCornerStyle, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMarkerStyle, BaseMaskStyle, BaseOrientedBoxStyle, BasePercentageBarStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, BoxStrokeAlignment, DEFAULT_DETECTION_CLASS_STYLES, DEFAULT_DETECTION_COLOR_SEQUENCE, DepthColormap, DepthMapKind, DepthPreviewLevels, DepthQuantity, DepthSampling, DetectionBufferStatus, DetectionFrameRetentionMode, DetectionFrameSelectionMode, DetectionInteractionState, DetectionMaskEncoding, DetectionPickTarget, FocusTargetMode, KeypointMarkerShape, KeypointVisibility, LabelPlacement, LabelVisibilityMode, MarkerShape, MarkerSizeSpace, MaskRenderMode, MediaErrorKind, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, PercentageBarPlacement, PlaybackGateReach, RegionRendererComposeMode, RegionRendererCoverageKind, RegionRendererMediaEffectKind, RegionRendererRegionKind, RegionRendererSizeSpace, RegionRendererSourceKind, SUPERVISION_ROBOFLOW_COLOR, TrackingGeometry, annotationRendererKinds, annotationRenderers, computeDepthPercentileRange, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createByteTrackTracker, createCBIoUTracker, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createMemoryColdDetectionFrameStore, createOCSortTracker, createProjectedDetectionFrameSource, createSortTracker, createWritableDetectionFrameSource, depthColormapColors, detectionPostProcessors, normalizeDetectionClassName, parseDepthManifest, pickDetectionAtPoint, projectDetectionFrame, projectDetectionFrameForTracking, projectDetectionFrames, readDepthAt, resolveDetectionClassColorStyle } from 'supervision-js-core';
|
|
3
3
|
import { c as createMediaFrameClock, M as MediaSourceError, t as toMediaSourceError, r as resolveDisplayPixelRatio } from './video-engine-media-source-DOFsuGS4.js';
|
|
4
4
|
export { a as createWebVideoEngineMediaRendererSource, g as getMediaErrorKind, i as isMediaSourceError, o as openWebVideoEngineMediaSource } from './video-engine-media-source-DOFsuGS4.js';
|
|
@@ -10132,6 +10132,9 @@ async function createMediaRendererCore(options, providers) {
|
|
|
10132
10132
|
polygonStyle: currentPresentation.polygonStyle,
|
|
10133
10133
|
polylineStyle: currentPresentation.polylineStyle,
|
|
10134
10134
|
presentedFrames: protectedPresentedFrames?.source,
|
|
10135
|
+
// A source with no duration, such as a still image, has one frame to
|
|
10136
|
+
// show and nothing to play.
|
|
10137
|
+
stillFrame: metadata.duration === 0,
|
|
10135
10138
|
regionRenderers: resolveRegionRenderers(currentPresentation),
|
|
10136
10139
|
heatmapRenderers: resolveHeatmapRenderers(currentPresentation),
|
|
10137
10140
|
depthRenderers: resolveDepthRenderers(currentPresentation),
|
|
@@ -10647,6 +10650,260 @@ function matches(left, right) {
|
|
|
10647
10650
|
left.every((value, index) => Object.is(value, right[index])));
|
|
10648
10651
|
}
|
|
10649
10652
|
|
|
10653
|
+
const BINS = 256;
|
|
10654
|
+
const TILE_COUNT = 8;
|
|
10655
|
+
const CLIP_LIMIT = 2;
|
|
10656
|
+
function computeMediaClahe(rgba, width, height) {
|
|
10657
|
+
const tilesX = Math.min(TILE_COUNT, width);
|
|
10658
|
+
const tilesY = Math.min(TILE_COUNT, height);
|
|
10659
|
+
const luts = new Uint8Array(tilesX * tilesY * BINS);
|
|
10660
|
+
const histogram = new Uint32Array(BINS);
|
|
10661
|
+
for (let ty = 0; ty < tilesY; ty++) {
|
|
10662
|
+
const y0 = Math.floor((ty * height) / tilesY);
|
|
10663
|
+
const y1 = Math.floor(((ty + 1) * height) / tilesY);
|
|
10664
|
+
for (let tx = 0; tx < tilesX; tx++) {
|
|
10665
|
+
const x0 = Math.floor((tx * width) / tilesX);
|
|
10666
|
+
const x1 = Math.floor(((tx + 1) * width) / tilesX);
|
|
10667
|
+
histogram.fill(0);
|
|
10668
|
+
for (let y = y0; y < y1; y++) {
|
|
10669
|
+
for (let x = x0; x < x1; x++) {
|
|
10670
|
+
const pixel = (y * width + x) * 4;
|
|
10671
|
+
const luma = Math.round(0.299 * rgba[pixel] +
|
|
10672
|
+
0.587 * rgba[pixel + 1] +
|
|
10673
|
+
0.114 * rgba[pixel + 2]);
|
|
10674
|
+
histogram[luma]++;
|
|
10675
|
+
}
|
|
10676
|
+
}
|
|
10677
|
+
const area = (x1 - x0) * (y1 - y0);
|
|
10678
|
+
const limit = Math.max(1, Math.floor((CLIP_LIMIT * area) / BINS));
|
|
10679
|
+
let excess = 0;
|
|
10680
|
+
for (let i = 0; i < BINS; i++) {
|
|
10681
|
+
if (histogram[i] > limit) {
|
|
10682
|
+
excess += histogram[i] - limit;
|
|
10683
|
+
histogram[i] = limit;
|
|
10684
|
+
}
|
|
10685
|
+
}
|
|
10686
|
+
const perBin = Math.floor(excess / BINS);
|
|
10687
|
+
let residual = excess - perBin * BINS;
|
|
10688
|
+
for (let i = 0; i < BINS; i++)
|
|
10689
|
+
histogram[i] += perBin;
|
|
10690
|
+
const residualStep = Math.max(Math.floor(BINS / Math.max(residual, 1)), 1);
|
|
10691
|
+
for (let i = 0; i < BINS && residual > 0; i += residualStep, residual--) {
|
|
10692
|
+
histogram[i]++;
|
|
10693
|
+
}
|
|
10694
|
+
const offset = (ty * tilesX + tx) * BINS;
|
|
10695
|
+
let cumulative = 0;
|
|
10696
|
+
for (let i = 0; i < BINS; i++) {
|
|
10697
|
+
cumulative += histogram[i];
|
|
10698
|
+
luts[offset + i] = Math.round((cumulative * (BINS - 1)) / area);
|
|
10699
|
+
}
|
|
10700
|
+
}
|
|
10701
|
+
}
|
|
10702
|
+
return { luts, tilesX, tilesY };
|
|
10703
|
+
}
|
|
10704
|
+
|
|
10705
|
+
/**
|
|
10706
|
+
* Pixi keys its program cache by source, so one program is shared by every
|
|
10707
|
+
* shader built from it. `destroy(true)` takes that program with it, leaving
|
|
10708
|
+
* both the peers still rendering through it and the next shader built from the
|
|
10709
|
+
* same source bound to nothing.
|
|
10710
|
+
*/
|
|
10711
|
+
function destroyShaderKeepingProgram(shader) {
|
|
10712
|
+
try {
|
|
10713
|
+
shader.destroy();
|
|
10714
|
+
}
|
|
10715
|
+
catch {
|
|
10716
|
+
// Pixi has already invalidated this shader's resource group.
|
|
10717
|
+
}
|
|
10718
|
+
}
|
|
10719
|
+
/**
|
|
10720
|
+
* Pixi uploads a placeholder while it builds a shader's first bind group, and
|
|
10721
|
+
* WebGPU rejects a canvas that was never given a rendering context.
|
|
10722
|
+
*/
|
|
10723
|
+
function createShaderPlaceholderCanvas() {
|
|
10724
|
+
if (typeof document === "undefined") {
|
|
10725
|
+
return { height: 1, width: 1 };
|
|
10726
|
+
}
|
|
10727
|
+
const canvas = document.createElement("canvas");
|
|
10728
|
+
canvas.height = 1;
|
|
10729
|
+
canvas.width = 1;
|
|
10730
|
+
canvas.getContext("2d");
|
|
10731
|
+
return canvas;
|
|
10732
|
+
}
|
|
10733
|
+
|
|
10734
|
+
const HISTOGRAM_MAX_DIMENSION = 1024;
|
|
10735
|
+
const vertex = `
|
|
10736
|
+
in vec2 aPosition;
|
|
10737
|
+
out vec2 vTextureCoord;
|
|
10738
|
+
out vec2 vImageCoord;
|
|
10739
|
+
uniform vec4 uInputSize;
|
|
10740
|
+
uniform vec4 uOutputFrame;
|
|
10741
|
+
uniform vec4 uOutputTexture;
|
|
10742
|
+
uniform mat3 uImageMatrix;
|
|
10743
|
+
void main() {
|
|
10744
|
+
vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
|
|
10745
|
+
position.x = position.x * (2.0 / uOutputTexture.x) - 1.0;
|
|
10746
|
+
position.y = position.y * (2.0 * uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z;
|
|
10747
|
+
gl_Position = vec4(position, 0.0, 1.0);
|
|
10748
|
+
vTextureCoord = aPosition * (uOutputFrame.zw * uInputSize.zw);
|
|
10749
|
+
vImageCoord = (uImageMatrix * vec3(vTextureCoord, 1.0)).xy;
|
|
10750
|
+
}
|
|
10751
|
+
`;
|
|
10752
|
+
const fragment = `
|
|
10753
|
+
in vec2 vTextureCoord;
|
|
10754
|
+
in vec2 vImageCoord;
|
|
10755
|
+
out vec4 finalColor;
|
|
10756
|
+
uniform sampler2D uTexture;
|
|
10757
|
+
uniform sampler2D uLut;
|
|
10758
|
+
uniform vec2 uTiles;
|
|
10759
|
+
uniform float uGamma;
|
|
10760
|
+
uniform float uAuto;
|
|
10761
|
+
float lookup(vec2 tile, float luma) {
|
|
10762
|
+
float row = tile.y * uTiles.x + tile.x;
|
|
10763
|
+
return texture(uLut, vec2((luma * 255.0 + 0.5) / 256.0,
|
|
10764
|
+
(row + 0.5) / (uTiles.x * uTiles.y))).r;
|
|
10765
|
+
}
|
|
10766
|
+
void main() {
|
|
10767
|
+
vec4 color = texture(uTexture, vTextureCoord);
|
|
10768
|
+
if (color.a == 0.0) { finalColor = color; return; }
|
|
10769
|
+
vec3 rgb = color.rgb / color.a;
|
|
10770
|
+
if (uAuto > 0.5) {
|
|
10771
|
+
float luma = dot(rgb, vec3(0.299, 0.587, 0.114));
|
|
10772
|
+
vec2 tilePosition = clamp(vImageCoord, 0.0, 1.0) * uTiles - 0.5;
|
|
10773
|
+
vec2 t0 = floor(tilePosition);
|
|
10774
|
+
vec2 f = tilePosition - t0;
|
|
10775
|
+
vec2 t1 = clamp(t0 + 1.0, vec2(0.0), uTiles - 1.0);
|
|
10776
|
+
t0 = clamp(t0, vec2(0.0), uTiles - 1.0);
|
|
10777
|
+
float top = mix(lookup(t0, luma), lookup(vec2(t1.x, t0.y), luma), f.x);
|
|
10778
|
+
float bottom = mix(lookup(vec2(t0.x, t1.y), luma), lookup(t1, luma), f.x);
|
|
10779
|
+
rgb = clamp(rgb + (mix(top, bottom, f.y) - luma), 0.0, 1.0);
|
|
10780
|
+
}
|
|
10781
|
+
finalColor = vec4(pow(rgb, vec3(1.0 / uGamma)) * color.a, color.a);
|
|
10782
|
+
}
|
|
10783
|
+
`;
|
|
10784
|
+
const gpuSource = `
|
|
10785
|
+
struct GlobalFilterUniforms {
|
|
10786
|
+
uInputSize: vec4<f32>, uInputPixel: vec4<f32>, uInputClamp: vec4<f32>,
|
|
10787
|
+
uOutputFrame: vec4<f32>, uGlobalFrame: vec4<f32>, uOutputTexture: vec4<f32>,
|
|
10788
|
+
};
|
|
10789
|
+
struct InspectionUniforms {
|
|
10790
|
+
uImageMatrix: mat3x3<f32>, uTiles: vec2<f32>, uGamma: f32, uAuto: f32,
|
|
10791
|
+
};
|
|
10792
|
+
@group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;
|
|
10793
|
+
@group(0) @binding(1) var uTexture: texture_2d<f32>;
|
|
10794
|
+
@group(0) @binding(2) var uSampler: sampler;
|
|
10795
|
+
@group(1) @binding(0) var<uniform> inspectionUniforms: InspectionUniforms;
|
|
10796
|
+
@group(1) @binding(1) var uLut: texture_2d<f32>;
|
|
10797
|
+
@group(1) @binding(2) var uLutSampler: sampler;
|
|
10798
|
+
struct VertexOutput {
|
|
10799
|
+
@builtin(position) position: vec4<f32>,
|
|
10800
|
+
@location(0) uv: vec2<f32>, @location(1) image: vec2<f32>,
|
|
10801
|
+
};
|
|
10802
|
+
@vertex fn mainVertex(@location(0) aPosition: vec2<f32>) -> VertexOutput {
|
|
10803
|
+
var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;
|
|
10804
|
+
position.x = position.x * (2.0 / gfu.uOutputTexture.x) - 1.0;
|
|
10805
|
+
position.y = position.y * (2.0 * gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z;
|
|
10806
|
+
let uv = aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw);
|
|
10807
|
+
return VertexOutput(vec4(position, 0.0, 1.0), uv,
|
|
10808
|
+
(inspectionUniforms.uImageMatrix * vec3(uv, 1.0)).xy);
|
|
10809
|
+
}
|
|
10810
|
+
fn lookup(tile: vec2<f32>, luma: f32) -> f32 {
|
|
10811
|
+
let tiles = inspectionUniforms.uTiles;
|
|
10812
|
+
let row = tile.y * tiles.x + tile.x;
|
|
10813
|
+
return textureSampleLevel(uLut, uLutSampler,
|
|
10814
|
+
vec2((luma * 255.0 + 0.5) / 256.0, (row + 0.5) / (tiles.x * tiles.y)), 0.0).r;
|
|
10815
|
+
}
|
|
10816
|
+
@fragment fn mainFragment(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
10817
|
+
let color = textureSample(uTexture, uSampler, input.uv);
|
|
10818
|
+
if (color.a == 0.0) { return color; }
|
|
10819
|
+
var rgb = color.rgb / color.a;
|
|
10820
|
+
if (inspectionUniforms.uAuto > 0.5) {
|
|
10821
|
+
let luma = dot(rgb, vec3(0.299, 0.587, 0.114));
|
|
10822
|
+
let tiles = inspectionUniforms.uTiles;
|
|
10823
|
+
let tilePosition = clamp(input.image, vec2(0.0), vec2(1.0)) * tiles - 0.5;
|
|
10824
|
+
let unbounded = floor(tilePosition);
|
|
10825
|
+
let f = tilePosition - unbounded;
|
|
10826
|
+
let t0 = clamp(unbounded, vec2(0.0), tiles - 1.0);
|
|
10827
|
+
let t1 = clamp(unbounded + 1.0, vec2(0.0), tiles - 1.0);
|
|
10828
|
+
let top = mix(lookup(t0, luma), lookup(vec2(t1.x, t0.y), luma), f.x);
|
|
10829
|
+
let bottom = mix(lookup(vec2(t0.x, t1.y), luma), lookup(t1, luma), f.x);
|
|
10830
|
+
rgb = clamp(rgb + (mix(top, bottom, f.y) - luma), vec3(0.0), vec3(1.0));
|
|
10831
|
+
}
|
|
10832
|
+
return vec4(pow(rgb, vec3(1.0 / inspectionUniforms.uGamma)) * color.a, color.a);
|
|
10833
|
+
}
|
|
10834
|
+
`;
|
|
10835
|
+
function createPixiMediaInspectionFilter(sprite, pixi) {
|
|
10836
|
+
const { BufferImageSource, Filter, Matrix } = pixi;
|
|
10837
|
+
const pixels = new Uint8Array(256 * 64 * 4);
|
|
10838
|
+
const lutSource = new BufferImageSource({
|
|
10839
|
+
resource: pixels,
|
|
10840
|
+
width: 256,
|
|
10841
|
+
height: 64,
|
|
10842
|
+
scaleMode: "linear",
|
|
10843
|
+
alphaMode: "no-premultiply-alpha",
|
|
10844
|
+
});
|
|
10845
|
+
const filter = Filter.from({
|
|
10846
|
+
gl: { vertex: vertex, fragment: fragment },
|
|
10847
|
+
gpu: {
|
|
10848
|
+
vertex: { source: gpuSource, entryPoint: "mainVertex" },
|
|
10849
|
+
fragment: { source: gpuSource, entryPoint: "mainFragment" },
|
|
10850
|
+
},
|
|
10851
|
+
resolution: "inherit",
|
|
10852
|
+
resources: {
|
|
10853
|
+
inspectionUniforms: {
|
|
10854
|
+
uImageMatrix: { value: new Matrix(), type: "mat3x3<f32>" },
|
|
10855
|
+
uTiles: { value: new Float32Array([8, 8]), type: "vec2<f32>" },
|
|
10856
|
+
uGamma: { value: 1, type: "f32" },
|
|
10857
|
+
uAuto: { value: 0, type: "f32" },
|
|
10858
|
+
},
|
|
10859
|
+
uLut: lutSource,
|
|
10860
|
+
uLutSampler: lutSource.style,
|
|
10861
|
+
},
|
|
10862
|
+
});
|
|
10863
|
+
const uniforms = filter.resources.inspectionUniforms.uniforms;
|
|
10864
|
+
let hasHistogram = false;
|
|
10865
|
+
filter.apply = (manager, input, output, clear) => {
|
|
10866
|
+
// The filter input is clipped by the viewport; lookup tiles stay anchored
|
|
10867
|
+
// to the full image when the user zooms or pans.
|
|
10868
|
+
manager.calculateSpriteMatrix(uniforms.uImageMatrix, sprite);
|
|
10869
|
+
manager.applyFilter(filter, input, output, clear);
|
|
10870
|
+
};
|
|
10871
|
+
return {
|
|
10872
|
+
filter,
|
|
10873
|
+
invalidateImage() {
|
|
10874
|
+
hasHistogram = false;
|
|
10875
|
+
},
|
|
10876
|
+
update(gamma, autoContrast, image) {
|
|
10877
|
+
uniforms.uGamma = Math.max(gamma, 0.0001);
|
|
10878
|
+
if (autoContrast && image && !hasHistogram) {
|
|
10879
|
+
const scale = Math.min(1, HISTOGRAM_MAX_DIMENSION / Math.max(image.width, image.height));
|
|
10880
|
+
const sample = document.createElement("canvas");
|
|
10881
|
+
sample.width = Math.max(1, Math.round(image.width * scale));
|
|
10882
|
+
sample.height = Math.max(1, Math.round(image.height * scale));
|
|
10883
|
+
const context = sample.getContext("2d", { willReadFrequently: true });
|
|
10884
|
+
context.drawImage(image, 0, 0, sample.width, sample.height);
|
|
10885
|
+
const { luts, tilesX, tilesY } = computeMediaClahe(context.getImageData(0, 0, sample.width, sample.height).data, sample.width, sample.height);
|
|
10886
|
+
for (let i = 0; i < luts.length; i++) {
|
|
10887
|
+
pixels[i * 4] = luts[i];
|
|
10888
|
+
pixels[i * 4 + 1] = luts[i];
|
|
10889
|
+
pixels[i * 4 + 2] = luts[i];
|
|
10890
|
+
pixels[i * 4 + 3] = 255;
|
|
10891
|
+
}
|
|
10892
|
+
uniforms.uTiles.set([tilesX, tilesY]);
|
|
10893
|
+
lutSource.resource = pixels.subarray(0, luts.length * 4);
|
|
10894
|
+
lutSource.resize(256, tilesX * tilesY);
|
|
10895
|
+
lutSource.update();
|
|
10896
|
+
hasHistogram = true;
|
|
10897
|
+
}
|
|
10898
|
+
uniforms.uAuto = autoContrast && hasHistogram ? 1 : 0;
|
|
10899
|
+
},
|
|
10900
|
+
destroy() {
|
|
10901
|
+
destroyShaderKeepingProgram(filter);
|
|
10902
|
+
lutSource.destroy();
|
|
10903
|
+
},
|
|
10904
|
+
};
|
|
10905
|
+
}
|
|
10906
|
+
|
|
10650
10907
|
/**
|
|
10651
10908
|
* Which frame a media time draws and how much of it is cooked, as two separate
|
|
10652
10909
|
* facts. Reporting readiness is what the snapshot and the token are for; a
|
|
@@ -10786,35 +11043,6 @@ void main(void) {
|
|
|
10786
11043
|
}
|
|
10787
11044
|
`;
|
|
10788
11045
|
|
|
10789
|
-
/**
|
|
10790
|
-
* Pixi keys its program cache by source, so one program is shared by every
|
|
10791
|
-
* shader built from it. `destroy(true)` takes that program with it, leaving
|
|
10792
|
-
* both the peers still rendering through it and the next shader built from the
|
|
10793
|
-
* same source bound to nothing.
|
|
10794
|
-
*/
|
|
10795
|
-
function destroyShaderKeepingProgram(shader) {
|
|
10796
|
-
try {
|
|
10797
|
-
shader.destroy();
|
|
10798
|
-
}
|
|
10799
|
-
catch {
|
|
10800
|
-
// Pixi has already invalidated this shader's resource group.
|
|
10801
|
-
}
|
|
10802
|
-
}
|
|
10803
|
-
/**
|
|
10804
|
-
* Pixi uploads a placeholder while it builds a shader's first bind group, and
|
|
10805
|
-
* WebGPU rejects a canvas that was never given a rendering context.
|
|
10806
|
-
*/
|
|
10807
|
-
function createShaderPlaceholderCanvas() {
|
|
10808
|
-
if (typeof document === "undefined") {
|
|
10809
|
-
return { height: 1, width: 1 };
|
|
10810
|
-
}
|
|
10811
|
-
const canvas = document.createElement("canvas");
|
|
10812
|
-
canvas.height = 1;
|
|
10813
|
-
canvas.width = 1;
|
|
10814
|
-
canvas.getContext("2d");
|
|
10815
|
-
return canvas;
|
|
10816
|
-
}
|
|
10817
|
-
|
|
10818
11046
|
/**
|
|
10819
11047
|
* The halo a detection actually paints, or `undefined`. A style may answer an
|
|
10820
11048
|
* instruction that draws nothing, and both the coverage the halo prepares and
|
|
@@ -13562,7 +13790,6 @@ function createPixiInteractionLayer(options) {
|
|
|
13562
13790
|
let hoveredPick = null;
|
|
13563
13791
|
let hoveredPickKey = null;
|
|
13564
13792
|
let selectedPick = null;
|
|
13565
|
-
let selectedPickKey = null;
|
|
13566
13793
|
let selectedPicks = [];
|
|
13567
13794
|
let marqueeStart = null;
|
|
13568
13795
|
let marqueeRect = null;
|
|
@@ -13615,9 +13842,19 @@ function createPixiInteractionLayer(options) {
|
|
|
13615
13842
|
setSelectedPick(null);
|
|
13616
13843
|
return null;
|
|
13617
13844
|
}
|
|
13618
|
-
const
|
|
13619
|
-
|
|
13620
|
-
|
|
13845
|
+
const nextPicks = selection.detectionIds
|
|
13846
|
+
? [...new Set(selection.detectionIds)].flatMap((detectionId) => {
|
|
13847
|
+
const pick = createPickFromSelection({
|
|
13848
|
+
detectionId,
|
|
13849
|
+
mediaTime: selection.mediaTime,
|
|
13850
|
+
point: selection.point,
|
|
13851
|
+
target: selection.target,
|
|
13852
|
+
});
|
|
13853
|
+
return pick ? [pick] : [];
|
|
13854
|
+
})
|
|
13855
|
+
: [createPickFromSelection(selection)].filter((pick) => pick !== null);
|
|
13856
|
+
setSelectedPicks(nextPicks);
|
|
13857
|
+
return nextPicks.at(-1) ?? null;
|
|
13621
13858
|
},
|
|
13622
13859
|
getState() {
|
|
13623
13860
|
return createState();
|
|
@@ -13744,6 +13981,18 @@ function createPixiInteractionLayer(options) {
|
|
|
13744
13981
|
if (event.button !== undefined && event.button !== 0) {
|
|
13745
13982
|
return;
|
|
13746
13983
|
}
|
|
13984
|
+
if (options.interaction.multiSelect &&
|
|
13985
|
+
event.shiftKey &&
|
|
13986
|
+
pick &&
|
|
13987
|
+
!options.editingEngine.hasCreationTool() &&
|
|
13988
|
+
options.editingEngine.getState().kind ===
|
|
13989
|
+
AnnotationGestureStateKind.Idle) {
|
|
13990
|
+
// Selection modifiers must run before the editing engine captures a
|
|
13991
|
+
// move. The following pointertap belongs to this same toggle.
|
|
13992
|
+
toggleSelectedPick(pick);
|
|
13993
|
+
suppressNextTap = true;
|
|
13994
|
+
return;
|
|
13995
|
+
}
|
|
13747
13996
|
// Keep selection owned by this layer. The editing engine deliberately
|
|
13748
13997
|
// treats a click as a cancelled move, so it cannot establish selection
|
|
13749
13998
|
// itself; selecting here also makes resize handles available on the next
|
|
@@ -13783,7 +14032,7 @@ function createPixiInteractionLayer(options) {
|
|
|
13783
14032
|
if (!marqueeStart)
|
|
13784
14033
|
return;
|
|
13785
14034
|
if (didDragMarquee && marqueeRect && activeFrame) {
|
|
13786
|
-
|
|
14035
|
+
const nextPicks = activeFrame.detections.flatMap((detection, detectionIndex) => {
|
|
13787
14036
|
if (!canPickDetection(detection))
|
|
13788
14037
|
return [];
|
|
13789
14038
|
const rect = getDetectionRect(detection);
|
|
@@ -13804,10 +14053,7 @@ function createPixiInteractionLayer(options) {
|
|
|
13804
14053
|
},
|
|
13805
14054
|
];
|
|
13806
14055
|
});
|
|
13807
|
-
|
|
13808
|
-
selectedPickKey = createDetectionPickKey(selectedPick);
|
|
13809
|
-
options.interaction.onSelect?.(selectedPick);
|
|
13810
|
-
options.interaction.onSelectionChange?.(selectedPicks);
|
|
14056
|
+
setSelectedPicks(nextPicks);
|
|
13811
14057
|
}
|
|
13812
14058
|
marqueeStart = null;
|
|
13813
14059
|
marqueeRect = null;
|
|
@@ -13903,7 +14149,6 @@ function createPixiInteractionLayer(options) {
|
|
|
13903
14149
|
if (nextKeys === currentKeys) {
|
|
13904
14150
|
return;
|
|
13905
14151
|
}
|
|
13906
|
-
selectedPickKey = createDetectionPickKey(selectedPick);
|
|
13907
14152
|
// A followed pick re-bases onto every new frame, so its full pick key
|
|
13908
14153
|
// changes at playback rate. The scene's normal frame path consumes this
|
|
13909
14154
|
// refreshed state immediately; emitting onStateChange here would redraw
|
|
@@ -13935,15 +14180,17 @@ function createPixiInteractionLayer(options) {
|
|
|
13935
14180
|
return true;
|
|
13936
14181
|
}
|
|
13937
14182
|
function setSelectedPick(nextPick) {
|
|
13938
|
-
|
|
13939
|
-
|
|
13940
|
-
|
|
14183
|
+
setSelectedPicks(nextPick ? [nextPick] : []);
|
|
14184
|
+
}
|
|
14185
|
+
function setSelectedPicks(nextPicks) {
|
|
14186
|
+
const unchanged = selectedPicks.length === nextPicks.length &&
|
|
14187
|
+
selectedPicks.every((pick, index) => createDetectionPickKey(pick) ===
|
|
14188
|
+
createDetectionPickKey(nextPicks[index]));
|
|
14189
|
+
selectedPicks = nextPicks;
|
|
14190
|
+
selectedPick = nextPicks.at(-1) ?? null;
|
|
14191
|
+
if (unchanged)
|
|
13941
14192
|
return;
|
|
13942
|
-
|
|
13943
|
-
selectedPick = nextPick;
|
|
13944
|
-
selectedPicks = nextPick ? [nextPick] : [];
|
|
13945
|
-
selectedPickKey = nextKey;
|
|
13946
|
-
options.interaction.onSelect?.(nextPick);
|
|
14193
|
+
options.interaction.onSelect?.(selectedPick);
|
|
13947
14194
|
options.interaction.onSelectionChange?.(selectedPicks);
|
|
13948
14195
|
notifyStateChange();
|
|
13949
14196
|
}
|
|
@@ -13962,17 +14209,20 @@ function createPixiInteractionLayer(options) {
|
|
|
13962
14209
|
function toggleSelectedPick(pick) {
|
|
13963
14210
|
if (!pick)
|
|
13964
14211
|
return;
|
|
13965
|
-
const key =
|
|
13966
|
-
const index = selectedPicks.findIndex((candidate) =>
|
|
13967
|
-
|
|
13968
|
-
|
|
13969
|
-
|
|
13970
|
-
|
|
13971
|
-
|
|
13972
|
-
|
|
13973
|
-
|
|
13974
|
-
|
|
13975
|
-
|
|
14212
|
+
const key = createSelectionKey(pick);
|
|
14213
|
+
const index = selectedPicks.findIndex((candidate) => createSelectionKey(candidate) === key);
|
|
14214
|
+
setSelectedPicks(index >= 0
|
|
14215
|
+
? selectedPicks.filter((_, candidateIndex) => candidateIndex !== index)
|
|
14216
|
+
: [...selectedPicks, pick]);
|
|
14217
|
+
}
|
|
14218
|
+
function createSelectionKey(pick) {
|
|
14219
|
+
// Selection toggles the detection, regardless of whether its body, label,
|
|
14220
|
+
// or keypoint was hit. Keep the picker's frame/index fallback for duplicate ids.
|
|
14221
|
+
return createDetectionPickKey({
|
|
14222
|
+
...pick,
|
|
14223
|
+
geometryIndex: undefined,
|
|
14224
|
+
target: DetectionPickTarget.Box,
|
|
14225
|
+
});
|
|
13976
14226
|
}
|
|
13977
14227
|
function getPoint(event) {
|
|
13978
14228
|
if (!container)
|
|
@@ -17438,6 +17688,13 @@ function createPixiRegionLayer(options) {
|
|
|
17438
17688
|
}
|
|
17439
17689
|
return { activeDetectionIndexes: [...activeDetectionIndexes] };
|
|
17440
17690
|
},
|
|
17691
|
+
hasAnimatedDisplay() {
|
|
17692
|
+
for (const entry of entries.values()) {
|
|
17693
|
+
if (isAnimatedDisplay(entry.display))
|
|
17694
|
+
return true;
|
|
17695
|
+
}
|
|
17696
|
+
return false;
|
|
17697
|
+
},
|
|
17441
17698
|
releaseMaskTexture(texture) {
|
|
17442
17699
|
if (destroyed)
|
|
17443
17700
|
return;
|
|
@@ -17760,12 +18017,15 @@ function destroyEntry(entry) {
|
|
|
17760
18017
|
// lightweight crop texture; the scene destroys the shared source.
|
|
17761
18018
|
entry.ownedTexture?.destroy(false);
|
|
17762
18019
|
}
|
|
18020
|
+
function isAnimatedDisplay(display) {
|
|
18021
|
+
return "play" in display && typeof display.play === "function";
|
|
18022
|
+
}
|
|
17763
18023
|
function pauseAnimatedDisplay(display) {
|
|
17764
18024
|
if ("stop" in display && typeof display.stop === "function")
|
|
17765
18025
|
display.stop();
|
|
17766
18026
|
}
|
|
17767
18027
|
function resumeAnimatedDisplay(display) {
|
|
17768
|
-
if (
|
|
18028
|
+
if (isAnimatedDisplay(display))
|
|
17769
18029
|
display.play();
|
|
17770
18030
|
}
|
|
17771
18031
|
function matchesTarget(renderer, detection, context) {
|
|
@@ -18466,6 +18726,11 @@ function observePixiContainerResize(container, resize) {
|
|
|
18466
18726
|
* sample it wants, hands it to `presentSample`, and Pixi's ticker paints the
|
|
18467
18727
|
* scene every frame.
|
|
18468
18728
|
*
|
|
18729
|
+
* A still frame is pulled once and then renders on change the way push
|
|
18730
|
+
* presentation does, with the sample it presents as one more change. The
|
|
18731
|
+
* ticker runs only while a looping GIF region or a loading spinner is on the
|
|
18732
|
+
* frame.
|
|
18733
|
+
*
|
|
18469
18734
|
* Push presentation engages when the opened source carries a presented-frame
|
|
18470
18735
|
* channel. The producer then decides which frame is on screen and announces it,
|
|
18471
18736
|
* so the ticker paints nothing and every render is an explicit answer to a
|
|
@@ -18496,6 +18761,18 @@ async function createPixiMediaScene(options) {
|
|
|
18496
18761
|
let maxTextureSide;
|
|
18497
18762
|
const maxTextureSize = () => (maxTextureSide ??= queryMaxTextureSize(app.renderer));
|
|
18498
18763
|
const frameChannel = options.presentedFrames;
|
|
18764
|
+
/**
|
|
18765
|
+
* A still frame has nothing to play, so it renders on change exactly as push
|
|
18766
|
+
* presentation does. The ticker runs only while something on the frame
|
|
18767
|
+
* animates by the clock.
|
|
18768
|
+
*/
|
|
18769
|
+
const stillFrame = frameChannel === undefined && options.stillFrame === true;
|
|
18770
|
+
const rendersOnChange = frameChannel !== undefined || stillFrame;
|
|
18771
|
+
let stillFrameTickerRunning = false;
|
|
18772
|
+
/** The content of the `previewOverlay` answer the overlay on screen drew. */
|
|
18773
|
+
let drawnPreviewOverlayContent = null;
|
|
18774
|
+
/** The editing state the overlay on screen was drawn from. */
|
|
18775
|
+
let drawnEditingState;
|
|
18499
18776
|
let currentFocusStyle = options.focusStyle ?? null;
|
|
18500
18777
|
let currentLabelStyle = options.labelStyle ?? null;
|
|
18501
18778
|
let currentMaskStyle = options.maskStyle ?? null;
|
|
@@ -18520,6 +18797,9 @@ async function createPixiMediaScene(options) {
|
|
|
18520
18797
|
let isScrubbing = false;
|
|
18521
18798
|
let displayBrightness = 1;
|
|
18522
18799
|
let displayContrast = 1;
|
|
18800
|
+
let displayGamma = 1;
|
|
18801
|
+
let displayAutoContrast = false;
|
|
18802
|
+
let inspectionFilter;
|
|
18523
18803
|
let viewportScale = 1;
|
|
18524
18804
|
let hasPresentedSample = false;
|
|
18525
18805
|
let mediaHeight = 0;
|
|
@@ -18736,7 +19016,7 @@ async function createPixiMediaScene(options) {
|
|
|
18736
19016
|
: undefined;
|
|
18737
19017
|
await app.init({
|
|
18738
19018
|
autoDensity: true,
|
|
18739
|
-
autoStart:
|
|
19019
|
+
autoStart: !rendersOnChange,
|
|
18740
19020
|
backgroundColor: options.backgroundColor ?? 0x111111,
|
|
18741
19021
|
/* Only the video-engine media path supplies a presented-frame channel, so
|
|
18742
19022
|
* which reader opens the clip is also what decides which backend draws. */
|
|
@@ -18755,7 +19035,10 @@ async function createPixiMediaScene(options) {
|
|
|
18755
19035
|
if (!stagingContext) {
|
|
18756
19036
|
throw new Error("Unable to create staging canvas context.");
|
|
18757
19037
|
}
|
|
18758
|
-
const renderScheduler = createSceneRenderScheduler(() =>
|
|
19038
|
+
const renderScheduler = createSceneRenderScheduler(() => {
|
|
19039
|
+
app.render();
|
|
19040
|
+
syncStillFrameTicker();
|
|
19041
|
+
});
|
|
18759
19042
|
const maskBrushPreview = options.maskBrush
|
|
18760
19043
|
? createPixiMaskBrushPreview({
|
|
18761
19044
|
CanvasSource,
|
|
@@ -18764,7 +19047,7 @@ async function createPixiMediaScene(options) {
|
|
|
18764
19047
|
Sprite,
|
|
18765
19048
|
Texture,
|
|
18766
19049
|
preview: options.maskBrush,
|
|
18767
|
-
onInvalidate:
|
|
19050
|
+
onInvalidate: rendersOnChange ? renderNow : undefined,
|
|
18768
19051
|
})
|
|
18769
19052
|
: undefined;
|
|
18770
19053
|
let appliedPresentation;
|
|
@@ -18933,6 +19216,17 @@ async function createPixiMediaScene(options) {
|
|
|
18933
19216
|
labelLayer?.drawFrame(currentMediaTime, viewportScale);
|
|
18934
19217
|
renderNow();
|
|
18935
19218
|
}
|
|
19219
|
+
// With no ticker to pick it up, an edit the host drives itself, such as a
|
|
19220
|
+
// cancel or a commit, reaches the screen through this notification. A
|
|
19221
|
+
// pointer gesture draws and renders its own state before the microtask
|
|
19222
|
+
// runs, which then finds nothing left to draw or render.
|
|
19223
|
+
if (rendersOnChange) {
|
|
19224
|
+
queueMicrotask(() => {
|
|
19225
|
+
if (isDestroyed || isPresenting)
|
|
19226
|
+
return;
|
|
19227
|
+
renderOnChange();
|
|
19228
|
+
});
|
|
19229
|
+
}
|
|
18936
19230
|
});
|
|
18937
19231
|
const handleKeyDown = (event) => {
|
|
18938
19232
|
if (event.key === "Tab" && interactionLayer) {
|
|
@@ -19122,15 +19416,17 @@ async function createPixiMediaScene(options) {
|
|
|
19122
19416
|
renderScene();
|
|
19123
19417
|
};
|
|
19124
19418
|
let unsubscribeDetectionTimeline;
|
|
19125
|
-
if (
|
|
19126
|
-
frameChannel.onPresentedFrame(handlePresentedFrame);
|
|
19419
|
+
if (rendersOnChange) {
|
|
19127
19420
|
document.addEventListener?.("visibilitychange", handleVisibilityChange);
|
|
19128
19421
|
// A detection load landing moves what the window covers exactly as a cook
|
|
19129
19422
|
// landing does. Nothing else brings that to a resting playhead: the layers
|
|
19130
19423
|
// read the timeline only while drawing, and this is what makes the draw.
|
|
19131
19424
|
unsubscribeDetectionTimeline = options.detectionTimeline.subscribe?.(handlePreparedWindowChange);
|
|
19132
19425
|
}
|
|
19133
|
-
|
|
19426
|
+
if (frameChannel) {
|
|
19427
|
+
frameChannel.onPresentedFrame(handlePresentedFrame);
|
|
19428
|
+
}
|
|
19429
|
+
else if (!stillFrame) {
|
|
19134
19430
|
app.ticker.add(updateMediaSceneFit);
|
|
19135
19431
|
app.ticker.add(drawAnnotationOverlayNow);
|
|
19136
19432
|
app.ticker.add(tickFocusLayer);
|
|
@@ -19200,7 +19496,9 @@ async function createPixiMediaScene(options) {
|
|
|
19200
19496
|
advanceFocus: () => undefined,
|
|
19201
19497
|
drawAnnotationOverlay: frameChannel
|
|
19202
19498
|
? presentLayers.drawAnnotationOverlay
|
|
19203
|
-
:
|
|
19499
|
+
: stillFrame
|
|
19500
|
+
? (mediaTime) => drawAnnotationOverlay(mediaTime, overlayNow())
|
|
19501
|
+
: () => undefined,
|
|
19204
19502
|
};
|
|
19205
19503
|
// A viewport-style redraw covers the layers whose geometry is a function of
|
|
19206
19504
|
// the viewport scale. Mask outlines reuse the artifact already on screen;
|
|
@@ -19250,7 +19548,7 @@ async function createPixiMediaScene(options) {
|
|
|
19250
19548
|
};
|
|
19251
19549
|
return {
|
|
19252
19550
|
getRenderCount() {
|
|
19253
|
-
return
|
|
19551
|
+
return rendersOnChange ? renderScheduler.getRenderCount() : null;
|
|
19254
19552
|
},
|
|
19255
19553
|
getPreparedAnnotationWindow() {
|
|
19256
19554
|
return frameChannel ? annotationWindow.getSnapshot() : null;
|
|
@@ -19359,10 +19657,12 @@ async function createPixiMediaScene(options) {
|
|
|
19359
19657
|
if (!collectFrameTimings) {
|
|
19360
19658
|
sample.draw(stagingContext, 0, 0, mediaWidth, mediaHeight);
|
|
19361
19659
|
presentedSampleTimestamp = sample.timestamp;
|
|
19660
|
+
refreshInspectionImage();
|
|
19362
19661
|
stagingTextureSource?.update();
|
|
19363
19662
|
stagingTexture?.update();
|
|
19364
19663
|
const { boxState, regionState } = drawFramePresentLayers(annotationDrawLayers, sample.timestamp);
|
|
19365
19664
|
updateMediaSceneFit();
|
|
19665
|
+
renderOnChange();
|
|
19366
19666
|
return {
|
|
19367
19667
|
...createPresentedSampleState(sample.timestamp, boxState, regionState),
|
|
19368
19668
|
duration: sample.duration,
|
|
@@ -19373,11 +19673,13 @@ async function createPixiMediaScene(options) {
|
|
|
19373
19673
|
const mediaUploadMs = measure(() => {
|
|
19374
19674
|
sample.draw(stagingContext, 0, 0, mediaWidth, mediaHeight);
|
|
19375
19675
|
presentedSampleTimestamp = sample.timestamp;
|
|
19676
|
+
refreshInspectionImage();
|
|
19376
19677
|
stagingTextureSource?.update();
|
|
19377
19678
|
stagingTexture?.update();
|
|
19378
19679
|
});
|
|
19379
19680
|
const { boxState, regionState } = drawFramePresentLayers(measuredAnnotationDrawLayers, sample.timestamp);
|
|
19380
19681
|
const fitMs = measure(updateMediaSceneFit);
|
|
19682
|
+
renderOnChange();
|
|
19381
19683
|
return {
|
|
19382
19684
|
...createPresentedSampleState(sample.timestamp, boxState, regionState),
|
|
19383
19685
|
duration: sample.duration,
|
|
@@ -19435,15 +19737,27 @@ async function createPixiMediaScene(options) {
|
|
|
19435
19737
|
},
|
|
19436
19738
|
setDisplayAdjustments(adjustments) {
|
|
19437
19739
|
void import('pixi.js').then(({ ColorMatrixFilter }) => {
|
|
19740
|
+
if (isDestroyed)
|
|
19741
|
+
return;
|
|
19438
19742
|
const brightness = adjustments.brightness ?? 1;
|
|
19439
19743
|
const contrast = adjustments.contrast ?? 1;
|
|
19440
19744
|
const filter = new ColorMatrixFilter();
|
|
19441
19745
|
// Preserve the renderer's pixel grid through the adjustment pass.
|
|
19442
19746
|
filter.resolution = "inherit";
|
|
19443
19747
|
filter.brightness(brightness, false);
|
|
19444
|
-
|
|
19445
|
-
|
|
19446
|
-
|
|
19748
|
+
displayGamma = adjustments.gamma ?? 1;
|
|
19749
|
+
displayAutoContrast = stillFrame && adjustments.autoContrast === true;
|
|
19750
|
+
filter.contrast(displayAutoContrast ? 0 : contrast - 1, true);
|
|
19751
|
+
if (mediaSprite) {
|
|
19752
|
+
if (displayGamma !== 1 || displayAutoContrast) {
|
|
19753
|
+
inspectionFilter ??= createPixiMediaInspectionFilter(mediaSprite, pixi);
|
|
19754
|
+
inspectionFilter.update(displayGamma, displayAutoContrast, hasPresentedSample ? stagingCanvas : null);
|
|
19755
|
+
mediaSprite.filters = [inspectionFilter.filter, filter];
|
|
19756
|
+
}
|
|
19757
|
+
else {
|
|
19758
|
+
mediaSprite.filters = [filter];
|
|
19759
|
+
}
|
|
19760
|
+
}
|
|
19447
19761
|
displayBrightness = brightness;
|
|
19448
19762
|
displayContrast = contrast;
|
|
19449
19763
|
renderOnChange();
|
|
@@ -19750,6 +20064,7 @@ async function createPixiMediaScene(options) {
|
|
|
19750
20064
|
// a decoder buffer in a producer that outlives the scene.
|
|
19751
20065
|
frameChannel?.onPresentedFrame((presented) => presented.frame.close());
|
|
19752
20066
|
destroyBatchTextureBindings();
|
|
20067
|
+
inspectionFilter?.destroy();
|
|
19753
20068
|
mediaCompositor?.destroy();
|
|
19754
20069
|
interactionLayer?.destroy();
|
|
19755
20070
|
interactionPresentationLayer?.destroy();
|
|
@@ -20256,7 +20571,7 @@ async function createPixiMediaScene(options) {
|
|
|
20256
20571
|
return;
|
|
20257
20572
|
}
|
|
20258
20573
|
const { boxState, regionState } = drawAnnotationFrame(currentMediaTime);
|
|
20259
|
-
if (
|
|
20574
|
+
if (rendersOnChange) {
|
|
20260
20575
|
const signature = describeSceneRender();
|
|
20261
20576
|
if (forceRender)
|
|
20262
20577
|
renderScheduler.render(signature);
|
|
@@ -20313,6 +20628,13 @@ async function createPixiMediaScene(options) {
|
|
|
20313
20628
|
labelLayer?.drawCreationPreview(editingState?.kind === AnnotationGestureStateKind.Creating
|
|
20314
20629
|
? editingState.preview
|
|
20315
20630
|
: null, mediaTime, viewportScale);
|
|
20631
|
+
const previewOverlay = options.previewOverlay?.() ?? null;
|
|
20632
|
+
drawnEditingState = editingState;
|
|
20633
|
+
// Only a scene that renders on change compares signatures.
|
|
20634
|
+
if (rendersOnChange) {
|
|
20635
|
+
drawnPreviewOverlayContent =
|
|
20636
|
+
describePreviewOverlayContent(previewOverlay);
|
|
20637
|
+
}
|
|
20316
20638
|
annotationOverlayLayer.draw({
|
|
20317
20639
|
frame,
|
|
20318
20640
|
marquee: interactionState?.marqueeRect ?? null,
|
|
@@ -20320,7 +20642,7 @@ async function createPixiMediaScene(options) {
|
|
|
20320
20642
|
mediaWidth,
|
|
20321
20643
|
now: overlayNowMs,
|
|
20322
20644
|
pointer: interactionState?.pointerPoint ?? null,
|
|
20323
|
-
previewOverlay
|
|
20645
|
+
previewOverlay,
|
|
20324
20646
|
selectedDetectionIds: (interactionState?.selectedPicks ?? []).flatMap(({ detection }) => (detection.id === undefined ? [] : [detection.id])),
|
|
20325
20647
|
viewportScale,
|
|
20326
20648
|
visibility: currentVisibility,
|
|
@@ -20328,8 +20650,9 @@ async function createPixiMediaScene(options) {
|
|
|
20328
20650
|
}
|
|
20329
20651
|
/**
|
|
20330
20652
|
* Milliseconds for animations that are not annotation geometry. A pull scene
|
|
20331
|
-
* has a ticker and reads the wall clock
|
|
20332
|
-
*
|
|
20653
|
+
* has a ticker and reads the wall clock, as does a still frame while its
|
|
20654
|
+
* ticker runs; a push scene has the presented media time and nothing else,
|
|
20655
|
+
* so its animations move only when the media does.
|
|
20333
20656
|
*/
|
|
20334
20657
|
function overlayNow() {
|
|
20335
20658
|
return frameChannel ? currentMediaTime * MILLISECONDS_PER_SECOND : now();
|
|
@@ -20378,8 +20701,21 @@ async function createPixiMediaScene(options) {
|
|
|
20378
20701
|
focusLayer.tick(focusAnimationTimeMs);
|
|
20379
20702
|
}
|
|
20380
20703
|
function renderScene() {
|
|
20704
|
+
drawOverlayForEditingState();
|
|
20381
20705
|
renderScheduler.render(describeSceneRender());
|
|
20382
20706
|
}
|
|
20707
|
+
/**
|
|
20708
|
+
* The render signature names the editing state, so a render must not record
|
|
20709
|
+
* a state the overlay has not drawn: the redraw that follows would match the
|
|
20710
|
+
* recorded signature and be skipped, leaving the old preview on screen.
|
|
20711
|
+
*/
|
|
20712
|
+
function drawOverlayForEditingState() {
|
|
20713
|
+
const editingState = options.editingEngine?.getState();
|
|
20714
|
+
if (editingState === undefined || editingState === drawnEditingState) {
|
|
20715
|
+
return;
|
|
20716
|
+
}
|
|
20717
|
+
drawAnnotationOverlay(currentMediaTime, overlayNow());
|
|
20718
|
+
}
|
|
20383
20719
|
/**
|
|
20384
20720
|
* Renders a present at most once per display refresh. Frames arrive as
|
|
20385
20721
|
* messages from the producer's own thread, so a main thread that falls
|
|
@@ -20405,15 +20741,52 @@ async function createPixiMediaScene(options) {
|
|
|
20405
20741
|
presentFrameNow(deferred);
|
|
20406
20742
|
}
|
|
20407
20743
|
function renderNow() {
|
|
20408
|
-
if (
|
|
20744
|
+
if (rendersOnChange)
|
|
20409
20745
|
renderScene();
|
|
20410
20746
|
}
|
|
20411
20747
|
function renderOnChange() {
|
|
20412
|
-
if (!
|
|
20748
|
+
if (!rendersOnChange)
|
|
20413
20749
|
return;
|
|
20414
20750
|
settleFocusAnimation();
|
|
20751
|
+
drawOverlayForEditingState();
|
|
20415
20752
|
renderScheduler.renderOnChange(describeSceneRender());
|
|
20416
20753
|
}
|
|
20754
|
+
/**
|
|
20755
|
+
* Runs the ticker on a still frame for as long as something on it moves by
|
|
20756
|
+
* the clock: a looping GIF region or a loading spinner. Pixi renders on
|
|
20757
|
+
* every tick it runs, and the overlay redraws so the spinner turns. Each
|
|
20758
|
+
* render checks again, so the ticker stops with the render that removes the
|
|
20759
|
+
* last of them.
|
|
20760
|
+
*/
|
|
20761
|
+
function syncStillFrameTicker() {
|
|
20762
|
+
if (!stillFrame || isDestroyed)
|
|
20763
|
+
return;
|
|
20764
|
+
const animating = hasPresentedSample &&
|
|
20765
|
+
(regionLayer.hasAnimatedDisplay() || hasLoadingDetections());
|
|
20766
|
+
if (animating === stillFrameTickerRunning)
|
|
20767
|
+
return;
|
|
20768
|
+
stillFrameTickerRunning = animating;
|
|
20769
|
+
if (animating) {
|
|
20770
|
+
app.ticker.add(drawAnnotationOverlayNow);
|
|
20771
|
+
app.ticker.start();
|
|
20772
|
+
}
|
|
20773
|
+
else {
|
|
20774
|
+
app.ticker.stop();
|
|
20775
|
+
app.ticker.remove(drawAnnotationOverlayNow);
|
|
20776
|
+
}
|
|
20777
|
+
}
|
|
20778
|
+
function refreshInspectionImage() {
|
|
20779
|
+
if (!stillFrame || !inspectionFilter)
|
|
20780
|
+
return;
|
|
20781
|
+
inspectionFilter.invalidateImage();
|
|
20782
|
+
inspectionFilter.update(displayGamma, displayAutoContrast, stagingCanvas);
|
|
20783
|
+
}
|
|
20784
|
+
function hasLoadingDetections() {
|
|
20785
|
+
const loading = currentVisibility?.loadingDetectionIds;
|
|
20786
|
+
if (!loading)
|
|
20787
|
+
return false;
|
|
20788
|
+
return "size" in loading ? loading.size > 0 : loading.length > 0;
|
|
20789
|
+
}
|
|
20417
20790
|
/**
|
|
20418
20791
|
* Everything a render would put on screen, as values that can be compared.
|
|
20419
20792
|
* Styles are opaque resolvers, so they are compared by identity: handing the
|
|
@@ -20439,6 +20812,8 @@ async function createPixiMediaScene(options) {
|
|
|
20439
20812
|
app.renderer.resolution,
|
|
20440
20813
|
displayBrightness,
|
|
20441
20814
|
displayContrast,
|
|
20815
|
+
displayGamma,
|
|
20816
|
+
displayAutoContrast,
|
|
20442
20817
|
...RENDERED_PRESENTATION_FIELDS.map((field) => appliedPresentation?.[field]),
|
|
20443
20818
|
// Styles above compare by identity; mask opacity is applied after
|
|
20444
20819
|
// preparation, so a host can move it inside a style object it keeps.
|
|
@@ -20451,6 +20826,9 @@ async function createPixiMediaScene(options) {
|
|
|
20451
20826
|
interactionState?.marqueeRect,
|
|
20452
20827
|
options.editingEngine?.getState(),
|
|
20453
20828
|
options.editingEngine ? interactionState?.pointerPoint : null,
|
|
20829
|
+
// The host's preview is read when the overlay draws, and a host may edit
|
|
20830
|
+
// its arrays in place, so its content is what tells a redraw it changed.
|
|
20831
|
+
drawnPreviewOverlayContent,
|
|
20454
20832
|
];
|
|
20455
20833
|
}
|
|
20456
20834
|
/**
|
|
@@ -20789,6 +21167,37 @@ function elapsedSince(start) {
|
|
|
20789
21167
|
function now() {
|
|
20790
21168
|
return globalThis.performance?.now?.() ?? Date.now();
|
|
20791
21169
|
}
|
|
21170
|
+
/**
|
|
21171
|
+
* A key for what a preview overlay draws, hashed from its coordinates rather
|
|
21172
|
+
* than its arrays, so a preview the host edits in place still changes it.
|
|
21173
|
+
* Coordinates count to a 256th of a media pixel, finer than anything drawn.
|
|
21174
|
+
*/
|
|
21175
|
+
function describePreviewOverlayContent(overlay) {
|
|
21176
|
+
if (!overlay)
|
|
21177
|
+
return null;
|
|
21178
|
+
let hash = 0x811c9dc5;
|
|
21179
|
+
const mix = (value) => {
|
|
21180
|
+
hash = Math.imul(hash ^ value, 0x01000193);
|
|
21181
|
+
};
|
|
21182
|
+
const mixPoint = (point) => {
|
|
21183
|
+
mix(Math.round(point.x * 256));
|
|
21184
|
+
mix(Math.round(point.y * 256));
|
|
21185
|
+
};
|
|
21186
|
+
for (const polygons of [overlay.hoverPolygons, overlay.draftPolygons]) {
|
|
21187
|
+
mix(polygons?.length ?? -1);
|
|
21188
|
+
for (const polygon of polygons ?? []) {
|
|
21189
|
+
mix(polygon.length);
|
|
21190
|
+
for (const point of polygon)
|
|
21191
|
+
mixPoint(point);
|
|
21192
|
+
}
|
|
21193
|
+
}
|
|
21194
|
+
mix(overlay.points?.length ?? -1);
|
|
21195
|
+
for (const point of overlay.points ?? []) {
|
|
21196
|
+
mixPoint(point);
|
|
21197
|
+
mix(point.positive ? 1 : 0);
|
|
21198
|
+
}
|
|
21199
|
+
return hash;
|
|
21200
|
+
}
|
|
20792
21201
|
/**
|
|
20793
21202
|
* Fills the container and paints the margin around the canvas in the colour the
|
|
20794
21203
|
* renderer clears to. The scene owns this element so the canvas is positioned
|