@photivra/engine 2.12.0 → 2.14.0

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 (148) hide show
  1. package/CHANGELOG.md +144 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -7
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +3 -0
  6. package/dist/camera/lens-projection.js +2 -1
  7. package/dist/camera/lens-projection.js.map +1 -1
  8. package/dist/capture/dng-import.js +1 -1
  9. package/dist/capture/dng-import.js.map +1 -1
  10. package/dist/capture/export-jpeg.d.ts +1 -1
  11. package/dist/capture/export-jpeg.js +6 -4
  12. package/dist/capture/export-jpeg.js.map +1 -1
  13. package/dist/capture/host-yield-cadence.js +1 -1
  14. package/dist/capture/host-yield-cadence.js.map +1 -1
  15. package/dist/capture/native-environment-optical-stage.js +1 -1
  16. package/dist/capture/native-environment-optical-stage.js.map +1 -1
  17. package/dist/capture/native-raw.js +1 -1
  18. package/dist/capture/native-raw.js.map +1 -1
  19. package/dist/capture/packed-sensor-raw.js +3 -3
  20. package/dist/capture/packed-sensor-raw.js.map +1 -1
  21. package/dist/capture/sensor-raw-producer.js +1 -1
  22. package/dist/capture/sensor-raw-producer.js.map +1 -1
  23. package/dist/color/capture-color.js +3 -3
  24. package/dist/color/capture-color.js.map +1 -1
  25. package/dist/color/chromatic-adaptation.js +2 -2
  26. package/dist/color/chromatic-adaptation.js.map +1 -1
  27. package/dist/color/color-matching-functions.js +1 -1
  28. package/dist/color/color-matching-functions.js.map +1 -1
  29. package/dist/color/colorimetry-math.js +2 -2
  30. package/dist/color/colorimetry-math.js.map +1 -1
  31. package/dist/color/illuminants.js +3 -3
  32. package/dist/color/illuminants.js.map +1 -1
  33. package/dist/color/rgb-color-spaces.js +1 -1
  34. package/dist/color/rgb-color-spaces.js.map +1 -1
  35. package/dist/color/sensor-color-development.js +1 -1
  36. package/dist/color/sensor-color-development.js.map +1 -1
  37. package/dist/core/record-validation.js +3 -3
  38. package/dist/core/record-validation.js.map +1 -1
  39. package/dist/core/version.d.ts +1 -1
  40. package/dist/core/version.js +1 -1
  41. package/dist/core/version.js.map +1 -1
  42. package/dist/equipment/generic-prime-lenses.js +3 -3
  43. package/dist/equipment/generic-prime-lenses.js.map +1 -1
  44. package/dist/equipment/generic-tier-assets.d.ts +23 -1
  45. package/dist/equipment/generic-tier-assets.js +58 -30
  46. package/dist/equipment/generic-tier-assets.js.map +1 -1
  47. package/dist/equipment/generic-tier-presets.d.ts +6 -3
  48. package/dist/equipment/generic-tier-presets.js +26 -13
  49. package/dist/equipment/generic-tier-presets.js.map +1 -1
  50. package/dist/exposure/auto-iso-slowest-shutter.d.ts +120 -0
  51. package/dist/exposure/auto-iso-slowest-shutter.js +217 -0
  52. package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -0
  53. package/dist/exposure/exposure-mode-resolver.d.ts +57 -1
  54. package/dist/exposure/exposure-mode-resolver.js +238 -33
  55. package/dist/exposure/exposure-mode-resolver.js.map +1 -1
  56. package/dist/exposure/flash-ttl.js +1 -1
  57. package/dist/exposure/flash-ttl.js.map +1 -1
  58. package/dist/exposure/flash.js +1 -1
  59. package/dist/exposure/flash.js.map +1 -1
  60. package/dist/facade/chart-material-layers.js +2 -2
  61. package/dist/facade/chart-material-layers.js.map +1 -1
  62. package/dist/facade/chart-radiance-batch.js +1 -1
  63. package/dist/facade/chart-radiance-batch.js.map +1 -1
  64. package/dist/facade/chart-scenes.d.ts +45 -8
  65. package/dist/facade/chart-scenes.js +101 -44
  66. package/dist/facade/chart-scenes.js.map +1 -1
  67. package/dist/facade/color-patches.d.ts +129 -0
  68. package/dist/facade/color-patches.js +209 -0
  69. package/dist/facade/color-patches.js.map +1 -0
  70. package/dist/facade/diffraction-provider.js +12 -13
  71. package/dist/facade/diffraction-provider.js.map +1 -1
  72. package/dist/facade/display-view-gpu.js +1 -1
  73. package/dist/facade/display-view-gpu.js.map +1 -1
  74. package/dist/facade/display-view.d.ts +27 -0
  75. package/dist/facade/display-view.js +234 -65
  76. package/dist/facade/display-view.js.map +1 -1
  77. package/dist/facade/scene-radiance.js +2 -2
  78. package/dist/facade/scene-radiance.js.map +1 -1
  79. package/dist/facade/simulate-capture.d.ts +139 -12
  80. package/dist/facade/simulate-capture.js +209 -36
  81. package/dist/facade/simulate-capture.js.map +1 -1
  82. package/dist/facade/simulate-display-view.js +2 -2
  83. package/dist/facade/simulate-display-view.js.map +1 -1
  84. package/dist/gpu/chart-composite-gpu.js +2 -2
  85. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  86. package/dist/gpu/display-view-composite-material-module.js +1 -1
  87. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  88. package/dist/gpu/display-view-quality-gpu.d.ts +2 -1
  89. package/dist/gpu/display-view-quality-gpu.js +24 -14
  90. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  91. package/dist/gpu/image-kernel-execution.js +2 -2
  92. package/dist/gpu/image-kernel-execution.js.map +1 -1
  93. package/dist/gpu/temporal-sum-gpu.js +23 -23
  94. package/dist/gpu/webgpu-runtime.js +1 -1
  95. package/dist/gpu/webgpu-runtime.js.map +1 -1
  96. package/dist/gpu/wgsl-chart-composite-kernel.js +318 -318
  97. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -1
  98. package/dist/gpu/wgsl-chart-kernel.js +117 -117
  99. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  100. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +13 -1
  101. package/dist/gpu/wgsl-display-view-quality-kernel.js +522 -488
  102. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  103. package/dist/gpu/wgsl-display-view-speed-kernel.js +248 -248
  104. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  105. package/dist/gpu/wgsl-kernels.js +203 -203
  106. package/dist/gpu/wgsl-kernels.js.map +1 -1
  107. package/dist/gpu/wgsl-sensor-kernel.js +302 -302
  108. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  109. package/dist/index.d.ts +8 -5
  110. package/dist/index.js +6 -3
  111. package/dist/index.js.map +1 -1
  112. package/dist/motion/temporal-exposure-sum.js +1 -1
  113. package/dist/motion/temporal-exposure-sum.js.map +1 -1
  114. package/dist/optics/diffraction-psf-profile.js +4 -4
  115. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  116. package/dist/optics/front-of-lens-filter.js +1 -1
  117. package/dist/optics/front-of-lens-filter.js.map +1 -1
  118. package/dist/optics/irradiance-convolution.d.ts +10 -0
  119. package/dist/optics/irradiance-convolution.js +49 -14
  120. package/dist/optics/irradiance-convolution.js.map +1 -1
  121. package/dist/output/native-capture-sdr.js +1 -1
  122. package/dist/output/native-capture-sdr.js.map +1 -1
  123. package/dist/output/png-encoder.js +2 -2
  124. package/dist/output/png-encoder.js.map +1 -1
  125. package/dist/output/print-jpeg.js +13 -10
  126. package/dist/output/print-jpeg.js.map +1 -1
  127. package/dist/output/text-metadata.d.ts +22 -2
  128. package/dist/output/text-metadata.js +58 -9
  129. package/dist/output/text-metadata.js.map +1 -1
  130. package/dist/sensor/fixed-pattern-noise.js +1 -1
  131. package/dist/sensor/fixed-pattern-noise.js.map +1 -1
  132. package/dist/sensor/readout-exposure-linkage.js +1 -1
  133. package/dist/sensor/readout-exposure-linkage.js.map +1 -1
  134. package/docs/API_REFERENCE.md +49 -1
  135. package/docs/QUICK_START.md +104 -9
  136. package/docs/USAGE.md +241 -2
  137. package/docs/api/core-version.md +1 -1
  138. package/docs/api/equipment-generic-tier-assets.md +41 -1
  139. package/docs/api/equipment-generic-tier-presets.md +11 -3
  140. package/docs/api/exports.json +172 -2
  141. package/docs/api/exposure-auto-iso-slowest-shutter.md +139 -0
  142. package/docs/api/exposure-exposure-mode-resolver.md +68 -0
  143. package/docs/api/facade-chart-scenes.md +36 -5
  144. package/docs/api/facade-color-patches.md +150 -0
  145. package/docs/api/facade-simulate-capture.md +140 -9
  146. package/docs/api/output-text-metadata.md +34 -0
  147. package/llms.txt +1 -0
  148. package/package.json +2 -2
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Photivra color patches (2.14): a built-in set of 30 reflectance spectra designed by Photivra for color test charts.
3
+ *
4
+ * Every spectrum is an engine-defined analytic function of wavelength, not measured or copied data: a constant plus a
5
+ * sum of Gaussian bumps and logistic steps,
6
+ *
7
+ * R(λ) = min(1, max(0, b + Σₖ aₖ·φₖ(λ))),
8
+ * gaussian: φ(λ) = exp(−½·((λ − μ)/σ)²)
9
+ * logistic-rise: φ(λ) = 1 / (1 + exp(−(λ − λ₀)/s))
10
+ * logistic-fall: φ(λ) = 1 − 1 / (1 + exp(−(λ − λ₀)/s)),
11
+ *
12
+ * with λ in nanometers on 360–830 nm (the CIE 1931 observer's domain). The parameters were fitted once, by
13
+ * Photivra, to Photivra's own target colors (docs/COLOR_PATCHES.md), then rounded. Every spectrum lies in
14
+ * [0.03, 0.901] on its whole domain, so the clamp to [0, 1] never acts; it only states the physical bound. The
15
+ * patches are non-fluorescent (pure reflectances) and the neutral patches are spectrally flat.
16
+ *
17
+ * No third-party chart is copied: the set uses no ColorChecker or other commercial target's patch data, names, order,
18
+ * layout or measured spectra. The names are Photivra's own, and every published color is checked to differ from openly
19
+ * published third-party colors (`scripts/check-color-patch-distinctness.mjs`). The reference CIELAB and sRGB values are computed from these spectra with the CIE 1931
20
+ * 2° observer and CIE illuminant D65 (`scripts/compute-color-patch-reference.mjs`).
21
+ *
22
+ * Captures evaluate the exponentials with `Math.exp`, like the engine's other continuous chart formulas. The display
23
+ * view passes the portable `portableExp` instead (`resolvePortableChartScene`), so its chart presets' patch
24
+ * reflectances have the same bits in every JavaScript engine (docs/DISPLAY_VIEW.md, "Same bytes in every engine").
25
+ */
26
+ import { type CalculationResult } from "../core/calculation-result.js";
27
+ import type { PreparedColorimetricSpectrum } from "../color/color-matching-functions.js";
28
+ /** Identity of the built-in patch set. */
29
+ export declare const COLOR_PATCH_SET_ID = "photivra-color-patches";
30
+ /** Version of the patch set's spectra and reference values; a change to any spectrum changes it. */
31
+ export declare const COLOR_PATCH_SET_VERSION = "1.0.0";
32
+ /** Wavelengths on which every patch spectrum is defined, nanometers, inclusive. */
33
+ export declare const COLOR_PATCH_WAVELENGTH_RANGE_NANOMETERS: Readonly<{
34
+ minimum: 360;
35
+ maximum: 830;
36
+ }>;
37
+ /** One term of a patch spectrum (see the module formula). */
38
+ export type ColorPatchSpectralTerm = {
39
+ kind: "gaussian";
40
+ amplitude: number;
41
+ centerNanometers: number;
42
+ standardDeviationNanometers: number;
43
+ } | {
44
+ kind: "logistic-rise" | "logistic-fall";
45
+ amplitude: number;
46
+ midpointNanometers: number;
47
+ scaleNanometers: number;
48
+ };
49
+ /** Groups of the patch set, in chart order. */
50
+ export type ColorPatchGroup = "neutral" | "strong" | "soft" | "skin" | "memory" | "gamut-edge";
51
+ /** The ids of the built-in patches, in the order of the `color-patches` chart (row-major, six per row). */
52
+ export declare const COLOR_PATCH_IDS: readonly ["neutral-1", "neutral-2", "neutral-3", "neutral-4", "neutral-5", "neutral-6", "red", "yellow", "green", "cyan", "blue", "magenta", "red-soft", "yellow-soft", "green-soft", "cyan-soft", "blue-soft", "magenta-soft", "skin-pale", "skin-medium", "skin-tan", "skin-deep", "sky-blue", "foliage-green", "brick-red", "sand", "teal", "lavender", "deep-orange", "deep-violet"];
53
+ export type ColorPatchId = (typeof COLOR_PATCH_IDS)[number];
54
+ /**
55
+ * One built-in patch: its id, short code, name, reason for being on the chart, spectrum definition and reference
56
+ * colorimetry.
57
+ */
58
+ export interface ColorPatch {
59
+ /** Stable API id, e.g. `"skin-medium"`. */
60
+ patchId: ColorPatchId;
61
+ /**
62
+ * Stable short code for labels and printed charts: `PV-` and a group letter and number, e.g. `"PV-N4"`. The letters are
63
+ * N neutral, S strong, T soft (tint), P skin (portrait), M memory and G gamut-edge, numbered in chart order.
64
+ */
65
+ code: string;
66
+ /** Photivra's own descriptive name, in US English, e.g. `"Studio Gray 4"`; not a third-party color name. */
67
+ name: string;
68
+ group: ColorPatchGroup;
69
+ /** One sentence: why the patch is on the chart. */
70
+ reason: string;
71
+ /** The constant b of the formula. */
72
+ base: number;
73
+ terms: readonly ColorPatchSpectralTerm[];
74
+ /**
75
+ * Reference colorimetry of the reflectance (not of a rendered capture): CIELAB under CIE illuminant D65 with the CIE
76
+ * 1931 2° observer, the CIE tables at 1 nm over 360–830 nm, white = the perfect diffuser under D65 (Y = 100); and the
77
+ * sRGB color of that XYZ (CSS Color 4 matrix, after XYZ scaling of the D65 table white to the sRGB white),
78
+ * 8-bit hex. Rounded for publication: L*, a*, b* to 0.01.
79
+ */
80
+ reference: {
81
+ cielabD65: readonly [number, number, number];
82
+ srgbHex: string;
83
+ inSrgbGamut: boolean;
84
+ };
85
+ }
86
+ /** The built-in patch set (see {@link getColorPatchSet}). */
87
+ export interface ColorPatchSet {
88
+ setId: typeof COLOR_PATCH_SET_ID;
89
+ version: typeof COLOR_PATCH_SET_VERSION;
90
+ wavelengthRangeNanometers: {
91
+ minimum: number;
92
+ maximum: number;
93
+ };
94
+ /** Patches per row of the `color-patches` chart's default layout. */
95
+ columns: number;
96
+ patches: readonly ColorPatch[];
97
+ }
98
+ /**
99
+ * The built-in Photivra color-patch set: every patch's id, code, name, group, reason, spectrum parameters and reference CIELAB (D65,
100
+ * CIE 1931 2°) and sRGB values. A static, deeply frozen engine contract. Pass a patch to a chart as
101
+ * `{ kind: "color-patch", patchId }` wherever a chart reflectance is accepted, or draw the whole set with the
102
+ * `color-patches` chart preset.
103
+ */
104
+ export declare function getColorPatchSet(): ColorPatchSet;
105
+ /** Internal: the patch with this id, or undefined. */
106
+ export declare function findColorPatch(patchId: unknown): ColorPatch | undefined;
107
+ /** An exponential: `Math.exp`, or the display view's portable one. */
108
+ type Exponential = (x: number) => number;
109
+ /** Internal (for tests): the formula before the clamp. */
110
+ export declare function evaluateUnclampedColorPatchFormula(patchId: ColorPatchId, wavelengthNanometers: number, exp?: Exponential): number;
111
+ /**
112
+ * Internal: a prepared reflectance spectrum of a built-in patch. Values are memoized per wavelength (the formula is pure),
113
+ * so a capture's per-query lookups cost one map access instead of the exponentials.
114
+ */
115
+ export declare function prepareColorPatchSpectrum(patch: ColorPatch, exp?: Exponential): PreparedColorimetricSpectrum;
116
+ /** Input of {@link evaluateColorPatchReflectance}. */
117
+ export interface EvaluateColorPatchReflectanceInput {
118
+ patchId: ColorPatchId;
119
+ /** Vacuum wavelength, nanometers, in 360–830. */
120
+ wavelengthNanometers: number;
121
+ }
122
+ /**
123
+ * The reflectance (dimensionless, in [0, 1]) of a built-in color patch at one wavelength, from its analytic formula.
124
+ * Labeled `approximation`: the spectra are Photivra's own smooth designs, not measurements of a physical sample.
125
+ */
126
+ export declare function evaluateColorPatchReflectance(input: EvaluateColorPatchReflectanceInput): CalculationResult<{
127
+ reflectance: number;
128
+ }>;
129
+ export {};
@@ -0,0 +1,209 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Photivra color patches (2.14): a built-in set of 30 reflectance spectra designed by Photivra for color test charts.
4
+ *
5
+ * Every spectrum is an engine-defined analytic function of wavelength, not measured or copied data: a constant plus a
6
+ * sum of Gaussian bumps and logistic steps,
7
+ *
8
+ * R(λ) = min(1, max(0, b + Σₖ aₖ·φₖ(λ))),
9
+ * gaussian: φ(λ) = exp(−½·((λ − μ)/σ)²)
10
+ * logistic-rise: φ(λ) = 1 / (1 + exp(−(λ − λ₀)/s))
11
+ * logistic-fall: φ(λ) = 1 − 1 / (1 + exp(−(λ − λ₀)/s)),
12
+ *
13
+ * with λ in nanometers on 360–830 nm (the CIE 1931 observer's domain). The parameters were fitted once, by
14
+ * Photivra, to Photivra's own target colors (docs/COLOR_PATCHES.md), then rounded. Every spectrum lies in
15
+ * [0.03, 0.901] on its whole domain, so the clamp to [0, 1] never acts; it only states the physical bound. The
16
+ * patches are non-fluorescent (pure reflectances) and the neutral patches are spectrally flat.
17
+ *
18
+ * No third-party chart is copied: the set uses no ColorChecker or other commercial target's patch data, names, order,
19
+ * layout or measured spectra. The names are Photivra's own, and every published color is checked to differ from openly
20
+ * published third-party colors (`scripts/check-color-patch-distinctness.mjs`). The reference CIELAB and sRGB values are computed from these spectra with the CIE 1931
21
+ * 2° observer and CIE illuminant D65 (`scripts/compute-color-patch-reference.mjs`).
22
+ *
23
+ * Captures evaluate the exponentials with `Math.exp`, like the engine's other continuous chart formulas. The display
24
+ * view passes the portable `portableExp` instead (`resolvePortableChartScene`), so its chart presets' patch
25
+ * reflectances have the same bits in every JavaScript engine (docs/DISPLAY_VIEW.md, "Same bytes in every engine").
26
+ */
27
+ import { approximationResult } from "../core/calculation-result.js";
28
+ import { InvalidConfigurationError } from "../core/configuration-error.js";
29
+ import { InvalidScientificInputError } from "../core/validation.js";
30
+ import { requireInputKeys } from "../core/record-validation.js";
31
+ /** Identity of the built-in patch set. */
32
+ export const COLOR_PATCH_SET_ID = "photivra-color-patches";
33
+ /** Version of the patch set's spectra and reference values; a change to any spectrum changes it. */
34
+ export const COLOR_PATCH_SET_VERSION = "1.0.0";
35
+ /** Wavelengths on which every patch spectrum is defined, nanometers, inclusive. */
36
+ export const COLOR_PATCH_WAVELENGTH_RANGE_NANOMETERS = /* @__PURE__ */ Object.freeze({ minimum: 360, maximum: 830 });
37
+ /** The ids of the built-in patches, in the order of the `color-patches` chart (row-major, six per row). */
38
+ export const COLOR_PATCH_IDS = /* @__PURE__ */ Object.freeze([
39
+ "neutral-1", "neutral-2", "neutral-3", "neutral-4", "neutral-5", "neutral-6",
40
+ "red", "yellow", "green", "cyan", "blue", "magenta",
41
+ "red-soft", "yellow-soft", "green-soft", "cyan-soft", "blue-soft", "magenta-soft",
42
+ "skin-pale", "skin-medium", "skin-tan", "skin-deep", "sky-blue", "foliage-green",
43
+ "brick-red", "sand", "teal", "lavender", "deep-orange", "deep-violet"
44
+ ]);
45
+ const g = (amplitude, centerNanometers, standardDeviationNanometers) => ({ kind: "gaussian", amplitude, centerNanometers, standardDeviationNanometers });
46
+ const rise = (amplitude, midpointNanometers, scaleNanometers) => ({ kind: "logistic-rise", amplitude, midpointNanometers, scaleNanometers });
47
+ const fall = (amplitude, midpointNanometers, scaleNanometers) => ({ kind: "logistic-fall", amplitude, midpointNanometers, scaleNanometers });
48
+ /* Generated reference values: node scripts/compute-color-patch-reference.mjs --print (see docs/COLOR_PATCHES.md). */
49
+ const DEFINITIONS = [
50
+ ["neutral-1", "PV-N1", "Studio Gray 1", "neutral", 0.901, [], [96.04, 0, 0], "#F4F4F4",
51
+ "The white end of the flat gray ramp (90.1 %): the reference for white balance, exposure and highlight clipping."],
52
+ ["neutral-2", "PV-N2", "Studio Gray 2", "neutral", 0.585, [], [81.02, 0, 0], "#C9C9C9",
53
+ "A light flat gray about 15 L* below the white, for the upper tone curve and highlight roll-off."],
54
+ ["neutral-3", "PV-N3", "Studio Gray 3", "neutral", 0.354, [], [66.06, 0, 0], "#A1A1A1",
55
+ "A light-mid flat gray that continues the ramp in equal lightness steps, for tone-curve and gamma checks."],
56
+ ["neutral-4", "PV-N4", "Studio Gray 4", "neutral", 0.197, [], [51.5, 0, 0], "#7B7B7B",
57
+ "The mid-gray of the ramp, near an 18 % card's lightness, for exposure and neutral balance in the midtones."],
58
+ ["neutral-5", "PV-N5", "Studio Gray 5", "neutral", 0.093, [], [36.56, 0, 0], "#565656",
59
+ "A dark flat gray that shows shadow tone separation, noise and any color cast in the shadows."],
60
+ ["neutral-6", "PV-N6", "Studio Gray 6", "neutral", 0.035, [], [21.94, 0, 0], "#353535",
61
+ "The black end of the ramp (3.5 %), for black level, shadow noise and flare."],
62
+ ["red", "PV-S1", "Darkroom Red", "strong", 0.031, [rise(0.757, 611.6, 16.2)], [43.7, 54.15, 39.93], "#BF3428",
63
+ "A strong red, for saturated reds, which clip and shift in hue easily in a camera's red channel."],
64
+ ["yellow", "PV-S2", "Flashbulb Yellow", "strong", 0.071, [rise(0.602, 518.7, 13.2)], [78.87, -0.23, 73.26], "#E6C02A",
65
+ "A strong yellow, bright and high in chroma, that shows whether light saturated colors keep their hue."],
66
+ ["green", "PV-S3", "Hedgerow Green", "strong", 0.051, [g(0.317, 535.8, 36.9)], [57.08, -47.07, 33.2], "#359C4C",
67
+ "A strong mid green, for the green primary, where camera and display gamuts differ most."],
68
+ ["cyan", "PV-S4", "Harbor Cyan", "strong", 0.096, [g(0.339, 482.5, 60.4)], [58.4, -21.56, -17.23], "#3C98AA",
69
+ "A strong cyan between the green and blue primaries, a region where overlapping camera responses mix the channels."],
70
+ ["blue", "PV-S5", "Cyanotype Blue", "strong", 0.064, [g(0.336, 460.7, 24.7)], [35.27, 19.89, -46.48], "#3C4C9E",
71
+ "A strong dark blue, for the blue channel, its noise and the shift of saturated blues toward purple."],
72
+ ["magenta", "PV-S6", "Filter Magenta", "strong", 0.05, [g(0.258, 440, 32.8), rise(0.549, 605, 15)], [46.11, 55, -18.91], "#B4408E",
73
+ "A strong magenta, a non-spectral hue made of both ends of the spectrum, so it tests the red and blue channels together."],
74
+ ["red-soft", "PV-T1", "Faded Print Red", "soft", 0.196, [rise(0.472, 601.8, 13)], [59.63, 29.17, 14.19], "#C77B78",
75
+ "A soft red at lower chroma and higher lightness, for gentle reds that a color matrix should not oversaturate."],
76
+ ["yellow-soft", "PV-T2", "Window Glow Yellow", "soft", 0.246, [rise(0.414, 509, 17.6)], [81.15, -2.88, 36.43], "#DCC985",
77
+ "A soft light yellow, for pale warm tones whose hue drifts easily toward green or orange."],
78
+ ["green-soft", "PV-T3", "Misty Hedgerow Green", "soft", 0.184, [g(0.251, 535.7, 47.4)], [66.82, -24.11, 16.11], "#7FAE85",
79
+ "A soft green, for the muted greens that fill much of a natural scene."],
80
+ ["cyan-soft", "PV-T4", "Shallow Water Cyan", "soft", 0.246, [g(0.339, 487.4, 61.9)], [71.51, -16.99, -10.71], "#7DB9C2",
81
+ "A soft cyan, for pale blue-greens such as water and glass, where small hue errors are easy to see."],
82
+ ["blue-soft", "PV-T5", "Distant Hill Blue", "soft", 0.207, [g(0.319, 457.6, 34.7)], [56.48, 7.42, -30.88], "#7486BD",
83
+ "A soft blue, for the hazy blues of distance and open shade that white balance acts on."],
84
+ ["magenta-soft", "PV-T6", "Faded Filter Magenta", "soft", 0.196, [g(0.231, 440, 33.1), rise(0.256, 575.8, 15)], [61.36, 26.09, -10.98], "#B984A8",
85
+ "A soft magenta, for pinks and mauves that lie between the red and blue channels."],
86
+ ["skin-pale", "PV-P1", "Fair Portrait Tone", "skin", 0.408, [rise(0.394, 562.5, 31.7)], [81.35, 9.48, 17.48], "#E8C3AA",
87
+ "A fair skin tone, the lightest of four, for portrait color, where viewers notice small errors."],
88
+ ["skin-medium", "PV-P2", "Medium Portrait Tone", "skin", 0.213, [rise(0.376, 570.3, 30.4)], [67.25, 13.65, 22.01], "#C99A7D",
89
+ "A medium skin tone, the second of four that span a range of skin lightness at similar hue."],
90
+ ["skin-tan", "PV-P3", "Tan Portrait Tone", "skin", 0.084, [rise(0.295, 573.7, 31.3)], [52.01, 15.71, 26.72], "#A3714F",
91
+ "A tan skin tone, the third of four, for warm midtone skin under different lights."],
92
+ ["skin-deep", "PV-P4", "Deep Portrait Tone", "skin", 0.034, [rise(0.133, 590.3, 32.4)], [33, 13.57, 18.02], "#6A4532",
93
+ "A deep skin tone, the darkest of four, for skin color and detail in the darker tones of a portrait."],
94
+ ["sky-blue", "PV-M1", "Midday Sky", "memory", 0.18, [fall(0.455, 507.2, 29.7)], [60.05, 0.53, -33.31], "#6893CB",
95
+ "A clear-sky blue, a memory color that viewers judge against what they remember of the sky."],
96
+ ["foliage-green", "PV-M2", "Canopy Green", "memory", 0.034, [g(0.139, 555, 30.7), rise(0.411, 716.8, 12)], [40.81, -22.4, 30.31], "#4B692C",
97
+ "A leaf green with the steep rise of leaf reflectance at the red edge, a memory color that also exposes infrared leakage."],
98
+ ["brick-red", "PV-M3", "Warehouse Brick", "memory", 0.05, [rise(0.308, 592.7, 16.2)], [41.63, 32.81, 25.53], "#9C4A3A",
99
+ "A dull brick red, a common building color between the saturated reds and the skin tones."],
100
+ ["sand", "PV-M4", "Low Tide Sand", "memory", 0.272, [rise(0.371, 504.9, 37.6)], [79.14, 0.48, 22.19], "#D5C29B",
101
+ "A pale sand, a light warm near-neutral that shows small casts a gray patch would hide."],
102
+ ["teal", "PV-M5", "Estuary Teal", "memory", 0.085, [g(0.197, 498.2, 42)], [48.43, -24.88, -5.64], "#2E7F7C",
103
+ "A dark blue-green between the green and cyan hues, for water and shaded foliage in that region."],
104
+ ["lavender", "PV-M6", "Wildflower Lilac", "memory", 0.331, [g(0.298, 445, 30.4), rise(0.137, 623.4, 20)], [66.69, 14.8, -22.7], "#A99BCB",
105
+ "A pale blue-violet, a flower color that cameras often render too blue."],
106
+ ["deep-orange", "PV-G1", "Sunset Flare Orange", "gamut-edge", 0.03, [rise(0.74, 591.3, 16.7)], [53.19, 52.38, 56.53], "#DC511A",
107
+ "A deep orange close to the sRGB gamut boundary, for gamut mapping and clipping of saturated warm colors."],
108
+ ["deep-violet", "PV-G2", "Blacklight Violet", "gamut-edge", 0.031, [g(0.37, 441.6, 25.6), rise(0.097, 640, 20)], [26.42, 48.9, -59.69], "#4C1E9C",
109
+ "A deep violet close to the sRGB gamut boundary, for gamut mapping and the camera's response at short wavelengths."]
110
+ ];
111
+ function deepFreezePatch(patch) {
112
+ for (const term of patch.terms)
113
+ Object.freeze(term);
114
+ Object.freeze(patch.terms);
115
+ Object.freeze(patch.reference.cielabD65);
116
+ Object.freeze(patch.reference);
117
+ return Object.freeze(patch);
118
+ }
119
+ const PATCHES = Object.freeze(DEFINITIONS.map(([patchId, code, name, group, base, terms, lab, hex, reason]) => deepFreezePatch({
120
+ patchId, code, name, group, reason, base, terms, reference: { cielabD65: lab, srgbHex: hex, inSrgbGamut: true }
121
+ })));
122
+ const BY_ID = new Map(PATCHES.map((p) => [p.patchId, p]));
123
+ const SET = /* @__PURE__ */ Object.freeze({ setId: COLOR_PATCH_SET_ID, version: COLOR_PATCH_SET_VERSION,
124
+ wavelengthRangeNanometers: COLOR_PATCH_WAVELENGTH_RANGE_NANOMETERS, columns: 6, patches: PATCHES });
125
+ /**
126
+ * The built-in Photivra color-patch set: every patch's id, code, name, group, reason, spectrum parameters and reference CIELAB (D65,
127
+ * CIE 1931 2°) and sRGB values. A static, deeply frozen engine contract. Pass a patch to a chart as
128
+ * `{ kind: "color-patch", patchId }` wherever a chart reflectance is accepted, or draw the whole set with the
129
+ * `color-patches` chart preset.
130
+ */
131
+ export function getColorPatchSet() {
132
+ return SET;
133
+ }
134
+ /** Internal: the patch with this id, or undefined. */
135
+ export function findColorPatch(patchId) {
136
+ return typeof patchId === "string" ? BY_ID.get(patchId) : undefined;
137
+ }
138
+ /** The unclamped formula value b + Σ aₖφₖ(λ) (the clamp never acts on the built-in set; a test checks it). */
139
+ function formula(patch, nm, exp) {
140
+ let value = patch.base;
141
+ for (const term of patch.terms) {
142
+ if (term.kind === "gaussian") {
143
+ const z = (nm - term.centerNanometers) / term.standardDeviationNanometers;
144
+ value += term.amplitude * exp(-0.5 * z * z);
145
+ }
146
+ else {
147
+ const step = 1 / (1 + exp(-(nm - term.midpointNanometers) / term.scaleNanometers));
148
+ value += term.amplitude * (term.kind === "logistic-rise" ? step : 1 - step);
149
+ }
150
+ }
151
+ return value;
152
+ }
153
+ function reflectanceOf(patch, nm, exp = Math.exp) {
154
+ return Math.min(1, Math.max(0, formula(patch, nm, exp)));
155
+ }
156
+ /** Internal (for tests): the formula before the clamp. */
157
+ export function evaluateUnclampedColorPatchFormula(patchId, wavelengthNanometers, exp = Math.exp) {
158
+ return formula(BY_ID.get(patchId), wavelengthNanometers, exp);
159
+ }
160
+ /** Prepared spectra per exponential (Math.exp for captures, the portable one for the display view), then per patch. */
161
+ const PREPARED = new Map();
162
+ /** Most wavelengths whose values a prepared patch spectrum keeps (a capture uses a few dozen quadrature nodes). */
163
+ const PREPARED_CACHE_LIMIT = 4096;
164
+ /**
165
+ * Internal: a prepared reflectance spectrum of a built-in patch. Values are memoized per wavelength (the formula is pure),
166
+ * so a capture's per-query lookups cost one map access instead of the exponentials.
167
+ */
168
+ export function prepareColorPatchSpectrum(patch, exp = Math.exp) {
169
+ let byPatch = PREPARED.get(exp);
170
+ if (!byPatch)
171
+ PREPARED.set(exp, byPatch = new Map());
172
+ const existing = byPatch.get(patch.patchId);
173
+ if (existing)
174
+ return existing;
175
+ const range = COLOR_PATCH_WAVELENGTH_RANGE_NANOMETERS, cache = new Map();
176
+ const prepared = Object.freeze({
177
+ wavelengthRangeNanometers: range,
178
+ evaluate(nm) {
179
+ const hit = cache.get(nm);
180
+ if (hit !== undefined)
181
+ return hit;
182
+ if (typeof nm !== "number" || !(nm >= range.minimum && nm <= range.maximum)) {
183
+ throw new InvalidScientificInputError("Color patch " + patch.patchId + " is defined on " + range.minimum + "–" + range.maximum + " nm.");
184
+ }
185
+ const value = reflectanceOf(patch, nm, exp);
186
+ if (cache.size < PREPARED_CACHE_LIMIT)
187
+ cache.set(nm, value);
188
+ return value;
189
+ }
190
+ });
191
+ byPatch.set(patch.patchId, prepared);
192
+ return prepared;
193
+ }
194
+ /**
195
+ * The reflectance (dimensionless, in [0, 1]) of a built-in color patch at one wavelength, from its analytic formula.
196
+ * Labeled `approximation`: the spectra are Photivra's own smooth designs, not measurements of a physical sample.
197
+ */
198
+ export function evaluateColorPatchReflectance(input) {
199
+ requireInputKeys(input, "evaluateColorPatchReflectance", ["patchId", "wavelengthNanometers"]);
200
+ const patch = findColorPatch(input.patchId);
201
+ if (!patch)
202
+ throw new InvalidConfigurationError("patchId must be one of the built-in color patches (COLOR_PATCH_IDS).");
203
+ const nm = input.wavelengthNanometers, range = COLOR_PATCH_WAVELENGTH_RANGE_NANOMETERS;
204
+ if (typeof nm !== "number" || !Number.isFinite(nm) || nm < range.minimum || nm > range.maximum) {
205
+ throw new InvalidScientificInputError("wavelengthNanometers must be a finite wavelength from 360 to 830 nm.");
206
+ }
207
+ return approximationResult({ reflectance: reflectanceOf(patch, nm) }, "photivra-color-patch-spectrum", COLOR_PATCH_SET_VERSION, ["Photivra's own analytic design (a constant plus Gaussian and logistic terms), not a measured sample", "Non-fluorescent Lambertian reflectance"]);
208
+ }
209
+ //# sourceMappingURL=color-patches.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"color-patches.js","sourceRoot":"","sources":["../../src/facade/color-patches.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Photivra color patches (2.14): a built-in set of 30 reflectance spectra designed by Photivra for color test charts.\n *\n * Every spectrum is an engine-defined analytic function of wavelength, not measured or copied data: a constant plus a\n * sum of Gaussian bumps and logistic steps,\n *\n * R(λ) = min(1, max(0, b + Σₖ aₖ·φₖ(λ))),\n * gaussian: φ(λ) = exp(−½·((λ − μ)/σ)²)\n * logistic-rise: φ(λ) = 1 / (1 + exp(−(λ − λ₀)/s))\n * logistic-fall: φ(λ) = 1 − 1 / (1 + exp(−(λ − λ₀)/s)),\n *\n * with λ in nanometers on 360–830 nm (the CIE 1931 observer's domain). The parameters were fitted once, by\n * Photivra, to Photivra's own target colors (docs/COLOR_PATCHES.md), then rounded. Every spectrum lies in\n * [0.03, 0.901] on its whole domain, so the clamp to [0, 1] never acts; it only states the physical bound. The\n * patches are non-fluorescent (pure reflectances) and the neutral patches are spectrally flat.\n *\n * No third-party chart is copied: the set uses no ColorChecker or other commercial target's patch data, names, order,\n * layout or measured spectra. The names are Photivra's own, and every published color is checked to differ from openly\n * published third-party colors (`scripts/check-color-patch-distinctness.mjs`). The reference CIELAB and sRGB values are computed from these spectra with the CIE 1931\n * 2° observer and CIE illuminant D65 (`scripts/compute-color-patch-reference.mjs`).\n *\n * Captures evaluate the exponentials with `Math.exp`, like the engine's other continuous chart formulas. The display\n * view passes the portable `portableExp` instead (`resolvePortableChartScene`), so its chart presets' patch\n * reflectances have the same bits in every JavaScript engine (docs/DISPLAY_VIEW.md, \"Same bytes in every engine\").\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport type { PreparedColorimetricSpectrum } from \"../color/color-matching-functions.js\";\n\n/** Identity of the built-in patch set. */\nexport const COLOR_PATCH_SET_ID = \"photivra-color-patches\";\n/** Version of the patch set's spectra and reference values; a change to any spectrum changes it. */\nexport const COLOR_PATCH_SET_VERSION = \"1.0.0\";\n/** Wavelengths on which every patch spectrum is defined, nanometers, inclusive. */\nexport const COLOR_PATCH_WAVELENGTH_RANGE_NANOMETERS = /* @__PURE__ */ Object.freeze({ minimum: 360, maximum: 830 });\n\n/** One term of a patch spectrum (see the module formula). */\nexport type ColorPatchSpectralTerm =\n | { kind: \"gaussian\"; amplitude: number; centerNanometers: number; standardDeviationNanometers: number }\n | { kind: \"logistic-rise\" | \"logistic-fall\"; amplitude: number; midpointNanometers: number; scaleNanometers: number };\n\n/** Groups of the patch set, in chart order. */\nexport type ColorPatchGroup = \"neutral\" | \"strong\" | \"soft\" | \"skin\" | \"memory\" | \"gamut-edge\";\n\n/** The ids of the built-in patches, in the order of the `color-patches` chart (row-major, six per row). */\nexport const COLOR_PATCH_IDS = /* @__PURE__ */ Object.freeze([\n \"neutral-1\", \"neutral-2\", \"neutral-3\", \"neutral-4\", \"neutral-5\", \"neutral-6\",\n \"red\", \"yellow\", \"green\", \"cyan\", \"blue\", \"magenta\",\n \"red-soft\", \"yellow-soft\", \"green-soft\", \"cyan-soft\", \"blue-soft\", \"magenta-soft\",\n \"skin-pale\", \"skin-medium\", \"skin-tan\", \"skin-deep\", \"sky-blue\", \"foliage-green\",\n \"brick-red\", \"sand\", \"teal\", \"lavender\", \"deep-orange\", \"deep-violet\"\n] as const);\nexport type ColorPatchId = (typeof COLOR_PATCH_IDS)[number];\n\n/**\n * One built-in patch: its id, short code, name, reason for being on the chart, spectrum definition and reference\n * colorimetry.\n */\nexport interface ColorPatch {\n /** Stable API id, e.g. `\"skin-medium\"`. */\n patchId: ColorPatchId;\n /**\n * Stable short code for labels and printed charts: `PV-` and a group letter and number, e.g. `\"PV-N4\"`. The letters are\n * N neutral, S strong, T soft (tint), P skin (portrait), M memory and G gamut-edge, numbered in chart order.\n */\n code: string;\n /** Photivra's own descriptive name, in US English, e.g. `\"Studio Gray 4\"`; not a third-party color name. */\n name: string;\n group: ColorPatchGroup;\n /** One sentence: why the patch is on the chart. */\n reason: string;\n /** The constant b of the formula. */\n base: number;\n terms: readonly ColorPatchSpectralTerm[];\n /**\n * Reference colorimetry of the reflectance (not of a rendered capture): CIELAB under CIE illuminant D65 with the CIE\n * 1931 2° observer, the CIE tables at 1 nm over 360–830 nm, white = the perfect diffuser under D65 (Y = 100); and the\n * sRGB color of that XYZ (CSS Color 4 matrix, after XYZ scaling of the D65 table white to the sRGB white),\n * 8-bit hex. Rounded for publication: L*, a*, b* to 0.01.\n */\n reference: { cielabD65: readonly [number, number, number]; srgbHex: string; inSrgbGamut: boolean };\n}\n\n/** The built-in patch set (see {@link getColorPatchSet}). */\nexport interface ColorPatchSet {\n setId: typeof COLOR_PATCH_SET_ID;\n version: typeof COLOR_PATCH_SET_VERSION;\n wavelengthRangeNanometers: { minimum: number; maximum: number };\n /** Patches per row of the `color-patches` chart's default layout. */\n columns: number;\n patches: readonly ColorPatch[];\n}\n\nconst g = (amplitude: number, centerNanometers: number, standardDeviationNanometers: number): ColorPatchSpectralTerm =>\n ({ kind: \"gaussian\", amplitude, centerNanometers, standardDeviationNanometers });\nconst rise = (amplitude: number, midpointNanometers: number, scaleNanometers: number): ColorPatchSpectralTerm =>\n ({ kind: \"logistic-rise\", amplitude, midpointNanometers, scaleNanometers });\nconst fall = (amplitude: number, midpointNanometers: number, scaleNanometers: number): ColorPatchSpectralTerm =>\n ({ kind: \"logistic-fall\", amplitude, midpointNanometers, scaleNanometers });\ntype Definition = [ColorPatchId, string, string, ColorPatchGroup, number, ColorPatchSpectralTerm[], [number, number, number], string, string];\n/* Generated reference values: node scripts/compute-color-patch-reference.mjs --print (see docs/COLOR_PATCHES.md). */\nconst DEFINITIONS: readonly Definition[] = [\n [\"neutral-1\", \"PV-N1\", \"Studio Gray 1\", \"neutral\", 0.901, [], [96.04, 0, 0], \"#F4F4F4\",\n \"The white end of the flat gray ramp (90.1 %): the reference for white balance, exposure and highlight clipping.\"],\n [\"neutral-2\", \"PV-N2\", \"Studio Gray 2\", \"neutral\", 0.585, [], [81.02, 0, 0], \"#C9C9C9\",\n \"A light flat gray about 15 L* below the white, for the upper tone curve and highlight roll-off.\"],\n [\"neutral-3\", \"PV-N3\", \"Studio Gray 3\", \"neutral\", 0.354, [], [66.06, 0, 0], \"#A1A1A1\",\n \"A light-mid flat gray that continues the ramp in equal lightness steps, for tone-curve and gamma checks.\"],\n [\"neutral-4\", \"PV-N4\", \"Studio Gray 4\", \"neutral\", 0.197, [], [51.5, 0, 0], \"#7B7B7B\",\n \"The mid-gray of the ramp, near an 18 % card's lightness, for exposure and neutral balance in the midtones.\"],\n [\"neutral-5\", \"PV-N5\", \"Studio Gray 5\", \"neutral\", 0.093, [], [36.56, 0, 0], \"#565656\",\n \"A dark flat gray that shows shadow tone separation, noise and any color cast in the shadows.\"],\n [\"neutral-6\", \"PV-N6\", \"Studio Gray 6\", \"neutral\", 0.035, [], [21.94, 0, 0], \"#353535\",\n \"The black end of the ramp (3.5 %), for black level, shadow noise and flare.\"],\n [\"red\", \"PV-S1\", \"Darkroom Red\", \"strong\", 0.031, [rise(0.757, 611.6, 16.2)], [43.7, 54.15, 39.93], \"#BF3428\",\n \"A strong red, for saturated reds, which clip and shift in hue easily in a camera's red channel.\"],\n [\"yellow\", \"PV-S2\", \"Flashbulb Yellow\", \"strong\", 0.071, [rise(0.602, 518.7, 13.2)], [78.87, -0.23, 73.26], \"#E6C02A\",\n \"A strong yellow, bright and high in chroma, that shows whether light saturated colors keep their hue.\"],\n [\"green\", \"PV-S3\", \"Hedgerow Green\", \"strong\", 0.051, [g(0.317, 535.8, 36.9)], [57.08, -47.07, 33.2], \"#359C4C\",\n \"A strong mid green, for the green primary, where camera and display gamuts differ most.\"],\n [\"cyan\", \"PV-S4\", \"Harbor Cyan\", \"strong\", 0.096, [g(0.339, 482.5, 60.4)], [58.4, -21.56, -17.23], \"#3C98AA\",\n \"A strong cyan between the green and blue primaries, a region where overlapping camera responses mix the channels.\"],\n [\"blue\", \"PV-S5\", \"Cyanotype Blue\", \"strong\", 0.064, [g(0.336, 460.7, 24.7)], [35.27, 19.89, -46.48], \"#3C4C9E\",\n \"A strong dark blue, for the blue channel, its noise and the shift of saturated blues toward purple.\"],\n [\"magenta\", \"PV-S6\", \"Filter Magenta\", \"strong\", 0.05, [g(0.258, 440, 32.8), rise(0.549, 605, 15)], [46.11, 55, -18.91], \"#B4408E\",\n \"A strong magenta, a non-spectral hue made of both ends of the spectrum, so it tests the red and blue channels together.\"],\n [\"red-soft\", \"PV-T1\", \"Faded Print Red\", \"soft\", 0.196, [rise(0.472, 601.8, 13)], [59.63, 29.17, 14.19], \"#C77B78\",\n \"A soft red at lower chroma and higher lightness, for gentle reds that a color matrix should not oversaturate.\"],\n [\"yellow-soft\", \"PV-T2\", \"Window Glow Yellow\", \"soft\", 0.246, [rise(0.414, 509, 17.6)], [81.15, -2.88, 36.43], \"#DCC985\",\n \"A soft light yellow, for pale warm tones whose hue drifts easily toward green or orange.\"],\n [\"green-soft\", \"PV-T3\", \"Misty Hedgerow Green\", \"soft\", 0.184, [g(0.251, 535.7, 47.4)], [66.82, -24.11, 16.11], \"#7FAE85\",\n \"A soft green, for the muted greens that fill much of a natural scene.\"],\n [\"cyan-soft\", \"PV-T4\", \"Shallow Water Cyan\", \"soft\", 0.246, [g(0.339, 487.4, 61.9)], [71.51, -16.99, -10.71], \"#7DB9C2\",\n \"A soft cyan, for pale blue-greens such as water and glass, where small hue errors are easy to see.\"],\n [\"blue-soft\", \"PV-T5\", \"Distant Hill Blue\", \"soft\", 0.207, [g(0.319, 457.6, 34.7)], [56.48, 7.42, -30.88], \"#7486BD\",\n \"A soft blue, for the hazy blues of distance and open shade that white balance acts on.\"],\n [\"magenta-soft\", \"PV-T6\", \"Faded Filter Magenta\", \"soft\", 0.196, [g(0.231, 440, 33.1), rise(0.256, 575.8, 15)], [61.36, 26.09, -10.98], \"#B984A8\",\n \"A soft magenta, for pinks and mauves that lie between the red and blue channels.\"],\n [\"skin-pale\", \"PV-P1\", \"Fair Portrait Tone\", \"skin\", 0.408, [rise(0.394, 562.5, 31.7)], [81.35, 9.48, 17.48], \"#E8C3AA\",\n \"A fair skin tone, the lightest of four, for portrait color, where viewers notice small errors.\"],\n [\"skin-medium\", \"PV-P2\", \"Medium Portrait Tone\", \"skin\", 0.213, [rise(0.376, 570.3, 30.4)], [67.25, 13.65, 22.01], \"#C99A7D\",\n \"A medium skin tone, the second of four that span a range of skin lightness at similar hue.\"],\n [\"skin-tan\", \"PV-P3\", \"Tan Portrait Tone\", \"skin\", 0.084, [rise(0.295, 573.7, 31.3)], [52.01, 15.71, 26.72], \"#A3714F\",\n \"A tan skin tone, the third of four, for warm midtone skin under different lights.\"],\n [\"skin-deep\", \"PV-P4\", \"Deep Portrait Tone\", \"skin\", 0.034, [rise(0.133, 590.3, 32.4)], [33, 13.57, 18.02], \"#6A4532\",\n \"A deep skin tone, the darkest of four, for skin color and detail in the darker tones of a portrait.\"],\n [\"sky-blue\", \"PV-M1\", \"Midday Sky\", \"memory\", 0.18, [fall(0.455, 507.2, 29.7)], [60.05, 0.53, -33.31], \"#6893CB\",\n \"A clear-sky blue, a memory color that viewers judge against what they remember of the sky.\"],\n [\"foliage-green\", \"PV-M2\", \"Canopy Green\", \"memory\", 0.034, [g(0.139, 555, 30.7), rise(0.411, 716.8, 12)], [40.81, -22.4, 30.31], \"#4B692C\",\n \"A leaf green with the steep rise of leaf reflectance at the red edge, a memory color that also exposes infrared leakage.\"],\n [\"brick-red\", \"PV-M3\", \"Warehouse Brick\", \"memory\", 0.05, [rise(0.308, 592.7, 16.2)], [41.63, 32.81, 25.53], \"#9C4A3A\",\n \"A dull brick red, a common building color between the saturated reds and the skin tones.\"],\n [\"sand\", \"PV-M4\", \"Low Tide Sand\", \"memory\", 0.272, [rise(0.371, 504.9, 37.6)], [79.14, 0.48, 22.19], \"#D5C29B\",\n \"A pale sand, a light warm near-neutral that shows small casts a gray patch would hide.\"],\n [\"teal\", \"PV-M5\", \"Estuary Teal\", \"memory\", 0.085, [g(0.197, 498.2, 42)], [48.43, -24.88, -5.64], \"#2E7F7C\",\n \"A dark blue-green between the green and cyan hues, for water and shaded foliage in that region.\"],\n [\"lavender\", \"PV-M6\", \"Wildflower Lilac\", \"memory\", 0.331, [g(0.298, 445, 30.4), rise(0.137, 623.4, 20)], [66.69, 14.8, -22.7], \"#A99BCB\",\n \"A pale blue-violet, a flower color that cameras often render too blue.\"],\n [\"deep-orange\", \"PV-G1\", \"Sunset Flare Orange\", \"gamut-edge\", 0.03, [rise(0.74, 591.3, 16.7)], [53.19, 52.38, 56.53], \"#DC511A\",\n \"A deep orange close to the sRGB gamut boundary, for gamut mapping and clipping of saturated warm colors.\"],\n [\"deep-violet\", \"PV-G2\", \"Blacklight Violet\", \"gamut-edge\", 0.031, [g(0.37, 441.6, 25.6), rise(0.097, 640, 20)], [26.42, 48.9, -59.69], \"#4C1E9C\",\n \"A deep violet close to the sRGB gamut boundary, for gamut mapping and the camera's response at short wavelengths.\"]\n];\n\nfunction deepFreezePatch(patch: ColorPatch): ColorPatch {\n for (const term of patch.terms) Object.freeze(term);\n Object.freeze(patch.terms);\n Object.freeze(patch.reference.cielabD65);\n Object.freeze(patch.reference);\n return Object.freeze(patch);\n}\nconst PATCHES: readonly ColorPatch[] = Object.freeze(DEFINITIONS.map(([patchId, code, name, group, base, terms, lab, hex, reason]) => deepFreezePatch({\n patchId, code, name, group, reason, base, terms, reference: { cielabD65: lab, srgbHex: hex, inSrgbGamut: true }\n})));\nconst BY_ID = new Map<string, ColorPatch>(PATCHES.map((p) => [p.patchId, p]));\nconst SET: ColorPatchSet = /* @__PURE__ */ Object.freeze({ setId: COLOR_PATCH_SET_ID, version: COLOR_PATCH_SET_VERSION,\n wavelengthRangeNanometers: COLOR_PATCH_WAVELENGTH_RANGE_NANOMETERS, columns: 6, patches: PATCHES });\n\n/**\n * The built-in Photivra color-patch set: every patch's id, code, name, group, reason, spectrum parameters and reference CIELAB (D65,\n * CIE 1931 2°) and sRGB values. A static, deeply frozen engine contract. Pass a patch to a chart as\n * `{ kind: \"color-patch\", patchId }` wherever a chart reflectance is accepted, or draw the whole set with the\n * `color-patches` chart preset.\n */\nexport function getColorPatchSet(): ColorPatchSet {\n return SET;\n}\n\n/** Internal: the patch with this id, or undefined. */\nexport function findColorPatch(patchId: unknown): ColorPatch | undefined {\n return typeof patchId === \"string\" ? BY_ID.get(patchId) : undefined;\n}\n\n/** An exponential: `Math.exp`, or the display view's portable one. */\ntype Exponential = (x: number) => number;\n/** The unclamped formula value b + Σ aₖφₖ(λ) (the clamp never acts on the built-in set; a test checks it). */\nfunction formula(patch: ColorPatch, nm: number, exp: Exponential): number {\n let value = patch.base;\n for (const term of patch.terms) {\n if (term.kind === \"gaussian\") {\n const z = (nm - term.centerNanometers) / term.standardDeviationNanometers;\n value += term.amplitude * exp(-0.5 * z * z);\n } else {\n const step = 1 / (1 + exp(-(nm - term.midpointNanometers) / term.scaleNanometers));\n value += term.amplitude * (term.kind === \"logistic-rise\" ? step : 1 - step);\n }\n }\n return value;\n}\nfunction reflectanceOf(patch: ColorPatch, nm: number, exp: Exponential = Math.exp): number {\n return Math.min(1, Math.max(0, formula(patch, nm, exp)));\n}\n/** Internal (for tests): the formula before the clamp. */\nexport function evaluateUnclampedColorPatchFormula(patchId: ColorPatchId, wavelengthNanometers: number, exp: Exponential = Math.exp): number {\n return formula(BY_ID.get(patchId)!, wavelengthNanometers, exp);\n}\n\n/** Prepared spectra per exponential (Math.exp for captures, the portable one for the display view), then per patch. */\nconst PREPARED = new Map<Exponential, Map<string, PreparedColorimetricSpectrum>>();\n/** Most wavelengths whose values a prepared patch spectrum keeps (a capture uses a few dozen quadrature nodes). */\nconst PREPARED_CACHE_LIMIT = 4096;\n/**\n * Internal: a prepared reflectance spectrum of a built-in patch. Values are memoized per wavelength (the formula is pure),\n * so a capture's per-query lookups cost one map access instead of the exponentials.\n */\nexport function prepareColorPatchSpectrum(patch: ColorPatch, exp: Exponential = Math.exp): PreparedColorimetricSpectrum {\n let byPatch = PREPARED.get(exp);\n if (!byPatch) PREPARED.set(exp, byPatch = new Map());\n const existing = byPatch.get(patch.patchId);\n if (existing) return existing;\n const range = COLOR_PATCH_WAVELENGTH_RANGE_NANOMETERS, cache = new Map<number, number>();\n const prepared: PreparedColorimetricSpectrum = Object.freeze({\n wavelengthRangeNanometers: range,\n evaluate(nm: number): number {\n const hit = cache.get(nm);\n if (hit !== undefined) return hit;\n if (typeof nm !== \"number\" || !(nm >= range.minimum && nm <= range.maximum)) {\n throw new InvalidScientificInputError(\"Color patch \" + patch.patchId + \" is defined on \" + range.minimum + \"–\" + range.maximum + \" nm.\");\n }\n const value = reflectanceOf(patch, nm, exp);\n if (cache.size < PREPARED_CACHE_LIMIT) cache.set(nm, value);\n return value;\n }\n });\n byPatch.set(patch.patchId, prepared);\n return prepared;\n}\n\n/** Input of {@link evaluateColorPatchReflectance}. */\nexport interface EvaluateColorPatchReflectanceInput {\n patchId: ColorPatchId;\n /** Vacuum wavelength, nanometers, in 360–830. */\n wavelengthNanometers: number;\n}\n\n/**\n * The reflectance (dimensionless, in [0, 1]) of a built-in color patch at one wavelength, from its analytic formula.\n * Labeled `approximation`: the spectra are Photivra's own smooth designs, not measurements of a physical sample.\n */\nexport function evaluateColorPatchReflectance(input: EvaluateColorPatchReflectanceInput): CalculationResult<{ reflectance: number }> {\n requireInputKeys(input, \"evaluateColorPatchReflectance\", [\"patchId\", \"wavelengthNanometers\"]);\n const patch = findColorPatch(input.patchId);\n if (!patch) throw new InvalidConfigurationError(\"patchId must be one of the built-in color patches (COLOR_PATCH_IDS).\");\n const nm = input.wavelengthNanometers, range = COLOR_PATCH_WAVELENGTH_RANGE_NANOMETERS;\n if (typeof nm !== \"number\" || !Number.isFinite(nm) || nm < range.minimum || nm > range.maximum) {\n throw new InvalidScientificInputError(\"wavelengthNanometers must be a finite wavelength from 360 to 830 nm.\");\n }\n return approximationResult({ reflectance: reflectanceOf(patch, nm) }, \"photivra-color-patch-spectrum\", COLOR_PATCH_SET_VERSION,\n [\"Photivra's own analytic design (a constant plus Gaussian and logistic terms), not a measured sample\", \"Non-fluorescent Lambertian reflectance\"]);\n}\n"]}
@@ -16,7 +16,9 @@
16
16
  * So E(q, λ) = T·B(q, λ) with B = K_λ ⊛ L sampled on that lattice: the same sum, in a different floating-point
17
17
  * order. B is computed once per spectral node with the engine's isoplanatic convolution
18
18
  * (`calculateIrradianceConvolution`, kernels from `resolveLensPsfConvolutionKernels`, exact site sampling) over a
19
- * grid that holds every source (the native raster plus the kernel radius on each side), in row bands. The
19
+ * grid that holds every source (the native raster plus the kernel radius on each side), in row bands; each band gathers only
20
+ * its own values, the grid's interior (#419, `createIrradianceConvolutionInteriorRunner`: each value bit-identical to
21
+ * `calculateIrradianceConvolution`'s there, without the 2R rows and columns of the band's grid that no query reads). The
20
22
  * provider then answers each site-centre query with B instead of L, and the environment request declares no
21
23
  * PSF. Incoherent imaging is a linear superposition of intensity PSFs, so convolving radiance on the object
22
24
  * plane and convolving irradiance on the image plane agree for a planar object and a space-invariant PSF
@@ -33,7 +35,7 @@
33
35
  * plus the largest offset on each side) are convolved, over a grid that adds the kernel radius on each side. Every
34
36
  * output value is the same gather as in the whole render: the grid always holds every tap of every value it keeps
35
37
  * (real scene radiance out to the kernel radius, never a truncated edge), the single kernel patch does not depend on
36
- * the grid's origin, and `convolveIrradianceRows` sums each value in a fixed tap order (v outer, u inner). Narrowing
38
+ * the grid's origin, and the gather (`convolveIrradianceRows`, and its interior form) sums each value in a fixed tap order (v outer, u inner). Narrowing
37
39
  * the grid therefore removes only values that no query reads, and the kept values are bit-identical.
38
40
  *
39
41
  * A query that does not land on one of the lattices (within 1e-6 site) is refused rather than approximated: the
@@ -43,12 +45,12 @@
43
45
  * passes `siteKernels` instead (the geometric defocus disc, or each node's diffraction kernel convolved with it). Their radius
44
46
  * may exceed the public 32-site convolution limit, so this path does not call `calculateIrradianceConvolution`: it checks the
45
47
  * samples and the kernel sums as that function does and gathers only the band's own values (`gatherInterior`), each summed
46
- * over the taps in the same fixed order (v outer, u inner, ascending; zero taps skipped, which adds exact zeros only). It
47
- * does not compute the 2R grid rows above and below a band that `calculateIrradianceConvolution` would also return (#419).
48
+ * over the taps in the same fixed order (v outer, u inner, ascending; zero taps skipped, which adds exact zeros only). Like
49
+ * the diffraction path (#419), it does not compute the 2R grid rows and columns around a band.
48
50
  */
49
51
  import { InvalidConfigurationError } from "../core/configuration-error.js";
50
52
  import { isInternalSceneRadianceEvaluator, markInternalSceneRadianceEvaluator, registerInternalRadianceAccess } from "../core/internal-radiance-evaluator.js";
51
- import { createIrradianceConvolutionRunner } from "../optics/irradiance-convolution.js";
53
+ import { createIrradianceConvolutionInteriorRunner } from "../optics/irradiance-convolution.js";
52
54
  import { siteDiffractionKernel } from "../optics/site-diffraction-kernel.js";
53
55
  /** Rows of native sites per convolution band, and the bands kept. Banding changes memory only, never values. */
54
56
  const BAND_ROWS = 32;
@@ -99,9 +101,10 @@ export function createDiffractedPlanarProvider(input) {
99
101
  }
100
102
  const started = input.onConvolution ? now() : 0;
101
103
  const y0 = ly0 + b * BAND_ROWS, rows = Math.min(BAND_ROWS, ly1 - y0), gridHeight = rows + 2 * R, size = gridWidth * gridHeight;
102
- // #453: the band's shifts × spectral nodes convolutions share one geometry, validated once (the first runs the full
103
- // calculateIrradianceConvolution; every later one still checks its samples and kernel).
104
- const convolve = siteKernels ? undefined : createIrradianceConvolutionRunner({ schemaVersion: "0.1.0", width: gridWidth, height: gridHeight, kernelRadius: R,
104
+ // #453: the band's shifts × spectral nodes convolutions share one geometry, validated once (the first with every check of
105
+ // calculateIrradianceConvolution; every later one still checks its samples and kernel). #419: only the band's own rows × NW
106
+ // values are gathered (the grid's interior), each bit-identical to calculateIrradianceConvolution's value there.
107
+ const convolve = siteKernels ? undefined : createIrradianceConvolutionInteriorRunner({ schemaVersion: "0.1.0", width: gridWidth, height: gridHeight, kernelRadius: R,
105
108
  patchWidth: gridWidth, patchHeight: gridHeight, boundary: "zero-outside-grid" });
106
109
  const values = shifts.map((shift) => {
107
110
  // The plane point of lattice grid sample (gx, gy) of this shift, the same expressions for both fills below.
@@ -142,11 +145,7 @@ export function createDiffractedPlanarProvider(input) {
142
145
  }
143
146
  if (!convolve)
144
147
  return gatherInterior(samples, taps[w], gridWidth, NW, rows, R);
145
- const blurred = convolve(samples, kernels[w]);
146
- const out = new Float64Array(NW * rows);
147
- for (let y = 0; y < rows; y++)
148
- out.set(blurred.subarray((y + R) * gridWidth + R, (y + R) * gridWidth + R + NW), y * NW);
149
- return out;
148
+ return convolve(samples, kernels[w]);
150
149
  });
151
150
  });
152
151
  bands.set(b, values);