@aura3d/rendering 1.4.5 → 1.5.2

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 (203) hide show
  1. package/dist/CameraFraming.d.ts +77 -0
  2. package/dist/CameraFraming.d.ts.map +1 -1
  3. package/dist/CameraFraming.js +176 -1
  4. package/dist/CameraFraming.js.map +1 -1
  5. package/dist/CascadedShadowMaps.d.ts +9 -1
  6. package/dist/CascadedShadowMaps.d.ts.map +1 -1
  7. package/dist/CascadedShadowMaps.js +68 -0
  8. package/dist/CascadedShadowMaps.js.map +1 -1
  9. package/dist/ClusteredForwardLighting.d.ts +28 -0
  10. package/dist/ClusteredForwardLighting.d.ts.map +1 -0
  11. package/dist/ClusteredForwardLighting.js +156 -0
  12. package/dist/ClusteredForwardLighting.js.map +1 -0
  13. package/dist/DepthPass.d.ts +14 -1
  14. package/dist/DepthPass.d.ts.map +1 -1
  15. package/dist/DepthPass.js +19 -4
  16. package/dist/DepthPass.js.map +1 -1
  17. package/dist/EnvironmentMapResources.d.ts +59 -1
  18. package/dist/EnvironmentMapResources.d.ts.map +1 -1
  19. package/dist/EnvironmentMapResources.js +140 -100
  20. package/dist/EnvironmentMapResources.js.map +1 -1
  21. package/dist/EnvironmentPlatform.d.ts +3 -2
  22. package/dist/EnvironmentPlatform.d.ts.map +1 -1
  23. package/dist/EnvironmentPlatform.js +99 -44
  24. package/dist/EnvironmentPlatform.js.map +1 -1
  25. package/dist/ForwardPass.d.ts +37 -0
  26. package/dist/ForwardPass.d.ts.map +1 -1
  27. package/dist/ForwardPass.js +234 -7
  28. package/dist/ForwardPass.js.map +1 -1
  29. package/dist/Geometry.d.ts +19 -0
  30. package/dist/Geometry.d.ts.map +1 -1
  31. package/dist/Geometry.js +64 -0
  32. package/dist/Geometry.js.map +1 -1
  33. package/dist/IBL.d.ts.map +1 -1
  34. package/dist/IBL.js +0 -1
  35. package/dist/IBL.js.map +1 -1
  36. package/dist/LightCollector.d.ts +5 -1
  37. package/dist/LightCollector.d.ts.map +1 -1
  38. package/dist/LightCollector.js +16 -3
  39. package/dist/LightCollector.js.map +1 -1
  40. package/dist/LightUniforms.d.ts +26 -1
  41. package/dist/LightUniforms.d.ts.map +1 -1
  42. package/dist/LightUniforms.js +140 -8
  43. package/dist/LightUniforms.js.map +1 -1
  44. package/dist/LightingRig.d.ts +77 -0
  45. package/dist/LightingRig.d.ts.map +1 -1
  46. package/dist/LightingRig.js +100 -11
  47. package/dist/LightingRig.js.map +1 -1
  48. package/dist/MeshConsolidation.d.ts +63 -0
  49. package/dist/MeshConsolidation.d.ts.map +1 -0
  50. package/dist/MeshConsolidation.js +251 -0
  51. package/dist/MeshConsolidation.js.map +1 -0
  52. package/dist/PMREM.d.ts +13 -1
  53. package/dist/PMREM.d.ts.map +1 -1
  54. package/dist/PMREM.js +18 -4
  55. package/dist/PMREM.js.map +1 -1
  56. package/dist/PostProcessPass.d.ts +24 -2
  57. package/dist/PostProcessPass.d.ts.map +1 -1
  58. package/dist/PostProcessPass.js +136 -18
  59. package/dist/PostProcessPass.js.map +1 -1
  60. package/dist/PrimitiveSubmissionAudit.d.ts +195 -0
  61. package/dist/PrimitiveSubmissionAudit.d.ts.map +1 -0
  62. package/dist/PrimitiveSubmissionAudit.js +280 -0
  63. package/dist/PrimitiveSubmissionAudit.js.map +1 -0
  64. package/dist/ProductTurntableFixtures.d.ts.map +1 -1
  65. package/dist/ProductTurntableFixtures.js +93 -13
  66. package/dist/ProductTurntableFixtures.js.map +1 -1
  67. package/dist/ReflectionProbe.d.ts +59 -0
  68. package/dist/ReflectionProbe.d.ts.map +1 -1
  69. package/dist/ReflectionProbe.js +224 -0
  70. package/dist/ReflectionProbe.js.map +1 -1
  71. package/dist/ReflectionSurfaces.d.ts +4 -2
  72. package/dist/ReflectionSurfaces.d.ts.map +1 -1
  73. package/dist/ReflectionSurfaces.js +13 -8
  74. package/dist/ReflectionSurfaces.js.map +1 -1
  75. package/dist/RenderBackend.d.ts +1 -1
  76. package/dist/RenderBackend.d.ts.map +1 -1
  77. package/dist/RenderBackend.js +5 -1
  78. package/dist/RenderBackend.js.map +1 -1
  79. package/dist/RenderDevice.d.ts +13 -2
  80. package/dist/RenderDevice.d.ts.map +1 -1
  81. package/dist/RenderDevice.js +5 -2
  82. package/dist/RenderDevice.js.map +1 -1
  83. package/dist/Renderer.d.ts +66 -3
  84. package/dist/Renderer.d.ts.map +1 -1
  85. package/dist/Renderer.js +417 -64
  86. package/dist/Renderer.js.map +1 -1
  87. package/dist/RendererPostprocessPlan.d.ts +10 -3
  88. package/dist/RendererPostprocessPlan.d.ts.map +1 -1
  89. package/dist/RendererPostprocessPlan.js +25 -5
  90. package/dist/RendererPostprocessPlan.js.map +1 -1
  91. package/dist/RendererVisualPipelineReport.d.ts +3 -2
  92. package/dist/RendererVisualPipelineReport.d.ts.map +1 -1
  93. package/dist/RendererVisualPipelineReport.js +2 -0
  94. package/dist/RendererVisualPipelineReport.js.map +1 -1
  95. package/dist/ScreenSpaceLineMaterial.d.ts +34 -0
  96. package/dist/ScreenSpaceLineMaterial.d.ts.map +1 -0
  97. package/dist/ScreenSpaceLineMaterial.js +93 -0
  98. package/dist/ScreenSpaceLineMaterial.js.map +1 -0
  99. package/dist/ShaderChunks.d.ts +7 -0
  100. package/dist/ShaderChunks.d.ts.map +1 -1
  101. package/dist/ShaderChunks.js +137 -0
  102. package/dist/ShaderChunks.js.map +1 -1
  103. package/dist/ShaderLibrary.d.ts +17 -0
  104. package/dist/ShaderLibrary.d.ts.map +1 -1
  105. package/dist/ShaderLibrary.js +940 -98
  106. package/dist/ShaderLibrary.js.map +1 -1
  107. package/dist/ShadowPass.d.ts +10 -0
  108. package/dist/ShadowPass.d.ts.map +1 -1
  109. package/dist/ShadowPass.js +8 -1
  110. package/dist/ShadowPass.js.map +1 -1
  111. package/dist/SkinnedLitMaterial.d.ts +6 -0
  112. package/dist/SkinnedLitMaterial.d.ts.map +1 -1
  113. package/dist/SkinnedLitMaterial.js +22 -8
  114. package/dist/SkinnedLitMaterial.js.map +1 -1
  115. package/dist/SkinnedUnlitMaterial.d.ts +8 -0
  116. package/dist/SkinnedUnlitMaterial.d.ts.map +1 -1
  117. package/dist/SkinnedUnlitMaterial.js +26 -9
  118. package/dist/SkinnedUnlitMaterial.js.map +1 -1
  119. package/dist/SpecularPrefilter.d.ts +42 -0
  120. package/dist/SpecularPrefilter.d.ts.map +1 -0
  121. package/dist/SpecularPrefilter.js +480 -0
  122. package/dist/SpecularPrefilter.js.map +1 -0
  123. package/dist/TerrainFixtures.d.ts +24 -0
  124. package/dist/TerrainFixtures.d.ts.map +1 -1
  125. package/dist/TerrainFixtures.js +96 -0
  126. package/dist/TerrainFixtures.js.map +1 -1
  127. package/dist/Texture.d.ts +7 -0
  128. package/dist/Texture.d.ts.map +1 -1
  129. package/dist/Texture.js +9 -0
  130. package/dist/Texture.js.map +1 -1
  131. package/dist/VertexFormat.d.ts +34 -1
  132. package/dist/VertexFormat.d.ts.map +1 -1
  133. package/dist/VertexFormat.js +61 -0
  134. package/dist/VertexFormat.js.map +1 -1
  135. package/dist/WebGL2Device.d.ts +48 -0
  136. package/dist/WebGL2Device.d.ts.map +1 -1
  137. package/dist/WebGL2Device.js +1298 -44
  138. package/dist/WebGL2Device.js.map +1 -1
  139. package/dist/WebGPUDevice.d.ts +7 -2
  140. package/dist/WebGPUDevice.d.ts.map +1 -1
  141. package/dist/WebGPUDevice.js +111 -17
  142. package/dist/WebGPUDevice.js.map +1 -1
  143. package/dist/WebGPUSkinningLimits.d.ts +14 -0
  144. package/dist/WebGPUSkinningLimits.d.ts.map +1 -0
  145. package/dist/WebGPUSkinningLimits.js +14 -0
  146. package/dist/WebGPUSkinningLimits.js.map +1 -0
  147. package/dist/cartoon/CartoonMaterialStyle.d.ts +19 -0
  148. package/dist/cartoon/CartoonMaterialStyle.d.ts.map +1 -0
  149. package/dist/cartoon/CartoonMaterialStyle.js +13 -0
  150. package/dist/cartoon/CartoonMaterialStyle.js.map +1 -0
  151. package/dist/cartoon/CartoonRenderPreset.d.ts +36 -0
  152. package/dist/cartoon/CartoonRenderPreset.d.ts.map +1 -0
  153. package/dist/cartoon/CartoonRenderPreset.js +23 -0
  154. package/dist/cartoon/CartoonRenderPreset.js.map +1 -0
  155. package/dist/cartoon/CartoonToonMaterial.d.ts +130 -0
  156. package/dist/cartoon/CartoonToonMaterial.d.ts.map +1 -0
  157. package/dist/cartoon/CartoonToonMaterial.js +303 -0
  158. package/dist/cartoon/CartoonToonMaterial.js.map +1 -0
  159. package/dist/cartoon/CartoonVisualQuality.d.ts +28 -0
  160. package/dist/cartoon/CartoonVisualQuality.d.ts.map +1 -0
  161. package/dist/cartoon/CartoonVisualQuality.js +39 -0
  162. package/dist/cartoon/CartoonVisualQuality.js.map +1 -0
  163. package/dist/cartoon/applyCartoonRenderPreset.d.ts +79 -0
  164. package/dist/cartoon/applyCartoonRenderPreset.d.ts.map +1 -0
  165. package/dist/cartoon/applyCartoonRenderPreset.js +88 -0
  166. package/dist/cartoon/applyCartoonRenderPreset.js.map +1 -0
  167. package/dist/cartoon/index.d.ts +6 -0
  168. package/dist/cartoon/index.d.ts.map +1 -0
  169. package/dist/cartoon/index.js +6 -0
  170. package/dist/cartoon/index.js.map +1 -0
  171. package/dist/index.d.ts +29 -18
  172. package/dist/index.d.ts.map +1 -1
  173. package/dist/index.js +16 -10
  174. package/dist/index.js.map +1 -1
  175. package/dist/postprocess/NativeLdrEffectLuts.d.ts +93 -0
  176. package/dist/postprocess/NativeLdrEffectLuts.d.ts.map +1 -0
  177. package/dist/postprocess/NativeLdrEffectLuts.js +202 -0
  178. package/dist/postprocess/NativeLdrEffectLuts.js.map +1 -0
  179. package/dist/production-runtime/PBRHDRPipeline.d.ts.map +1 -1
  180. package/dist/production-runtime/PBRHDRPipeline.js +3 -0
  181. package/dist/production-runtime/PBRHDRPipeline.js.map +1 -1
  182. package/dist/production-runtime/ProductionRendererTypes.d.ts +1 -0
  183. package/dist/production-runtime/ProductionRendererTypes.d.ts.map +1 -1
  184. package/dist/production-runtime/ProductionRendererTypes.js.map +1 -1
  185. package/dist/production-runtime/ProductionWebGL2Renderer.d.ts.map +1 -1
  186. package/dist/production-runtime/ProductionWebGL2Renderer.js +5 -3
  187. package/dist/production-runtime/ProductionWebGL2Renderer.js.map +1 -1
  188. package/dist/production-runtime/ProductionWebGPURenderer.d.ts.map +1 -1
  189. package/dist/production-runtime/ProductionWebGPURenderer.js +9 -5
  190. package/dist/production-runtime/ProductionWebGPURenderer.js.map +1 -1
  191. package/dist/production-runtime/TransmissionBackdropCapture.d.ts +5 -0
  192. package/dist/production-runtime/TransmissionBackdropCapture.d.ts.map +1 -1
  193. package/dist/production-runtime/TransmissionBackdropCapture.js +40 -0
  194. package/dist/production-runtime/TransmissionBackdropCapture.js.map +1 -1
  195. package/dist/production-runtime/environment/HDRLoader.d.ts +6 -0
  196. package/dist/production-runtime/environment/HDRLoader.d.ts.map +1 -1
  197. package/dist/production-runtime/environment/HDRLoader.js +19 -0
  198. package/dist/production-runtime/environment/HDRLoader.js.map +1 -1
  199. package/dist/production-runtime/index.d.ts +3 -1
  200. package/dist/production-runtime/index.d.ts.map +1 -1
  201. package/dist/production-runtime/index.js +1 -0
  202. package/dist/production-runtime/index.js.map +1 -1
  203. package/package.json +3 -3
@@ -1,5 +1,6 @@
1
1
  import { BaseRenderPass } from "./RenderPass.js";
2
2
  import { RenderDeviceError } from "./RenderDevice.js";
3
+ import { OUTLINE_LUMA_SCALE, createOutlineGradientBound } from "./postprocess/NativeLdrEffectLuts.js";
3
4
  export class ToneMappingPass extends BaseRenderPass {
4
5
  options;
5
6
  exposure;
@@ -511,16 +512,20 @@ export function outlinePixels(pixels, width, height, options = {}) {
511
512
  validateRange(opacity, 0, 1, "Outline opacity");
512
513
  validateOutlineColor(color);
513
514
  const edgeMask = new Uint8Array(width * height);
514
- let maxGradient = 0;
515
+ const gradientBound = createOutlineGradientBound(threshold).numberValue;
516
+ let maxGradientNumerator = 0;
515
517
  for (let y = 0; y < height; y += 1) {
516
518
  for (let x = 0; x < width; x += 1) {
517
- const gradient = sobelLumaGradient(pixels, width, height, x, y);
518
- maxGradient = Math.max(maxGradient, gradient);
519
- if (gradient >= threshold) {
519
+ const { gx, gy } = sobelLumaNumeratorGradient(pixels, width, height, x, y);
520
+ const squared = gx * gx + gy * gy;
521
+ if (squared > maxGradientNumerator)
522
+ maxGradientNumerator = squared;
523
+ if (outlineEdgeExceedsBound(gx, gy, gradientBound)) {
520
524
  edgeMask[y * width + x] = 255;
521
525
  }
522
526
  }
523
527
  }
528
+ const maxGradient = Math.sqrt(maxGradientNumerator) / OUTLINE_LUMA_SCALE;
524
529
  const outlineMask = dilateMask(edgeMask, width, height, outlineWidth);
525
530
  const output = new Uint8Array(pixels);
526
531
  let outlinedPixels = 0;
@@ -636,6 +641,80 @@ export function depthOfFieldPixels(pixels, width, height, options) {
636
641
  }
637
642
  return { width, height, pixels: output, blurredPixels, maxBlurRadius: maxRadius, focusDepth, focusRange };
638
643
  }
644
+ export function volumetricLightPixels(pixels, width, height, options = {}) {
645
+ validatePixelBuffer(pixels, width, height, "Volumetric light");
646
+ const depth = requireDepthTextureBinding(options.depth, "Volumetric light");
647
+ if (depth.width !== width || depth.height !== height) {
648
+ throw new Error("Volumetric light depth texture dimensions must match source pixels.");
649
+ }
650
+ const lightPosition = options.lightPosition ?? [0.5, 0.18];
651
+ if (lightPosition.length !== 2 || !lightPosition.every((value) => Number.isFinite(value) && value >= 0 && value <= 1)) {
652
+ throw new RangeError("Volumetric light position must contain two finite normalized coordinates.");
653
+ }
654
+ const color = options.color ?? [1, 0.88, 0.62];
655
+ if (color.length !== 3 || !color.every((value) => Number.isFinite(value) && value >= 0 && value <= 1)) {
656
+ throw new RangeError("Volumetric light color must contain three finite values within [0, 1].");
657
+ }
658
+ const density = numberInRange(options.density ?? 0.9, 0.01, 2, "Volumetric light density");
659
+ const decay = numberInRange(options.decay ?? 0.96, 0, 1, "Volumetric light decay");
660
+ const weight = numberInRange(options.weight ?? 0.18, 0, 2, "Volumetric light weight");
661
+ const exposure = numberInRange(options.exposure ?? 0.72, 0, 4, "Volumetric light exposure");
662
+ const samples = integerNumberInRange(options.samples ?? 32, 4, 128, "Volumetric light samples");
663
+ const occlusionThreshold = numberInRange(options.occlusionThreshold ?? 0.985, 0, 1, "Volumetric light occlusionThreshold");
664
+ const output = new Uint8Array(pixels);
665
+ const lightX = lightPosition[0] * (width - 1);
666
+ const lightY = lightPosition[1] * (height - 1);
667
+ let changedPixels = 0;
668
+ let occludedSamples = 0;
669
+ let maxScattering = 0;
670
+ for (let y = 0; y < height; y += 1) {
671
+ for (let x = 0; x < width; x += 1) {
672
+ const deltaX = ((lightX - x) / samples) * density;
673
+ const deltaY = ((lightY - y) / samples) * density;
674
+ let sampleX = x;
675
+ let sampleY = y;
676
+ let illuminationDecay = 1;
677
+ let scattering = 0;
678
+ for (let sample = 0; sample < samples; sample += 1) {
679
+ sampleX += deltaX;
680
+ sampleY += deltaY;
681
+ const sx = clampInt(Math.round(sampleX), 0, width - 1);
682
+ const sy = clampInt(Math.round(sampleY), 0, height - 1);
683
+ const offset = (sy * width + sx) * 4;
684
+ const luminance = ((pixels[offset] ?? 0) * 0.2126 + (pixels[offset + 1] ?? 0) * 0.7152 + (pixels[offset + 2] ?? 0) * 0.0722) / 255;
685
+ const source = Math.max(0, luminance - 0.42) / 0.58;
686
+ const sampleDepth = depthAt(depth, sx, sy);
687
+ const visible = sampleDepth >= occlusionThreshold || source > 0.05 ? 1 : 0;
688
+ if (visible === 0)
689
+ occludedSamples += 1;
690
+ scattering += source * visible * illuminationDecay * weight;
691
+ illuminationDecay *= decay;
692
+ }
693
+ const boost = scattering * exposure;
694
+ maxScattering = Math.max(maxScattering, boost);
695
+ const offset = (y * width + x) * 4;
696
+ const beforeR = pixels[offset] ?? 0;
697
+ const beforeG = pixels[offset + 1] ?? 0;
698
+ const beforeB = pixels[offset + 2] ?? 0;
699
+ output[offset] = clampByte(beforeR + color[0] * boost * 255);
700
+ output[offset + 1] = clampByte(beforeG + color[1] * boost * 255);
701
+ output[offset + 2] = clampByte(beforeB + color[2] * boost * 255);
702
+ if (output[offset] !== beforeR || output[offset + 1] !== beforeG || output[offset + 2] !== beforeB) {
703
+ changedPixels += 1;
704
+ }
705
+ }
706
+ }
707
+ return {
708
+ width,
709
+ height,
710
+ pixels: output,
711
+ changedPixels,
712
+ occludedSamples,
713
+ maxScattering: Number(maxScattering.toFixed(5)),
714
+ sampleCount: samples,
715
+ method: "depth-aware-radial-participating-media"
716
+ };
717
+ }
639
718
  export function motionBlurPixels(pixels, width, height, options) {
640
719
  validatePixelBuffer(pixels, width, height, "Motion blur");
641
720
  const samples = options.samples ?? 5;
@@ -1253,6 +1332,7 @@ export function fxaaPixels(pixels, width, height, options = {}) {
1253
1332
  }
1254
1333
  export function fusedLdrPostprocessPixels(pixels, width, height, passes, options = {}) {
1255
1334
  validatePixelBuffer(pixels, width, height, "Fused LDR postprocess");
1335
+ const bloom = passes.find((pass) => pass.name === "bloom");
1256
1336
  const toneMapping = passes.find((pass) => pass.name === "tone-mapping");
1257
1337
  const colorGrade = passes.find((pass) => pass.name === "color-grade");
1258
1338
  const fxaa = passes.find((pass) => pass.name === "fxaa");
@@ -1264,7 +1344,9 @@ export function fusedLdrPostprocessPixels(pixels, width, height, passes, options
1264
1344
  : undefined;
1265
1345
  const colorGradeSettings = colorGrade ? normalizeColorGradeOptions(colorGrade.options) : undefined;
1266
1346
  const byteLength = pixels.byteLength;
1267
- let current = pixels;
1347
+ let current = bloom
1348
+ ? bloomPixels(pixels, width, height, bloom.options).pixels
1349
+ : pixels;
1268
1350
  if (toneMappingLookup || colorGradeSettings) {
1269
1351
  const target = options.mutateInput
1270
1352
  ? pixels
@@ -1272,7 +1354,7 @@ export function fusedLdrPostprocessPixels(pixels, width, height, passes, options
1272
1354
  const vignetteFactors = colorGradeSettings && colorGradeSettings.vignette > 0
1273
1355
  ? getVignetteFactors(width, height, colorGradeSettings.vignette, options.scratch)
1274
1356
  : undefined;
1275
- applyToneMappingAndColorGrade(pixels, target, width, height, toneMappingLookup, colorGradeSettings, vignetteFactors);
1357
+ applyToneMappingAndColorGrade(current, target, width, height, toneMappingLookup, colorGradeSettings, vignetteFactors);
1276
1358
  current = target;
1277
1359
  }
1278
1360
  if (colorGradeSettings && colorGradeSettings.sharpening > 0) {
@@ -1536,18 +1618,37 @@ function validateOutlineColor(color) {
1536
1618
  }
1537
1619
  }
1538
1620
  }
1539
- function sobelLumaGradient(pixels, width, height, x, y) {
1540
- const nw = lumaAt(pixels, width, height, x - 1, y - 1);
1541
- const n = lumaAt(pixels, width, height, x, y - 1);
1542
- const ne = lumaAt(pixels, width, height, x + 1, y - 1);
1543
- const w = lumaAt(pixels, width, height, x - 1, y);
1544
- const e = lumaAt(pixels, width, height, x + 1, y);
1545
- const sw = lumaAt(pixels, width, height, x - 1, y + 1);
1546
- const s = lumaAt(pixels, width, height, x, y + 1);
1547
- const se = lumaAt(pixels, width, height, x + 1, y + 1);
1548
- const gx = -nw - 2 * w - sw + ne + 2 * e + se;
1549
- const gy = -nw - 2 * n - ne + sw + 2 * s + se;
1550
- return Math.hypot(gx, gy);
1621
+ /**
1622
+ * Sobel response of the integer luma numerator `2126 * r + 7152 * g + 722 * b`.
1623
+ *
1624
+ * This is the same operator as {@link sobelLumaGradient} scaled by `OUTLINE_LUMA_SCALE`, but every
1625
+ * intermediate is an exact integer: each numerator is at most `2550000`, so a response is at most
1626
+ * `4 * 2550000` and the squared magnitude used by {@link outlineEdgeExceedsBound} stays below 2^48,
1627
+ * well inside the range where float64 represents integers exactly.
1628
+ */
1629
+ function sobelLumaNumeratorGradient(pixels, width, height, x, y) {
1630
+ const nw = lumaNumeratorAt(pixels, width, height, x - 1, y - 1);
1631
+ const n = lumaNumeratorAt(pixels, width, height, x, y - 1);
1632
+ const ne = lumaNumeratorAt(pixels, width, height, x + 1, y - 1);
1633
+ const w = lumaNumeratorAt(pixels, width, height, x - 1, y);
1634
+ const e = lumaNumeratorAt(pixels, width, height, x + 1, y);
1635
+ const sw = lumaNumeratorAt(pixels, width, height, x - 1, y + 1);
1636
+ const s = lumaNumeratorAt(pixels, width, height, x, y + 1);
1637
+ const se = lumaNumeratorAt(pixels, width, height, x + 1, y + 1);
1638
+ return {
1639
+ gx: -nw - 2 * w - sw + ne + 2 * e + se,
1640
+ gy: -nw - 2 * n - ne + sw + 2 * s + se
1641
+ };
1642
+ }
1643
+ /**
1644
+ * Exact outline edge predicate shared by the CPU kernel and the WebGL2 shader.
1645
+ *
1646
+ * Equivalent to `hypot(gx, gy) >= threshold` on the real numbers, but decided with integers so
1647
+ * that it never depends on float rounding. `bound` comes from {@link createOutlineGradientBound},
1648
+ * which pre-squares the threshold as an exact rational and rounds up.
1649
+ */
1650
+ function outlineEdgeExceedsBound(gx, gy, bound) {
1651
+ return gx * gx + gy * gy >= bound;
1551
1652
  }
1552
1653
  function dilateMask(mask, width, height, radius) {
1553
1654
  const output = new Uint8Array(mask.length);
@@ -1793,6 +1894,16 @@ function validateRange(value, min, max, label) {
1793
1894
  throw new Error(`${label} must be finite and within [${min}, ${max}].`);
1794
1895
  }
1795
1896
  }
1897
+ function numberInRange(value, min, max, label) {
1898
+ validateRange(value, min, max, label);
1899
+ return value;
1900
+ }
1901
+ function integerNumberInRange(value, min, max, label) {
1902
+ validateRange(value, min, max, label);
1903
+ if (!Number.isInteger(value))
1904
+ throw new Error(`${label} must be an integer.`);
1905
+ return value;
1906
+ }
1796
1907
  function clampByte(value) {
1797
1908
  return Math.round(Math.max(0, Math.min(255, value)));
1798
1909
  }
@@ -1805,6 +1916,13 @@ function lumaAt(pixels, width, height, x, y) {
1805
1916
  const index = (sampleY * width + sampleX) * 4;
1806
1917
  return (0.2126 * pixels[index] + 0.7152 * pixels[index + 1] + 0.0722 * pixels[index + 2]) / 255;
1807
1918
  }
1919
+ /** Integer luma numerator at a clamped coordinate, matching the outline shader exactly. */
1920
+ function lumaNumeratorAt(pixels, width, height, x, y) {
1921
+ const sampleX = clampInt(x, 0, width - 1);
1922
+ const sampleY = clampInt(y, 0, height - 1);
1923
+ const index = (sampleY * width + sampleX) * 4;
1924
+ return 2126 * pixels[index] + 7152 * pixels[index + 1] + 722 * pixels[index + 2];
1925
+ }
1808
1926
  function channelAt(pixels, width, height, x, y, channel) {
1809
1927
  const sampleX = clampInt(x, 0, width - 1);
1810
1928
  const sampleY = clampInt(y, 0, height - 1);