@photivra/engine 0.1.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 (97) hide show
  1. package/CITATION.cff +23 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +9 -0
  4. package/README.md +235 -0
  5. package/dist/camera/field-of-view.d.ts +37 -0
  6. package/dist/camera/field-of-view.d.ts.map +1 -0
  7. package/dist/camera/field-of-view.js +47 -0
  8. package/dist/camera/field-of-view.js.map +1 -0
  9. package/dist/camera/projected-object-size.d.ts +34 -0
  10. package/dist/camera/projected-object-size.d.ts.map +1 -0
  11. package/dist/camera/projected-object-size.js +57 -0
  12. package/dist/camera/projected-object-size.js.map +1 -0
  13. package/dist/core/calculation-result.d.ts +125 -0
  14. package/dist/core/calculation-result.d.ts.map +1 -0
  15. package/dist/core/calculation-result.js +175 -0
  16. package/dist/core/calculation-result.js.map +1 -0
  17. package/dist/core/validation.d.ts +30 -0
  18. package/dist/core/validation.d.ts.map +1 -0
  19. package/dist/core/validation.js +45 -0
  20. package/dist/core/validation.js.map +1 -0
  21. package/dist/exposure/exposure.d.ts +50 -0
  22. package/dist/exposure/exposure.d.ts.map +1 -0
  23. package/dist/exposure/exposure.js +62 -0
  24. package/dist/exposure/exposure.js.map +1 -0
  25. package/dist/index.d.ts +28 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +27 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/motion/projected-motion.d.ts +41 -0
  30. package/dist/motion/projected-motion.d.ts.map +1 -0
  31. package/dist/motion/projected-motion.js +84 -0
  32. package/dist/motion/projected-motion.js.map +1 -0
  33. package/dist/optics/aperture.d.ts +39 -0
  34. package/dist/optics/aperture.d.ts.map +1 -0
  35. package/dist/optics/aperture.js +62 -0
  36. package/dist/optics/aperture.js.map +1 -0
  37. package/dist/optics/circle-of-confusion.d.ts +23 -0
  38. package/dist/optics/circle-of-confusion.d.ts.map +1 -0
  39. package/dist/optics/circle-of-confusion.js +32 -0
  40. package/dist/optics/circle-of-confusion.js.map +1 -0
  41. package/dist/optics/depth-of-field.d.ts +53 -0
  42. package/dist/optics/depth-of-field.d.ts.map +1 -0
  43. package/dist/optics/depth-of-field.js +81 -0
  44. package/dist/optics/depth-of-field.js.map +1 -0
  45. package/dist/optics/diffraction.d.ts +26 -0
  46. package/dist/optics/diffraction.d.ts.map +1 -0
  47. package/dist/optics/diffraction.js +21 -0
  48. package/dist/optics/diffraction.js.map +1 -0
  49. package/dist/optics/thin-lens.d.ts +30 -0
  50. package/dist/optics/thin-lens.d.ts.map +1 -0
  51. package/dist/optics/thin-lens.js +33 -0
  52. package/dist/optics/thin-lens.js.map +1 -0
  53. package/dist/output/crop.d.ts +27 -0
  54. package/dist/output/crop.d.ts.map +1 -0
  55. package/dist/output/crop.js +26 -0
  56. package/dist/output/crop.js.map +1 -0
  57. package/dist/output/subject-framing-crop.d.ts +36 -0
  58. package/dist/output/subject-framing-crop.d.ts.map +1 -0
  59. package/dist/output/subject-framing-crop.js +42 -0
  60. package/dist/output/subject-framing-crop.js.map +1 -0
  61. package/dist/schema/camera.d.ts +34 -0
  62. package/dist/schema/camera.d.ts.map +1 -0
  63. package/dist/schema/camera.js +3 -0
  64. package/dist/schema/camera.js.map +1 -0
  65. package/dist/schema/scene.d.ts +69 -0
  66. package/dist/schema/scene.d.ts.map +1 -0
  67. package/dist/schema/scene.js +3 -0
  68. package/dist/schema/scene.js.map +1 -0
  69. package/dist/schema/validation.d.ts +25 -0
  70. package/dist/schema/validation.d.ts.map +1 -0
  71. package/dist/schema/validation.js +180 -0
  72. package/dist/schema/validation.js.map +1 -0
  73. package/dist/sensor/pixel-pitch.d.ts +21 -0
  74. package/dist/sensor/pixel-pitch.d.ts.map +1 -0
  75. package/dist/sensor/pixel-pitch.js +19 -0
  76. package/dist/sensor/pixel-pitch.js.map +1 -0
  77. package/dist/sensor/signal-noise.d.ts +37 -0
  78. package/dist/sensor/signal-noise.d.ts.map +1 -0
  79. package/dist/sensor/signal-noise.js +53 -0
  80. package/dist/sensor/signal-noise.js.map +1 -0
  81. package/dist/simulation/poc-simulation.d.ts +264 -0
  82. package/dist/simulation/poc-simulation.d.ts.map +1 -0
  83. package/dist/simulation/poc-simulation.js +449 -0
  84. package/dist/simulation/poc-simulation.js.map +1 -0
  85. package/dist/stabilization/camera-shake.d.ts +41 -0
  86. package/dist/stabilization/camera-shake.d.ts.map +1 -0
  87. package/dist/stabilization/camera-shake.js +79 -0
  88. package/dist/stabilization/camera-shake.js.map +1 -0
  89. package/docs/API_STYLE.md +65 -0
  90. package/docs/ARCHITECTURE.md +51 -0
  91. package/docs/MOTION_AND_SIGNAL.md +53 -0
  92. package/docs/PHYSICS_FOUNDATION.md +92 -0
  93. package/docs/POC_API.md +210 -0
  94. package/docs/PROVENANCE.md +87 -0
  95. package/docs/STABILIZATION.md +78 -0
  96. package/docs/USAGE.md +538 -0
  97. package/package.json +91 -0
@@ -0,0 +1,84 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { calculatedResult } from "../core/calculation-result.js";
3
+ import { InvalidScientificInputError, requirePositiveFinite } from "../core/validation.js";
4
+ import { calculateThinLensImageDistance } from "../optics/thin-lens.js";
5
+ function requireFiniteVector3(name, value) {
6
+ for (const component of ["x", "y", "z"]) {
7
+ if (!Number.isFinite(value[component])) {
8
+ throw new InvalidScientificInputError(`${name}.${component} must be finite.`);
9
+ }
10
+ }
11
+ }
12
+ /**
13
+ * Calculates sensor-plane motion during an exposure using ideal rectilinear
14
+ * projection and constant linear object velocity.
15
+ *
16
+ * Without focusDistanceM, nominal focal length is the backwards-compatible
17
+ * pinhole projection distance. With focusDistanceM, the selected thin-lens
18
+ * sensor-plane image distance is used.
19
+ *
20
+ * The calculation projects the object's shutter-open and shutter-close
21
+ * positions onto the image plane and measures the displacement between them.
22
+ * It therefore handles lateral and depth-direction motion without relying on a
23
+ * generic "subject speed" category.
24
+ *
25
+ * @param input Camera, exposure, object-position, and velocity inputs.
26
+ * @returns Projected sensor-plane displacement.
27
+ */
28
+ export function calculateProjectedMotionBlur(input) {
29
+ requirePositiveFinite("focalLengthMm", input.focalLengthMm);
30
+ requirePositiveFinite("shutterSeconds", input.shutterSeconds);
31
+ requireFiniteVector3("positionM", input.positionM);
32
+ requireFiniteVector3("velocityMps", input.velocityMps);
33
+ if (input.positionM.z <= 0) {
34
+ throw new InvalidScientificInputError("positionM.z must place the object in front of the camera.");
35
+ }
36
+ const endPositionM = {
37
+ x: input.positionM.x + input.velocityMps.x * input.shutterSeconds,
38
+ y: input.positionM.y + input.velocityMps.y * input.shutterSeconds,
39
+ z: input.positionM.z + input.velocityMps.z * input.shutterSeconds
40
+ };
41
+ if (endPositionM.z <= 0) {
42
+ throw new InvalidScientificInputError("Object motion crosses or reaches the camera plane during the exposure.");
43
+ }
44
+ const focusProjection = input.focusDistanceM === undefined
45
+ ? undefined
46
+ : calculateThinLensImageDistance({
47
+ focalLengthMm: input.focalLengthMm,
48
+ objectDistanceM: input.focusDistanceM
49
+ });
50
+ const projectionDistanceMm = focusProjection?.value.imageDistanceMm ?? input.focalLengthMm;
51
+ const startXmm = projectionDistanceMm * (input.positionM.x / input.positionM.z);
52
+ const startYmm = projectionDistanceMm * (input.positionM.y / input.positionM.z);
53
+ const endXmm = projectionDistanceMm * (endPositionM.x / endPositionM.z);
54
+ const endYmm = projectionDistanceMm * (endPositionM.y / endPositionM.z);
55
+ const deltaXMm = endXmm - startXmm;
56
+ const deltaYMm = endYmm - startYmm;
57
+ const distanceMm = Math.hypot(deltaXMm, deltaYMm);
58
+ let distancePixels;
59
+ if (input.pixelPitchMicrometers !== undefined) {
60
+ requirePositiveFinite("pixelPitchMicrometers", input.pixelPitchMicrometers);
61
+ distancePixels = distanceMm / (input.pixelPitchMicrometers / 1000);
62
+ }
63
+ return calculatedResult({
64
+ deltaXMm,
65
+ deltaYMm,
66
+ distanceMm,
67
+ projectionDistanceMm,
68
+ ...(distancePixels === undefined ? {} : { distancePixels })
69
+ }, focusProjection === undefined
70
+ ? "constant-velocity-pinhole-motion"
71
+ : "focus-aware-constant-velocity-thin-lens-motion", "1.0.0", focusProjection === undefined
72
+ ? [
73
+ "Ideal rectilinear/pinhole projection",
74
+ "Constant object velocity during the exposure",
75
+ "Camera motion is not included in this calculation"
76
+ ]
77
+ : [
78
+ "Ideal rectilinear paraxial thin-lens projection to the selected sensor plane",
79
+ "Constant object velocity during the exposure",
80
+ "Camera motion is not included in this calculation",
81
+ "Real-lens focus breathing is not modeled"
82
+ ]);
83
+ }
84
+ //# sourceMappingURL=projected-motion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projected-motion.js","sourceRoot":"","sources":["../../src/motion/projected-motion.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import { type CalculationResult } from "../core/calculation-result.js";
2
+ export interface CalculateIdealApertureInput {
3
+ /** Number of straight diaphragm blades/sides. Must be from 3 through 1024. */
4
+ bladeCount: number;
5
+ /**
6
+ * Rotation of the first blade edge in degrees, measured counter-clockwise
7
+ * in image coordinates before the diffraction-normal offset is applied.
8
+ */
9
+ firstBladeEdgeAngleDegrees?: number;
10
+ }
11
+ export interface ApertureVertex {
12
+ x: number;
13
+ y: number;
14
+ }
15
+ export interface IdealApertureGeometry {
16
+ bladeCount: number;
17
+ /** Number of diffraction ray directions for the ideal straight-edged polygon. */
18
+ sunstarRayCount: number;
19
+ /**
20
+ * Diffraction-ray directions in degrees, normalized to [0, 360).
21
+ *
22
+ * Each ray is perpendicular to a blade edge. Parallel opposite edges overlap
23
+ * for even blade counts; odd blade counts therefore produce twice as many
24
+ * visible ray directions.
25
+ */
26
+ sunstarRayAnglesDegrees: readonly number[];
27
+ /** Unit-circumradius regular-polygon vertices. */
28
+ normalizedVertices: readonly ApertureVertex[];
29
+ }
30
+ /**
31
+ * Calculates ideal regular-polygon aperture geometry and the corresponding
32
+ * straight-edge diffraction-ray directions.
33
+ *
34
+ * This models only geometry/symmetry. It does not calculate diffraction
35
+ * intensity, wavelength-dependent star length, blade curvature, lens
36
+ * aberrations, coatings, or sensor blooming.
37
+ */
38
+ export declare function calculateIdealApertureGeometry(input: CalculateIdealApertureInput): CalculationResult<IdealApertureGeometry>;
39
+ //# sourceMappingURL=aperture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aperture.d.ts","sourceRoot":"","sources":["../../src/optics/aperture.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAQvC,MAAM,WAAW,2BAA2B;IAC1C,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,uBAAuB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,kDAAkD;IAClD,kBAAkB,EAAE,SAAS,cAAc,EAAE,CAAC;CAC/C;AAOD;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,2BAA2B,GACjC,iBAAiB,CAAC,qBAAqB,CAAC,CAoE1C"}
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { calculatedResult } from "../core/calculation-result.js";
3
+ import { InvalidScientificInputError, requirePositiveInteger } from "../core/validation.js";
4
+ const MAX_IDEAL_APERTURE_BLADES = 1024;
5
+ function normalizeDegrees(value) {
6
+ const normalized = value % 360;
7
+ return normalized < 0 ? normalized + 360 : normalized;
8
+ }
9
+ /**
10
+ * Calculates ideal regular-polygon aperture geometry and the corresponding
11
+ * straight-edge diffraction-ray directions.
12
+ *
13
+ * This models only geometry/symmetry. It does not calculate diffraction
14
+ * intensity, wavelength-dependent star length, blade curvature, lens
15
+ * aberrations, coatings, or sensor blooming.
16
+ */
17
+ export function calculateIdealApertureGeometry(input) {
18
+ requirePositiveInteger("bladeCount", input.bladeCount);
19
+ if (input.bladeCount < 3) {
20
+ throw new InvalidScientificInputError("bladeCount must be greater than or equal to 3.");
21
+ }
22
+ if (input.bladeCount > MAX_IDEAL_APERTURE_BLADES) {
23
+ throw new InvalidScientificInputError(`bladeCount must be less than or equal to ${MAX_IDEAL_APERTURE_BLADES}.`);
24
+ }
25
+ const rawFirstBladeEdgeAngleDegrees = input.firstBladeEdgeAngleDegrees ?? 0;
26
+ if (!Number.isFinite(rawFirstBladeEdgeAngleDegrees)) {
27
+ throw new InvalidScientificInputError("firstBladeEdgeAngleDegrees must be finite.");
28
+ }
29
+ const firstBladeEdgeAngleDegrees = normalizeDegrees(rawFirstBladeEdgeAngleDegrees);
30
+ const edgeStepDegrees = 360 / input.bladeCount;
31
+ const rayAngles = [];
32
+ for (let index = 0; index < input.bladeCount; index += 1) {
33
+ const bladeEdgeAngle = firstBladeEdgeAngleDegrees + index * edgeStepDegrees;
34
+ const rayAngle = normalizeDegrees(bladeEdgeAngle + 90);
35
+ rayAngles.push(rayAngle);
36
+ if (input.bladeCount % 2 !== 0) {
37
+ rayAngles.push(normalizeDegrees(rayAngle + 180));
38
+ }
39
+ }
40
+ rayAngles.sort((left, right) => left - right);
41
+ const vertexRotationRadians = ((firstBladeEdgeAngleDegrees - 90 + edgeStepDegrees / 2) * Math.PI) / 180;
42
+ const vertices = Array.from({ length: input.bladeCount }, (_, index) => {
43
+ const angle = vertexRotationRadians + (index * 2 * Math.PI) / input.bladeCount;
44
+ return {
45
+ x: Math.cos(angle),
46
+ y: Math.sin(angle)
47
+ };
48
+ });
49
+ return calculatedResult({
50
+ bladeCount: input.bladeCount,
51
+ sunstarRayCount: rayAngles.length,
52
+ sunstarRayAnglesDegrees: rayAngles,
53
+ normalizedVertices: vertices
54
+ }, "ideal-regular-polygon-aperture", "1.0.0", [
55
+ "Aperture opening is a regular polygon with straight edges",
56
+ "Each diffraction ray is perpendicular to a blade edge",
57
+ "Opposing parallel-edge diffraction overlaps for even blade counts",
58
+ "Ray intensity/length and blade curvature are not modeled",
59
+ "Blade count is computationally bounded to 1024"
60
+ ]);
61
+ }
62
+ //# sourceMappingURL=aperture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aperture.js","sourceRoot":"","sources":["../../src/optics/aperture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { type CalculationResult } from "../core/calculation-result.js";
2
+ export interface EstimateEquivalentViewingCircleOfConfusionInput {
3
+ sensorWidthMm: number;
4
+ sensorHeightMm: number;
5
+ referenceSensorWidthMm: number;
6
+ referenceSensorHeightMm: number;
7
+ referenceCircleOfConfusionMm: number;
8
+ }
9
+ export interface EquivalentViewingCircleOfConfusion {
10
+ circleOfConfusionMm: number;
11
+ sensorDiagonalMm: number;
12
+ referenceSensorDiagonalMm: number;
13
+ scaleFactor: number;
14
+ }
15
+ /**
16
+ * Estimates a circle-of-confusion criterion for equivalent final viewing by
17
+ * scaling a caller-supplied reference criterion in proportion to sensor
18
+ * diagonal.
19
+ *
20
+ * This is a viewing/acceptability convention, not a physical blur threshold.
21
+ */
22
+ export declare function estimateEquivalentViewingCircleOfConfusion(input: EstimateEquivalentViewingCircleOfConfusionInput): CalculationResult<EquivalentViewingCircleOfConfusion>;
23
+ //# sourceMappingURL=circle-of-confusion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circle-of-confusion.d.ts","sourceRoot":"","sources":["../../src/optics/circle-of-confusion.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAGvC,MAAM,WAAW,+CAA+C;IAC9D,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,uBAAuB,EAAE,MAAM,CAAC;IAChC,4BAA4B,EAAE,MAAM,CAAC;CACtC;AAED,MAAM,WAAW,kCAAkC;IACjD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,yBAAyB,EAAE,MAAM,CAAC;IAClC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,0CAA0C,CACxD,KAAK,EAAE,+CAA+C,GACrD,iBAAiB,CAAC,kCAAkC,CAAC,CA2CvD"}
@@ -0,0 +1,32 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { approximationResult } from "../core/calculation-result.js";
3
+ import { requirePositiveFinite } from "../core/validation.js";
4
+ /**
5
+ * Estimates a circle-of-confusion criterion for equivalent final viewing by
6
+ * scaling a caller-supplied reference criterion in proportion to sensor
7
+ * diagonal.
8
+ *
9
+ * This is a viewing/acceptability convention, not a physical blur threshold.
10
+ */
11
+ export function estimateEquivalentViewingCircleOfConfusion(input) {
12
+ requirePositiveFinite("sensorWidthMm", input.sensorWidthMm);
13
+ requirePositiveFinite("sensorHeightMm", input.sensorHeightMm);
14
+ requirePositiveFinite("referenceSensorWidthMm", input.referenceSensorWidthMm);
15
+ requirePositiveFinite("referenceSensorHeightMm", input.referenceSensorHeightMm);
16
+ requirePositiveFinite("referenceCircleOfConfusionMm", input.referenceCircleOfConfusionMm);
17
+ const sensorDiagonalMm = Math.hypot(input.sensorWidthMm, input.sensorHeightMm);
18
+ const referenceSensorDiagonalMm = Math.hypot(input.referenceSensorWidthMm, input.referenceSensorHeightMm);
19
+ const scaleFactor = sensorDiagonalMm / referenceSensorDiagonalMm;
20
+ return approximationResult({
21
+ circleOfConfusionMm: input.referenceCircleOfConfusionMm * scaleFactor,
22
+ sensorDiagonalMm,
23
+ referenceSensorDiagonalMm,
24
+ scaleFactor
25
+ }, "equivalent-viewing-circle-of-confusion", "1.0.0", [
26
+ "Reference and target images are compared at equivalent final display/print size",
27
+ "Viewing distance and visual-acuity criterion are treated as equivalent",
28
+ "Acceptable circle of confusion scales linearly with sensor diagonal",
29
+ "This is a viewing convention, not a physical lens/sensor blur threshold"
30
+ ]);
31
+ }
32
+ //# sourceMappingURL=circle-of-confusion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circle-of-confusion.js","sourceRoot":"","sources":["../../src/optics/circle-of-confusion.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC,OAAO,EACL,mBAAmB,EAEpB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAiB9D;;;;;;GAMG;AACH,MAAM,UAAU,0CAA0C,CACxD,KAAsD;IAEtD,qBAAqB,CAAC,eAAe,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;IAC5D,qBAAqB,CAAC,gBAAgB,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC;IAC9D,qBAAqB,CACnB,wBAAwB,EACxB,KAAK,CAAC,sBAAsB,CAC7B,CAAC;IACF,qBAAqB,CACnB,yBAAyB,EACzB,KAAK,CAAC,uBAAuB,CAC9B,CAAC;IACF,qBAAqB,CACnB,8BAA8B,EAC9B,KAAK,CAAC,4BAA4B,CACnC,CAAC;IAEF,MAAM,gBAAgB,GAAG,IAAI,CAAC,KAAK,CACjC,KAAK,CAAC,aAAa,EACnB,KAAK,CAAC,cAAc,CACrB,CAAC;IACF,MAAM,yBAAyB,GAAG,IAAI,CAAC,KAAK,CAC1C,KAAK,CAAC,sBAAsB,EAC5B,KAAK,CAAC,uBAAuB,CAC9B,CAAC;IACF,MAAM,WAAW,GAAG,gBAAgB,GAAG,yBAAyB,CAAC;IAEjE,OAAO,mBAAmB,CACxB;QACE,mBAAmB,EACjB,KAAK,CAAC,4BAA4B,GAAG,WAAW;QAClD,gBAAgB;QAChB,yBAAyB;QACzB,WAAW;KACZ,EACD,wCAAwC,EACxC,OAAO,EACP;QACE,iFAAiF;QACjF,wEAAwE;QACxE,qEAAqE;QACrE,yEAAyE;KAC1E,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { type CalculationResult } from "../core/calculation-result.js";
2
+ export interface CalculateDepthOfFieldInput {
3
+ /** Lens focal length in millimetres. */
4
+ focalLengthMm: number;
5
+ /** F-number. */
6
+ aperture: number;
7
+ /** Focus distance from the lens plane in metres. */
8
+ focusDistanceM: number;
9
+ /** Acceptable circle of confusion diameter in millimetres. */
10
+ circleOfConfusionMm: number;
11
+ }
12
+ export interface DepthOfField {
13
+ hyperfocalDistanceM: number;
14
+ nearLimitM: number;
15
+ /** Null represents an infinite far limit. */
16
+ farLimitM: number | null;
17
+ /** Null represents infinite total depth of field. */
18
+ totalDepthOfFieldM: number | null;
19
+ }
20
+ /**
21
+ * Calculates thin-lens depth-of-field limits using the conventional
22
+ * hyperfocal-distance formulation.
23
+ *
24
+ * The model is geometric and does not include diffraction, aberrations,
25
+ * focus breathing, pupil magnification, or macro/high-magnification effects.
26
+ *
27
+ * @param input Lens, focus, aperture, and acceptable blur-circle inputs.
28
+ * @returns Hyperfocal, near, and far depth-of-field distances.
29
+ */
30
+ export declare function calculateDepthOfField(input: CalculateDepthOfFieldInput): CalculationResult<DepthOfField>;
31
+ export interface CalculateDefocusCircleInput {
32
+ /** Lens focal length in millimetres. */
33
+ focalLengthMm: number;
34
+ /** F-number. */
35
+ aperture: number;
36
+ /** Focus-plane distance from the lens plane in metres. */
37
+ focusDistanceM: number;
38
+ /** Subject-plane distance from the lens plane in metres. */
39
+ subjectDistanceM: number;
40
+ }
41
+ export interface DefocusCircle {
42
+ /** Geometric blur-circle diameter at the sensor plane in millimetres. */
43
+ diameterMm: number;
44
+ }
45
+ /**
46
+ * Calculates the geometric defocus-circle diameter at the sensor plane for a
47
+ * subject plane away from the selected focus plane.
48
+ *
49
+ * @param input Lens, aperture, focus-plane, and subject-plane distances.
50
+ * @returns Geometric blur-circle diameter.
51
+ */
52
+ export declare function calculateDefocusCircle(input: CalculateDefocusCircleInput): CalculationResult<DefocusCircle>;
53
+ //# sourceMappingURL=depth-of-field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"depth-of-field.d.ts","sourceRoot":"","sources":["../../src/optics/depth-of-field.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAOvC,MAAM,WAAW,0BAA0B;IACzC,wCAAwC;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,cAAc,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,qDAAqD;IACrD,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,0BAA0B,GAChC,iBAAiB,CAAC,YAAY,CAAC,CA6CjC;AAED,MAAM,WAAW,2BAA2B;IAC1C,wCAAwC;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,cAAc,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,2BAA2B,GACjC,iBAAiB,CAAC,aAAa,CAAC,CAsClC"}
@@ -0,0 +1,81 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { calculatedResult } from "../core/calculation-result.js";
3
+ import { InvalidScientificInputError, requirePositiveFinite } from "../core/validation.js";
4
+ import { calculateThinLensImageDistance } from "./thin-lens.js";
5
+ /**
6
+ * Calculates thin-lens depth-of-field limits using the conventional
7
+ * hyperfocal-distance formulation.
8
+ *
9
+ * The model is geometric and does not include diffraction, aberrations,
10
+ * focus breathing, pupil magnification, or macro/high-magnification effects.
11
+ *
12
+ * @param input Lens, focus, aperture, and acceptable blur-circle inputs.
13
+ * @returns Hyperfocal, near, and far depth-of-field distances.
14
+ */
15
+ export function calculateDepthOfField(input) {
16
+ requirePositiveFinite("focalLengthMm", input.focalLengthMm);
17
+ requirePositiveFinite("aperture", input.aperture);
18
+ requirePositiveFinite("focusDistanceM", input.focusDistanceM);
19
+ requirePositiveFinite("circleOfConfusionMm", input.circleOfConfusionMm);
20
+ const focusDistanceMm = input.focusDistanceM * 1000;
21
+ if (focusDistanceMm <= input.focalLengthMm) {
22
+ throw new InvalidScientificInputError("focusDistanceM must place the focus plane beyond the focal length.");
23
+ }
24
+ const hyperfocalDistanceMm = (input.focalLengthMm * input.focalLengthMm) /
25
+ (input.aperture * input.circleOfConfusionMm) +
26
+ input.focalLengthMm;
27
+ const focusOffsetMm = focusDistanceMm - input.focalLengthMm;
28
+ const nearLimitMm = (hyperfocalDistanceMm * focusDistanceMm) /
29
+ (hyperfocalDistanceMm + focusOffsetMm);
30
+ const farDenominator = hyperfocalDistanceMm - focusOffsetMm;
31
+ const farLimitMm = farDenominator <= 0
32
+ ? null
33
+ : (hyperfocalDistanceMm * focusDistanceMm) / farDenominator;
34
+ return calculatedResult({
35
+ hyperfocalDistanceM: hyperfocalDistanceMm / 1000,
36
+ nearLimitM: nearLimitMm / 1000,
37
+ farLimitM: farLimitMm === null ? null : farLimitMm / 1000,
38
+ totalDepthOfFieldM: farLimitMm === null ? null : (farLimitMm - nearLimitMm) / 1000
39
+ }, "thin-lens-depth-of-field", "1.0.0", [
40
+ "Geometric thin-lens model",
41
+ "Circle of confusion is supplied by the caller",
42
+ "Macro/high-magnification effects are not modeled"
43
+ ]);
44
+ }
45
+ /**
46
+ * Calculates the geometric defocus-circle diameter at the sensor plane for a
47
+ * subject plane away from the selected focus plane.
48
+ *
49
+ * @param input Lens, aperture, focus-plane, and subject-plane distances.
50
+ * @returns Geometric blur-circle diameter.
51
+ */
52
+ export function calculateDefocusCircle(input) {
53
+ requirePositiveFinite("focalLengthMm", input.focalLengthMm);
54
+ requirePositiveFinite("aperture", input.aperture);
55
+ requirePositiveFinite("focusDistanceM", input.focusDistanceM);
56
+ requirePositiveFinite("subjectDistanceM", input.subjectDistanceM);
57
+ let focusImageDistanceMm;
58
+ let subjectImageDistanceMm;
59
+ try {
60
+ focusImageDistanceMm = calculateThinLensImageDistance({
61
+ focalLengthMm: input.focalLengthMm,
62
+ objectDistanceM: input.focusDistanceM
63
+ }).value.imageDistanceMm;
64
+ subjectImageDistanceMm = calculateThinLensImageDistance({
65
+ focalLengthMm: input.focalLengthMm,
66
+ objectDistanceM: input.subjectDistanceM
67
+ }).value.imageDistanceMm;
68
+ }
69
+ catch (error) {
70
+ if (error instanceof InvalidScientificInputError) {
71
+ throw new InvalidScientificInputError("focusDistanceM and subjectDistanceM must be beyond the focal length.");
72
+ }
73
+ throw error;
74
+ }
75
+ const apertureDiameterMm = input.focalLengthMm / input.aperture;
76
+ const diameterMm = apertureDiameterMm *
77
+ (Math.abs(subjectImageDistanceMm - focusImageDistanceMm) /
78
+ subjectImageDistanceMm);
79
+ return calculatedResult({ diameterMm }, "thin-lens-defocus-circle", "1.0.0", ["Geometric thin-lens model", "Ideal circular entrance pupil"]);
80
+ }
81
+ //# sourceMappingURL=depth-of-field.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"depth-of-field.js","sourceRoot":"","sources":["../../src/optics/depth-of-field.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { type CalculationResult } from "../core/calculation-result.js";
2
+ export interface CalculateAiryDiskInput {
3
+ /** F-number. */
4
+ aperture: number;
5
+ /** Wavelength in nanometres. Defaults are intentionally left to callers. */
6
+ wavelengthNm: number;
7
+ }
8
+ export interface AiryDisk {
9
+ /**
10
+ * Diameter from the center of the diffraction pattern to the first dark-ring
11
+ * diameter, in micrometres: 2.44 * wavelength * f-number.
12
+ */
13
+ firstZeroDiameterMicrometers: number;
14
+ }
15
+ /**
16
+ * Calculates the first-zero Airy-disk diameter for an ideal circular aperture.
17
+ *
18
+ * This uses the established diffraction relation d = 2.44 * lambda * N.
19
+ * It is an independent implementation of the mathematical relation and does
20
+ * not model lens aberrations or non-circular pupil geometry.
21
+ *
22
+ * @param input F-number and wavelength.
23
+ * @returns Diffraction diameter at the image plane.
24
+ */
25
+ export declare function calculateAiryDisk(input: CalculateAiryDiskInput): CalculationResult<AiryDisk>;
26
+ //# sourceMappingURL=diffraction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diffraction.d.ts","sourceRoot":"","sources":["../../src/optics/diffraction.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAGvC,MAAM,WAAW,sBAAsB;IACrC,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB;;;OAGG;IACH,4BAA4B,EAAE,MAAM,CAAC;CACtC;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,sBAAsB,GAC5B,iBAAiB,CAAC,QAAQ,CAAC,CAa7B"}
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { calculatedResult } from "../core/calculation-result.js";
3
+ import { requirePositiveFinite } from "../core/validation.js";
4
+ /**
5
+ * Calculates the first-zero Airy-disk diameter for an ideal circular aperture.
6
+ *
7
+ * This uses the established diffraction relation d = 2.44 * lambda * N.
8
+ * It is an independent implementation of the mathematical relation and does
9
+ * not model lens aberrations or non-circular pupil geometry.
10
+ *
11
+ * @param input F-number and wavelength.
12
+ * @returns Diffraction diameter at the image plane.
13
+ */
14
+ export function calculateAiryDisk(input) {
15
+ requirePositiveFinite("aperture", input.aperture);
16
+ requirePositiveFinite("wavelengthNm", input.wavelengthNm);
17
+ return calculatedResult({
18
+ firstZeroDiameterMicrometers: 2.44 * (input.wavelengthNm / 1000) * input.aperture
19
+ }, "ideal-circular-aperture-airy-disk", "1.0.0", ["Ideal circular aperture", "Monochromatic wavelength"]);
20
+ }
21
+ //# sourceMappingURL=diffraction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diffraction.js","sourceRoot":"","sources":["../../src/optics/diffraction.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC,OAAO,EACL,gBAAgB,EAEjB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAiB9D;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAA6B;IAE7B,qBAAqB,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;IAClD,qBAAqB,CAAC,cAAc,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAE1D,OAAO,gBAAgB,CACrB;QACE,4BAA4B,EAC1B,IAAI,GAAG,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,QAAQ;KACtD,EACD,mCAAmC,EACnC,OAAO,EACP,CAAC,yBAAyB,EAAE,0BAA0B,CAAC,CACxD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { type CalculationResult } from "../core/calculation-result.js";
2
+ export interface CalculateThinLensImageDistanceInput {
3
+ /** Lens focal length in millimetres. */
4
+ focalLengthMm: number;
5
+ /** Object/focus-plane distance from the lens principal plane in metres. */
6
+ objectDistanceM: number;
7
+ }
8
+ export interface ThinLensImageDistance {
9
+ /** Image/sensor-plane distance from the lens principal plane in millimetres. */
10
+ imageDistanceMm: number;
11
+ /** Absolute paraxial magnification at the supplied object distance. */
12
+ magnification: number;
13
+ /**
14
+ * Image-distance scale relative to the infinity-focus approximation f.
15
+ * Values approach 1 as object distance becomes large.
16
+ */
17
+ infinityProjectionScale: number;
18
+ }
19
+ /**
20
+ * Calculates Gaussian thin-lens image distance:
21
+ *
22
+ * 1/f = 1/s + 1/v
23
+ *
24
+ * where f is focal length, s is object distance, and v is image distance.
25
+ *
26
+ * This is a paraxial ideal-lens model. It does not model real-lens focus
27
+ * breathing, pupil magnification, principal-plane movement, or aberrations.
28
+ */
29
+ export declare function calculateThinLensImageDistance(input: CalculateThinLensImageDistanceInput): CalculationResult<ThinLensImageDistance>;
30
+ //# sourceMappingURL=thin-lens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thin-lens.d.ts","sourceRoot":"","sources":["../../src/optics/thin-lens.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAMvC,MAAM,WAAW,mCAAmC;IAClD,wCAAwC;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,gFAAgF;IAChF,eAAe,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,aAAa,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,uBAAuB,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,mCAAmC,GACzC,iBAAiB,CAAC,qBAAqB,CAAC,CA6B1C"}
@@ -0,0 +1,33 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { calculatedResult } from "../core/calculation-result.js";
3
+ import { InvalidScientificInputError, requirePositiveFinite } from "../core/validation.js";
4
+ /**
5
+ * Calculates Gaussian thin-lens image distance:
6
+ *
7
+ * 1/f = 1/s + 1/v
8
+ *
9
+ * where f is focal length, s is object distance, and v is image distance.
10
+ *
11
+ * This is a paraxial ideal-lens model. It does not model real-lens focus
12
+ * breathing, pupil magnification, principal-plane movement, or aberrations.
13
+ */
14
+ export function calculateThinLensImageDistance(input) {
15
+ requirePositiveFinite("focalLengthMm", input.focalLengthMm);
16
+ requirePositiveFinite("objectDistanceM", input.objectDistanceM);
17
+ const objectDistanceMm = input.objectDistanceM * 1000;
18
+ if (objectDistanceMm <= input.focalLengthMm) {
19
+ throw new InvalidScientificInputError("objectDistanceM must place the object plane beyond the focal length.");
20
+ }
21
+ const imageDistanceMm = (input.focalLengthMm * objectDistanceMm) /
22
+ (objectDistanceMm - input.focalLengthMm);
23
+ return calculatedResult({
24
+ imageDistanceMm,
25
+ magnification: imageDistanceMm / objectDistanceMm,
26
+ infinityProjectionScale: imageDistanceMm / input.focalLengthMm
27
+ }, "gaussian-thin-lens-image-distance", "1.0.0", [
28
+ "Paraxial Gaussian thin-lens model",
29
+ "Distances are measured from ideal principal planes",
30
+ "Real-lens focus breathing and pupil magnification are not modeled"
31
+ ]);
32
+ }
33
+ //# sourceMappingURL=thin-lens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thin-lens.js","sourceRoot":"","sources":["../../src/optics/thin-lens.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC,OAAO,EACL,gBAAgB,EAEjB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,2BAA2B,EAC3B,qBAAqB,EACtB,MAAM,uBAAuB,CAAC;AAqB/B;;;;;;;;;GASG;AACH,MAAM,UAAU,8BAA8B,CAC5C,KAA0C;IAE1C,qBAAqB,CAAC,eAAe,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;IAC5D,qBAAqB,CAAC,iBAAiB,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;IAEhE,MAAM,gBAAgB,GAAG,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC;IACtD,IAAI,gBAAgB,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;QAC5C,MAAM,IAAI,2BAA2B,CACnC,sEAAsE,CACvE,CAAC;IACJ,CAAC;IAED,MAAM,eAAe,GACnB,CAAC,KAAK,CAAC,aAAa,GAAG,gBAAgB,CAAC;QACxC,CAAC,gBAAgB,GAAG,KAAK,CAAC,aAAa,CAAC,CAAC;IAE3C,OAAO,gBAAgB,CACrB;QACE,eAAe;QACf,aAAa,EAAE,eAAe,GAAG,gBAAgB;QACjD,uBAAuB,EAAE,eAAe,GAAG,KAAK,CAAC,aAAa;KAC/D,EACD,mCAAmC,EACnC,OAAO,EACP;QACE,mCAAmC;QACnC,oDAAoD;QACpD,mEAAmE;KACpE,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { type CalculationResult } from "../core/calculation-result.js";
2
+ export interface CalculateCenteredCropInput {
3
+ /** Original image width in pixels. */
4
+ pixelWidth: number;
5
+ /** Original image height in pixels. */
6
+ pixelHeight: number;
7
+ /**
8
+ * Linear crop factor. 1 means no crop; 1.5 retains 1 / 1.5 of each
9
+ * dimension while preserving aspect ratio.
10
+ */
11
+ cropFactor: number;
12
+ }
13
+ export interface CenteredCrop {
14
+ pixelWidth: number;
15
+ pixelHeight: number;
16
+ megapixels: number;
17
+ /** Fraction of original pixel area retained, from 0 to 1. */
18
+ retainedAreaFraction: number;
19
+ }
20
+ /**
21
+ * Calculates dimensions remaining after a centered, same-aspect-ratio crop.
22
+ *
23
+ * @param input Original dimensions and linear crop factor.
24
+ * @returns Remaining pixel dimensions and area fraction.
25
+ */
26
+ export declare function calculateCenteredCrop(input: CalculateCenteredCropInput): CalculationResult<CenteredCrop>;
27
+ //# sourceMappingURL=crop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crop.d.ts","sourceRoot":"","sources":["../../src/output/crop.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAOvC,MAAM,WAAW,0BAA0B;IACzC,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,0BAA0B,GAChC,iBAAiB,CAAC,YAAY,CAAC,CA6BjC"}
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { calculatedResult } from "../core/calculation-result.js";
3
+ import { InvalidScientificInputError, requirePositiveFinite, requirePositiveInteger } from "../core/validation.js";
4
+ /**
5
+ * Calculates dimensions remaining after a centered, same-aspect-ratio crop.
6
+ *
7
+ * @param input Original dimensions and linear crop factor.
8
+ * @returns Remaining pixel dimensions and area fraction.
9
+ */
10
+ export function calculateCenteredCrop(input) {
11
+ requirePositiveInteger("pixelWidth", input.pixelWidth);
12
+ requirePositiveInteger("pixelHeight", input.pixelHeight);
13
+ requirePositiveFinite("cropFactor", input.cropFactor);
14
+ if (input.cropFactor < 1) {
15
+ throw new InvalidScientificInputError("cropFactor must be greater than or equal to 1.");
16
+ }
17
+ const pixelWidth = Math.max(1, Math.floor(input.pixelWidth / input.cropFactor));
18
+ const pixelHeight = Math.max(1, Math.floor(input.pixelHeight / input.cropFactor));
19
+ return calculatedResult({
20
+ pixelWidth,
21
+ pixelHeight,
22
+ megapixels: (pixelWidth * pixelHeight) / 1_000_000,
23
+ retainedAreaFraction: (pixelWidth * pixelHeight) / (input.pixelWidth * input.pixelHeight)
24
+ }, "centered-same-aspect-crop", "1.0.0", ["Centered crop", "Original aspect ratio is preserved"]);
25
+ }
26
+ //# sourceMappingURL=crop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crop.js","sourceRoot":"","sources":["../../src/output/crop.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC,OAAO,EACL,gBAAgB,EAEjB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,2BAA2B,EAC3B,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,uBAAuB,CAAC;AAsB/B;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAiC;IAEjC,sBAAsB,CAAC,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACvD,sBAAsB,CAAC,aAAa,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IACzD,qBAAqB,CAAC,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IAEtD,IAAI,KAAK,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,2BAA2B,CACnC,gDAAgD,CACjD,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IAChF,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAC1B,CAAC,EACD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,GAAG,KAAK,CAAC,UAAU,CAAC,CACjD,CAAC;IAEF,OAAO,gBAAgB,CACrB;QACE,UAAU;QACV,WAAW;QACX,UAAU,EAAE,CAAC,UAAU,GAAG,WAAW,CAAC,GAAG,SAAS;QAClD,oBAAoB,EAClB,CAAC,UAAU,GAAG,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,WAAW,CAAC;KACtE,EACD,2BAA2B,EAC3B,OAAO,EACP,CAAC,eAAe,EAAE,oCAAoC,CAAC,CACxD,CAAC;AACJ,CAAC"}