diff --git a/.eslintignore b/.eslintignore index 68203872..3c3629e6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1 @@ node_modules -gui diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 48972b4a..34fcf7d3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,7 +11,7 @@ updates: interval: daily open-pull-requests-limit: 5 - package-ecosystem: npm - directory: "/package" + directory: "/module" schedule: interval: daily open-pull-requests-limit: 5 diff --git a/.github/workflows/publish-npm-github-packages.yml b/.github/workflows/publish-npm-github-packages.yml index 1cb5e88c..e9f6c17e 100644 --- a/.github/workflows/publish-npm-github-packages.yml +++ b/.github/workflows/publish-npm-github-packages.yml @@ -32,8 +32,8 @@ jobs: node-version: "24.x" registry-url: "https://registry.npmjs.org" - run: yarn install - - run: yarn --cwd package build - - run: yarn --cwd package publish + - run: yarn --cwd module build + - run: yarn --cwd module publish env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} @@ -50,7 +50,7 @@ jobs: node-version: "24.x" registry-url: https://npm.pkg.github.com/ - run: yarn install - - run: yarn --cwd package build - - run: yarn --cwd package publish + - run: yarn --cwd module build + - run: yarn --cwd module publish env: NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLISH_TOKEN}} diff --git a/.gitignore b/.gitignore index 83af21eb..3c7064b5 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -package/coverage/ +module/coverage/ dist/ node_modules/ yarn-error.log diff --git a/app/index.d.ts b/app/index.d.ts index 56ce106c..d39adea0 100644 --- a/app/index.d.ts +++ b/app/index.d.ts @@ -16,7 +16,7 @@ interface LensInputs { } interface LensDefinition extends LensInputs { - depthOfField: import('dof').DepthOfFieldDetails + depthOfField: import('../module/dist').DepthOfFieldDetails } type DefaultLensData = Pick diff --git a/app/src/store/lensSlice.ts b/app/src/store/lensSlice.ts index 8dd62850..1fb12837 100644 --- a/app/src/store/lensSlice.ts +++ b/app/src/store/lensSlice.ts @@ -1,4 +1,4 @@ -import { Lens } from 'dof' +import { Lens } from '../../../module/dist' import { compact, defaults } from 'lodash' import { StateCreator } from 'zustand' import areDuplicateLenses from '../utilities/areDuplicateLenses' diff --git a/app/src/ui/Graph/useData.ts b/app/src/ui/Graph/useData.ts index c88f146e..3418775d 100644 --- a/app/src/ui/Graph/useData.ts +++ b/app/src/ui/Graph/useData.ts @@ -1,7 +1,7 @@ import { LineSeries } from '@nivo/line' -import { Lens } from 'dof' import { compact } from 'lodash' import { useMemo } from 'react' +import { Lens } from '../../../../package/dist/Lens' import useDoFStore from '../../store' import sensorList from '../../utilities/sensorList' import useIsMobile from '../../utilities/useIsMobile' diff --git a/app/src/ui/LensTable/Table/Row/Aperture.tsx b/app/src/ui/LensTable/Table/Row/Aperture.tsx index b9cc4f8b..56353979 100644 --- a/app/src/ui/LensTable/Table/Row/Aperture.tsx +++ b/app/src/ui/LensTable/Table/Row/Aperture.tsx @@ -1,5 +1,5 @@ import { FormControl, InputLabel, MenuItem, Select, SelectChangeEvent } from '@mui/material' -import { apertureMap } from 'dof' +import { apertureMap } from '../../../../../../module/dist' import useDoFStore from '../../../../store' import useIsMobile from '../../../../utilities/useIsMobile' diff --git a/app/src/utilities/sensorList.ts b/app/src/utilities/sensorList.ts index b52ca77a..e4cb8dac 100644 --- a/app/src/utilities/sensorList.ts +++ b/app/src/utilities/sensorList.ts @@ -30,9 +30,11 @@ const fullList = { name: '1" CCD, Nikon CX, Sony RX100', value: 2.72, }, - iPhone14: { - name: 'iPhone 14 (2022)', - value: 2.86, // 9.8x7.3mm, or 71.54mm^2 + iPhone14Pro: { + name: 'iPhone 14 Pro (2022)', + // https://www.dpreview.com/articles/6110937480 + // 48MP Type 1/1.28 (9.8x7.3mm, or 71.54mm^2) + value: 2.86, isCommon: true, }, BlackmagicCC: { diff --git a/app/src/utilities/useReadFromHash.ts b/app/src/utilities/useReadFromHash.ts index 2005f241..50c99496 100644 --- a/app/src/utilities/useReadFromHash.ts +++ b/app/src/utilities/useReadFromHash.ts @@ -1,4 +1,4 @@ -import { isApertureString } from 'dof' +import { isApertureString } from '../../../module/dist' import { compact } from 'lodash' import { useEffect, useState } from 'react' import useDoFStore from '../store' diff --git a/package/.gitignore b/module/.gitignore similarity index 100% rename from package/.gitignore rename to module/.gitignore diff --git a/package/.ts-prunerc b/module/.ts-prunerc similarity index 100% rename from package/.ts-prunerc rename to module/.ts-prunerc diff --git a/package/jest.config.js b/module/jest.config.js similarity index 100% rename from package/jest.config.js rename to module/jest.config.js diff --git a/package/package.json b/module/package.json similarity index 98% rename from package/package.json rename to module/package.json index 42e8b16d..34cfd942 100644 --- a/package/package.json +++ b/module/package.json @@ -1,7 +1,7 @@ { "name": "dof", "description": "Depth-of-field calculator for comparing camera lenses", - "version": "3.0.0", + "version": "3.1.0", "repository": { "type": "git", "url": "https://github.com/patik/dof.git" diff --git a/package/src/Lens.test.ts b/module/src/Lens.test.ts similarity index 100% rename from package/src/Lens.test.ts rename to module/src/Lens.test.ts diff --git a/package/src/Lens.ts b/module/src/Lens.ts similarity index 86% rename from package/src/Lens.ts rename to module/src/Lens.ts index 1a1e0ed9..4a61788f 100644 --- a/package/src/Lens.ts +++ b/module/src/Lens.ts @@ -1,5 +1,5 @@ +import { calculateDepthOfField } from './calculators/calculateDepthOfField' import { combineSettings } from './utilities/combineSettings' -import { calculateDepthOfField } from './utilities/calculateDepthOfField' export const builtInDefaults: DefaultOptions = Object.freeze({ focalLength: 35, @@ -45,6 +45,12 @@ export class Lens { distance = imperialUnits ? defaultDistanceImperial : defaultDistanceMetric } - return calculateDepthOfField(this.focalLength, this.aperture, this.cropFactor, distance, imperialUnits) + return calculateDepthOfField({ + focalLength: this.focalLength, + aperture: this.aperture, + cropFactor: this.cropFactor, + distance, + imperialUnits, + }) } } diff --git a/module/src/calculators/calculateAperture.test.ts b/module/src/calculators/calculateAperture.test.ts new file mode 100644 index 00000000..6fb47b56 --- /dev/null +++ b/module/src/calculators/calculateAperture.test.ts @@ -0,0 +1,86 @@ +import { calculateAperture } from './calculateAperture' + +describe('Calculating the depth of field with calculateAperture()', () => { + describe('metric units (meters)', () => { + test('DoF 2.6 meters, 35mm, f/2, crop factor of 1, 5 meters', () => { + const result = calculateAperture({ + focalLength: 35, + cropFactor: 1, + distance: 5, + dof: 2.584690961719362, + near: 4.021931840567339, + imperialUnits: false, + }) + + expect(result.aperture).toBe(2) + }) + + test('DoF 11 meters, 50mm, f/1.4, crop factor of 2, 25 meters', () => { + const result = calculateAperture({ + focalLength: 50, + cropFactor: 2, + distance: 25, + dof: 11.082093523926748, + near: 20.63203625607849, + imperialUnits: false, + }) + + expect(result.aperture).toBe(1.4142139999999992) + }) + + test('DoF 7.9 meters, 28mm, f/5, crop factor of 3.02, 7 meters', () => { + // Blackmagic Cine Cam sensor + const result = calculateAperture({ + focalLength: 28, + cropFactor: 3.02, + distance: 7, + dof: 7.851429380291624, + near: 4.833680315786585, + imperialUnits: false, + }) + + expect(result.aperture).toBe(5.039684) + }) + + test('such that the far end of the range is infinity', () => { + const result = calculateAperture({ + focalLength: 24, + cropFactor: 1, + distance: 5, + dof: Infinity, + near: 0.9715025906735751, + imperialUnits: false, + }) + + expect(result.aperture).toBe(NaN) + }) + }) + + describe('imperial units (feet)', () => { + test('DoF 7 feet, 35mm, f/2, crop factor of 1, 15 feet', () => { + const result = calculateAperture({ + focalLength: 35, + cropFactor: 1, + distance: 15, + dof: 7.012923816256398, + near: 12.27274548830499, + imperialUnits: true, + }) + + expect(result.aperture).toBe(2.000000000000001) + }) + + test('DoF 12.6 feet, 55mm, f/3.2, crop factor of 2.7, 42 feet', () => { + const result = calculateAperture({ + focalLength: 55, + cropFactor: 2.7, + distance: 42, + dof: 12.634717336600715, + near: 36.61227928000945, + imperialUnits: true, + }) + + expect(result.aperture).toBe(3.1748020000000055) + }) + }) +}) diff --git a/module/src/calculators/calculateAperture.ts b/module/src/calculators/calculateAperture.ts new file mode 100644 index 00000000..a6fc6e13 --- /dev/null +++ b/module/src/calculators/calculateAperture.ts @@ -0,0 +1,38 @@ +import { getApertureName } from '../utilities/aperture' +import { toMillimeters } from '../utilities/units' + +/** + * Returns the aperture for a given lens' attributes and depth of field + */ +export function calculateAperture({ + focalLength, + cropFactor, + distance, + dof, + near, + imperialUnits, +}: { + focalLength: number + cropFactor: number + distance: number + dof: number + near: number + imperialUnits?: boolean +}): ApertureResult { + const mmDist = toMillimeters(distance, imperialUnits) + const mmDof = toMillimeters(dof, imperialUnits) + const mmNear = toMillimeters(near, imperialUnits) + + const mmFar = mmNear + mmDof + const mmHF = (-1 * mmDist * focalLength + mmDist * mmFar) / (-1 * mmDist + mmFar) + const cropMultiplier = 1 / cropFactor + const coc = Math.round(0.03 * cropMultiplier * 1000) / 1000 + const aperture = Math.pow(focalLength, 2) / (coc * mmHF - coc * focalLength) + + const result: ApertureResult = { + aperture, + fStop: getApertureName(aperture), + } + + return result +} diff --git a/module/src/calculators/calculateCropFactor.test.ts b/module/src/calculators/calculateCropFactor.test.ts new file mode 100644 index 00000000..cec74265 --- /dev/null +++ b/module/src/calculators/calculateCropFactor.test.ts @@ -0,0 +1,73 @@ +import { calculateCropFactor } from './calculateCropFactor' + +describe('Calculating the depth of field with calculateCropFactor()', () => { + describe('metric units (meters)', () => { + test('35mm, f/2, crop factor of 1, 5 meters', () => { + const result = calculateCropFactor({ + dof: 2.584690961719362, + near: 4.021931840567339, + focalLength: 35, + aperture: 2, + distance: 5, + imperialUnits: false, + }) + + expect(result.cropFactor).toBe(1) + }) + + test('50mm, f/1.4, crop factor of 2, 25 meters', () => { + const result = calculateCropFactor({ + dof: 11.082093523926748, + near: 20.63203625607849, + focalLength: 50, + aperture: 1.414214, + distance: 25, + imperialUnits: false, + }) + + expect(result.cropFactor).toBe(2) + }) + + test('28mm, f/5, crop factor of 3.02, 7 meters', () => { + // Blackmagic Cine Cam sensor + const result = calculateCropFactor({ + focalLength: 28, + aperture: 5.039684, + dof: 7.851429380291624, + near: 4.833680315786585, + distance: 7, + imperialUnits: false, + }) + + expect(result.cropFactor).toBe(3) + }) + }) + + describe('imperial units (feet)', () => { + test('35mm, f/2, crop factor of 1, 15 feet', () => { + const result = calculateCropFactor({ + focalLength: 35, + aperture: 2, + near: 12.27274548830499, + dof: 7.012923816256398, + distance: 15, + imperialUnits: true, + }) + + expect(result.cropFactor).toBe(1) + }) + + test('55mm, f/3.2, crop factor of 2.7, 42 feet', () => { + const result = calculateCropFactor({ + focalLength: 55, + aperture: 3.174802, + dof: 12.634717336600715, + near: 36.61227928000945, + distance: 42, + imperialUnits: true, + }) + + expect(result.cropFactor).toBe(2.727) + }) + }) +}) diff --git a/module/src/calculators/calculateCropFactor.ts b/module/src/calculators/calculateCropFactor.ts new file mode 100644 index 00000000..2594c6e4 --- /dev/null +++ b/module/src/calculators/calculateCropFactor.ts @@ -0,0 +1,32 @@ +import { toMillimeters } from '../utilities/units' + +/** + * Returns the depth of field characteristics for a given lens' attributes + */ +export function calculateCropFactor({ + near, + dof, + focalLength, + aperture, + distance, + imperialUnits, +}: { + near: number + dof: number + focalLength: number + aperture: number + distance: number + imperialUnits: boolean +}): CropFactorResult { + const mmDist = toMillimeters(distance, imperialUnits) + const mmDof = toMillimeters(dof, imperialUnits) + const mmNear = toMillimeters(near, imperialUnits) + + const mmFar = mmNear + mmDof + const mmHF = (-1 * mmDist * focalLength + mmDist * mmFar) / (-1 * mmDist + mmFar) + const coc = Math.pow(focalLength, 2) / (aperture * mmHF - aperture * focalLength) + const cropMultiplier = (100 * coc) / 3 + const cropFactor = Math.round((1 / cropMultiplier) * 1000) / 1000 + + return { cropFactor } +} diff --git a/package/src/utilities/calculateDepthOfField.test.ts b/module/src/calculators/calculateDepthOfField.test.ts similarity index 65% rename from package/src/utilities/calculateDepthOfField.test.ts rename to module/src/calculators/calculateDepthOfField.test.ts index 031e521a..87f707b4 100644 --- a/package/src/utilities/calculateDepthOfField.test.ts +++ b/module/src/calculators/calculateDepthOfField.test.ts @@ -1,9 +1,15 @@ import { calculateDepthOfField } from './calculateDepthOfField' -describe('Calculating the depth of field with calculateDepthOfField', () => { +describe('Calculating the depth of field with calculateDepthOfField()', () => { describe('metric units (meters)', () => { - test('35mm, f/2, crop factor of 1, 15 meters', () => { - const result = calculateDepthOfField(35, 2, 1, 5, false) + test('35mm, f/2, crop factor of 1, 5 meters', () => { + const result = calculateDepthOfField({ + focalLength: 35, + aperture: 2, + cropFactor: 1, + distance: 5, + imperialUnits: false, + }) expect(result.dof).toBe(2.584690961719362) expect(result.dof.toString()).toBe('2.584690961719362') @@ -26,7 +32,13 @@ describe('Calculating the depth of field with calculateDepthOfField', () => { }) test('50mm, f/1.4, crop factor of 2, 25 meters', () => { - const result = calculateDepthOfField(50, 1.414214, 2, 25, false) + const result = calculateDepthOfField({ + focalLength: 50, + aperture: 1.414214, + cropFactor: 2, + distance: 25, + imperialUnits: false, + }) expect(result.dof).toBe(11.082093523926748) expect(result.dof.toString()).toBe('11.082093523926748') @@ -48,8 +60,44 @@ describe('Calculating the depth of field with calculateDepthOfField', () => { expect(result.coc.toString()).toBe('0.015') }) + test('28mm, f/5, crop factor of 3.02, 7 meters', () => { + // Blackmagic Cine Cam sensor + const result = calculateDepthOfField({ + focalLength: 28, + aperture: 5.039684, + cropFactor: 3.02, + distance: 7, + imperialUnits: false, + }) + + expect(result.dof).toBe(7.851429380291624) + expect(result.dof.toString()).toBe('7.851429380291624') + expect(result.toString()).toBe(`7.851429380291624`) + + expect(result.eighthDof).toBe(0.981428672536453) + expect(result.eighthDof.toString()).toBe('0.981428672536453') + + expect(result.hf).toBe(15.584530925351668) + expect(result.hf.toString()).toBe('15.584530925351668') + + expect(result.near).toBe(4.833680315786585) + expect(result.near.toString()).toBe('4.833680315786585') + + expect(result.far).toBe(12.685109696078209) + expect(result.far.toString()).toBe('12.685109696078209') + + expect(result.coc).toBe(0.01) + expect(result.coc.toString()).toBe('0.01') + }) + test('such that the far end of the range is infinity', () => { - const result = calculateDepthOfField(24, 16, 1, 5, false) + const result = calculateDepthOfField({ + focalLength: 24, + aperture: 16, + cropFactor: 1, + distance: 5, + imperialUnits: false, + }) expect(result.dof).toBe(Infinity) expect(result.dof.toString()).toBe('Infinity') @@ -74,7 +122,13 @@ describe('Calculating the depth of field with calculateDepthOfField', () => { describe('imperial units (feet)', () => { test('35mm, f/2, crop factor of 1, 15 feet', () => { - const result = calculateDepthOfField(35, 2, 1, 15, true) + const result = calculateDepthOfField({ + focalLength: 35, + aperture: 2, + cropFactor: 1, + distance: 15, + imperialUnits: true, + }) expect(result.dof).toBe(7.012923816256398) expect(result.dof.toString()).toBe(`7.012923816256398`) @@ -97,7 +151,13 @@ describe('Calculating the depth of field with calculateDepthOfField', () => { }) test('55mm, f/3.2, crop factor of 2.7, 42 feet', () => { - const result = calculateDepthOfField(55, 3.174802, 2.7, 42, true) + const result = calculateDepthOfField({ + focalLength: 55, + aperture: 3.174802, + cropFactor: 2.7, + distance: 42, + imperialUnits: true, + }) expect(result.dof).toBe(12.634717336600715) expect(result.dof.toString()).toBe('12.634717336600715') diff --git a/module/src/calculators/calculateDepthOfField.ts b/module/src/calculators/calculateDepthOfField.ts new file mode 100644 index 00000000..80f8bbba --- /dev/null +++ b/module/src/calculators/calculateDepthOfField.ts @@ -0,0 +1,59 @@ +import { decimalAdjust } from '../utilities/decimalAdjust' +import { formatFeet } from '../utilities/formatFeet' +import { fromMillimeters, toMillimeters } from '../utilities/units' + +/** + * Returns the depth of field characteristics for a given lens' attributes + */ +export function calculateDepthOfField({ + focalLength, + aperture, + cropFactor, + distance, + imperialUnits, +}: { + focalLength: number + aperture: number + cropFactor: number + distance: number + imperialUnits: boolean +}): DoFResult { + // Convert to millimeters + const mmDist = toMillimeters(distance, imperialUnits) + + const cropMultiplier = 1 / cropFactor + const coc = Math.round(0.03 * cropMultiplier * 1000) / 1000 + const mmHF = Math.pow(focalLength, 2) / (aperture * coc) + focalLength * 1.0 + const mmNear = (mmDist * (mmHF - focalLength)) / (mmHF + mmDist - 2 * focalLength) + const mmFar = (mmDist * (mmHF - focalLength)) / (mmHF - mmDist) + + // Undo conversion to millimeters + const near = fromMillimeters(mmNear, imperialUnits) + const trueFar = fromMillimeters(mmFar, imperialUnits) + + const isInfinite = trueFar <= 0 + const dof = isInfinite ? Infinity : trueFar - near + const far = isInfinite ? Infinity : trueFar + + const result: DoFResult = { + dof, + focalLengthEquiv: decimalAdjust(cropFactor * focalLength), + eighthDof: dof / 8, + hf: fromMillimeters(mmHF, imperialUnits), + near, + far, + coc, + } + + if (imperialUnits) { + result.toString = function () { + return formatFeet(dof) + } + } else { + result.toString = function () { + return `${dof}` + } + } + + return result +} diff --git a/module/src/calculators/calculateFocalLength.test.ts b/module/src/calculators/calculateFocalLength.test.ts new file mode 100644 index 00000000..a4e36207 --- /dev/null +++ b/module/src/calculators/calculateFocalLength.test.ts @@ -0,0 +1,80 @@ +import { calculateFocalLength } from './calculateFocalLength' + +describe('Calculating the depth of field with calculateFocalLength()', () => { + describe('metric units (meters)', () => { + test('35mm, f/2, crop factor of 1, 5 meters', () => { + const result = calculateFocalLength({ + near: 4.021931840567339, + aperture: 2, + cropFactor: 1, + distance: 5, + imperialUnits: false, + }) + + expect(result.focalLength).toBe(35) + }) + + test('50mm, f/1.4, crop factor of 2, 25 meters', () => { + const result = calculateFocalLength({ + near: 20.63203625607849, + aperture: 1.414214, + cropFactor: 2, + distance: 25, + imperialUnits: false, + }) + + expect(result.focalLength).toBe(50) + }) + + test('28mm, f/5, crop factor of 3.02, 7 meters', () => { + // Blackmagic Cine Cam sensor + const result = calculateFocalLength({ + near: 4.833680315786585, + aperture: 5.039684, + cropFactor: 3.02, + distance: 7, + imperialUnits: false, + }) + + expect(result.focalLength).toBe(28) + }) + + test('such that the far end of the range is infinity', () => { + const result = calculateFocalLength({ + near: 0.9715025906735751, + aperture: 16, + cropFactor: 1, + distance: 5, + imperialUnits: false, + }) + + expect(result.focalLength).toBe(24) + }) + }) + + describe('imperial units (feet)', () => { + test('35mm, f/2, crop factor of 1, 15 feet', () => { + const result = calculateFocalLength({ + near: 12.27274548830499, + aperture: 2, + cropFactor: 1, + distance: 15, + imperialUnits: true, + }) + + expect(result.focalLength).toBe(35) + }) + + test('55mm, f/3.2, crop factor of 2.7, 42 feet', () => { + const result = calculateFocalLength({ + near: 36.61227928000945, + aperture: 3.174802, + cropFactor: 2.7, + distance: 42, + imperialUnits: true, + }) + + expect(result.focalLength).toBe(55) + }) + }) +}) diff --git a/module/src/calculators/calculateFocalLength.ts b/module/src/calculators/calculateFocalLength.ts new file mode 100644 index 00000000..2a8dd1ee --- /dev/null +++ b/module/src/calculators/calculateFocalLength.ts @@ -0,0 +1,42 @@ +import { decimalAdjust } from '../utilities/decimalAdjust' +import { toMillimeters } from '../utilities/units' + +/** + * Returns the focal length for a given lens' attributes and depth of field + */ +export function calculateFocalLength({ + near, + aperture, + cropFactor, + distance, + imperialUnits, +}: { + near: number + aperture: number + cropFactor: number + distance: number + imperialUnits: boolean +}): FocalLengthResult { + // Convert to millimeters + const mmDist = toMillimeters(distance, imperialUnits) + const mmNear = toMillimeters(near, imperialUnits) + + const cropMultiplier = 1 / cropFactor + const coc = Math.round(0.03 * cropMultiplier * 1000) / 1000 + + const focalLength = Math.round( + (mmNear * aperture * coc - + Math.sqrt( + Math.pow(mmNear, 2) * Math.pow(aperture, 2) * Math.pow(coc, 2) - + 4 * mmNear * mmDist * aperture * coc * (mmNear - mmDist) + )) / + (2 * (mmNear - mmDist)) + ) + + const result = { + focalLength, + focalLengthEquiv: decimalAdjust(cropFactor * focalLength), + } + + return result +} diff --git a/package/src/createLensMaker.test.ts b/module/src/createLensMaker.test.ts similarity index 100% rename from package/src/createLensMaker.test.ts rename to module/src/createLensMaker.test.ts diff --git a/package/src/createLensMaker.ts b/module/src/createLensMaker.ts similarity index 100% rename from package/src/createLensMaker.ts rename to module/src/createLensMaker.ts diff --git a/module/src/data/preciseApertureMap.ts b/module/src/data/preciseApertureMap.ts new file mode 100644 index 00000000..5067fd67 --- /dev/null +++ b/module/src/data/preciseApertureMap.ts @@ -0,0 +1,46 @@ +/** + * Map of human-friendly f-stop values to their precise numeric values + */ +export const preciseApertureMap: Record = { + 'f/1': 1, + 'f/1.2': 1.189207, + 'f/1.4': 1.414214, + 'f/1.6': 1.587401, + 'f/1.7': 1.681793, + 'f/1.8': 1.781797, + 'f/2': 2.0, + 'f/2.2': 2.244924, + 'f/2.4': 2.378414, + 'f/2.5': 2.519842, + 'f/2.8': 2.828427, + 'f/3.2': 3.174802, + 'f/3.4': 3.363586, + 'f/3.6': 3.563595, + 'f/4': 4.0, + 'f/4.5': 4.489848, + 'f/4.8': 4.756828, + 'f/5': 5.039684, + 'f/5.6': 5.656854, + 'f/6.4': 6.349604, + 'f/6.7': 6.727171, + 'f/7.1': 7.12719, + 'f/8': 8.0, + 'f/9': 8.979696, + 'f/9.5': 9.513657, + 'f/10': 10.07937, + 'f/11': 11.313708, + 'f/12.7': 12.699208, + 'f/13.5': 13.454343, + 'f/14.3': 14.254379, + 'f/16': 16.0, + 'f/18': 17.959393, + 'f/19': 19.027314, + 'f/20': 20.158737, + 'f/22': 22.627417, + 'f/25': 25.398417, + 'f/27': 26.908685, + 'f/28': 28.508759, + 'f/32': 32, + 'f/45': 45.254834, + 'f/64': 64, +} diff --git a/module/src/index.ts b/module/src/index.ts new file mode 100644 index 00000000..05d68468 --- /dev/null +++ b/module/src/index.ts @@ -0,0 +1,11 @@ +export { Lens } from './Lens' +export { createLensMaker } from './createLensMaker' +export { getApertureName, isApertureString } from './utilities/aperture' +export { preciseApertureMap as apertureMap } from './data/preciseApertureMap' +type DepthOfFieldDetails = DoFResult +export type { DepthOfFieldDetails } + +export { calculateDepthOfField } from './calculators/calculateDepthOfField' +export { calculateAperture } from './calculators/calculateAperture' +export { calculateFocalLength } from './calculators/calculateFocalLength' +export { calculateCropFactor } from './calculators/calculateCropFactor' diff --git a/package/src/types.d.ts b/module/src/types.d.ts similarity index 74% rename from package/src/types.d.ts rename to module/src/types.d.ts index 90d34a93..0946175b 100644 --- a/package/src/types.d.ts +++ b/module/src/types.d.ts @@ -33,3 +33,17 @@ type DoFResult = { coc: number // Circle of confusion toString: () => string } + +type FocalLengthResult = { + focalLength: number + focalLengthEquiv: number // The focal length in 35mm-equivalency +} + +type ApertureResult = { + aperture: number + fStop: undefined | keyof typeof import('./data/preciseApertureMap').preciseApertureMap +} + +type CropFactorResult = { + cropFactor: number +} diff --git a/package/src/utilities/aperture.test.ts b/module/src/utilities/aperture.test.ts similarity index 67% rename from package/src/utilities/aperture.test.ts rename to module/src/utilities/aperture.test.ts index 373c5dcb..c0e7d819 100644 --- a/package/src/utilities/aperture.test.ts +++ b/module/src/utilities/aperture.test.ts @@ -1,10 +1,12 @@ import { builtInDefaults } from '../Lens' -import { toActualAperture } from './aperture' +import { getApertureName, toActualAperture } from './aperture' +import { preciseApertureMap } from '../data/preciseApertureMap' +import { objectKeysArray } from './objectKeysArray' const defaultOptionsAperture = builtInDefaults.aperture const defaultOptionsApertureNumber = Number(builtInDefaults.aperture.replace('f/', '')) -const customSettingsApertures = [undefined /* , 2.4, 'f/3.4' */] +const customSettingsApertures = [undefined, 2.4, 'f/3.4'] describe.each(customSettingsApertures)('toActualAperture', (customSettingsAperture) => { describe(`customSettingsAperture: ${customSettingsAperture}`, () => { @@ -63,3 +65,35 @@ describe.each(customSettingsApertures)('toActualAperture', (customSettingsApertu }) }) }) + +describe('getApertureName', () => { + describe.each(objectKeysArray(preciseApertureMap))('Verify every value in the mapping', (fstop) => { + test(`${preciseApertureMap[fstop]} -> ${fstop}`, () => { + expect(getApertureName(preciseApertureMap[fstop])).toBe(fstop) + }) + + test(`1% below the documented value still finds the applicable f-stop: ${ + preciseApertureMap[fstop] * 0.99 + } -> ${fstop}`, () => { + expect(getApertureName(preciseApertureMap[fstop] * 0.99)).toBe(fstop) + }) + + test(`1% above the documented value still finds the applicable f-stop: ${ + preciseApertureMap[fstop] * 1.01 + } -> ${fstop}`, () => { + expect(getApertureName(preciseApertureMap[fstop] * 1.01)).toBe(fstop) + }) + }) + + test(`Documented value 5.039684 results in the value it's mapped to, f/5`, () => { + expect(getApertureName(5.039684)).toBe('f/5') + }) + + test(`Undocumented value 5.01 results in the nearest aperture, f/5`, () => { + expect(getApertureName(5.01)).toBe('f/5') + }) + + test(`Undocumented value 4.99 results in the nearest aperture, f/5`, () => { + expect(getApertureName(4.99)).toBe('f/5') + }) +}) diff --git a/package/src/utilities/aperture.ts b/module/src/utilities/aperture.ts similarity index 79% rename from package/src/utilities/aperture.ts rename to module/src/utilities/aperture.ts index face240d..deb36915 100644 --- a/package/src/utilities/aperture.ts +++ b/module/src/utilities/aperture.ts @@ -1,52 +1,8 @@ +import { preciseApertureMap } from '../data/preciseApertureMap' import { objectKeysArray } from './objectKeysArray' const apertureRegex = /^f\/(\d+(?:\.\d+)?)$/ -// Map of human-friendly values to their precise numeric values -export const preciseApertureMap: Record = { - 'f/1': 1, - 'f/1.2': 1.189207, - 'f/1.4': 1.414214, - 'f/1.6': 1.587401, - 'f/1.7': 1.681793, - 'f/1.8': 1.781797, - 'f/2': 2.0, - 'f/2.2': 2.244924, - 'f/2.4': 2.378414, - 'f/2.5': 2.519842, - 'f/2.8': 2.828427, - 'f/3.2': 3.174802, - 'f/3.4': 3.363586, - 'f/3.6': 3.563595, - 'f/4': 4.0, - 'f/4.5': 4.489848, - 'f/4.8': 4.756828, - 'f/5': 5.039684, - 'f/5.6': 5.656854, - 'f/6.4': 6.349604, - 'f/6.7': 6.727171, - 'f/7.1': 7.12719, - 'f/8': 8.0, - 'f/9': 8.979696, - 'f/9.5': 9.513657, - 'f/10': 10.07937, - 'f/11': 11.313708, - 'f/12.7': 12.699208, - 'f/13.5': 13.454343, - 'f/14.3': 14.254379, - 'f/16': 16.0, - 'f/18': 17.959393, - 'f/19': 19.027314, - 'f/20': 20.158737, - 'f/22': 22.627417, - 'f/25': 25.398417, - 'f/27': 26.908685, - 'f/28': 28.508759, - 'f/32': 32, - 'f/45': 45.254834, - 'f/64': 64, -} - // It's possible that lenses might excede what we have listed in this map, so let's give the benefit of the doubt and accept those numbers as-is. To do this, we need to know the smallest and largest values in our map. // We need to ignore test coverage for this line, otherwise it is marked as uncovered; this happens because we're ignoring the thrown exception near the end /* istanbul ignore next line */ @@ -58,7 +14,7 @@ const largestDocumentedAperture = sortedValues.slice(-1)[0] * Takes a human-friendly string and returns a precise numeric value that is equivalent * @example 'f/5' => 5.039684 */ -function getPreciseAperture(humanValue: string): number | undefined { +function getPreciseAperture(humanValue: ApertureString): number | undefined { if ( humanValue in preciseApertureMap && Object.prototype.hasOwnProperty.call(preciseApertureMap, humanValue) && @@ -71,13 +27,25 @@ function getPreciseAperture(humanValue: string): number | undefined { return } +/** + * Finds the nearest known aperture to a given number + * @example nearestValue(1.99) // 2.0 + * + * @param {number} targetAperture the ideal value for which the nearest or equal should be found + */ +const nearestValue = (targetAperture: number) => + sortedValues.reduce((p, n) => (Math.abs(p) > Math.abs(n - targetAperture) ? n - targetAperture : p), Infinity) + + targetAperture + /** * Takes a numeric value and returns a human-friendly string that is equivalent * @example 5.039684 => 'f/5' */ export function getApertureName(value: number): keyof typeof preciseApertureMap | undefined { + const asKnownAperture = nearestValue(value) + return objectKeysArray(preciseApertureMap).find((key) => { - return preciseApertureMap[key] === value + return preciseApertureMap[key] === asKnownAperture }) } diff --git a/package/src/utilities/combineSettings.test.ts b/module/src/utilities/combineSettings.test.ts similarity index 100% rename from package/src/utilities/combineSettings.test.ts rename to module/src/utilities/combineSettings.test.ts diff --git a/package/src/utilities/combineSettings.ts b/module/src/utilities/combineSettings.ts similarity index 100% rename from package/src/utilities/combineSettings.ts rename to module/src/utilities/combineSettings.ts diff --git a/package/src/utilities/decimalAdjust.test.ts b/module/src/utilities/decimalAdjust.test.ts similarity index 100% rename from package/src/utilities/decimalAdjust.test.ts rename to module/src/utilities/decimalAdjust.test.ts diff --git a/package/src/utilities/decimalAdjust.ts b/module/src/utilities/decimalAdjust.ts similarity index 100% rename from package/src/utilities/decimalAdjust.ts rename to module/src/utilities/decimalAdjust.ts diff --git a/package/src/utilities/formatFeet.test.ts b/module/src/utilities/formatFeet.test.ts similarity index 100% rename from package/src/utilities/formatFeet.test.ts rename to module/src/utilities/formatFeet.test.ts diff --git a/package/src/utilities/formatFeet.ts b/module/src/utilities/formatFeet.ts similarity index 100% rename from package/src/utilities/formatFeet.ts rename to module/src/utilities/formatFeet.ts diff --git a/package/src/utilities/objectKeysArray.ts b/module/src/utilities/objectKeysArray.ts similarity index 100% rename from package/src/utilities/objectKeysArray.ts rename to module/src/utilities/objectKeysArray.ts diff --git a/module/src/utilities/units.ts b/module/src/utilities/units.ts new file mode 100644 index 00000000..ee9bb705 --- /dev/null +++ b/module/src/utilities/units.ts @@ -0,0 +1,29 @@ +/** + * Returns a number that a value can be multiplied by to assure the value is metric + * + * @param imperialUnits true if value is in feet instead of meters + */ +function getUnitMultiplier(imperialUnits = false): number { + // e.g. 1 foot is 30.48% of 1 meter + return imperialUnits ? 0.3048 : 1 +} + +/** + * Convert a value from meters or feet, to millimeters + * + * @param value value in meters or feet + * @param imperialUnits true if value is in feet instead of meters + */ +export function toMillimeters(value: number, imperialUnits = false): number { + return value * 1000 * getUnitMultiplier(imperialUnits) +} + +/** + * Convert a value from millimeters to meters or feet + * + * @param value value in millimeters + * @param imperialUnits true if value should be in feet instead of meters + */ +export function fromMillimeters(value: number, imperialUnits = false): number { + return value / 1000.0 / getUnitMultiplier(imperialUnits) +} diff --git a/package/tsconfig.json b/module/tsconfig.json similarity index 100% rename from package/tsconfig.json rename to module/tsconfig.json diff --git a/package.json b/package.json index cec8cdd1..b7f4d2aa 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,19 @@ { "name": "@patik/dof", - "version": "3.0.0", + "version": "3.1.0", "private": true, "workspaces": [ - "package", + "module", "app" ], "scripts": { - "dev": "yarn --cwd package tsc && yarn --cwd app dev", + "dev": "yarn --cwd module tsc && yarn --cwd app dev", "start": "yarn --cwd app start", "start:ci": "yarn build && yarn --cwd app start", - "build": "yarn --cwd package lint && yarn --cwd package tsc && yarn --cwd app build", - "deploy": "yarn --cwd package lint && yarn --cwd package tsc && IS_DEPLOYMENT=true yarn --cwd app build", + "build": "yarn --cwd module lint && yarn --cwd module tsc && yarn --cwd app build", + "deploy": "yarn --cwd module lint && yarn --cwd module tsc && IS_DEPLOYMENT=true yarn --cwd app build", "clean": "git clean -fdx", - "cover": "yarn --cwd package test --collectCoverage", + "cover": "yarn --cwd module test --collectCoverage", "lint": "yarn workspaces run lint", "preversion": "yarn workspaces run preversion", "postversion": "git push && git push --tags", diff --git a/package/README.md b/package/README.md new file mode 100644 index 00000000..b742f5bd --- /dev/null +++ b/package/README.md @@ -0,0 +1,138 @@ +# [Depth of Field Calculator](https://patik.com/dof/) + +A JavaScript tool for calculating the depth of field of camera lenses + +## Node module + +The general idea is to create a **lens object**, with specified aperture, focal length, and crop factor values; and then retrieving the object's **depth of field object** for a given **distance** to the subject. + +### Install + +```sh +yarn add dof + +# or + +npm install dof +``` + +### Create a lens object + +```js +import { Lens } from 'dof' + +const lens = new Lens() +``` + +That will give us a lens object with default values: + +```js +lens.focalLength // 35 (millimeters) +lens.aperture // 'f/2' +lens.cropFactor // 1 (i.e. standard full frame) +``` + +Let's change those to fit the particular lens we have in mind: + +```js +const lens = new Lens(35, 'f/2.5', 1.62); +``` + +#### Reusing default values + +You can create function which generates lenses in bulk using your own set of defauls. For example, you might want to calculate the depth of field for many lenses with a common sensor crop factor of `1.62`: + +```js +const lensMaker = createLensMaker({ cropFactor: 1.62 }) + +const lens1 = lensMaker() +const lens2 = lensMaker({ aperture: 'f/3.6' }) +``` + +The complete list of configurable defaults: + +```js +focalLength: 35 // Number, in millimeters; this must be the actual focal length, not the 35mm equivalent value +aperture: 'f/2' // String in the format `"f/2.5"` +cropFactor: 1 // Floating point number; sensor's crop factor compared to full frame +``` + +#### Managing multiple lenses + +Lenses can be assigned arbitrary IDs to make it easier to keep track of them: + +```js +const lens = new Lens(35, 'f/2.5', 1.62, 'my-lens-ID');// Optional; string + +console.log(lens.id) // 'my-lens-ID' +``` + +### Calculate the depth of field + +To perform a calculation you must specify the distance between the camera and the subject. You can either set a default or pass a particular distance. The value must be a number as measured in meters (default) or feet. + +```js +// Specify distance directly +const result1 = lens.dof(5) // 5 mmeters, the default value +const result2 = lens.dof(22.6) // 22 meters and 60 centimeters + +// Imperial units +const result2 = lens.dof(15, true) // 15 feet, the default value +``` + +The `result` object contains several properties: + +```js +result.dof // Total length of the depth of field; float (e.g. `20.5`), meters or feet +result.eighthDof // One-eighth of the depth of field; float (e.g. `2.5625`), meters or feet +result.hf // Hyperfocal distance; float, meters or feet +result.near // Distance to the nearest edge of the in-focus field; float, meters or feet +result.far // Distance to the farthest edge of the in-focus field; float, meters or feet +result.coc // Circle of confusion; float, always millimeters +``` + +Note that the value of those properties may be `Infinity`. This is especially common with small sensor sizes (large crop factors), short focal lengths, and/or small apertures (large `f/` values). + +You can use the `.toString()` method to get the depth of field value as a string. For example, `20' 5.2"` is 20 feet, 5.2 inches. + +```js +const str = result.toString() +``` + +All properties have an equivalent string representation, accessible through the `.toString` property: + +```js +result.toString.dof // Same as `result.toString()` +result.toString.eighthDof +result.toString.hf +result.toString.near +result.toString.far +``` + +A shorthand way to quickly acquire the depth of field value: + +```js +const num = lens.dof().dof // float +const str = lens.dof().toString() // string +``` + +Or with a specific distance value: + +```js +const num = lens.dof(15).dof // float +const str = lens.dof(15).toString() // string +``` + +### TypeScript support + +`Lens()` instances use the `DepthOfFieldLens` type which can be imported from the module. + +## GUI Web App + +Calculate the depth of field for multiple lenses and compare them side-by-side + +**[Try it at patik.com/dof](https://patik.com/dof/)** + +[Documentation](https://patik.com/dof/about/) + +[![Screenshot of two lens configurations](./app/public/images/with-graph.png "Lens comparison")](https://patik.com/dof/#5m;Panasonic%2025mm,25,f-1.4,mft;Olympus%2025mm,25,f-1.8,mft) diff --git a/package/src/index.ts b/package/src/index.ts deleted file mode 100644 index 8b745eec..00000000 --- a/package/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { Lens } from './Lens' -export { createLensMaker } from './createLensMaker' -export { preciseApertureMap as apertureMap, getApertureName, isApertureString } from './utilities/aperture' -type DepthOfFieldDetails = DoFResult -export type { DepthOfFieldDetails } diff --git a/package/src/utilities/calculateDepthOfField.ts b/package/src/utilities/calculateDepthOfField.ts deleted file mode 100644 index 646c2fd9..00000000 --- a/package/src/utilities/calculateDepthOfField.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { decimalAdjust } from './decimalAdjust' -import { formatFeet } from './formatFeet' - -/** - * Returns the depth of field characteristics for a given lens' attributes - * - * @param focalLength - * @param aperture - * @param cropFactor - * @param distance - * @param imperialUnits - */ -export function calculateDepthOfField( - focalLength: number, - aperture: number, - cropFactor: number, - distance: number, - imperialUnits: boolean -): DoFResult { - const result: DoFResult = { - dof: 0, - focalLengthEquiv: 0, - eighthDof: 0, - hf: 0, - near: 0, - far: 0, - coc: 0, - } - // e.g. 1 foot is 30.48% of 1 meter - const unitMultiplier = imperialUnits ? 0.3048 : 1 - - // Convert to millimeters - distance = distance * 1000 * unitMultiplier - - // Get 35mm-equivalent focal length - result.focalLengthEquiv = decimalAdjust(cropFactor * focalLength) - - // Convert sensor crop factor to a multiplier - cropFactor = 1 / cropFactor - - result.coc = Math.round(0.03 * cropFactor * 1000) / 1000 - - result.hf = Math.pow(focalLength, 2) / (aperture * result.coc) + focalLength * 1.0 - result.near = (distance * (result.hf - focalLength)) / (result.hf + distance - 2 * focalLength) - result.far = (distance * (result.hf - focalLength)) / (result.hf - distance) - - // Undo conversion to millimeters - result.hf = result.hf / 1000.0 / unitMultiplier - result.near = result.near / 1000.0 / unitMultiplier - result.far = result.far / 1000.0 / unitMultiplier - - if (result.far <= 0) { - result.far = Infinity - result.dof = Infinity - } else { - result.dof = result.far - result.near - } - - result.eighthDof = result.dof / 8 - - if (imperialUnits) { - result.toString = function () { - return formatFeet(result.dof) - } - } else { - result.toString = function () { - return `${result.dof}` - } - } - - return result -}