supervision 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/{depth-preview-probe-DG7McQEN.js → depth-preview-probe-BG0k-33U.js} +3 -3
  2. package/dist/{depth-preview-probe-DG7McQEN.js.map → depth-preview-probe-BG0k-33U.js.map} +1 -1
  3. package/dist/{depth-preview-track-BvgwNEE9.js → depth-preview-track-BEag1ZWF.js} +2 -2
  4. package/dist/{depth-preview-track-BvgwNEE9.js.map → depth-preview-track-BEag1ZWF.js.map} +1 -1
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2261 -447
  8. package/dist/index.js.map +1 -1
  9. package/dist/mask-preparation.worker.js +166 -45
  10. package/dist/mask-preparation.worker.js.map +1 -1
  11. package/dist/media/display-pixel-ratio.d.ts +3 -8
  12. package/dist/media/display-pixel-ratio.d.ts.map +1 -1
  13. package/dist/media/video-engine-media-source.js +1 -1
  14. package/dist/render-preparation/mask-frame-artifact.d.ts +7 -1
  15. package/dist/render-preparation/mask-frame-artifact.d.ts.map +1 -1
  16. package/dist/render-preparation/mask-frame-compositor.d.ts +1 -1
  17. package/dist/render-preparation/mask-frame-compositor.d.ts.map +1 -1
  18. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts +3 -0
  19. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts.map +1 -1
  20. package/dist/render-preparation/prepared-render-window.d.ts +2 -0
  21. package/dist/render-preparation/prepared-render-window.d.ts.map +1 -1
  22. package/dist/renderers/annotation-fxaa.d.ts +37 -0
  23. package/dist/renderers/annotation-fxaa.d.ts.map +1 -0
  24. package/dist/renderers/id-mask-stroke-coverage.d.ts +2 -2
  25. package/dist/renderers/id-mask-stroke-coverage.d.ts.map +1 -1
  26. package/dist/renderers/mask-palette.d.ts +4 -8
  27. package/dist/renderers/mask-palette.d.ts.map +1 -1
  28. package/dist/renderers/media-clahe.d.ts +6 -0
  29. package/dist/renderers/media-clahe.d.ts.map +1 -0
  30. package/dist/renderers/media-renderer-core.d.ts.map +1 -1
  31. package/dist/renderers/media-renderer-scene.d.ts +1 -0
  32. package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
  33. package/dist/renderers/pixi-annotation-antialias-layer.d.ts +15 -0
  34. package/dist/renderers/pixi-annotation-antialias-layer.d.ts.map +1 -0
  35. package/dist/renderers/pixi-annotation-antialias.d.ts +14 -0
  36. package/dist/renderers/pixi-annotation-antialias.d.ts.map +1 -0
  37. package/dist/renderers/pixi-annotation-overlay-layer.d.ts +6 -1
  38. package/dist/renderers/pixi-annotation-overlay-layer.d.ts.map +1 -1
  39. package/dist/renderers/pixi-box-layer.d.ts +6 -1
  40. package/dist/renderers/pixi-box-layer.d.ts.map +1 -1
  41. package/dist/renderers/pixi-filter-bindings.d.ts +6 -0
  42. package/dist/renderers/pixi-filter-bindings.d.ts.map +1 -0
  43. package/dist/renderers/pixi-focus-heatmap-cutout.d.ts +17 -0
  44. package/dist/renderers/pixi-focus-heatmap-cutout.d.ts.map +1 -0
  45. package/dist/renderers/pixi-focus-layer.d.ts +18 -1
  46. package/dist/renderers/pixi-focus-layer.d.ts.map +1 -1
  47. package/dist/renderers/pixi-heatmap-layer.d.ts +4 -1
  48. package/dist/renderers/pixi-heatmap-layer.d.ts.map +1 -1
  49. package/dist/renderers/pixi-id-mask-shader.d.ts +2 -1
  50. package/dist/renderers/pixi-id-mask-shader.d.ts.map +1 -1
  51. package/dist/renderers/pixi-interaction-presentation-layer.d.ts +5 -1
  52. package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -1
  53. package/dist/renderers/pixi-label-layer.d.ts +8 -4
  54. package/dist/renderers/pixi-label-layer.d.ts.map +1 -1
  55. package/dist/renderers/pixi-mask-layer.d.ts +2 -1
  56. package/dist/renderers/pixi-mask-layer.d.ts.map +1 -1
  57. package/dist/renderers/pixi-mask-screen-stroke.d.ts +1 -1
  58. package/dist/renderers/pixi-mask-screen-stroke.d.ts.map +1 -1
  59. package/dist/renderers/pixi-mask-viewport.d.ts +21 -0
  60. package/dist/renderers/pixi-mask-viewport.d.ts.map +1 -0
  61. package/dist/renderers/pixi-media-inspection-filter.d.ts +8 -0
  62. package/dist/renderers/pixi-media-inspection-filter.d.ts.map +1 -0
  63. package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
  64. package/dist/renderers/pixi-polygon-layer.d.ts +3 -0
  65. package/dist/renderers/pixi-polygon-layer.d.ts.map +1 -1
  66. package/dist/renderers/pixi-region-coverage-mask.d.ts +14 -3
  67. package/dist/renderers/pixi-region-coverage-mask.d.ts.map +1 -1
  68. package/dist/renderers/pixi-region-layer.d.ts +7 -1
  69. package/dist/renderers/pixi-region-layer.d.ts.map +1 -1
  70. package/dist/sessions/media-session.d.ts.map +1 -1
  71. package/dist/tracking.worker.js.map +1 -1
  72. package/dist/types/media-renderer.d.ts +1 -1
  73. package/dist/types/media-renderer.d.ts.map +1 -1
  74. package/dist/{video-engine-media-source-DOFsuGS4.js → video-engine-media-source-MFthAkbe.js} +4 -9
  75. package/dist/video-engine-media-source-MFthAkbe.js.map +1 -0
  76. package/node_modules/supervision-js-core/dist/index.d.ts +2 -2
  77. package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
  78. package/node_modules/supervision-js-core/dist/index.js +57 -25
  79. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  80. package/node_modules/supervision-js-core/dist/styles/default-annotation-presentation.d.ts.map +1 -1
  81. package/node_modules/supervision-js-core/dist/styles/focus-style.d.ts +4 -3
  82. package/node_modules/supervision-js-core/dist/styles/focus-style.d.ts.map +1 -1
  83. package/node_modules/supervision-js-core/dist/styles/mask-style.d.ts.map +1 -1
  84. package/node_modules/supervision-js-core/dist/types/focus-style.d.ts +2 -1
  85. package/node_modules/supervision-js-core/dist/types/focus-style.d.ts.map +1 -1
  86. package/node_modules/supervision-js-core/dist/types/label-style.d.ts +4 -1
  87. package/node_modules/supervision-js-core/dist/types/label-style.d.ts.map +1 -1
  88. package/node_modules/supervision-js-core/dist/types/mask-style.d.ts +8 -1
  89. package/node_modules/supervision-js-core/dist/types/mask-style.d.ts.map +1 -1
  90. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +18 -0
  91. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
  92. package/node_modules/supervision-js-core/dist/utils/detection-conversions.d.ts +1 -0
  93. package/node_modules/supervision-js-core/dist/utils/detection-conversions.d.ts.map +1 -1
  94. package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts +21 -7
  95. package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts.map +1 -1
  96. package/package.json +1 -1
  97. package/dist/video-engine-media-source-DOFsuGS4.js.map +0 -1
@@ -508,8 +508,15 @@
508
508
  new Set(Object.values(DepthSampling));
509
509
  function rasterizePolygonToMask(points, dimensions) {
510
510
  const data = createEmptyMask(dimensions);
511
+ forEachRasterPolygonSpan(points, dimensions, (y, left, right) => {
512
+ data.fill(1, y * dimensions.width + left, y * dimensions.width + right);
513
+ });
514
+ return data;
515
+ }
516
+ function forEachRasterPolygonSpan(points, dimensions, writeSpan) {
517
+ assertMediaDimensions(dimensions);
511
518
  if (points.length < 3) {
512
- return data;
519
+ return;
513
520
  }
514
521
  const bounds = centerRectToTopLeftRect(getPointsRect(points));
515
522
  const startY = Math.max(0, Math.floor(bounds.y));
@@ -530,27 +537,29 @@
530
537
  for (let index = 0; index < intersections.length - 1; index += 2) {
531
538
  const left = Math.max(0, Math.round(intersections[index]));
532
539
  const right = Math.min(dimensions.width, Math.round(intersections[index + 1]));
533
- for (let x = left; x < right; x += 1) {
534
- data[y * dimensions.width + x] = 1;
540
+ if (left < right) {
541
+ writeSpan(y, left, right);
535
542
  }
536
543
  }
537
544
  }
538
- return data;
539
545
  }
540
546
  function createEmptyMask(dimensions) {
547
+ assertMediaDimensions(dimensions);
548
+ return new Uint8Array(dimensions.width * dimensions.height);
549
+ }
550
+ function assertMediaDimensions(dimensions) {
541
551
  if (!Number.isInteger(dimensions.width) ||
542
552
  dimensions.width <= 0 ||
543
553
  !Number.isInteger(dimensions.height) ||
544
554
  dimensions.height <= 0) {
545
555
  throw new Error("Media dimensions must be positive integers.");
546
556
  }
547
- return new Uint8Array(dimensions.width * dimensions.height);
548
557
  }
549
558
 
550
559
  /**
551
560
  * Palette slots an id raster can name, one of them the background. A GLSL
552
561
  * fragment stage is guaranteed only 224 uniform vectors, and a palette entry
553
- * costs 2.25 of them, so this stays a multiple of four and well inside that
562
+ * costs 2.5 of them, so this stays a multiple of four and well inside that
554
563
  * budget; past it a frame draws from the RGBA composite instead.
555
564
  */
556
565
  const MAX_ID_MASK_PALETTE_ENTRIES = 80;
@@ -561,8 +570,8 @@
561
570
  }
562
571
  /* Pixel-grid dimensions use integer parts throughout allocation and RLE
563
572
  traversal. */
564
- const maskWidth = Math.floor(Math.max(...instructions.map(({ mask }) => mask.width)));
565
- const maskHeight = Math.floor(Math.max(...instructions.map(({ mask }) => mask.height)));
573
+ const maskWidth = Math.floor(Math.max(...instructions.map((instruction) => (instruction.mask ?? instruction.polygon).width)));
574
+ const maskHeight = Math.floor(Math.max(...instructions.map((instruction) => (instruction.mask ?? instruction.polygon).height)));
566
575
  const width = resolveRasterWidth(maskWidth, options.maxWidth);
567
576
  const height = width === maskWidth
568
577
  ? maskHeight
@@ -574,6 +583,7 @@
574
583
  const fillPalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));
575
584
  const strokePalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));
576
585
  const strokeWidths = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4));
586
+ const strokeAlignments = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4));
577
587
  let hasStroke = false;
578
588
  let maxStrokeWidth = 0;
579
589
  for (const instruction of instructions) {
@@ -583,13 +593,22 @@
583
593
  }
584
594
  writeIdMaskPaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);
585
595
  if (instruction.stroke && instruction.stroke.width > 0) {
586
- const strokeWidth = resolveIdMaskStrokeTexels(instruction.stroke.width, maskWidth, width);
596
+ const strokeWidth = instruction.stroke.width;
597
+ const alignment = instruction.stroke.alignment === StrokeAlignment.Inside
598
+ ? 1
599
+ : instruction.stroke.alignment === StrokeAlignment.Center
600
+ ? 0.5
601
+ : 0;
587
602
  hasStroke = true;
588
603
  strokeWidths[detectionMaskId] = strokeWidth;
589
- maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth);
604
+ strokeAlignments[detectionMaskId] = alignment;
605
+ maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth * (1 - alignment));
590
606
  writeIdMaskPaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);
591
607
  }
592
- if (scaledAxes) {
608
+ if (instruction.polygon) {
609
+ writePolygonSpans(data, width, instruction.polygon, detectionMaskId, scaledAxes);
610
+ }
611
+ else if (scaledAxes) {
593
612
  writeScaledMaskRuns(data, scaledAxes, instruction.mask, detectionMaskId);
594
613
  }
595
614
  else {
@@ -604,10 +623,19 @@
604
623
  maxStrokeWidth,
605
624
  sourceWidth: maskWidth,
606
625
  strokePalette,
626
+ strokeAlignments,
607
627
  strokeWidths,
608
628
  width,
609
629
  };
610
630
  }
631
+ function writePolygonSpans(data, frameWidth, polygon, detectionMaskId, axes) {
632
+ forEachRasterPolygonSpan(polygon.points, polygon, (y, left, right) => {
633
+ const row = axes ? axes.rows[y] : y * frameWidth;
634
+ const first = axes ? axes.columns[left] : left;
635
+ const last = axes ? axes.columns[right - 1] + 1 : right;
636
+ data.fill(detectionMaskId, row + first, row + last);
637
+ });
638
+ }
611
639
  /**
612
640
  * Compressed RLE counts runs down each column in turn, so a foreground run is
613
641
  * a contiguous walk down one column that wraps into the next.
@@ -696,13 +724,13 @@
696
724
  : maskWidth;
697
725
  }
698
726
  /**
699
- * Converts a source-grid stroke width into raster texels. Fractional widths
700
- * let coverage shaders draw a border within a texel. The ceiling bounds the
701
- * neighbourhood the shaders scan.
727
+ * Converts total CSS outline width into raster texels. Each side is bounded
728
+ * independently so centered outlines can use the full supported radius.
702
729
  */
703
- function resolveIdMaskStrokeTexels(strokeWidth, maskWidth, rasterWidth) {
704
- const scale = maskWidth > 0 ? rasterWidth / maskWidth : 1;
705
- return Math.min(strokeWidth * scale, MAX_ID_MASK_STROKE_WIDTH);
730
+ function resolveIdMaskStrokeTexels(strokeWidth, displayWidth, rasterWidth, alignment = StrokeAlignment.Outside) {
731
+ const scale = displayWidth > 0 ? rasterWidth / displayWidth : 1;
732
+ const sideFraction = alignment === StrokeAlignment.Center ? 0.5 : 1;
733
+ return Math.min(strokeWidth * scale, MAX_ID_MASK_STROKE_WIDTH / sideFraction);
706
734
  }
707
735
  function createScaledMaskAxes(frame) {
708
736
  return {
@@ -948,7 +976,7 @@
948
976
  }
949
977
  return { data, detectionIndex, height, width, x: minX, y: minY };
950
978
  }
951
- function compositeMaskFrame(instructions) {
979
+ function compositeMaskFrame(instructions, displayWidth) {
952
980
  const maskInstructions = materializeMaskInstructions(instructions);
953
981
  if (maskInstructions.length === 0) {
954
982
  return undefined;
@@ -956,18 +984,22 @@
956
984
  const width = Math.max(...maskInstructions.map(({ mask }) => mask.width));
957
985
  const height = Math.max(...maskInstructions.map(({ mask }) => mask.height));
958
986
  const data = new Uint8ClampedArray(new ArrayBuffer(width * height * 4));
987
+ const strokeCoverage = maskInstructions.some(({ stroke }) => stroke && stroke.width > 0 && stroke.alpha > 0)
988
+ ? new Uint8Array(width * height)
989
+ : undefined;
990
+ const strokeKernels = new Map();
959
991
  for (const instruction of maskInstructions) {
960
- compositeInstruction(data, width, instruction);
992
+ compositeInstruction(data, width, instruction, strokeCoverage, strokeKernels, displayWidth);
961
993
  }
962
994
  return { data, height, width };
963
995
  }
964
996
  function createIdMaskRasterFrame(instructions, maxRasterWidth) {
965
997
  try {
966
- return createIdMaskFrame(materializeMaskInstructions(instructions).map((instruction) => instruction.stroke?.widthUnit === "screen"
998
+ return createIdMaskFrame(instructions
999
+ .filter((instruction) => instruction.visible !== false)
1000
+ .map((instruction) => instruction.stroke?.widthUnit
967
1001
  ? { ...instruction, stroke: undefined }
968
- : instruction), {
969
- maxWidth: maxRasterWidth,
970
- });
1002
+ : instruction), { maxWidth: maxRasterWidth });
971
1003
  }
972
1004
  catch {
973
1005
  // The id raster is the fast path, not the only one: answering with nothing
@@ -997,25 +1029,29 @@
997
1029
  ? { data: frame.data, height: frame.height, width: frame.width }
998
1030
  : undefined;
999
1031
  }
1000
- function compositeInstruction(rgba, canvasWidth, instruction) {
1032
+ function compositeInstruction(rgba, canvasWidth, instruction, strokeCoverage, strokeKernels, displayWidth) {
1001
1033
  const fill = resolveRgbaColor(instruction.color, instruction.alpha);
1002
1034
  const bounds = compositeMaskFill(rgba, canvasWidth, instruction.mask, fill);
1003
1035
  if (instruction.stroke &&
1004
- instruction.stroke.widthUnit !== "screen" &&
1005
- bounds) {
1006
- compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, instruction.stroke);
1036
+ instruction.stroke.widthUnit === undefined &&
1037
+ bounds &&
1038
+ strokeCoverage) {
1039
+ compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, {
1040
+ ...instruction.stroke,
1041
+ width: resolveIdMaskStrokeTexels(instruction.stroke.width, displayWidth ?? canvasWidth, canvasWidth, instruction.stroke.alignment),
1042
+ }, strokeCoverage, strokeKernels);
1007
1043
  }
1008
1044
  }
1009
1045
  function createMaskScreenStrokes(instructions, width, height, preparedRaster) {
1010
1046
  const outlined = instructions.filter(({ visible, stroke }) => visible !== false &&
1011
- stroke?.widthUnit === "screen" &&
1047
+ stroke?.widthUnit !== undefined &&
1012
1048
  stroke.width > 0 &&
1013
1049
  stroke.alpha > 0);
1014
1050
  if (outlined.length === 0)
1015
1051
  return undefined;
1016
- const masks = materializeMaskInstructions(instructions);
1017
1052
  const raster = preparedRaster ?? new Uint32Array(width * height);
1018
1053
  if (!preparedRaster) {
1054
+ const masks = materializeMaskInstructions(instructions);
1019
1055
  for (const { detectionIndex, mask } of masks) {
1020
1056
  const counts = decodeCompressedRleCounts(mask.counts);
1021
1057
  let offset = 0;
@@ -1038,6 +1074,8 @@
1038
1074
  alpha: stroke.alpha,
1039
1075
  color: stroke.color,
1040
1076
  width: stroke.width,
1077
+ widthUnit: stroke.widthUnit,
1078
+ alignment: stroke.alignment,
1041
1079
  paths: paths.get(detectionIndex + 1) ?? [],
1042
1080
  }));
1043
1081
  }
@@ -1103,31 +1141,110 @@
1103
1141
  }
1104
1142
  return maxX < minX || maxY < minY ? undefined : { maxX, maxY, minX, minY };
1105
1143
  }
1106
- function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke) {
1107
- const width = Math.round(stroke.width);
1108
- if (width <= 0) {
1109
- return;
1144
+ function createStrokeKernel(width, alignment) {
1145
+ const innerFraction = alignment === StrokeAlignment.Inside
1146
+ ? 1
1147
+ : alignment === StrokeAlignment.Center
1148
+ ? 0.5
1149
+ : 0;
1150
+ const inside = [];
1151
+ const outside = [];
1152
+ const radius = Math.min(MAX_ID_MASK_STROKE_WIDTH + 1, Math.ceil(width * Math.max(innerFraction, 1 - innerFraction) + 0.5));
1153
+ const clamp = (value) => Math.max(0, Math.min(1, value));
1154
+ const coverage = (signed) => Math.round(255 *
1155
+ (clamp(0.5 + width * (1 - innerFraction) - signed) -
1156
+ clamp(0.5 - width * innerFraction - signed)));
1157
+ for (let y = -radius; y <= radius; y += 1) {
1158
+ for (let x = -radius; x <= radius; x += 1) {
1159
+ if (x === 0 && y === 0)
1160
+ continue;
1161
+ const distance = Math.max(Math.abs(x) - 0.5, Math.abs(y) - 0.5, 0);
1162
+ const innerCoverage = coverage(-distance);
1163
+ const outerCoverage = coverage(distance);
1164
+ if (innerCoverage > 0)
1165
+ inside.push([x, y, innerCoverage]);
1166
+ if (outerCoverage > 0)
1167
+ outside.push([x, y, outerCoverage]);
1168
+ }
1110
1169
  }
1111
- const strokeColor = resolveRgbaColor(stroke.color, stroke.alpha);
1170
+ return { inside, outside, radius };
1171
+ }
1172
+ function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke, coverage, kernels) {
1173
+ if (stroke.width <= 0 || stroke.alpha <= 0)
1174
+ return;
1175
+ const key = `${stroke.width}:${stroke.alignment}`;
1176
+ let kernel = kernels.get(key);
1177
+ if (!kernel) {
1178
+ kernel = createStrokeKernel(stroke.width, stroke.alignment);
1179
+ kernels.set(key, kernel);
1180
+ }
1181
+ const visitedAir = kernel.inside.length > 0 ? new Set() : undefined;
1182
+ const mark = (sourceX, sourceY, offsets, foreground) => {
1183
+ for (const [dx, dy, value] of offsets) {
1184
+ const x = sourceX + dx;
1185
+ const y = sourceY + dy;
1186
+ if (isOutsideMaskBounds(decodedMask, x, y) ||
1187
+ isMaskPixel(decodedMask, x, y) !== foreground)
1188
+ continue;
1189
+ const index = y * canvasWidth + x;
1190
+ coverage[index] = Math.max(coverage[index], value);
1191
+ }
1192
+ };
1112
1193
  for (let y = bounds.minY; y <= bounds.maxY; y += 1) {
1113
1194
  for (let x = bounds.minX; x <= bounds.maxX; x += 1) {
1114
1195
  if (!isMaskPixel(decodedMask, x, y) ||
1115
- !isBoundaryPixel(decodedMask, x, y)) {
1196
+ !isBoundaryPixel(decodedMask, x, y))
1116
1197
  continue;
1117
- }
1118
- for (let offsetY = -width; offsetY <= width; offsetY += 1) {
1119
- for (let offsetX = -width; offsetX <= width; offsetX += 1) {
1120
- const strokeX = x + offsetX;
1121
- const strokeY = y + offsetY;
1122
- if (isOutsideMaskBounds(decodedMask, strokeX, strokeY) ||
1123
- isMaskPixel(decodedMask, strokeX, strokeY)) {
1198
+ mark(x, y, kernel.outside, false);
1199
+ if (!visitedAir)
1200
+ continue;
1201
+ for (let dy = -1; dy <= 1; dy += 1) {
1202
+ for (let dx = -1; dx <= 1; dx += 1) {
1203
+ const airX = x + dx;
1204
+ const airY = y + dy;
1205
+ if (isOutsideMaskBounds(decodedMask, airX, airY) ||
1206
+ isMaskPixel(decodedMask, airX, airY))
1124
1207
  continue;
1125
- }
1126
- writePixel(rgba, canvasWidth, strokeX, strokeY, strokeColor);
1208
+ const airIndex = airY * decodedMask.width + airX;
1209
+ if (visitedAir.has(airIndex))
1210
+ continue;
1211
+ visitedAir.add(airIndex);
1212
+ mark(airX, airY, kernel.inside, true);
1127
1213
  }
1128
1214
  }
1129
1215
  }
1130
1216
  }
1217
+ const color = resolveRgbaColor(stroke.color, stroke.alpha);
1218
+ for (let y = Math.max(0, bounds.minY - kernel.radius); y <= Math.min(decodedMask.height - 1, bounds.maxY + kernel.radius); y += 1) {
1219
+ for (let x = Math.max(0, bounds.minX - kernel.radius); x <= Math.min(decodedMask.width - 1, bounds.maxX + kernel.radius); x += 1) {
1220
+ const index = y * canvasWidth + x;
1221
+ const amount = coverage[index] / 255;
1222
+ if (amount <= 0)
1223
+ continue;
1224
+ coverage[index] = 0;
1225
+ const offset = index * 4;
1226
+ const alpha = rgba[offset + 3] * (1 - amount) + color.alpha * amount;
1227
+ rgba[offset] =
1228
+ alpha > 0
1229
+ ? (rgba[offset] * rgba[offset + 3] * (1 - amount) +
1230
+ color.red * color.alpha * amount) /
1231
+ alpha
1232
+ : 0;
1233
+ rgba[offset + 1] =
1234
+ alpha > 0
1235
+ ? (rgba[offset + 1] * rgba[offset + 3] * (1 - amount) +
1236
+ color.green * color.alpha * amount) /
1237
+ alpha
1238
+ : 0;
1239
+ rgba[offset + 2] =
1240
+ alpha > 0
1241
+ ? (rgba[offset + 2] * rgba[offset + 3] * (1 - amount) +
1242
+ color.blue * color.alpha * amount) /
1243
+ alpha
1244
+ : 0;
1245
+ rgba[offset + 3] = alpha;
1246
+ }
1247
+ }
1131
1248
  }
1132
1249
  function isBoundaryPixel(mask, x, y) {
1133
1250
  for (let offsetY = -1; offsetY <= 1; offsetY += 1) {
@@ -1910,6 +2027,7 @@
1910
2027
  requestId: message.requestId,
1911
2028
  sourceWidth: idMaskFrame.sourceWidth,
1912
2029
  strokePalette: idMaskFrame.strokePalette,
2030
+ strokeAlignments: idMaskFrame.strokeAlignments,
1913
2031
  strokeWidths: idMaskFrame.strokeWidths,
1914
2032
  type: MaskPreparationWorkerMessageType.Complete,
1915
2033
  width: idMaskFrame.width,
@@ -1917,13 +2035,16 @@
1917
2035
  idMaskFrame.data.buffer,
1918
2036
  idMaskFrame.fillPalette.buffer,
1919
2037
  idMaskFrame.strokePalette.buffer,
2038
+ ...(idMaskFrame.strokeAlignments
2039
+ ? [idMaskFrame.strokeAlignments.buffer]
2040
+ : []),
1920
2041
  idMaskFrame.strokeWidths.buffer,
1921
2042
  ...strokeTransfers,
1922
2043
  ...coverageTransfers,
1923
2044
  ]);
1924
2045
  return;
1925
2046
  }
1926
- const compositedFrame = compositeMaskFrame(message.job.instructions);
2047
+ const compositedFrame = compositeMaskFrame(message.job.instructions, message.job.displayWidth);
1927
2048
  if (!compositedFrame && !regionMaskCoverage) {
1928
2049
  workerScope.postMessage({
1929
2050
  key: message.job.key,