├── lib
├── Lab.ts
├── SxPropsTheme.ts
├── wlmap.ts
├── cct.ts
├── download_blob.ts
├── duv.ts
├── locale.ts
├── use-interval.tsx
├── Oklab.ts
├── wl2rgb.ts
├── lm3calc.ts
├── exposure.ts
├── ab.ts
├── ssi.ts
├── CMF.ts
├── vector.ts
├── planckian.ts
├── spd.ts
├── flicker.ts
├── fftshift.ts
├── ble
│ ├── index.ts
│ └── lm3.ts
├── CIEConv.ts
├── tint.ts
├── matrix.ts
├── spline.ts
├── global.ts
├── cri.ts
└── spdIlluminants.ts
├── .npmrc
├── .gitignore
├── .eslintrc.json
├── public
└── favicon.ico
├── .prettierrc.js
├── next-env.d.ts
├── components
├── Tint.tsx
├── MyHead.tsx
├── CCT.tsx
├── Title
│ ├── PowerStatus.tsx
│ ├── index.tsx
│ └── Notifications.tsx
├── Duv.tsx
├── MeasControl.tsx
├── SetupDialog.tsx
├── WarningDialog.tsx
├── settings
│ ├── Parameters.tsx
│ ├── SettingsCard.tsx
│ └── ble.tsx
├── BatteryLevel.tsx
├── Chart.tsx
├── Polar.tsx
├── CIE1931.tsx
└── Memory.tsx
├── styles
├── globals.css
└── Home.module.css
├── README.md
├── tsconfig.json
├── .editorconfig
├── pages
├── _app.js
├── wb.tsx
├── setup
│ └── index.tsx
├── _document.js
├── index.tsx
├── color.tsx
├── ssi.tsx
├── exposure.tsx
├── spectrum.tsx
├── cri.tsx
├── flicker.tsx
└── text.tsx
└── package.json
/lib/Lab.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save-exact=true
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.next
2 | /node_modules
3 | *.swp
4 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lib/SxPropsTheme.ts:
--------------------------------------------------------------------------------
1 | import { SxProps } from '@mui/system';
2 | import { Theme } from '@mui/material';
3 |
4 | type SxPropsTheme = SxProps;
5 | export default SxPropsTheme;
6 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'always',
3 | bracketSpacing: true,
4 | printWidth: 120,
5 | semi: true,
6 | singleQuote: true,
7 | tabWidth: 4,
8 | trailingComma: 'es5',
9 | useTabs: true,
10 | };
11 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/components/Tint.tsx:
--------------------------------------------------------------------------------
1 | import TextField from '@mui/material/TextField';
2 |
3 | export default function Tint({ value }: { value: number }) {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/lib/wlmap.ts:
--------------------------------------------------------------------------------
1 | export default function wlMap(
2 | fn: (wl: number, index: number) => U,
3 | increment: number = 5,
4 | min: number = 380,
5 | max: number = 780
6 | ) {
7 | return Array.from({ length: (max - min) / increment + 1 }, (_, index) => fn(min + index * increment, index));
8 | }
9 |
--------------------------------------------------------------------------------
/components/MyHead.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 |
3 | export default function MyHead({ title }: { title?: string }) {
4 | return (
5 |
6 | {`Open Light Master ${title ?? ''}`}
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
--------------------------------------------------------------------------------
/lib/cct.ts:
--------------------------------------------------------------------------------
1 | export default function calcCCT(x: number, y: number) {
2 | // McCamy’s (CCT) formula
3 | // RFE Explore better formulas
4 | // - Hernández-Andrés et al. formula
5 | // - Accurate method for computing correlated color temperature, Changjun Li et al.
6 | const n = (x - 0.332) / (0.1858 - y);
7 | const CCT = 449 * n * n * n + 3525 * n * n + 6823.3 * n + 5520.33;
8 |
9 | return CCT;
10 | }
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Open Light Master
2 | =================
3 |
4 | https://open-light-master.vercel.app/
5 |
6 | An open source "web app" for the Opple Light Master 3 (III).
7 |
8 | Features
9 | --------
10 |
11 | - Exposure meter
12 | - Lux, ft⋅cd
13 | - CCT, WB, Duv, Tint,
14 | - CRI (Ra, R1-R14), SSI
15 | - Spectrum (SPD)
16 | - Flicker analysis (FFT)
17 | - CSV Export, printable reports
18 | - Memory Save/Recall
19 |
--------------------------------------------------------------------------------
/components/CCT.tsx:
--------------------------------------------------------------------------------
1 | import TextField from '@mui/material/TextField';
2 | import InputAdornment from '@mui/material/InputAdornment';
3 |
4 | export default function CCT({ value }: { value: number }) {
5 | return (
6 | K,
13 | }}
14 | />
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/components/Title/PowerStatus.tsx:
--------------------------------------------------------------------------------
1 | import { BatteryLevel } from 'components/BatteryLevel';
2 | import { useGlobalState } from 'lib/global';
3 | import { useEffect } from 'react';
4 |
5 | export default function PowerStatus() {
6 | // @ts-ignore
7 | // The whole battery_status stuff is commented out for now
8 | const [batteryLevel] = useGlobalState('res_battery_level');
9 |
10 | useEffect(() => {}, [batteryLevel]);
11 |
12 | return ;
13 | }
14 |
--------------------------------------------------------------------------------
/lib/download_blob.ts:
--------------------------------------------------------------------------------
1 | export default function downloadBlob(blob: Blob, filename: string) {
2 | const url = URL.createObjectURL(blob);
3 | const a = document.createElement('a');
4 |
5 | a.href = url;
6 | a.download = filename;
7 |
8 | const clickHandler = () => {
9 | setTimeout(() => {
10 | URL.revokeObjectURL(url);
11 | a.removeEventListener('click', clickHandler);
12 | }, 150);
13 | };
14 |
15 | a.addEventListener('click', clickHandler, false);
16 | a.click();
17 |
18 | return a;
19 | }
20 |
--------------------------------------------------------------------------------
/lib/duv.ts:
--------------------------------------------------------------------------------
1 | const k = Object.freeze([-0.471106, 1.925865, -2.4243787, 1.5317403, -0.5179722, 0.0893944, -0.00616793]);
2 |
3 | // ANSI C78.377-2011
4 | export default function calcDuv(u: number, v: number) {
5 | const Lfp = Math.sqrt(Math.pow(u - 0.292, 2) + Math.pow(v - 0.24, 2));
6 | const a = Math.acos((u - 0.292) / Lfp);
7 | const Lbb =
8 | k[6] * Math.pow(a, 6) +
9 | k[5] * Math.pow(a, 5) +
10 | k[4] * Math.pow(a, 4) +
11 | k[3] * Math.pow(a, 3) +
12 | k[2] * Math.pow(a, 2) +
13 | k[1] * a +
14 | k[0];
15 |
16 | return Lfp - Lbb;
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "baseUrl": ".",
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext"
9 | ],
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "strict": false,
13 | "forceConsistentCasingInFileNames": true,
14 | "noEmit": true,
15 | "esModuleInterop": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "jsx": "preserve",
21 | "incremental": true
22 | },
23 | "include": [
24 | "next-env.d.ts",
25 | "**/*.ts",
26 | "**/*.tsx"
27 | ],
28 | "exclude": [
29 | "node_modules"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/components/Duv.tsx:
--------------------------------------------------------------------------------
1 | import TextField from '@mui/material/TextField';
2 |
3 | export default function Duv({ value }: { value: number }) {
4 | // "The concept of correlated color temperature should not be used if the
5 | // chromaticity of the test source differs more than Δuv = 5×10-2 from the
6 | // Planckian radiator."
7 | // Schanda, János (2007). "3: CIE Colorimetry".
8 | // Colorimetry: Understanding the CIE System.
9 | const duvErr = Math.abs(value) > 5e-2;
10 |
11 | return (
12 | >}
20 | />
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/components/MeasControl.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@mui/material/Box';
2 | import IconButton from '@mui/material/IconButton';
3 | import PauseCircleIcon from '@mui/icons-material/PauseCircle';
4 | import PlayCircleIcon from '@mui/icons-material/PlayCircle';
5 | import { useGlobalState } from 'lib/global';
6 |
7 | export default function MeasControl() {
8 | const [running, setRunning] = useGlobalState('running');
9 | const [lm3] = useGlobalState('lm3');
10 | const toggle = () => setRunning(!running);
11 |
12 | return (
13 |
14 |
21 | {lm3 && running ? : }
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/lib/locale.ts:
--------------------------------------------------------------------------------
1 | export function getClientLang(): string {
2 | if (typeof Intl !== 'undefined') {
3 | try {
4 | return Intl.NumberFormat().resolvedOptions().locale;
5 | } catch (_err) {
6 | if (window.navigator.languages) {
7 | return window.navigator.languages[0];
8 | } else {
9 | // @ts-ignore
10 | return window.navigator.userLanguage || window.navigator.language;
11 | }
12 | }
13 | }
14 |
15 | return 'en-US';
16 | }
17 |
18 | export function getDayPeriod(date: Date): string {
19 | return new Intl.DateTimeFormat(getClientLang(), { dayPeriod: 'short' }).format(date);
20 | }
21 |
22 | export function getDateTime(date: Date): string {
23 | return new Intl.DateTimeFormat(getClientLang(), {
24 | day: '2-digit',
25 | month: '2-digit',
26 | year: '2-digit',
27 | hour: '2-digit',
28 | minute: '2-digit',
29 | }).format(date);
30 | }
31 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | indent_size = 4
6 | tab_width = 4
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [{*.json,*.json.example,*.gyp,*.yml,*.yaml}]
13 | indent_style = space
14 | indent_size = 2
15 |
16 | [{*.py,*.asm}]
17 | indent_style = space
18 |
19 | [*.py]
20 | indent_size = 4
21 |
22 | [*.asm]
23 | indent_size = 8
24 |
25 | [*.md]
26 | trim_trailing_whitespace = false
27 |
28 | # Ideal settings - some plugins might support these.
29 | [*.js]
30 | quote_type = single
31 |
32 | [{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}]
33 | curly_bracket_next_line = false
34 | spaces_around_operators = true
35 | spaces_around_brackets = outside
36 | # close enough to 1TB
37 | indent_brace_style = K&R
38 |
--------------------------------------------------------------------------------
/lib/use-interval.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 |
3 | // https://overreacted.io/making-setinterval-declarative-with-react-hooks/
4 | // useInterval(() => {
5 | // // Your custom logic here
6 | // setCount(count + 1);
7 | // }, 1000);
8 |
9 | function useInterval(callback: () => Promise, delay: number | null) {
10 | const savedCallback = useRef<() => Promise>();
11 |
12 | // Remember the latest callback.
13 | useEffect(() => {
14 | savedCallback.current = callback;
15 | }, [callback]);
16 |
17 | // Set up the interval.
18 | useEffect(() => {
19 | function tick() {
20 | savedCallback.current().catch(console.error);
21 | }
22 | if (delay !== null) {
23 | tick();
24 | let id = setInterval(tick, delay);
25 | return () => clearInterval(id);
26 | }
27 | }, [delay]);
28 | }
29 |
30 | export default useInterval;
31 |
--------------------------------------------------------------------------------
/lib/Oklab.ts:
--------------------------------------------------------------------------------
1 | import { matrixMul } from './matrix';
2 |
3 | const M1 = [
4 | [0.8189330101, 0.3618667424, -0.1288597137],
5 | [0.0329845436, 0.9293118715, 0.0361456387],
6 | [0.0482003018, 0.2643662691, 0.633851707],
7 | ];
8 | const M2 = [
9 | [0.2104542553, 0.793617785, -0.0040720468],
10 | [1.9779984951, -2.428592205, 0.4505937099],
11 | [0.0259040371, 0.7827717662, -0.808675766],
12 | ];
13 |
14 | export function XYZD65toOklab(X: number, Y: number, Z: number): [number, number, number] {
15 | const [[l], [m], [s]] = matrixMul(M1, [[X], [Y], [Z]]);
16 | const q = 1 / 3;
17 | const lms_prime = [[l ** q], [m ** q], [s ** q]];
18 | const Lab = matrixMul(M2, lms_prime);
19 |
20 | return [Lab[0][0], Lab[1][0], Lab[2][0]] as const;
21 | }
22 |
23 | export function Oklab2Oklch(L: number, a: number, b: number) {
24 | const C = Math.sqrt(a ** 2 + b ** 2);
25 | const h = Math.atan2(b, a);
26 |
27 | return [L, C, h] as const;
28 | }
29 |
30 | export function Oklch2Oklab(L: number, C: number, h: number) {
31 | const a = C * Math.cos(h);
32 | const b = C * Math.sin(h);
33 |
34 | return [L, a, b] as const;
35 | }
36 |
--------------------------------------------------------------------------------
/lib/wl2rgb.ts:
--------------------------------------------------------------------------------
1 | export default function wavelengthToColor(wl: number): [string, number, number, number, number] {
2 | let R: number;
3 | let G: number;
4 | let B: number;
5 | let alpha: number;
6 |
7 | if (wl >= 380 && wl < 440) {
8 | R = (-1 * (wl - 440)) / (440 - 380);
9 | G = 0;
10 | B = 1;
11 | } else if (wl >= 440 && wl < 490) {
12 | R = 0;
13 | G = (wl - 440) / (490 - 440);
14 | B = 1;
15 | } else if (wl >= 490 && wl < 510) {
16 | R = 0;
17 | G = 1;
18 | B = (-1 * (wl - 510)) / (510 - 490);
19 | } else if (wl >= 510 && wl < 580) {
20 | R = (wl - 510) / (580 - 510);
21 | G = 1;
22 | B = 0;
23 | } else if (wl >= 580 && wl < 645) {
24 | R = 1;
25 | G = (-1 * (wl - 645)) / (645 - 580);
26 | B = 0.0;
27 | } else if (wl >= 645 && wl <= 780) {
28 | R = 1;
29 | G = 0;
30 | B = 0;
31 | } else {
32 | R = 0;
33 | G = 0;
34 | B = 0;
35 | }
36 |
37 | if (wl > 780 || wl < 380) {
38 | alpha = 0;
39 | } else if (wl > 700) {
40 | alpha = (780 - wl) / (780 - 700);
41 | } else if (wl < 420) {
42 | alpha = (wl - 380) / (420 - 380);
43 | } else {
44 | alpha = 1;
45 | }
46 |
47 | return [`rgba(${R * 100}%,${G * 100}%,${B * 100}%,${alpha})`, R, G, B, alpha];
48 | }
49 |
--------------------------------------------------------------------------------
/lib/lm3calc.ts:
--------------------------------------------------------------------------------
1 | import { calcCRI } from './cri';
2 | import { interpolateSPD } from './spd';
3 | import { normalize2 } from './vector';
4 |
5 | type MeasurementData = {
6 | V1: number;
7 | B1: number;
8 | G1: number;
9 | Y1: number;
10 | O1: number;
11 | R1: number;
12 | Lux: number;
13 | CCT: number;
14 | };
15 |
16 | export default function lm3CalcCRI(meas: MeasurementData) {
17 | const spd = interpolateSPD([
18 | { l: 450, v: meas.V1 },
19 | { l: 500, v: meas.B1 },
20 | { l: 550, v: meas.G1 },
21 | { l: 570, v: meas.Y1 },
22 | { l: 600, v: meas.O1 },
23 | { l: 650, v: meas.R1 },
24 | ]);
25 |
26 | return calcCRI(
27 | meas.CCT,
28 | Float64Array.from(spd, ({ v }) => v)
29 | );
30 | }
31 |
32 | export function lm3NormSPD(meas: MeasurementData) {
33 | const norm = normalize2([meas.V1, meas.B1, meas.G1, meas.Y1, meas.O1, meas.R1]);
34 | return Object.freeze([
35 | {
36 | l: 450,
37 | v: norm[0],
38 | },
39 | {
40 | l: 500,
41 | v: norm[1],
42 | },
43 | {
44 | l: 550,
45 | v: norm[2],
46 | },
47 | {
48 | l: 570,
49 | v: norm[3],
50 | },
51 | {
52 | l: 600,
53 | v: norm[4],
54 | },
55 | {
56 | l: 650,
57 | v: norm[5],
58 | },
59 | ]);
60 | }
61 |
--------------------------------------------------------------------------------
/components/SetupDialog.tsx:
--------------------------------------------------------------------------------
1 | import { useState, Fragment, ReactNode } from 'react';
2 | import Button from '@mui/material/Button';
3 | import Dialog from '@mui/material/Dialog';
4 | import DialogActions from '@mui/material/DialogActions';
5 | import DialogContent from '@mui/material/DialogContent';
6 | import DialogTitle from '@mui/material/DialogTitle';
7 |
8 | export default function SetupDialog({
9 | btnText,
10 | title,
11 | children,
12 | }: {
13 | btnText: ReactNode;
14 | title: string;
15 | children: ReactNode;
16 | }) {
17 | const [open, setOpen] = useState(false);
18 |
19 | const handleClickOpen = () => {
20 | setOpen(true);
21 | };
22 |
23 | const handleClose = () => {
24 | setOpen(false);
25 | };
26 |
27 | return (
28 |
29 |
32 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/components/WarningDialog.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@mui/material/Button';
2 | import Dialog from '@mui/material/Dialog';
3 | import DialogActions from '@mui/material/DialogActions';
4 | import DialogContent from '@mui/material/DialogContent';
5 | import DialogContentText from '@mui/material/DialogContentText';
6 | import DialogTitle from '@mui/material/DialogTitle';
7 |
8 | export default function WarningDialog({
9 | title,
10 | show,
11 | handleCancel,
12 | handleContinue,
13 | children: message,
14 | }: {
15 | title: string;
16 | show: boolean;
17 | handleCancel: (e: any) => void;
18 | handleContinue: (e: any) => void;
19 | children?: any;
20 | }) {
21 | return (
22 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import { ThemeProvider, createTheme } from '@mui/material/styles';
2 | import '../styles/globals.css'
3 | import CssBaseline from '@mui/material/CssBaseline';
4 | import { red } from '@mui/material/colors';
5 | import PropTypes from 'prop-types';
6 | import createCache from '@emotion/cache';
7 | import { CacheProvider } from '@emotion/react';
8 |
9 | export const cache = createCache({
10 | key: 'css',
11 | prepend: true,
12 | });
13 |
14 | // Create a theme instance.
15 | const theme = createTheme({
16 | palette: {
17 | background: {
18 | default: '#fafafa',
19 | },
20 | primary: {
21 | main: '#1976D2',
22 | },
23 | secondary: {
24 | main: red.A400,
25 | },
26 | error: {
27 | main: red.A400,
28 | },
29 | },
30 | });
31 |
32 | function App({ Component, pageProps }) {
33 | return (
34 |
35 |
36 | {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
44 | export default App
45 |
46 | App.propTypes = {
47 | Component: PropTypes.elementType.isRequired,
48 | emotionCache: PropTypes.object,
49 | pageProps: PropTypes.object.isRequired,
50 | };
51 |
--------------------------------------------------------------------------------
/pages/wb.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@mui/system/Box';
2 | import Container from '@mui/material/Container';
3 | import Paper from '@mui/material/Paper';
4 | import CCT from 'components/CCT';
5 | import CIE1931 from 'components/CIE1931';
6 | import Duv from 'components/Duv';
7 | import Memory from 'components/Memory';
8 | import MyHead from 'components/MyHead';
9 | import Tint from 'components/Tint';
10 | import Title from 'components/Title';
11 | import { useGlobalState, useMemoryRecall } from 'lib/global';
12 |
13 | export default function Text() {
14 | const [meas] = useGlobalState('res_lm_measurement');
15 | const secondary = useMemoryRecall().map(({ name, meas }) => ({
16 | label: name,
17 | Ex: meas.Ex,
18 | Ey: meas.Ey,
19 | CCT: meas.CCT,
20 | Duv: meas.Duv,
21 | }));
22 |
23 | return (
24 |
25 |
26 |
27 | OLM - WB
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/components/settings/Parameters.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import TextField from '@mui/material/TextField';
3 | import DisplaySettingsIcon from '@mui/icons-material/DisplaySettings';
4 | import { SettingsCard, iconStyle } from './SettingsCard';
5 | import { useGlobalState } from 'lib/global';
6 |
7 | export default function Parameters() {
8 | const [hz, setHz] = useGlobalState('hz');
9 | const [avg, setAvg] = useGlobalState('avg');
10 | const [hzError, setHzError] = useState('');
11 | const [avgError, setAvgError] = useState('');
12 |
13 | return (
14 | } title="Parameters">
15 | {
22 | const newHz = parseInt(e.target.value);
23 | if (Number.isNaN(newHz) || newHz <= 0 || newHz > 100) {
24 | setHzError('Incorrect entry.');
25 | } else {
26 | if (hzError) setHzError('');
27 | setHz(newHz);
28 | }
29 | }}
30 | />
31 |
32 |
33 | {
40 | const newAvg = parseInt(e.target.value);
41 | if (Number.isNaN(newAvg) || newAvg < 0 || newAvg > 300) {
42 | setAvgError('Incorrect entry.');
43 | } else {
44 | if (avgError) setAvgError('');
45 | setAvg(newAvg);
46 | }
47 | }}
48 | />
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "open-light-master",
3 | "version": "1.0.0",
4 | "private": true,
5 | "engines": {
6 | "node": ">=20.0.0"
7 | },
8 | "scripts": {
9 | "dev": "NEXT_TELEMETRY_DISABLED=${NEXT_TELEMETRY_DISABLED:-1} next dev --turbopack",
10 | "build": "NEXT_TELEMETRY_DISABLED=${NEXT_TELEMETRY_DISABLED:-1} next build",
11 | "clean": "rm -rf .next",
12 | "start": "NEXT_TELEMETRY_DISABLED=${NEXT_TELEMETRY_DISABLED:-1} next start",
13 | "prettier": "prettier --write './{pages,components,lib}/**/*.{ts,tsx}'",
14 | "lint": "NEXT_TELEMETRY_DISABLED=${NEXT_TELEMETRY_DISABLED:-1} next lint"
15 | },
16 | "dependencies": {
17 | "@emotion/css": "11.13.5",
18 | "@emotion/react": "11.14.0",
19 | "@emotion/server": "11.11.0",
20 | "@emotion/styled": "11.14.0",
21 | "@mui/icons-material": "5.15.15",
22 | "@mui/material": "5.15.15",
23 | "@mui/x-data-grid": "7.1.1",
24 | "chartjs-plugin-annotation": "3.0.1",
25 | "chartjs-plugin-datalabels": "2.2.0",
26 | "next": "15.1.9",
27 | "prop-types": "15.8.1",
28 | "react": "18.3.1",
29 | "react-chartjs-2": "5.2.0",
30 | "react-dom": "18.3.1",
31 | "react-hooks-global-state": "2.1.0",
32 | "react-material-ui-carousel": "3.4.2",
33 | "webfft": "1.0.3"
34 | },
35 | "devDependencies": {
36 | "@types/node": "20.14.8",
37 | "@types/react": "18.3.12",
38 | "@types/web-bluetooth": "0.0.21",
39 | "eslint": "9.21.0",
40 | "eslint-config-next": "15.2.1",
41 | "prettier": "3.5.3",
42 | "typescript": "5.8.2"
43 | },
44 | "overrides": {
45 | "@types/react": "18.3.12"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/pages/setup/index.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@mui/material/Box';
2 | import Container from '@mui/material/Container';
3 | import Grid from '@mui/material/Grid';
4 | import MyHead from 'components/MyHead';
5 | import Parameters from 'components/settings/Parameters';
6 | import Title from 'components/Title';
7 | import { MemorySettings } from 'components/Memory';
8 | import Ble from 'components/settings/ble';
9 | import { BLE_SERVICE_UUID as LM3_SERVICE_UUID, createLm3 } from 'lib/ble/lm3';
10 | import { getGlobalState, useGlobalState } from 'lib/global';
11 | import { Paired } from 'lib/ble';
12 |
13 | function LM3() {
14 | const [, setDevice] = useGlobalState('lm3');
15 | const connectCb = async (server: BluetoothRemoteGATTServer) => {
16 | const lm3 = await createLm3(server);
17 | await lm3.startNotifications();
18 | await lm3.readCal();
19 | lm3.startMeasuring((1 / getGlobalState('hz')) * 1000, getGlobalState('avg'));
20 | setDevice(lm3);
21 | };
22 | const disconnectCb = (_btd: Paired) => {
23 | setDevice(null);
24 | };
25 |
26 | return (
27 |
34 | );
35 | }
36 |
37 | export default function Setup() {
38 | return (
39 |
40 |
41 |
42 | Setup
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/lib/exposure.ts:
--------------------------------------------------------------------------------
1 | const fStops = Object.freeze([
2 | 0.5, 0.7, 0.8, 1.0, 1.2, 1.4, 1.7, 2.0, 2.4, 2.8, 3.3, 4.0, 4.8, 5.6, 6.7, 8.0, 9.5, 11, 13, 16, 19, 22, 27, 32, 38,
3 | 45, 54, 64, 76, 90, 107, 128, 180, 256,
4 | ]);
5 |
6 | const sspeeds = Object.freeze([
7 | 1 / 8000,
8 | 1 / 4000,
9 | 1 / 2000,
10 | 1 / 1000,
11 | 1 / 800,
12 | 1 / 500,
13 | 1 / 400,
14 | 1 / 320,
15 | 1 / 250,
16 | 1 / 200,
17 | 1 / 125,
18 | 1 / 60,
19 | 1 / 30,
20 | 1 / 15,
21 | 1 / 8,
22 | 1 / 4,
23 | 1 / 2,
24 | 1,
25 | 5 / 6,
26 | 2 / 3,
27 | 2,
28 | 4,
29 | 8,
30 | 15,
31 | 25,
32 | 30,
33 | 60,
34 | 120,
35 | ]);
36 |
37 | function closest(needle: number, haystack: readonly number[]): number {
38 | return haystack.reduce((a, b) => {
39 | let aDiff = Math.abs(a - needle);
40 | let bDiff = Math.abs(b - needle);
41 |
42 | if (aDiff === bDiff) {
43 | return a > b ? a : b;
44 | } else {
45 | return bDiff < aDiff ? b : a;
46 | }
47 | });
48 | }
49 |
50 | export function closestShutter(shutter: number): number {
51 | return closest(shutter, sspeeds);
52 | }
53 |
54 | export function closestAperture(fstop: number): number {
55 | return closest(fstop, fStops);
56 | }
57 |
58 | export function calcEV(lux: number, iso: number = 100, gain: number = 0): number {
59 | const C = 250;
60 | const EV100 = Math.log2((lux * 100) / C);
61 |
62 | if (iso === 100) {
63 | return EV100 + gain / 6;
64 | }
65 |
66 | return EV100 + Math.log2(iso / 100) + gain / 6;
67 | }
68 |
69 | export function calcShutter(ev: number, fstop: number): number {
70 | return (fstop * fstop) / 2 ** ev;
71 | }
72 |
73 | export function calcFstop(ev: number, shutter: number): number {
74 | return Math.sqrt(shutter * 2 ** ev);
75 | }
76 |
--------------------------------------------------------------------------------
/components/BatteryLevel.tsx:
--------------------------------------------------------------------------------
1 | import BatteryUnknownIcon from '@mui/icons-material/BatteryUnknown';
2 | import Battery0Icon from '@mui/icons-material/BatteryAlert';
3 | import Battery20Icon from '@mui/icons-material/Battery20';
4 | import Battery30Icon from '@mui/icons-material/Battery30';
5 | import Battery50Icon from '@mui/icons-material/Battery50';
6 | import Battery60Icon from '@mui/icons-material/Battery60';
7 | import Battery80Icon from '@mui/icons-material/Battery80';
8 | import Battery90Icon from '@mui/icons-material/Battery90';
9 | import Battery100Icon from '@mui/icons-material/BatteryFull';
10 | import ElectricalServicesIcon from '@mui/icons-material/ElectricalServices';
11 | import SxPropsTheme from 'lib/SxPropsTheme';
12 |
13 | const style: SxPropsTheme = {
14 | verticalAlign: 'center',
15 | fontSize: '25px !important',
16 | };
17 |
18 | function BatteryIcon({ batteryLevel }: { batteryLevel: number }) {
19 | return batteryLevel < 0 ? (
20 |
21 | ) : batteryLevel < 20 ? (
22 |
23 | ) : batteryLevel < 30 ? (
24 |
25 | ) : batteryLevel < 50 ? (
26 |
27 | ) : batteryLevel < 60 ? (
28 |
29 | ) : batteryLevel < 80 ? (
30 |
31 | ) : batteryLevel < 90 ? (
32 |
33 | ) : batteryLevel < 100 ? (
34 |
35 | ) : (
36 |
37 | );
38 | }
39 |
40 | export function BatteryLevel({ batteryLevel }: { batteryLevel: number }) {
41 | return ;
42 | }
43 |
44 | export function PowerAdapter() {
45 | return ;
46 | }
47 |
--------------------------------------------------------------------------------
/lib/ab.ts:
--------------------------------------------------------------------------------
1 | export function arrayBufferToString(buffer: ArrayBuffer, encoding: string): Promise {
2 | return new Promise((resolve) => {
3 | const blob = new Blob([buffer], { type: 'text/plain' });
4 | const reader = new FileReader();
5 |
6 | reader.onload = (e) => resolve(e.target.result as string);
7 | reader.readAsText(blob, encoding);
8 | });
9 | }
10 |
11 | export function stringToArrayBuffer(str: string, encoding: string): Promise {
12 | return new Promise((resolve) => {
13 | const blob = new Blob([str], { type: `text/plain;charset=${encoding}` });
14 | const reader = new FileReader();
15 |
16 | reader.onload = (evt) => resolve(evt.target.result as ArrayBuffer);
17 | reader.readAsArrayBuffer(blob);
18 | });
19 | }
20 |
21 | export function arrayBufferToBase64(buf: ArrayBuffer): string {
22 | if (typeof window === 'undefined') {
23 | return Buffer.from(buf).toString('base64');
24 | } else {
25 | return window.btoa(String.fromCharCode.apply(null, new Uint8Array(buf)));
26 | }
27 | }
28 |
29 | export function base64ToString(str: string): string {
30 | if (typeof window === 'undefined') {
31 | return Buffer.from(str, 'base64').toString();
32 | } else {
33 | return window.atob(str);
34 | }
35 | }
36 |
37 | export async function stringToBase64(str: string): Promise {
38 | if (typeof window === 'undefined') {
39 | return Buffer.from(str).toString('base64');
40 | } else {
41 | const buf = await stringToArrayBuffer(str, 'utf-8');
42 |
43 | return arrayBufferToBase64(buf);
44 | }
45 | }
46 |
47 | export async function digestSHA1(str: string): Promise {
48 | const data = await stringToArrayBuffer(str, 'utf-8');
49 | const digest = await crypto.subtle.digest('SHA-1', data);
50 |
51 | return arrayBufferToBase64(digest);
52 | }
53 |
--------------------------------------------------------------------------------
/lib/ssi.ts:
--------------------------------------------------------------------------------
1 | import { SPD, interpolateSPD } from './spd';
2 | import { matrixMul } from './matrix';
3 | import { normalize3, sub as vecSub, convolve } from './vector';
4 |
5 | const trap30x301 = Object.preventExtensions(
6 | (() => {
7 | const trap = [0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.5];
8 | const out = Array.from({ length: 30 }, () => Array.from({ length: 301 }, () => 0));
9 |
10 | for (let i = 0; i < 30; i++) {
11 | for (let j = 0; j < trap.length; j++) {
12 | out[i][10 * i + j] = trap[j];
13 | }
14 | }
15 |
16 | return out;
17 | })()
18 | );
19 |
20 | const spectralWeight = Object.freeze([
21 | 12 / 45,
22 | 22 / 45,
23 | 32 / 45,
24 | 40 / 45,
25 | 44 / 45,
26 | 1,
27 | 1,
28 | 1,
29 | 1,
30 | 1,
31 | 1,
32 | 1,
33 | 1,
34 | 1,
35 | 1,
36 | 1,
37 | 1,
38 | 1,
39 | 1,
40 | 1,
41 | 1,
42 | 1,
43 | 1,
44 | 1,
45 | 1,
46 | 1,
47 | 1,
48 | 1,
49 | 11 / 15,
50 | 3 / 15,
51 | ]);
52 | const convVec = Object.freeze([0.22, 0.56, 0.22]);
53 |
54 | export function ssi(ref: Readonly, test: Readonly) {
55 | const refVec = matrixMul(
56 | trap30x301,
57 | interpolateSPD(ref, 1, 380, 670).map(({ v }) => [v])
58 | ).map(([v]) => v); // step 1
59 | const testVec = matrixMul(
60 | trap30x301,
61 | interpolateSPD(test, 1, 380, 670).map(({ v }) => [v])
62 | ).map(([v]) => v);
63 |
64 | const refNorm = normalize3(refVec); // step 2
65 | const testNorm = normalize3(testVec);
66 |
67 | const diffNorm = vecSub(testNorm, refNorm); // step 3
68 | const diffRela = diffNorm.map((x, i) => x / (refNorm[i] + 1 / refNorm.length)); // step 4
69 | const weightedDiffRela = diffRela.map((x, i) => x * spectralWeight[i]); // step 5
70 | const sWeightedDiffRela = convolve([0, ...weightedDiffRela, 0], convVec); // step 6
71 | const metric = sWeightedDiffRela.reduce((sum, cur) => sum + cur ** 2); // step 7
72 | const SSI = Math.round(100 - 32 * Math.sqrt(metric)); // step 8
73 |
74 | return SSI;
75 | }
76 |
--------------------------------------------------------------------------------
/lib/CMF.ts:
--------------------------------------------------------------------------------
1 | // 2deg CIE 1931 Color Matching Functions
2 | // x(lambda), y(lambda), z(lambda)
3 | // 380 - 780 nm
4 | // step: 5 nm
5 | export const CIE1931_2DEG_CMF = Float64Array.from([
6 | 0.0014, 0.0, 0.0065, 0.0022, 0.0001, 0.0105, 0.0042, 0.0001, 0.0201, 0.0076, 0.0002, 0.0362, 0.0143, 0.0004, 0.0679,
7 | 0.0232, 0.0006, 0.1102, 0.0435, 0.0012, 0.2074, 0.0776, 0.0022, 0.3713, 0.1344, 0.004, 0.6456, 0.2148, 0.0073,
8 | 1.0391, 0.2839, 0.0116, 1.3856, 0.3285, 0.0168, 1.623, 0.3483, 0.023, 1.7471, 0.3481, 0.0298, 1.7825, 0.3362, 0.038,
9 | 1.7721, 0.3187, 0.048, 1.7441, 0.2908, 0.06, 1.6692, 0.2511, 0.0739, 1.5281, 0.1954, 0.091, 1.2876, 0.1421, 0.1126,
10 | 1.0419, 0.0956, 0.139, 0.813, 0.058, 0.1693, 0.6162, 0.032, 0.208, 0.4652, 0.0147, 0.2586, 0.3533, 0.0049, 0.323,
11 | 0.272, 0.0024, 0.4073, 0.2123, 0.0093, 0.503, 0.1582, 0.0291, 0.6082, 0.1117, 0.0633, 0.71, 0.0782, 0.1096, 0.7932,
12 | 0.0573, 0.1655, 0.862, 0.0422, 0.2257, 0.9149, 0.0298, 0.2904, 0.954, 0.0203, 0.3597, 0.9803, 0.0134, 0.4333, 0.995,
13 | 0.0087, 0.5121, 1.0, 0.0057, 0.5945, 0.995, 0.0039, 0.6784, 0.9786, 0.0027, 0.7621, 0.952, 0.0021, 0.8425, 0.9154,
14 | 0.0018, 0.9163, 0.87, 0.0017, 0.9786, 0.8163, 0.0014, 1.0263, 0.757, 0.0011, 1.0567, 0.6949, 0.001, 1.0622, 0.631,
15 | 0.0008, 1.0456, 0.5668, 0.0006, 1.0026, 0.503, 0.0003, 0.9384, 0.4412, 0.0002, 0.8544, 0.381, 0.0002, 0.7514, 0.321,
16 | 0.0, 0.6424, 0.265, 0.0, 0.5419, 0.217, 0.0, 0.4479, 0.175, 0.0, 0.3608, 0.1382, 0.0, 0.2835, 0.107, 0.0, 0.2187,
17 | 0.0816, 0.0, 0.1649, 0.061, 0.0, 0.1212, 0.0446, 0.0, 0.0874, 0.032, 0.0, 0.0636, 0.0232, 0.0, 0.0468, 0.017, 0.0,
18 | 0.0329, 0.0119, 0.0, 0.0227, 0.0082, 0.0, 0.0158, 0.0057, 0.0, 0.0114, 0.0041, 0.0, 0.0081, 0.0029, 0.0, 0.0058,
19 | 0.0021, 0.0, 0.0041, 0.0015, 0.0, 0.0029, 0.001, 0.0, 0.002, 0.0007, 0.0, 0.0014, 0.0005, 0.0, 0.001, 0.0003, 0.0,
20 | 0.0007, 0.00025, 0.0, 0.0005, 0.0002, 0.0, 0.0003, 0.0001, 0.0, 0.0003, 0.0001, 0.0, 0.0002, 0.0001, 0.0, 0.0002,
21 | 0.0001, 0.0, 0.0001, 0.0001, 0.0, 0.0001, 0.0, 0.0, 0.0, 0.0, 0.0,
22 | ]);
23 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Document, { Html, Head, Main, NextScript } from 'next/document';
3 | import createEmotionServer from '@emotion/server/create-instance';
4 | import { cache } from './_app.js';
5 |
6 | const { extractCritical } = createEmotionServer(cache);
7 |
8 | export default class MyDocument extends Document {
9 | render() {
10 | return (
11 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
27 | // `getInitialProps` belongs to `_document` (instead of `_app`),
28 | // it's compatible with static-site generation (SSG).
29 | MyDocument.getInitialProps = async (ctx) => {
30 | // Resolution order
31 | //
32 | // On the server:
33 | // 1. app.getInitialProps
34 | // 2. page.getInitialProps
35 | // 3. document.getInitialProps
36 | // 4. app.render
37 | // 5. page.render
38 | // 6. document.render
39 | //
40 | // On the server with error:
41 | // 1. document.getInitialProps
42 | // 2. app.render
43 | // 3. page.render
44 | // 4. document.render
45 | //
46 | // On the client
47 | // 1. app.getInitialProps
48 | // 2. page.getInitialProps
49 | // 3. app.render
50 | // 4. page.render
51 |
52 | // Render app and page and get the context of the page with collected side effects.
53 | const initialProps = await Document.getInitialProps(ctx);
54 | const styles = extractCritical(initialProps.html);
55 |
56 | return {
57 | ...initialProps,
58 | // Styles fragment is rendered after the app and page rendering finish.
59 | styles: [
60 | ...React.Children.toArray(initialProps.styles),
61 | ,
67 | ],
68 | };
69 | };
70 |
--------------------------------------------------------------------------------
/components/settings/SettingsCard.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import Box from '@mui/material/Box';
3 | import Button from '@mui/material/Button';
4 | import Card from '@mui/material/Card';
5 | import CardActions from '@mui/material/CardActions';
6 | import CardContent from '@mui/material/CardContent';
7 | import CircularProgress from '@mui/material/CircularProgress';
8 | import Grid from '@mui/material/Grid';
9 | import Typography from '@mui/material/Typography';
10 | import { green } from '@mui/material/colors';
11 | import SxPropsTheme from 'lib/SxPropsTheme';
12 |
13 | const buttonProgressStyle: SxPropsTheme = {
14 | color: green[500],
15 | position: 'absolute',
16 | top: '50%',
17 | left: '50%',
18 | marginTop: -12,
19 | marginLeft: -12,
20 | };
21 |
22 | export const settingsCardStyle: SxPropsTheme = {
23 | height: '19em',
24 | width: '15em',
25 | };
26 |
27 | export const iconStyle: SxPropsTheme = {
28 | fontSize: '18px !important',
29 | };
30 |
31 | export function ActionButton({
32 | wait,
33 | onClick,
34 | disabled,
35 | children,
36 | }: {
37 | wait?: boolean;
38 | onClick?: () => void;
39 | disabled?: boolean;
40 | children: any;
41 | }) {
42 | return (
43 |
44 |
48 |
49 | );
50 | }
51 |
52 | export function SettingsCard({
53 | icon,
54 | title,
55 | actions,
56 | children,
57 | }: {
58 | icon: ReactNode;
59 | title: string;
60 | actions?: ReturnType;
61 | children: ReactNode;
62 | }) {
63 | return (
64 |
65 |
66 |
67 |
68 | {icon} {`${title}`}
69 |
70 | {children}
71 |
72 | {actions || ''}
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 0 0.5rem;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | }
9 |
10 | .main {
11 | padding: 5rem 0;
12 | flex: 1;
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: center;
16 | align-items: center;
17 | }
18 |
19 | .footer {
20 | width: 100%;
21 | height: 100px;
22 | border-top: 1px solid #eaeaea;
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | }
27 |
28 | .footer img {
29 | margin-left: 0.5rem;
30 | }
31 |
32 | .footer a {
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | }
37 |
38 | .description {
39 | line-height: 1.5;
40 | font-size: 1.5rem;
41 | }
42 |
43 | .code {
44 | background: #fafafa;
45 | border-radius: 5px;
46 | padding: 0.75rem;
47 | font-size: 1.1rem;
48 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
49 | Bitstream Vera Sans Mono, Courier New, monospace;
50 | }
51 |
52 | .grid {
53 | display: flex;
54 | align-items: center;
55 | justify-content: center;
56 | flex-wrap: wrap;
57 | max-width: 800px;
58 | margin-top: 3rem;
59 | }
60 |
61 | .card {
62 | margin: 1rem;
63 | flex-basis: 45%;
64 | padding: 1.5rem;
65 | text-align: left;
66 | color: inherit;
67 | text-decoration: none;
68 | border: 1px solid #eaeaea;
69 | border-radius: 10px;
70 | transition: color 0.15s ease, border-color 0.15s ease;
71 | }
72 |
73 | .card:hover,
74 | .card:focus,
75 | .card:active {
76 | color: #0070f3;
77 | border-color: #0070f3;
78 | }
79 |
80 | .card h3 {
81 | margin: 0 0 1rem 0;
82 | font-size: 1.5rem;
83 | }
84 |
85 | .card p {
86 | margin: 0;
87 | font-size: 1.25rem;
88 | line-height: 1.5;
89 | }
90 |
91 | .logo {
92 | height: 1em;
93 | }
94 |
95 | @media (max-width: 600px) {
96 | .grid {
97 | width: 100%;
98 | flex-direction: column;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/lib/vector.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Normalize by the sum of squared values.
3 | */
4 | export function normalize(v: readonly number[]) {
5 | const div = Math.sqrt(v.reduce((res, vn) => res + Math.pow(vn, 2), 0));
6 |
7 | return v.map((vn) => vn / div);
8 | }
9 |
10 | /**
11 | * Normalize by the largest element; i.e. so that the largest element will be 1.
12 | */
13 | export function normalize2(v: readonly number[] | Float64Array) {
14 | const max = Math.max(...v);
15 | return max === 0 ? v.slice(0) : v.map((v: number) => v / max);
16 | }
17 |
18 | /**
19 | * Normalize so that the sum of elements is 1.
20 | */
21 | export function normalize3(v: readonly number[]): number[] {
22 | const sum = v.reduce((sum, cur) => sum + cur, 0);
23 | return v.map((x) => x / sum);
24 | }
25 |
26 | export function sub(a: readonly number[], b: readonly number[]): number[] {
27 | return a.map((x, i) => x - b[i]);
28 | }
29 |
30 | export function transform(v: readonly number[], sub: readonly number[], coeff: readonly number[]): number[] {
31 | return v.map((xn, i) => (xn - sub[i]) / coeff[i]);
32 | }
33 |
34 | export function dotProdC(a: readonly number[], b: readonly number[], C: number): number {
35 | return a.reduce((prev, an, n) => prev + an * b[n], C);
36 | }
37 |
38 | export function convolve(volume: readonly number[], kernel: readonly number[]): number[] {
39 | const r = Array.from({ length: volume.length + kernel.length }, () => 0);
40 |
41 | for (let j = 0; j < kernel.length; ++j) {
42 | r[j] = volume[0] * kernel[j];
43 | }
44 |
45 | for (let i = 1; i < volume.length; ++i) {
46 | for (let j = 0; j < kernel.length; ++j) {
47 | r[i + j] += volume[i] * kernel[j];
48 | }
49 | }
50 |
51 | return r;
52 | }
53 |
54 | export function convolveFloat64Array(volume: Float64Array, kernel: Float64Array): Float64Array {
55 | const r = new Float64Array(volume.length + kernel.length);
56 |
57 | for (let j = 0; j < kernel.length; ++j) {
58 | r[j] = volume[0] * kernel[j];
59 | }
60 |
61 | for (let i = 1; i < volume.length; ++i) {
62 | for (let j = 0; j < kernel.length; ++j) {
63 | r[i + j] += volume[i] * kernel[j];
64 | }
65 | }
66 |
67 | return r;
68 | }
69 |
--------------------------------------------------------------------------------
/components/Chart.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Chart as ChartJS,
3 | BarElement,
4 | CategoryScale,
5 | Filler,
6 | Legend,
7 | LineElement,
8 | LinearScale,
9 | PointElement,
10 | RadialLinearScale,
11 | Title,
12 | Tooltip,
13 | ScriptableContext,
14 | ScriptableScaleContext,
15 | } from 'chart.js';
16 | import { Bar } from 'react-chartjs-2';
17 | import { Line } from 'react-chartjs-2';
18 | import { Scatter } from 'react-chartjs-2';
19 | import Datalabels from 'chartjs-plugin-datalabels';
20 | import Annotation from 'chartjs-plugin-annotation';
21 |
22 | export type ScatterDataset = Parameters[0]['data']['datasets'][0];
23 |
24 | const customCanvasBackgroundColor = {
25 | id: 'customCanvasBackgroundColor',
26 | beforeDraw: (chart, args, options) => {
27 | const { ctx } = chart;
28 | ctx.save();
29 | ctx.globalCompositeOperation = 'destination-over';
30 | ctx.fillStyle = options.color || 'rgb(255,255,255)';
31 | ctx.fillRect(0, 0, chart.width, chart.height);
32 | ctx.restore();
33 | },
34 | };
35 |
36 | function pointRotationAuto(ctx: ScriptableContext<'line'>) {
37 | const i = ctx.dataIndex;
38 | const { data } = ctx.dataset;
39 | const point1 = data[i];
40 | const point2 = i >= data.length - 1 && i > 0 ? data[i - 1] : i > 0 && data.length > 0 ? data[i + 1] : point1;
41 | if (point1 === point2 || typeof point1 === 'number' || typeof point2 === 'number') {
42 | return 0;
43 | }
44 |
45 | const dx = point2.x - point1.x;
46 | const dy = point2.y - point1.y;
47 | return 180 - (180 / Math.PI) * Math.atan2(Math.abs(dx), Math.abs(dy));
48 | }
49 |
50 | function gridColorAuto({ tick }: ScriptableScaleContext) {
51 | return tick.value === 0 ? 'black' : 'lightgrey';
52 | }
53 |
54 | function makeChartTitle(title: string): {
55 | display: boolean;
56 | text: string;
57 | position: 'bottom';
58 | padding: { top: number };
59 | } {
60 | return {
61 | display: true,
62 | text: title,
63 | position: 'bottom',
64 | padding: {
65 | top: -10, // This fixes the aspect ratio shift
66 | },
67 | };
68 | }
69 |
70 | ChartJS.register(
71 | Annotation,
72 | BarElement,
73 | CategoryScale,
74 | customCanvasBackgroundColor,
75 | Datalabels,
76 | Filler,
77 | Legend,
78 | LineElement,
79 | LinearScale,
80 | PointElement,
81 | RadialLinearScale,
82 | Title,
83 | Tooltip
84 | );
85 |
86 | export { Bar, Line, Scatter, pointRotationAuto, gridColorAuto, makeChartTitle };
87 |
--------------------------------------------------------------------------------
/lib/planckian.ts:
--------------------------------------------------------------------------------
1 | // Krystek, Michael P. (January 1985).
2 | // "An algorithm to calculate correlated colour temperature".
3 | // Color Research & Application. 10 (1): 38–40.
4 |
5 | import { uv2xy, xy2uv } from './CIEConv';
6 |
7 | // doi:10.1002/col.5080100109
8 | function calc_xyc_low(T: number) {
9 | const uT =
10 | (0.860117757 + 1.54118254e-4 * T + 1.28641212e-7 * T ** 2) / (1 + 8.42420235e-4 * T + 7.08145163e-7 * T ** 2);
11 | const vT =
12 | (0.317398726 + 4.22806245e-5 * T + 4.20481691e-8 * T ** 2) / (1 - 2.89741816e-5 * T + 1.61456053e-7 * T ** 2);
13 | const div = 2 * uT - 8 * vT + 4;
14 |
15 | return [(3 * uT) / div, (2 * vT) / div];
16 | }
17 |
18 | // Kim et al. cubic spline
19 | function calc_xc_high(T: number) {
20 | if (1667 <= T && T <= 4000) {
21 | return -0.2661239 * (10 ** 9 / T ** 3) - 0.2343589 * (10 ** 6 / T ** 2) + 0.8776956 * (10 ** 3 / T) + 0.17991;
22 | } else if (4000 <= T && T <= 25000) {
23 | return -3.0258469 * (10 ** 9 / T ** 3) + 2.1070379 * (10 ** 6 / T ** 2) + 0.2226347 * (10 ** 3 / T) + 0.24039;
24 | }
25 | return NaN;
26 | }
27 |
28 | // Kim et al. cubic spline
29 | function calc_yc_high(T: number, xc: number) {
30 | if (1667 <= T && T <= 2222) {
31 | return -1.1063814 * xc ** 3 - 1.3481102 * xc ** 2 + 2.18555832 * xc - 0.20219683;
32 | } else if (2222 <= T && T <= 4000) {
33 | return -0.9549476 * xc ** 3 - 1.37418593 * xc ** 2 + 2.09137015 * xc - 0.16748867;
34 | } else if (4000 <= T && T <= 25000) {
35 | return 3.081758 * xc ** 3 - 5.8733867 * xc ** 2 + 3.75112997 * xc - 0.37001483;
36 | }
37 | return NaN;
38 | }
39 |
40 | /**
41 | * Calculate Planckian locus for T
42 | */
43 | function calc_xy(T: number) {
44 | if (T < 1000) {
45 | return [NaN, NaN];
46 | } else if (T < 1667) {
47 | return calc_xyc_low(T);
48 | } else {
49 | const xc = calc_xc_high(T);
50 | return [xc, calc_yc_high(T, xc)];
51 | }
52 | }
53 |
54 | /**
55 | * Calculate Planckian locus for T and Duv
56 | */
57 | export default function planckian_xy(T: number, Duv: number = 0) {
58 | if (Duv === 0) {
59 | return calc_xy(T);
60 | } else {
61 | const dT = 0.01; // K
62 | const xy0 = calc_xy(T);
63 | const xy1 = calc_xy(T + dT);
64 | const [u0, v0] = xy2uv(xy0[0], xy0[1]);
65 | const [u1, v1] = xy2uv(xy1[0], xy1[1]);
66 | const du = u1 - u0;
67 | const dv = v1 - v0;
68 | const sq = Math.sqrt(du * du + dv * dv);
69 | const u = u0 - (Duv * dv) / sq;
70 | const v = v0 + (Duv * du) / sq;
71 | return uv2xy(u, v);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/components/Title/index.tsx:
--------------------------------------------------------------------------------
1 | import AppBar from '@mui/material/AppBar';
2 | import Box from '@mui/material/Box';
3 | import IconButton from '@mui/material/IconButton';
4 | import IconSettings from '@mui/icons-material/Settings';
5 | import FirstPageIcon from '@mui/icons-material/FirstPage';
6 | import Toolbar from '@mui/material/Toolbar';
7 | import Typography from '@mui/material/Typography';
8 | import { MouseEvent } from 'react';
9 | import { styled } from '@mui/material/styles';
10 | import { useRouter } from 'next/router';
11 | import Notifications from './Notifications';
12 | import PowerStatus from './PowerStatus';
13 | import MeasControl from 'components/MeasControl';
14 |
15 | const sxArrowEnabled = {
16 | '&:hover': {
17 | color: 'grey',
18 | cursor: 'pointer',
19 | },
20 | };
21 |
22 | const sxArrowDisabled = {
23 | visibility: 'hidden',
24 | };
25 |
26 | const Offset = styled('div')(({ theme }) => ({ margin: '2px', ...theme.mixins.toolbar }));
27 |
28 | function BackButton({ disable, onClick }: { disable: boolean; onClick?: (e?: MouseEvent) => void }) {
29 | return (
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | function Setup() {
37 | const router = useRouter();
38 |
39 | const openSettings = (e: MouseEvent) => {
40 | e.preventDefault();
41 | router.push('/setup');
42 | };
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | export default function Title({
54 | disableBack,
55 | href,
56 | className,
57 | children,
58 | }: {
59 | disableBack?: boolean;
60 | href?: string;
61 | className?: string;
62 | children: any;
63 | }) {
64 | const router = useRouter();
65 |
66 | const goBack = (e: MouseEvent) => {
67 | if (disableBack) {
68 | e.preventDefault();
69 | } else if (href) {
70 | router.push(href);
71 | } else {
72 | router.back();
73 | }
74 | };
75 |
76 | return (
77 |
78 |
79 |
80 |
81 |
82 | {children}
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | );
94 | }
95 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import Box from '@mui/system/Box';
3 | import { experimentalStyled as styled } from '@mui/material/styles';
4 | import Paper from '@mui/material/Paper';
5 | import Container from '@mui/material/Container';
6 | import Grid from '@mui/material/Grid';
7 | import ContrastIcon from '@mui/icons-material/Contrast';
8 | import DataArrayIcon from '@mui/icons-material/DataArray';
9 | import DifferenceIcon from '@mui/icons-material/Difference';
10 | import EqualizerIcon from '@mui/icons-material/Equalizer';
11 | import ExposureIcon from '@mui/icons-material/Exposure';
12 | import FluorescentIcon from '@mui/icons-material/Fluorescent';
13 | import TipsAndUpdatesIcon from '@mui/icons-material/TipsAndUpdates';
14 | import WbIncandescentIcon from '@mui/icons-material/WbIncandescent';
15 | import MyHead from 'components/MyHead';
16 | import Title from 'components/Title';
17 |
18 | const Item = styled(Paper)(({ theme }) => ({
19 | backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
20 | ...theme.typography.body2,
21 | margin: 4,
22 | padding: theme.spacing(2),
23 | textAlign: 'center',
24 | color: theme.palette.text.secondary,
25 | width: 100,
26 | height: 100,
27 | }));
28 |
29 | function MenuItem({ href, children }) {
30 | return (
31 |
32 | - {children}
33 |
34 | );
35 | }
36 |
37 | export default function Home() {
38 | return (
39 |
40 |
41 |
42 | OLM
43 |
44 |
51 |
55 |
59 |
63 |
67 |
71 |
75 |
79 |
83 |
84 |
85 |
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/lib/spd.ts:
--------------------------------------------------------------------------------
1 | import { CIE1931_2DEG_CMF } from './CMF';
2 | import Spline from './spline';
3 | import wlMap from './wlmap';
4 | import { XYZ2xy, xy2uv, XYZ2UVW } from './CIEConv';
5 | import calcCCT from './cct';
6 | import calcDuv from './duv';
7 | import calcTint from './tint';
8 | import { RefMeasurement } from './global';
9 |
10 | export type SPD = {
11 | l: number /*!< wavelength. */;
12 | v: number /*!< power. */;
13 | }[];
14 |
15 | export function interpolateSPD(input: Readonly, increment: number = 5, min: number = 380, max: number = 780): SPD {
16 | const xs = [...input.map(({ l }) => l)];
17 | const ys = [...input.map(({ v }) => v)];
18 |
19 | if (xs[0] > min) {
20 | xs.unshift(min);
21 | ys.unshift(0);
22 | }
23 | if (xs[xs.length - 1] < max) {
24 | xs.push(max);
25 | ys.push(ys[ys.length - 1]);
26 | }
27 |
28 | const spline = new Spline(xs, ys);
29 | return wlMap((l) => ({ l, v: spline.at(l) }), increment);
30 | }
31 |
32 | // Convert any SPD that covers 380..780 nm to an spd array.
33 | export function SPD2spd(input: SPD): Float64Array {
34 | return Float64Array.from(input.filter(({ l }) => l >= 380 && l <= 780 && l % 5 == 0).map(({ v }) => v));
35 | }
36 |
37 | /**
38 | * Calculate tristimulus from an spd.
39 | * @param spd spd must be 380..780 nm with 5 nm steps.
40 | * @param cmf Color matching function. This should always almost be the CIE1931_2DEG_CMF.
41 | */
42 | export function spd2XYZ(spd: Float64Array, cmf: Float64Array) {
43 | const xsum = spd.reduce((sum: number, v: number, i: number) => sum + v * cmf[i * 3], 0);
44 | const ysum = spd.reduce((sum: number, v: number, i: number) => sum + v * cmf[i * 3 + 1], 0);
45 | const zsum = spd.reduce((sum: number, v: number, i: number) => sum + v * cmf[i * 3 + 2], 0);
46 |
47 | return [(100 * xsum) / ysum, 100, (100 * zsum) / ysum];
48 | }
49 |
50 | /**
51 | * Normalize by 560 nm.
52 | * @param spd spd must be 380..780 nm with 5 nm steps.
53 | */
54 | export function normalizeSPD(spd: Float64Array) {
55 | const center = spd[36];
56 |
57 | return spd.map((v: number) => v / center);
58 | }
59 |
60 | /**
61 | * Calculate a reference.
62 | * Calculate a reference measurement that can be used to compare against real
63 | * measurements.
64 | * @param spd spd must be 380..780 nm with 5 nm steps.
65 | */
66 | export function calcRefMeas(spd: Float64Array): RefMeasurement {
67 | const ref = normalizeSPD(spd);
68 | const XYZ = spd2XYZ(ref, CIE1931_2DEG_CMF);
69 | const [x, y] = XYZ2xy(XYZ);
70 | const [u, v] = xy2uv(x, y);
71 |
72 | return {
73 | SPD: spd,
74 | Ex: x,
75 | Ey: y,
76 | Eu: u,
77 | Ev: v,
78 | CCT: calcCCT(x, y),
79 | Duv: calcDuv(u, v),
80 | tint: calcTint(x, y)[1],
81 | Lux: XYZ[1],
82 | };
83 | }
84 |
--------------------------------------------------------------------------------
/lib/flicker.ts:
--------------------------------------------------------------------------------
1 | import webfft from 'webfft';
2 | import { fftshift } from 'lib/fftshift';
3 | import { setGlobalState } from './global';
4 |
5 | const fftSize = 1024; // must be a power of 2
6 | const fft = new webfft(fftSize);
7 |
8 | function mean(x: readonly number[]) {
9 | return x.reduce((prev: number, xn: number) => prev + xn) / x.length;
10 | }
11 |
12 | export function calcFft(wave: readonly number[]): Float32Array {
13 | if (wave.length != fftSize) return Float32Array.from([0]);
14 |
15 | const DC = mean(wave);
16 | // The input is an interleaved complex array (IQIQIQIQ...), so it's twice the size
17 | const fftOut = fft.fft(new Float32Array(wave.map((xn: number) => xn - DC).flatMap((xn: number) => [xn, 0])));
18 | const mag = new Float32Array(fftSize);
19 | for (let i = 0; i < fftSize; i++) {
20 | mag[i] = Math.sqrt(fftOut[2 * i] * fftOut[2 * i] + fftOut[2 * i + 1] * fftOut[2 * i + 1]);
21 | }
22 |
23 | //return Array.from(fftshift(mag).slice(fftSize / 2, fftSize));
24 | return fftshift(mag).subarray(fftSize / 2, fftSize);
25 | }
26 |
27 | export function calcFlicker(
28 | waveData: { n: number; sRange: number; x: readonly number[] },
29 | CCT: number,
30 | Lux: number,
31 | Ksensor: readonly number[]
32 | ) {
33 | if (waveData.x.length != 1024) {
34 | throw new Error('Expected 1024 samples');
35 | }
36 | let c: number;
37 | switch (waveData.n) {
38 | case 25:
39 | c = 26;
40 | break;
41 | case 146:
42 | c = 150;
43 | break;
44 | case 11:
45 | c = 12.285;
46 | }
47 | let y: number;
48 | switch (waveData.sRange) {
49 | case 0:
50 | y = 67.003906;
51 | break;
52 | case 1:
53 | y = 35.363281;
54 | break;
55 | case 2:
56 | default:
57 | y = 34.516602;
58 | }
59 |
60 | let { x: wave } = waveData;
61 | const corrected = wave.map(function (xn) {
62 | var t = xn - y;
63 | return t < 0 ? 0 : t;
64 | });
65 | //const wavMax = Math.max(...corrected);
66 | //const wavMin = Math.min(...corrected);
67 | const wavMean = mean(corrected);
68 | const k = wavMean !== 0 ? Lux / wavMean : 1;
69 | //console.log('Lux:', Lux, 'average:', wavMean, 'max:', wavMax, 'min:', wavMin, 'k:', k);
70 | wave = corrected.map((v) => k * v);
71 | const sortedWave = wave.slice(0).sort((n, t) => {
72 | return n - t;
73 | });
74 | const rSortedWave = sortedWave.slice(0, 30).reverse();
75 | const N = mean(sortedWave.slice(-30));
76 | const T = mean(rSortedWave);
77 | const V = mean(wave);
78 | let fluDepth = N + T <= 0 ? 0 : ((N - T) / (N + T)) * 100;
79 | fluDepth = fluDepth > 99.5 ? 99.5 : fluDepth; // fluctuation depth
80 | const J = wave.reduce((xn, t) => xn + (t - V > 0 ? t - V : 0));
81 | const waveSum = wave.reduce((prev, xn) => prev + xn);
82 | const flickerIndex = waveSum === 0 ? 0 : J / waveSum;
83 |
84 | setGlobalState('res_lm_freq', {
85 | CCT,
86 | Lux,
87 | fluDepth,
88 | flickerIndex,
89 | freqDiv: c,
90 | wave,
91 | });
92 | }
93 |
--------------------------------------------------------------------------------
/lib/fftshift.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * MIT License
4 | *
5 | * Copyright (c) 2017 Oramics
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 |
25 | * > Cyclic rotation for phase-zero windowing
26 | *
27 | * [](https://npmjs.org/package/dsp-fftshift/)
28 | *
29 | * @example
30 | * var shift = require('dsp-fftshift')
31 | * shift.fftshift(signal)
32 | * shift.ifftshift(signal)
33 | *
34 | * @example
35 | * // ES6 syntax
36 | * import { fftshift, ifftshift } from 'dsp-fftshift'
37 | * fftshift(signal)
38 | *
39 | * @module fftshift
40 | */
41 |
42 | /**
43 | * Rotate a buffer in place
44 | *
45 | * from: http://stackoverflow.com/questions/876293/fastest-algorithm-for-circle-shift-n-sized-array-for-m-position
46 | *
47 | * @param {Array} source - the buffer to rotate
48 | * @param {Number} rotations - the number of rotations
49 | * @private
50 | */
51 | function rotate(src, n) {
52 | var len = src.length;
53 | reverse(src, 0, len);
54 | reverse(src, 0, n);
55 | reverse(src, n, len);
56 | return src;
57 | }
58 | function reverse(src, from, to) {
59 | --from;
60 | while (++from < --to) {
61 | var tmp = src[from];
62 | src[from] = src[to];
63 | src[to] = tmp;
64 | }
65 | }
66 |
67 | /**
68 | * Zero-phase windowing alignment
69 | *
70 | * __CAUTION__: this function mutates the array
71 | *
72 | * Perform a cyclic shifting (rotation) to set the first sample at the middle
73 | * of the buffer (it reorder buffer samples from (0:N-1) to [(N/2:N-1) (0:(N/2-1))])
74 | *
75 | * Named by the same function in mathlab: `fftshift`
76 | *
77 | * @param {Array} buffer
78 | * @return {Array} the same buffer (with the data rotated)
79 | */
80 | export function fftshift(src) {
81 | const len = src.length;
82 | return rotate(src, Math.floor(len / 2));
83 | }
84 |
85 | /**
86 | * Inverse of zero-phase windowing alignment
87 | *
88 | * __CAUTION__: this function mutates the array
89 | *
90 | * @see fftshift
91 | * @param {Array} buffer
92 | * @return {Array} the same buffer (with the data rotated)
93 | */
94 | export function ifftshift(src) {
95 | const len = src.length;
96 | return rotate(src, Math.floor((len + 1) / 2));
97 | }
98 |
--------------------------------------------------------------------------------
/lib/ble/index.ts:
--------------------------------------------------------------------------------
1 | export interface BtDevice {
2 | device: BluetoothDevice;
3 | server: BluetoothRemoteGATTServer;
4 | }
5 | export type Paired = Awaited>;
6 |
7 | async function connect(device: BluetoothDevice): Promise {
8 | try {
9 | const server = await exponentialBackoff(
10 | 3 /* max retries */,
11 | 2 /* seconds delay */,
12 | async (): Promise => {
13 | time(`Connecting to Bluetooth Device (${device.name})...`);
14 | return await device.gatt.connect();
15 | }
16 | );
17 |
18 | console.log(`Bluetooth Device connected (${device.name}).`);
19 | return server;
20 | } catch (err) {
21 | throw err;
22 | }
23 | }
24 |
25 | /*
26 | * @param connectCb is called on the initial connect as well as on reconnects. This allows restarting the notifications.
27 | */
28 | export async function pairDevice(
29 | filters: any | null,
30 | optionalServices: string[] | null,
31 | connectCb: (dev: BtDevice) => Promise,
32 | onDisconnectedCb: () => void
33 | ) {
34 | const options = {
35 | acceptAllDevices: !filters,
36 | filters: filters || undefined,
37 | optionalServices,
38 | };
39 |
40 | const device = await navigator.bluetooth.requestDevice(options);
41 | const onDisconnected = (e) => {
42 | console.log(`> Bluetooth Device "${e.currentTarget.gatt.device.name}" disconnected`);
43 | connect(device)
44 | .then(async (server) => {
45 | const btDevice = {
46 | device,
47 | server,
48 | };
49 |
50 | await connectCb(btDevice);
51 | })
52 | .catch((err) => {
53 | console.error(`> Bluetooth Device "${device.name}" reconnect failed: `, err);
54 | onDisconnectedCb();
55 | });
56 | };
57 |
58 | device.addEventListener('gattserverdisconnected', onDisconnected);
59 | let server: BluetoothRemoteGATTServer | null;
60 | try {
61 | server = await connect(device);
62 | } catch (err) {
63 | console.error('> Bluetooth Device connect failed');
64 | server = null;
65 | }
66 | connectCb({
67 | device,
68 | server,
69 | });
70 |
71 | return {
72 | device,
73 | disconnect: () => {
74 | console.log(`> Disconnecting ${device.name}`);
75 | device.removeEventListener('gattserverdisconnected', onDisconnected);
76 | device.gatt.disconnect();
77 | },
78 | };
79 | }
80 |
81 | // RFE is this ever needed? We can just unpair and throwaway everything.
82 | export async function stopNotifications(characteristic) {
83 | characteristic.stopNotifications();
84 | }
85 |
86 | async function exponentialBackoff(max: number, delay: number, toTry: () => Promise) {
87 | return new Promise>((resolve, reject) =>
88 | _exponentialBackoff(max, delay, toTry, resolve, reject)
89 | );
90 | }
91 |
92 | async function _exponentialBackoff(max: number, delay: number, toTry, success, fail) {
93 | try {
94 | success(await toTry());
95 | } catch (error) {
96 | if (max === 0) {
97 | return fail(error);
98 | }
99 | time('Retrying in ' + delay + 's... (' + max + ' tries left)');
100 | setTimeout(function () {
101 | _exponentialBackoff(--max, delay * 2, toTry, success, fail);
102 | }, delay * 1000);
103 | }
104 | }
105 |
106 | function time(text: string) {
107 | console.log(`[${new Date().toJSON().substr(11, 8)}]${text}`);
108 | }
109 |
--------------------------------------------------------------------------------
/lib/CIEConv.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Reference white D50.
3 | * Y = 100, relative luminance.
4 | */
5 | export const XYZnD50 = Object.freeze([96.4212, 100, 82.5188]);
6 |
7 | /**
8 | * Reference white D65.
9 | * Y = 100, relative luminance.
10 | */
11 | export const XYZnD65 = Object.freeze([95.0489, 100, 108.884]);
12 |
13 | /**
14 | * XYZ to tristimulus to CIE 1931 (x, y) chromaticity.
15 | */
16 | export function XYZ2xy(XYZ: readonly number[]) {
17 | const x = XYZ[0] / (XYZ[0] + XYZ[1] + XYZ[2]);
18 | const y = XYZ[1] / (XYZ[0] + XYZ[1] + XYZ[2]);
19 |
20 | return [x, y] as const;
21 | }
22 |
23 | /**
24 | * CIE 1931 (x, y) chromaticity to CIE 1960 UCS (u, v) chromaticity.
25 | * MacAdam simplified Judd's
26 | */
27 | export function xy2uv(x: number, y: number) {
28 | const nj = -2 * x + 12 * y + 3;
29 | const u = (4 * x) / nj;
30 | const v = (6 * y) / nj;
31 | return [u, v] as const;
32 | }
33 |
34 | export function uv2xy(u: number, v: number) {
35 | const d = 2 * u - 8 * v + 4;
36 | const x = (3 * u) / d;
37 | const y = (2 * v) / d;
38 | return [x, y] as const;
39 | }
40 |
41 | /**
42 | * CIE XYZ color space to CIE 1964 UVW color space.
43 | */
44 | export function XYZ2UVW(XYZ: readonly number[], u0: number, v0: number) {
45 | const [u, v] = xy2uv(...XYZ2xy(XYZ));
46 |
47 | const W = 25 * XYZ[1] ** (1 / 3) - 17;
48 | const U = 13 * W * (u - u0);
49 | const V = 13 * W * (v - v0);
50 |
51 | return [U, V, W] as const;
52 | }
53 |
54 | /**
55 | * xyY color space to XYZ tristimulus.
56 | */
57 | export function xy2XYZ(x: number, y: number, Y: number) {
58 | const X = (Y / y) * x;
59 | const Z = (Y / y) * (1 - x - y);
60 |
61 | return [X, Y, Z] as const;
62 | }
63 |
64 | const δ = 6 / 29;
65 |
66 | function f(t: number): number {
67 | if (t > δ ** 3) {
68 | return Math.cbrt(t);
69 | } else {
70 | return (1 / 3) * t * Math.pow(δ, -2) + 4 / 29;
71 | }
72 | }
73 |
74 | function finv(t: number): number {
75 | if (t > δ) {
76 | return t ** 3;
77 | } else {
78 | return 3 * δ ** 2 * (t - 4 / 29);
79 | }
80 | }
81 |
82 | /**
83 | * CIE XYZ color space to CIELAB color space.
84 | */
85 | export function XYZ2Lab(X: number, Y: number, Z: number, XYZn: readonly number[]) {
86 | const L = 116 * f(Y / XYZn[1]) - 16;
87 | const a = 500 * (f(X / XYZn[0]) - f(Y / XYZn[1]));
88 | const b = 200 * (f(Y / XYZn[1]) - f(Z / XYZn[2]));
89 |
90 | return [L, a, b] as const;
91 | }
92 |
93 | export function Lab2XYZ(L: number, a: number, b: number, XYZn: readonly number[]) {
94 | const X = XYZn[0] * finv((L + 16) / 116 + a / 500);
95 | const Y = XYZn[1] * finv((L + 16) / 116);
96 | const Z = XYZn[2] * finv((L + 16) / 116 - b / 200);
97 |
98 | return [X, Y, Z] as const;
99 | }
100 |
101 | function LabHue(a: number, b: number) {
102 | return Math.atan2(b, a);
103 | }
104 |
105 | // C_ab
106 | function LabChroma(a: number, b: number) {
107 | return Math.sqrt(a ** 2 + b ** 2);
108 | }
109 |
110 | // S_ab
111 | function LabSat(Cab: number, L: number) {
112 | return Cab / Math.sqrt(Cab ** 2 + L ** 2);
113 | }
114 |
115 | export function LabHueSatChroma(L: number, a: number, b: number) {
116 | const hab = LabHue(a, b) * (180 / Math.PI) || 0;
117 | const posHab = Math.round((hab + 360) % 360);
118 | const chroma = LabChroma(a, b);
119 | const sat = LabSat(chroma, L);
120 |
121 | return {
122 | hab: posHab,
123 | sat,
124 | chroma,
125 | };
126 | }
127 |
--------------------------------------------------------------------------------
/components/Title/Notifications.tsx:
--------------------------------------------------------------------------------
1 | import Alert from '@mui/material/Alert';
2 | import Badge from '@mui/material/Badge';
3 | import Box from '@mui/material/Box';
4 | import IconButton from '@mui/material/IconButton';
5 | import NotificationsIcon from '@mui/icons-material/Notifications';
6 | import SensorWindowIcon from '@mui/icons-material/SensorWindow';
7 | import Popover from '@mui/material/Popover';
8 | import Stack from '@mui/material/Stack';
9 | import Typography from '@mui/material/Typography';
10 | import { AlertColor } from '@mui/material/Alert';
11 | import { BatteryLevel } from 'components/BatteryLevel';
12 | import { ReactNode, MouseEvent, useState } from 'react';
13 | import { useGlobalState } from 'lib/global';
14 |
15 | type Notification = {
16 | severity: AlertColor;
17 | icon?: ReactNode;
18 | permanent?: boolean; // can't be cleared with X, i.e. action is mandatory
19 | text: string;
20 | };
21 |
22 | function useLm3Alerts(): Notification[] {
23 | const [btDevice_lm3] = useGlobalState('btDevice_lm3');
24 | const [battLevel] = useGlobalState('res_battery_level');
25 |
26 | if (!btDevice_lm3) {
27 | return [
28 | {
29 | severity: 'error',
30 | icon: ,
31 | text: 'LM3 not connected',
32 | },
33 | ];
34 | } else if (battLevel >= 0 && battLevel <= 20) {
35 | const getIcon = (l: number) => ;
36 | return [
37 | {
38 | severity: 'warning',
39 | icon: getIcon(battLevel),
40 | text: 'Low battery',
41 | },
42 | ];
43 | } else {
44 | return [];
45 | }
46 | }
47 |
48 | function useNotifications(): [Notification[], (notification: Notification) => void] {
49 | const lm3Alerts = useLm3Alerts();
50 | const [clearedNotifications, setClearedNotifications] = useState([]);
51 | const notifications: Notification[] = [...lm3Alerts].filter(({ text }) => !clearedNotifications.includes(text));
52 | const clearNotification = (notification: Notification) =>
53 | setClearedNotifications([...clearedNotifications, notification.text]);
54 |
55 | return [notifications, clearNotification];
56 | }
57 |
58 | export default function Notifications() {
59 | const [anchorEl, setAnchorEl] = useState(null);
60 | const [notifications, clearNotification] = useNotifications();
61 |
62 | const handleClick = (event: MouseEvent) => {
63 | setAnchorEl(event.currentTarget);
64 | };
65 |
66 | const handleClose = () => {
67 | setAnchorEl(null);
68 | };
69 |
70 | const open = Boolean(anchorEl);
71 | const id = open ? 'simple-popover' : undefined;
72 |
73 | return (
74 |
75 |
81 |
82 |
83 |
84 |
85 |
95 |
96 | {notifications.length ? (
97 | notifications.map((msg, i) => (
98 | clearNotification(msg)}
102 | key={`notification_${i}`}
103 | >
104 | {msg.text}
105 |
106 | ))
107 | ) : (
108 | No notifications
109 | )}
110 |
111 |
112 |
113 | );
114 | }
115 |
--------------------------------------------------------------------------------
/lib/tint.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2018 GoPro, Inc.
2 | // Copyright 2024-2025 Olli Vanhoja
3 | // SPDX-License-Identifier: MIT
4 |
5 | const kTintScale = -3000.0;
6 |
7 | /*****************************************************************************/
8 |
9 | // Table from Wyszecki & Stiles, "Color Science", second edition, page 228.
10 |
11 | // kTempTable[i * 4 + j]
12 | // j:
13 | // r = 0
14 | // u = 1
15 | // v = 2
16 | // t = 3
17 | const kTempTable = Float64Array.from(
18 | [
19 | [0, 0.18006, 0.26352, -0.24341],
20 | [10, 0.18066, 0.26589, -0.25479],
21 | [20, 0.18133, 0.26846, -0.26876],
22 | [30, 0.18208, 0.27119, -0.28539],
23 | [40, 0.18293, 0.27407, -0.3047],
24 | [50, 0.18388, 0.27709, -0.32675],
25 | [60, 0.18494, 0.28021, -0.35156],
26 | [70, 0.18611, 0.28342, -0.37915],
27 | [80, 0.1874, 0.28668, -0.40955],
28 | [90, 0.1888, 0.28997, -0.44278],
29 | [100, 0.19032, 0.29326, -0.47888],
30 | [125, 0.19462, 0.30141, -0.58204],
31 | [150, 0.19962, 0.30921, -0.70471],
32 | [175, 0.20525, 0.31647, -0.84901],
33 | [200, 0.21142, 0.32312, -1.0182],
34 | [225, 0.21807, 0.32909, -1.2168],
35 | [250, 0.22511, 0.33439, -1.4512],
36 | [275, 0.23247, 0.33904, -1.7298],
37 | [300, 0.2401, 0.34308, -2.0637],
38 | [325, 0.24702, 0.34655, -2.4681],
39 | [350, 0.25591, 0.34951, -2.9641],
40 | [375, 0.264, 0.352, -3.5814],
41 | [400, 0.27218, 0.35407, -4.3633],
42 | [425, 0.28039, 0.35577, -5.3762],
43 | [450, 0.28863, 0.35714, -6.7262],
44 | [475, 0.29685, 0.35823, -8.5955],
45 | [500, 0.30505, 0.35907, -11.324],
46 | [525, 0.3132, 0.35968, -15.628],
47 | [550, 0.32129, 0.36011, -23.325],
48 | [575, 0.32931, 0.36038, -40.77],
49 | [600, 0.33724, 0.36051, -116.45],
50 | ].flat()
51 | );
52 |
53 | export default function calcTint(x: number, y: number) {
54 | let fTemperature: number;
55 | let fTint: number;
56 |
57 | const u = (2.0 * x) / (1.5 - x + 6.0 * y);
58 | const v = (3.0 * y) / (1.5 - x + 6.0 * y);
59 |
60 | // Search for line pair coordinate is between.
61 | let last_dt = 0.0;
62 | let last_dv = 0.0;
63 | let last_du = 0.0;
64 |
65 | for (let i = 1; i <= 30; i++) {
66 | // Convert slope to delta-u and delta-v, with length 1.
67 | let du = 1.0;
68 | let dv = kTempTable[i * 4 + 3];
69 | let len = Math.sqrt(1.0 + dv * dv);
70 |
71 | du /= len;
72 | dv /= len;
73 |
74 | // Find delta from black body point to test coordinate.
75 | let uu = u - kTempTable[i * 4 + 1];
76 | let vv = v - kTempTable[i * 4 + 2];
77 |
78 | // Find distance above or below line.
79 | let dt = -uu * dv + vv * du;
80 |
81 | // If below line, we have found line pair.
82 | if (dt <= 0.0 || i === 30) {
83 | // Find fractional weight of two lines.
84 | if (dt > 0.0) {
85 | dt = 0.0;
86 | }
87 |
88 | dt = -dt;
89 |
90 | const f = i === 1 ? 0.0 : dt / (last_dt + dt);
91 |
92 | // Interpolate the temperature.
93 | fTemperature = 1.0e6 / (kTempTable[(i - 1) * 4 + 0] * f + kTempTable[i * 4 + 0] * (1.0 - f));
94 |
95 | // Find delta from black body point to test coordinate.
96 | uu = u - (kTempTable[(i - 1) * 4 + 1] * f + kTempTable[i * 4 + 1] * (1.0 - f));
97 | vv = v - (kTempTable[(i - 1) * 4 + 2] * f + kTempTable[i * 4 + 2] * (1.0 - f));
98 |
99 | // Interpolate vectors along slope.
100 |
101 | du = du * (1.0 - f) + last_du * f;
102 | dv = dv * (1.0 - f) + last_dv * f;
103 |
104 | len = Math.sqrt(du * du + dv * dv);
105 |
106 | du /= len;
107 | dv /= len;
108 |
109 | // Find distance along slope.
110 | fTint = (uu * du + vv * dv) * kTintScale;
111 |
112 | break;
113 | }
114 |
115 | // Try next line pair.
116 | last_dt = dt;
117 | last_du = du;
118 | last_dv = dv;
119 | }
120 |
121 | return [fTemperature, fTint];
122 | }
123 |
--------------------------------------------------------------------------------
/lib/matrix.ts:
--------------------------------------------------------------------------------
1 | // Original matrix funcs Riky Perdana
2 | // slightly edited from https://rikyperdana.medium.com/matrix-operations-in-functional-js-e3463f36b160
3 |
4 | const withAs = (obj: T, cb: (obj: T) => T) => cb(obj);
5 | const sum = (arr: readonly number[]) => arr.reduce((a, b) => a + b);
6 | const mul = (arr: readonly number[]) => arr.reduce((a, b) => a * b);
7 | const sub = (arr: number[]) => arr.splice(1).reduce((a, b) => a - b, arr[0]);
8 | // TODO do something better
9 | const deepClone = (obj: any) => JSON.parse(JSON.stringify(obj));
10 |
11 | const shifter = (arr: number[], step: number) => [...arr.splice(step), ...arr.splice(arr.length - step)] as const;
12 |
13 | export const makeMatrix = (rows: number, cols: number, fill?: (i: number, j: number) => number) =>
14 | Array.from({ length: rows }, (_, i) => Array.from({ length: cols }, (_, j) => (fill ? fill(i, j) : 0)));
15 |
16 | export const matrixSize = (matrix: number[][]) => [matrix.length, matrix[0].length] as const;
17 |
18 | export const arr2mat = (rows: number, cols: number, arr: number[]) =>
19 | Array.from({ length: rows }, (_, i) => arr.slice(i * cols, i * cols + cols));
20 |
21 | const matrixMap = (
22 | matrix: readonly (readonly number[])[],
23 | cb: (args: {
24 | i: readonly number[];
25 | ix: number;
26 | j: number;
27 | jx: number;
28 | matrix: readonly (readonly number[])[];
29 | }) => number
30 | ) =>
31 | deepClone(matrix).map((i: readonly number[], ix: number) =>
32 | i.map((j: number, jx: number) => cb({ i, ix, j, jx, matrix }))
33 | );
34 |
35 | export const matrixScalar = (n: number, matrix: number[][]) => matrixMap(matrix, ({ j }) => n * j);
36 |
37 | export const matrixAdd = (matrices: number[][][]) =>
38 | matrices.reduce(
39 | (acc, inc) => matrixMap(acc, ({ j, ix, jx }) => j + inc[ix][jx]),
40 | makeMatrix(...matrixSize(matrices[0]))
41 | );
42 |
43 | export const matrixSub = (matrices: number[][][]) =>
44 | matrices.splice(1).reduce((acc, inc) => matrixMap(acc, ({ j, ix, jx }) => j - inc[ix][jx]), matrices[0]);
45 |
46 | export const matrixMul = (m1: readonly (readonly number[])[], m2: readonly (readonly number[])[]) =>
47 | makeMatrix(m1.length, m2[0].length, (i, j) => sum(m1[i].map((k, kx) => k * m2[kx][j])));
48 |
49 | export const matrixMuls = (matrices: readonly (readonly (readonly number[])[])[]) =>
50 | deepClone(matrices)
51 | .splice(1)
52 | .reduce(
53 | (acc: number[][], inc: number[][]) =>
54 | makeMatrix(acc.length, inc[0].length, (ix, jx) => sum(acc[ix].map((k, kx) => k * inc[kx][jx]))),
55 | deepClone(matrices[0])
56 | );
57 |
58 | const matrixMinor = (matrix: readonly (readonly number[])[], row: number, col: number) =>
59 | matrix.length < 3
60 | ? matrix
61 | : matrix
62 | .filter((_i: number[], ix: number) => ix !== row - 1)
63 | .map((i) => i.filter((_j: number, jx: number) => jx !== col - 1));
64 |
65 | // TODO FIX
66 | export const matrixTrans = (matrix: readonly (readonly number[])[]) =>
67 | // @ts-ignore
68 | makeMatrix(...shifter(matrixSize(matrix), 1), (i: number, j: number) => matrix[j][i]);
69 |
70 | export const matrixDet = (matrix: readonly (readonly number[])[]) =>
71 | withAs(deepClone(matrix), (clone) =>
72 | matrix.length < 3
73 | ? sub(matrixTrans(clone.map(shifter)).map(mul))
74 | : sum(
75 | clone[0].map(
76 | (i: number, ix: number) => matrixDet(matrixMinor(matrix, 1, ix + 1)) * Math.pow(-1, ix + 2) * i
77 | )
78 | )
79 | );
80 |
81 | export const matrixCofactor = (matrix: readonly (readonly number[])[]) =>
82 | matrixMap(matrix, ({ ix, jx }) =>
83 | matrix[0].length > 2
84 | ? Math.pow(-1, ix + jx + 2) * matrixDet(matrixMinor(matrix, ix + 1, jx + 1))
85 | : ix != jx
86 | ? -matrix[jx][ix]
87 | : matrix[+!ix][+!jx]
88 | );
89 |
90 | export const matrixInverse = (matrix: readonly (readonly number[])[]) =>
91 | matrixMap(matrixTrans(matrixCofactor(matrix)), ({ j }) => j / matrixDet(matrix));
92 |
--------------------------------------------------------------------------------
/pages/color.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@mui/system/Box';
2 | import Paper from '@mui/material/Paper';
3 | import TextField from '@mui/material/TextField';
4 | import InputAdornment from '@mui/material/InputAdornment';
5 | import Container from '@mui/material/Container';
6 | import MyHead from 'components/MyHead';
7 | import Title from 'components/Title';
8 | import Polar from 'components/Polar';
9 | import { useGlobalState } from 'lib/global';
10 | import { XYZnD65, xy2XYZ, XYZ2Lab, LabHueSatChroma } from 'lib/CIEConv';
11 | import { Oklab2Oklch, XYZD65toOklab } from 'lib/Oklab';
12 | import { normalize2 } from 'lib/vector';
13 | import wl2rgb from 'lib/wl2rgb';
14 |
15 | // TODO This is picking wrong colors most of the time
16 | const wls = Object.freeze([0, 600, 570, 550, 500, 450, 650].map((wl, i, arr) => [i / arr.length, wl2rgb(wl)[0]]));
17 |
18 | function borderColorSelector(context) {
19 | const ctx = context.chart.ctx;
20 | const gradient = ctx.createLinearGradient(0, 0, 0, 200);
21 |
22 | for (let v of wls) {
23 | gradient.addColorStop(...v);
24 | }
25 |
26 | return gradient;
27 | }
28 |
29 | export default function Text() {
30 | const [meas] = useGlobalState('res_lm_measurement');
31 | // It may seem like a good idea to put useMemo here but it actually
32 | // consumes 2x the time.
33 | const r = normalize2([meas.V1, meas.B1, meas.G1, meas.Y1, meas.O1, meas.R1]);
34 | const wls = {
35 | borderColor: borderColorSelector,
36 | backgroundColor: 'hsla(35, 40%, 40%, 50%)',
37 | data: [
38 | { r: r[5], angle: 0, label: '650 nm' },
39 | { r: r[4], angle: 0.7803367085666647, label: '600 nm' },
40 | { r: r[3], angle: 1.1704177963873974, label: '570 nm' },
41 | { r: r[2], angle: 1.4250613342533702, label: '550 nm' },
42 | { r: r[1], angle: 2.6939157004532475, label: '500 nm' },
43 | { r: r[0], angle: 3.9013344769829246, label: '450 nm' },
44 | { r: r[5], angle: 2 * Math.PI, label: '650 nm' },
45 | ],
46 | };
47 | const [X, Y, Z] = xy2XYZ(meas.Ex, meas.Ey, meas.Lux);
48 | const Lab = XYZ2Lab(X, Y, Z, XYZnD65);
49 | const { hab: posHab, chroma, sat } = LabHueSatChroma(...Lab);
50 | const oklab = XYZD65toOklab(X, Y, Z);
51 | const [, okC, okH] = Oklab2Oklch(...XYZD65toOklab(X, Y, Z));
52 |
53 | return (
54 |
55 |
56 |
57 | OLM - Color
58 |
59 |
60 |
66 | °,
73 | }}
74 | />
75 |
81 | %,
88 | }}
89 | />
90 |
91 |
92 |
103 |
104 |
105 |
106 |
107 | );
108 | }
109 |
--------------------------------------------------------------------------------
/lib/spline.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 Morgan Herlocker
3 | * SPDX-License-Identifier: MIT
4 | */
5 |
6 | export default class Spline {
7 | readonly xs: number[];
8 | readonly ys: number[];
9 | readonly ks: Float64Array;
10 |
11 | constructor(xs: number[], ys: number[]) {
12 | this.xs = xs;
13 | this.ys = ys;
14 | this.ks = this.getNaturalKs(new Float64Array(this.xs.length));
15 | }
16 |
17 | getNaturalKs(ks: Float64Array): Float64Array {
18 | const n = this.xs.length - 1;
19 | const A = zerosMat(n + 1, n + 2);
20 |
21 | for (
22 | let i = 1;
23 | i < n;
24 | i++ // rows
25 | ) {
26 | A[i][i - 1] = 1 / (this.xs[i] - this.xs[i - 1]);
27 | A[i][i] = 2 * (1 / (this.xs[i] - this.xs[i - 1]) + 1 / (this.xs[i + 1] - this.xs[i]));
28 | A[i][i + 1] = 1 / (this.xs[i + 1] - this.xs[i]);
29 | A[i][n + 1] =
30 | 3 *
31 | ((this.ys[i] - this.ys[i - 1]) / ((this.xs[i] - this.xs[i - 1]) * (this.xs[i] - this.xs[i - 1])) +
32 | (this.ys[i + 1] - this.ys[i]) / ((this.xs[i + 1] - this.xs[i]) * (this.xs[i + 1] - this.xs[i])));
33 | }
34 |
35 | A[0][0] = 2 / (this.xs[1] - this.xs[0]);
36 | A[0][1] = 1 / (this.xs[1] - this.xs[0]);
37 | A[0][n + 1] = (3 * (this.ys[1] - this.ys[0])) / ((this.xs[1] - this.xs[0]) * (this.xs[1] - this.xs[0]));
38 |
39 | A[n][n - 1] = 1 / (this.xs[n] - this.xs[n - 1]);
40 | A[n][n] = 2 / (this.xs[n] - this.xs[n - 1]);
41 | A[n][n + 1] =
42 | (3 * (this.ys[n] - this.ys[n - 1])) / ((this.xs[n] - this.xs[n - 1]) * (this.xs[n] - this.xs[n - 1]));
43 |
44 | return solve(A, ks);
45 | }
46 |
47 | /**
48 | * inspired by https://stackoverflow.com/a/40850313/4417327
49 | */
50 | getIndexBefore(target: number): number {
51 | let low = 0;
52 | let high = this.xs.length;
53 | let mid = 0;
54 | while (low < high) {
55 | mid = Math.floor((low + high) / 2);
56 | if (this.xs[mid] < target && mid !== low) {
57 | low = mid;
58 | } else if (this.xs[mid] >= target && mid !== high) {
59 | high = mid;
60 | } else {
61 | high = low;
62 | }
63 | }
64 |
65 | if (low === this.xs.length - 1) {
66 | return this.xs.length - 1;
67 | }
68 |
69 | return low + 1;
70 | }
71 |
72 | at(x: number): number {
73 | let i = this.getIndexBefore(x);
74 | const t = (x - this.xs[i - 1]) / (this.xs[i] - this.xs[i - 1]);
75 | const a = this.ks[i - 1] * (this.xs[i] - this.xs[i - 1]) - (this.ys[i] - this.ys[i - 1]);
76 | const b = -this.ks[i] * (this.xs[i] - this.xs[i - 1]) + (this.ys[i] - this.ys[i - 1]);
77 | const q = (1 - t) * this.ys[i - 1] + t * this.ys[i] + t * (1 - t) * (a * (1 - t) + b * t);
78 | return q;
79 | }
80 | }
81 |
82 | function solve(A: Float64Array[], ks: Float64Array): Float64Array {
83 | const m = A.length;
84 | let h = 0;
85 | let k = 0;
86 | while (h < m && k <= m) {
87 | let i_max = 0;
88 | let max = -Infinity;
89 | for (let i = h; i < m; i++) {
90 | const v = Math.abs(A[i][k]);
91 | if (v > max) {
92 | i_max = i;
93 | max = v;
94 | }
95 | }
96 |
97 | if (A[i_max][k] === 0) {
98 | k++;
99 | } else {
100 | swapRows(A, h, i_max);
101 | for (let i = h + 1; i < m; i++) {
102 | const f = A[i][k] / A[h][k];
103 | A[i][k] = 0;
104 | for (let j = k + 1; j <= m; j++) A[i][j] -= A[h][j] * f;
105 | }
106 | h++;
107 | k++;
108 | }
109 | }
110 |
111 | for (
112 | let i = m - 1;
113 | i >= 0;
114 | i-- // rows = columns
115 | ) {
116 | var v = 0;
117 | if (A[i][i]) {
118 | v = A[i][m] / A[i][i];
119 | }
120 | ks[i] = v;
121 | for (
122 | let j = i - 1;
123 | j >= 0;
124 | j-- // rows
125 | ) {
126 | A[j][m] -= A[j][i] * v;
127 | A[j][i] = 0;
128 | }
129 | }
130 | return ks;
131 | }
132 |
133 | function zerosMat(r: number, c: number): Float64Array[] {
134 | const A = [];
135 | for (let i = 0; i < r; i++) A.push(new Float64Array(c));
136 | return A;
137 | }
138 |
139 | function swapRows(m: Float64Array[], k: number, l: number): void {
140 | let p = m[k];
141 | m[k] = m[l];
142 | m[l] = p;
143 | }
144 |
--------------------------------------------------------------------------------
/pages/ssi.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useState } from 'react';
2 | import Box from '@mui/system/Box';
3 | import CCT from 'components/CCT';
4 | import Container from '@mui/material/Container';
5 | import Duv from 'components/Duv';
6 | import Memory from 'components/Memory';
7 | import MyHead from 'components/MyHead';
8 | import Paper from '@mui/material/Paper';
9 | import Table from '@mui/material/Table';
10 | import TableBody from '@mui/material/TableBody';
11 | import TableCell from '@mui/material/TableCell';
12 | import TableContainer from '@mui/material/TableContainer';
13 | import TableHead from '@mui/material/TableHead';
14 | import TableRow from '@mui/material/TableRow';
15 | import TextField from '@mui/material/TextField';
16 | import Title from 'components/Title';
17 | import wlMap from 'lib/wlmap';
18 | import { interpolateSPD } from 'lib/spd';
19 | import { lm3NormSPD } from 'lib/lm3calc';
20 | import { makeMatrix } from 'lib/matrix';
21 | import { normalize2 } from 'lib/vector';
22 | import { ssi } from 'lib/ssi';
23 | import { useGlobalState, useMemoryRecall } from 'lib/global';
24 |
25 | function Matrix({ head, m }: { head: string[]; m: number[][] }) {
26 | const cellStyle = { borderWidth: 1, borderStyle: 'solid', borderColor: 'black' };
27 | return (
28 |
29 |
30 |
31 |
32 |
33 | {head.map((col, i) => (
34 |
35 | {col}
36 |
37 | ))}
38 |
39 |
40 |
41 | {m.map((row, i) => (
42 |
43 |
44 | {head[i]}
45 |
46 | {row.map((col, j) => (
47 | {`${col}`}
48 | ))}
49 |
50 | ))}
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | export default function Text() {
58 | const [isClient, setIsClient] = useState(false);
59 | const [meas] = useGlobalState('res_lm_measurement');
60 | const measSpd = interpolateSPD(lm3NormSPD(meas));
61 | const recall = useMemoryRecall();
62 | const recallSpd = useMemo<{ name: string; spd: readonly { l: number; v: number }[] }[]>(() => {
63 | return recall
64 | .filter((m) => ['ref', 'LM3'].includes(m.type))
65 | .map(({ name, type, meas }) => {
66 | if (type === 'ref') {
67 | // @ts-ignore
68 | const norm = normalize2(meas.SPD);
69 | const lv = wlMap((l, i) => ({ l, v: norm[i] }));
70 |
71 | return { name, spd: lv };
72 | } else if (type === 'LM3') {
73 | // @ts-ignore
74 | return { name, spd: lm3NormSPD(meas) };
75 | }
76 | });
77 | }, [recall]);
78 | const [head, SSImat] = useMemo<[string[], number[][]]>(() => {
79 | const allSpd = [{ name: 'current', spd: measSpd }, ...recallSpd];
80 | const mat = makeMatrix(allSpd.length, allSpd.length);
81 |
82 | for (let i = 0; i < mat.length; i++) {
83 | for (let j = 0; j < mat.length; j++) {
84 | mat[i][j] = ssi(allSpd[i].spd, allSpd[j].spd);
85 | }
86 | }
87 |
88 | return [allSpd.map((m) => m.name), mat];
89 | }, [measSpd, recallSpd]);
90 | useEffect(() => {
91 | setIsClient(true);
92 | }, []);
93 |
94 | return (
95 |
96 |
97 |
98 | OLM - SSI
99 |
100 |
101 |
102 |
103 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | );
118 | }
119 |
--------------------------------------------------------------------------------
/components/Polar.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { Scatter, makeChartTitle } from './Chart';
3 |
4 | const axisAngles = [
5 | 0, // red
6 | (1 / 3) * Math.PI, // yellow
7 | (2 / 3) * Math.PI, // green
8 | Math.PI, // cyan
9 | (4 / 3) * Math.PI, // blue
10 | (5 / 3) * Math.PI, // magenta
11 | ];
12 | const lines = axisAngles.map((t) => ({
13 | type: 'line',
14 | drawTime: 'beforeDatasetsDraw',
15 | borderColor: 'lightGrey',
16 | borderWidth: 1,
17 | xMin: 0,
18 | yMin: 0,
19 | xMax: Math.cos(t),
20 | yMax: Math.sin(t),
21 | }));
22 | const points = axisAngles.map((t) => ({
23 | type: 'point',
24 | drawTime: 'beforeDatasetsDraw',
25 | backgroundColor: `hsl(${(t * 180) / Math.PI}, 100%, 45%)`,
26 | xValue: Math.cos(t),
27 | yValue: Math.sin(t),
28 | }));
29 |
30 | function polar2xy(r: number, angle: number) {
31 | return { x: r * Math.cos(angle), y: r * Math.sin(angle) };
32 | }
33 |
34 | type PolarPointer = {
35 | borderColor?: string;
36 | backgroundColor?: string;
37 | r: number;
38 | angle: number;
39 | };
40 |
41 | type PolarDataset = {
42 | labels?: string[];
43 | borderColor?: string;
44 | backgroundColor?: string;
45 | data: { r: number; angle: number; label: string }[];
46 | };
47 |
48 | export default function Polar({
49 | title,
50 | pointer,
51 | datasets,
52 | }: {
53 | title?: string;
54 | pointer?: PolarPointer;
55 | datasets: PolarDataset[];
56 | }) {
57 | const scatterDatasets = useMemo(() => {
58 | const ds: any[] = datasets.map((dataset) => ({
59 | borderColor: dataset.borderColor,
60 | backgroundColor: dataset.backgroundColor,
61 | pointBackgroundColor: dataset.backgroundColor,
62 | animation: false,
63 | showLine: true,
64 | tension: 0.7,
65 | pointRadius: 3,
66 | datalabels: { display: false },
67 | labels: dataset?.labels || (dataset.data[0]?.label && dataset.data.map(({ label }) => label)),
68 | data: dataset.data.map(({ r, angle }) => polar2xy(r, angle)),
69 | }));
70 | if (pointer) {
71 | ds.unshift({
72 | borderColor: pointer.borderColor,
73 | backgroundColor: pointer.backgroundColor,
74 | pointBackgroundColor: pointer.backgroundColor,
75 | pointStyle: 'circle',
76 | pointRadius: (ctx) => (ctx.dataIndex === 0 ? 0 : 5),
77 | showLine: true,
78 | animation: {
79 | easing: 'linear',
80 | },
81 | datalabels: { display: false },
82 | labels: ['origo', `(r: ${pointer.r.toFixed(3)}, ∠: ${(pointer.angle * 180) / Math.PI} °)`],
83 | data: [{ x: 0, y: 0 }, polar2xy(pointer.r, pointer.angle)],
84 | });
85 | }
86 | return ds;
87 | }, [pointer, datasets]);
88 |
89 | return (
90 |
104 | // @ts-ignore
105 | (ctx.dataset.labels && ctx.dataset.labels[ctx.dataIndex]) ||
106 | `(x: ${ctx.parsed.x}, y: ${ctx.parsed.y})`,
107 | },
108 | },
109 | legend: {
110 | display: false,
111 | },
112 | annotation: {
113 | annotations: [
114 | {
115 | type: 'ellipse',
116 | drawTime: 'beforeDatasetsDraw',
117 | xMin: -1,
118 | xMax: 1,
119 | yMin: -1,
120 | yMax: 1,
121 | borderColor: 'lightGrey',
122 | backgroundColor: 'white',
123 | },
124 | {
125 | type: 'ellipse',
126 | drawTime: 'beforeDatasetsDraw',
127 | xMin: -0.5,
128 | xMax: 0.5,
129 | yMin: -0.5,
130 | yMax: 0.5,
131 | borderColor: 'lightGrey',
132 | backgroundColor: 'white',
133 | },
134 | // @ts-ignore
135 | ...lines,
136 | // @ts-ignore
137 | ...points,
138 | ],
139 | },
140 | },
141 | scales: {
142 | x: {
143 | min: -1.1,
144 | max: 1.1,
145 | display: false,
146 | grid: {
147 | display: true,
148 | },
149 | title: {
150 | display: false,
151 | },
152 | ticks: {
153 | display: true,
154 | },
155 | },
156 | y: {
157 | min: -1.1,
158 | max: 1.1,
159 | display: false,
160 | grid: {
161 | display: true,
162 | },
163 | title: {
164 | display: false,
165 | },
166 | ticks: {
167 | display: true,
168 | },
169 | },
170 | },
171 | }}
172 | />
173 | );
174 | }
175 |
--------------------------------------------------------------------------------
/lib/global.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { createGlobalState } from 'react-hooks-global-state';
3 | import { Paired } from './ble';
4 |
5 | export type LM3Measurement = {
6 | // corrected
7 | V1: number;
8 | B1: number;
9 | G1: number;
10 | Y1: number;
11 | O1: number;
12 | R1: number;
13 | temperature: number;
14 | // calculated
15 | mode: number;
16 | Ex: number;
17 | Ey: number;
18 | Eu: number;
19 | Ev: number;
20 | CCT: number;
21 | Duv: number;
22 | tint: number;
23 | Lux: number;
24 | eml: number;
25 | };
26 |
27 | export type RefMeasurement = {
28 | SPD: Float64Array;
29 | Ex: number;
30 | Ey: number;
31 | Eu: number;
32 | Ev: number;
33 | CCT: number;
34 | Duv: number;
35 | tint: number;
36 | Lux: number;
37 | };
38 |
39 | export type MemoryItem = {
40 | name: string;
41 | type: string;
42 | created: number;
43 | /**
44 | * Set true if should be shown on the screen.
45 | */
46 | recall: boolean;
47 | meas: {
48 | Ex: number;
49 | Ey: number;
50 | Eu: number;
51 | Ev: number;
52 | CCT: number;
53 | Duv: number;
54 | tint: number;
55 | Lux: number;
56 | };
57 | };
58 |
59 | export type RefMemoryItem = {
60 | type: 'ref';
61 | meas: RefMeasurement;
62 | } & MemoryItem;
63 |
64 | export type LM3MemoryItem = {
65 | type: 'LM3';
66 | meas: LM3Measurement;
67 | } & MemoryItem;
68 |
69 | export type GlobalState = {
70 | // Devices
71 | btDevice_lm3: null | Paired;
72 | // Control
73 | lm3: any; // TODO Type
74 | // Set values
75 | running: boolean;
76 | // Reported values
77 | res_lm_measurement: LM3Measurement;
78 | res_lm_freq: {
79 | CCT: number;
80 | Lux: number;
81 | fluDepth: number;
82 | flickerIndex: number;
83 | freqDiv: number;
84 | wave: readonly number[];
85 | };
86 | res_battery_level: number;
87 | // Settings
88 | hz: number;
89 | avg: number;
90 | // Memory function
91 | memory: Array;
92 | };
93 |
94 | const LOCAL_STORAGE_KEY = 'olm_settings';
95 | const initialState: GlobalState = {
96 | // Devices
97 | btDevice_lm3: null,
98 | // Control
99 | lm3: null,
100 | // Set values
101 | running: false,
102 | // Reported values
103 | res_lm_measurement: {
104 | V1: 0,
105 | B1: 0,
106 | G1: 0,
107 | Y1: 0,
108 | O1: 0,
109 | R1: 0,
110 | temperature: 20,
111 | mode: 0,
112 | Ex: 0,
113 | Ey: 0,
114 | Eu: 0,
115 | Ev: 0,
116 | CCT: 0,
117 | Duv: 0,
118 | tint: 0,
119 | Lux: 0,
120 | eml: 0,
121 | },
122 | res_lm_freq: {
123 | CCT: 0,
124 | Lux: 0,
125 | fluDepth: 0,
126 | flickerIndex: 0,
127 | freqDiv: 1,
128 | wave: [],
129 | },
130 | res_battery_level: -1,
131 | // Settings
132 | hz: 1,
133 | avg: 1,
134 | // Memory feature
135 | memory: [],
136 | // Load config from local storage
137 | ...(typeof window === 'undefined' ? {} : JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))),
138 | };
139 |
140 | const { useGlobalState: _useGlobalState, getGlobalState, setGlobalState } = createGlobalState(initialState);
141 |
142 | type ConfigKey = 'avg' | 'hz' | 'memory';
143 |
144 | type SetStateAction = S | ((prevState: S) => S);
145 | function useGlobalState(
146 | key: StateKey
147 | ): readonly [GlobalState[StateKey], (u: SetStateAction) => void] {
148 | const [value, setValue] = _useGlobalState(key);
149 |
150 | const setAndSaveValue = (value: Parameters[0]) => {
151 | setValue(value);
152 |
153 | if (['hz', 'avg', 'memory'].includes(key)) {
154 | // Defer saving to not disturb the render loop.
155 | globalThis.scheduler.postTask(() => saveConfig(), {
156 | priority: 'background',
157 | });
158 |
159 | }
160 | };
161 |
162 | return [value, setAndSaveValue] as const;
163 | }
164 |
165 | function saveConfig() {
166 | const config: { [k in ConfigKey]: any } = {
167 | hz: getGlobalState('hz'),
168 | avg: getGlobalState('avg'),
169 | memory: getGlobalState('memory'),
170 | };
171 | localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(config));
172 | }
173 |
174 | function useMemoryRecall(): MemoryItem[] {
175 | const [memory] = useGlobalState('memory');
176 | return useMemo(() => memory.filter((m: MemoryItem) => m.recall).map((v) => {
177 | if (v.type == 'ref') {
178 | return {
179 | ...v,
180 | meas: {
181 | ...v.meas,
182 | // @ts-ignore
183 | SPD: new Float64Array(Object.values(v.meas.SPD)),
184 | },
185 | };
186 | }
187 | return v;
188 | }), [memory]);
189 | }
190 |
191 | export { useGlobalState, getGlobalState, setGlobalState /* saveConfig */, useMemoryRecall };
192 |
--------------------------------------------------------------------------------
/components/settings/ble.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import Alert from '@mui/material/Alert';
3 | import CardActions from '@mui/material/CardActions';
4 | import CardContent from '@mui/material/CardContent';
5 | import SensorWindowIcon from '@mui/icons-material/SensorWindow';
6 | import { iconStyle, SettingsCard, ActionButton } from 'components/settings/SettingsCard';
7 | import { Paired, pairDevice } from 'lib/ble';
8 | import { GlobalState, useGlobalState } from 'lib/global';
9 |
10 | type Severity = 'error' | 'info' | 'success' | 'warning';
11 |
12 | type InfoMessage = {
13 | message: string;
14 | severity: Severity;
15 | };
16 |
17 | function DeviceStatus({ wait, severity, children }: { wait?: boolean; severity: Severity; children: any }) {
18 | return (
19 |
20 | {children}
21 |
22 | );
23 | }
24 |
25 | export default function Ble({
26 | title,
27 | globalBtDeviceName,
28 | filter,
29 | optionalServices,
30 | connectCb,
31 | disconnectCb,
32 | }: {
33 | title: string;
34 | globalBtDeviceName: keyof GlobalState;
35 | filter?: Parameters[0];
36 | optionalServices: Parameters[1];
37 | connectCb: (server: BluetoothRemoteGATTServer) => Promise;
38 | disconnectCb: (btd: Paired) => void;
39 | }) {
40 | const pairedWithMessage = (btd: Paired): InfoMessage => ({
41 | message: btd ? `Paired with\n${btd.device.name}` : 'Not configured',
42 | severity: 'info',
43 | });
44 | const [btAvailable, setBtAvailable] = useState(false);
45 | const [pairingRequest, setPairingRequest] = useState(false);
46 | const [isPairing, setIsPairing] = useState(false);
47 | const [btDevice, setBtDevice] = useGlobalState(globalBtDeviceName);
48 | let [info, setInfo] = useState(pairedWithMessage(btDevice));
49 |
50 | const unpairDevice = () => {
51 | if (btDevice) {
52 | if (btDevice.device.gatt.connected) {
53 | btDevice.disconnect();
54 | }
55 | setBtDevice(null);
56 | setInfo(pairedWithMessage(null));
57 | disconnectCb(btDevice);
58 | setIsPairing(false);
59 | }
60 | };
61 |
62 | useEffect(() => {
63 | navigator.bluetooth
64 | .getAvailability()
65 | .then((v) => setBtAvailable(v))
66 | .catch(() => {});
67 | }, []);
68 |
69 | useEffect(() => {
70 | if (pairingRequest) {
71 | setPairingRequest(false);
72 | setIsPairing(true);
73 | if (btDevice && btDevice.device.gatt.connected) {
74 | unpairDevice();
75 | }
76 |
77 | (async () => {
78 | try {
79 | setInfo({ message: 'Requesting BLE Device...', severity: 'info' });
80 |
81 | const newBtDevice = await pairDevice(
82 | filter || null,
83 | optionalServices,
84 | async ({ device: _device, server }) => {
85 | try {
86 | await connectCb(server);
87 | } catch (err) {
88 | console.error(err);
89 | setInfo({ message: `${err}`, severity: 'error' });
90 | }
91 | },
92 | () => {
93 | // Unpair if we can't reconnect.
94 | unpairDevice();
95 | }
96 | );
97 |
98 | const { device } = newBtDevice;
99 | console.log(`> Name: ${device.name}\n> Id: ${device.id}\n> Connected: ${device.gatt.connected}`);
100 | setInfo(pairedWithMessage(newBtDevice));
101 | setBtDevice(newBtDevice);
102 | } catch (err) {
103 | const msg = `${err}`;
104 | if (msg.startsWith('NotFoundError: User cancelled')) {
105 | setInfo({ message: 'Pairing cancelled', severity: 'warning' });
106 | } else {
107 | setInfo({ message: `${err}`, severity: 'error' });
108 | }
109 | } finally {
110 | setIsPairing(false);
111 | }
112 | })();
113 | }
114 | }, [pairingRequest]); // eslint-disable-line react-hooks/exhaustive-deps
115 |
116 | const scanDevices = () => {
117 | setPairingRequest(true);
118 | };
119 |
120 | return (
121 | }
123 | title={title}
124 | actions={
125 |
126 |
127 | Scan
128 |
129 |
130 | Unpair
131 |
132 |
133 | }
134 | >
135 |
136 | {info.message.split('\n').map((line, i) => (
137 |
138 | {`${line}`}
139 |
140 |
141 | ))}
142 |
143 |
144 | );
145 | }
146 |
--------------------------------------------------------------------------------
/pages/exposure.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo, useState } from 'react';
2 | import Box from '@mui/system/Box';
3 | import Container from '@mui/material/Container';
4 | import InputAdornment from '@mui/material/InputAdornment';
5 | import Paper from '@mui/material/Paper';
6 | import Stack from '@mui/material/Stack';
7 | import Switch from '@mui/material/Switch';
8 | import TextField from '@mui/material/TextField';
9 | import Typography from '@mui/material/Typography';
10 | import MyHead from 'components/MyHead';
11 | import Title from 'components/Title';
12 | import { useGlobalState } from 'lib/global';
13 | import { calcEV, calcFstop, calcShutter, closestAperture, closestShutter } from 'lib/exposure';
14 |
15 | const DEFAULT_ISO = 100;
16 |
17 | export default function Exposure() {
18 | const [meas] = useGlobalState('res_lm_measurement');
19 | const [iso, setIso] = useState(DEFAULT_ISO);
20 | const [gain, setGain] = useState(0);
21 | const [auto, setAuto] = useState(0);
22 | const [[shutter, aperture], setParam] = useState([1 / 100, 4]);
23 | const [shutterStr, setShutterStr] = useState('4');
24 | const [apertureStr, setApertureStr] = useState('4');
25 | const [invalid, setInvalid] = useState(0);
26 | const updateParam = useCallback(
27 | (newEv: number, newShutter: number, newAperture: number) => {
28 | if (auto === 0) {
29 | const autoShutter = calcShutter(newEv, newAperture);
30 | const str = `${1 / closestShutter(autoShutter)}`;
31 | setParam([autoShutter, newAperture]);
32 | setShutterStr(str);
33 | } else {
34 | const autoAperture = calcFstop(newEv, newShutter);
35 | const tradFstop = closestAperture(autoAperture);
36 | const str = tradFstop < 10 ? tradFstop.toFixed(1) : `${tradFstop}`;
37 | setParam([newShutter, autoAperture]);
38 | setApertureStr(str);
39 | }
40 | },
41 | [setParam, setShutterStr, setApertureStr, auto]
42 | );
43 | const ev = useMemo(() => {
44 | const ev = calcEV(meas.Lux, Number.isNaN(iso) ? DEFAULT_ISO : iso, gain);
45 | updateParam(ev, shutter, aperture);
46 | return ev;
47 | }, [updateParam, meas.Lux, iso, gain, shutter, aperture]);
48 | const updateExposure = (newShutter: number, newAperture: number) => {
49 | const newInvalid = (Number(Number.isNaN(newShutter)) & 1) | ((Number(Number.isNaN(newAperture)) & 1) << 1);
50 | setInvalid(newInvalid);
51 | updateParam(ev, newShutter, newAperture);
52 | };
53 |
54 | return (
55 |
56 |
57 |
66 | OLM - Exposure
67 |
68 |
69 |
70 | setIso(parseInt(e.target.value))}
77 | />
78 | setGain(Number(e.target.value))}
83 | />
84 |
85 |
86 |
87 | Shutter
88 | setAuto(Number(e.target.checked))}
92 | />
93 | Aperture
94 |
95 |
96 |
97 | 1/,
101 | }}
102 | variant="outlined"
103 | required
104 | disabled={auto === 0}
105 | error={!!(invalid & 1)}
106 | value={shutterStr}
107 | onChange={(e) => {
108 | const v = e.target.value;
109 | setShutterStr(v);
110 | const num = 1 / Number(v);
111 | if (!Number.isNaN(num)) updateExposure(num, aperture);
112 | }}
113 | />
114 | f/,
118 | }}
119 | variant="outlined"
120 | required
121 | disabled={auto === 1}
122 | error={!!(invalid & 2)}
123 | value={apertureStr}
124 | onChange={(e) => {
125 | const v = e.target.value;
126 | setApertureStr(v);
127 | const num = Number(v);
128 | if (!Number.isNaN(num)) updateExposure(shutter, num);
129 | }}
130 | />
131 |
132 |
133 |
134 |
135 | );
136 | }
137 |
--------------------------------------------------------------------------------
/pages/spectrum.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import Box from '@mui/system/Box';
3 | import Carousel from 'react-material-ui-carousel';
4 | import Container from '@mui/material/Container';
5 | import InputAdornment from '@mui/material/InputAdornment';
6 | import Paper from '@mui/material/Paper';
7 | import TextField from '@mui/material/TextField';
8 | import Memory from 'components/Memory';
9 | import MyHead from 'components/MyHead';
10 | import Title from 'components/Title';
11 | import { Bar, Scatter, makeChartTitle } from 'components/Chart';
12 | import wavelengthToColor from 'lib/wl2rgb';
13 | import wlMap from 'lib/wlmap';
14 | import { SPD, interpolateSPD } from 'lib/spd';
15 | import { lm3NormSPD } from 'lib/lm3calc';
16 | import { normalize2 } from 'lib/vector';
17 | import { useMemoryRecall, useGlobalState, LM3Measurement, RefMeasurement } from 'lib/global';
18 |
19 | type RecallData = {
20 | name: string;
21 | norm: SPD;
22 | scatter: Array<{ x: number; y: number }>;
23 | };
24 |
25 | function refToRecallData(name: string, meas: RefMeasurement): RecallData {
26 | const normBars = normalize2([
27 | meas.SPD[14], // 450 nm
28 | meas.SPD[24], // 500 nm
29 | meas.SPD[34], // 550 nm
30 | meas.SPD[38], // 570 nm
31 | meas.SPD[44], // 600 nm
32 | meas.SPD[54], // 650 nm
33 | ]);
34 | const normScatter = normalize2(meas.SPD);
35 |
36 | return {
37 | name,
38 | norm: wlMap((l, i) => ({ l, v: normBars[i] }), 5),
39 | scatter: wlMap((l, i) => ({ x: l, y: normScatter[i] }), 5),
40 | };
41 | }
42 |
43 | function lm3ToRecallData(name: string, meas: LM3Measurement) {
44 | const norm = lm3NormSPD(meas);
45 |
46 | return {
47 | name,
48 | norm,
49 | scatter: interpolateSPD2Chart(norm),
50 | };
51 | }
52 |
53 | const measToRecallData = Object.freeze({
54 | ref: refToRecallData,
55 | LM3: lm3ToRecallData,
56 | });
57 |
58 | const barDataLabels = Object.freeze(['450 nm', '500 nm', '550 nm', '570 nm', '600 nm', '650 nm']);
59 |
60 | function spd2bar(spd: Readonly) {
61 | return spd.map(({ v }) => v);
62 | }
63 |
64 | function spd2color(spd: Readonly) {
65 | return spd.map(({ l }) => wavelengthToColor(l)[0]);
66 | }
67 |
68 | function SpectrumBar({ data, recallData }: { data: Readonly; recallData: RecallData[] }) {
69 | return (
70 | ({
78 | label: e.name,
79 | data: spd2bar(e.norm),
80 | borderWidth: 1,
81 | borderColor: 'black',
82 | backgroundColor: 'rgb(0,0,0,0)',
83 | })),
84 | {
85 | label: 'current',
86 | data: spd2bar(data),
87 | backgroundColor: spd2color(data),
88 | },
89 | ],
90 | }}
91 | options={{
92 | plugins: {
93 | title: makeChartTitle('Measured SPD'),
94 | legend: {
95 | display: false,
96 | },
97 | datalabels: { display: false },
98 | },
99 | scales: {
100 | x: {
101 | stacked: true,
102 | },
103 | y: {
104 | min: 0,
105 | max: 1,
106 | stacked: false,
107 | },
108 | },
109 | }}
110 | />
111 | );
112 | }
113 |
114 | function SpectrumScatter({ data, recallData }: { data: Array<{ x: number; y: number }>; recallData: RecallData[] }) {
115 | return (
116 | ({
125 | label: m.name,
126 | data: m.scatter,
127 | })),
128 | ],
129 | }}
130 | options={{
131 | plugins: {
132 | title: makeChartTitle('Interpolated SPD'),
133 | legend: {
134 | display: false,
135 | },
136 | datalabels: { display: false },
137 | },
138 | scales: {
139 | x: {
140 | min: 380,
141 | max: 780,
142 | },
143 | y: {
144 | min: 0,
145 | },
146 | },
147 | showLine: true,
148 | elements: {
149 | point: {
150 | pointStyle: false,
151 | },
152 | },
153 | }}
154 | />
155 | );
156 | }
157 |
158 | function interpolateSPD2Chart(data: Readonly) {
159 | return interpolateSPD(data).map(({ l, v }) => ({ x: l, y: v }));
160 | }
161 |
162 | export default function Spectrum() {
163 | const [meas] = useGlobalState('res_lm_measurement');
164 | const recall = useMemoryRecall();
165 | const norm = lm3NormSPD(meas);
166 | const scatter = interpolateSPD2Chart(norm);
167 | const recallData = useMemo(
168 | () =>
169 | recall
170 | .filter((m) => ['ref', 'LM3'].includes(m.type))
171 | .map(({ name, type, meas }): RecallData => measToRecallData[type](name, meas)),
172 | [recall]
173 | );
174 |
175 | return (
176 |
177 |
178 |
179 | OLM - Spectrum
180 |
181 |
182 | lx,
189 | }}
190 | />
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 | );
209 | }
210 |
--------------------------------------------------------------------------------
/pages/cri.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo, useState } from 'react';
2 | import Box from '@mui/system/Box';
3 | import Carousel from 'react-material-ui-carousel';
4 | import Container from '@mui/material/Container';
5 | import FormControlLabel from '@mui/material/FormControlLabel';
6 | import FormGroup from '@mui/material/FormGroup';
7 | import Paper from '@mui/material/Paper';
8 | import Switch from '@mui/material/Switch';
9 | import TextField from '@mui/material/TextField';
10 | import CCT from 'components/CCT';
11 | import Duv from 'components/Duv';
12 | import MyHead from 'components/MyHead';
13 | import Title from 'components/Title';
14 | import { useGlobalState } from 'lib/global';
15 | import { Bar, Scatter, gridColorAuto, pointRotationAuto } from 'components/Chart';
16 | import { calcCRI } from 'lib/cri';
17 | import lm3CalcCRI from 'lib/lm3calc';
18 |
19 | const lightBlack = 'rgb(50,50,50)';
20 | const swatch = [
21 | 'rgb(242, 185, 158)',
22 | 'rgb(206, 177, 82)',
23 | 'rgb(128, 186, 76)',
24 | 'rgb(0, 168, 166)',
25 | 'rgb(0, 159, 222)',
26 | 'rgb(0, 134, 205)',
27 | 'rgb(165, 148, 198)',
28 | 'rgb(233, 155, 193)',
29 | 'rgb(230, 0, 54)',
30 | 'rgb(255, 255, 255)',
31 | 'rgb(0, 137, 94)',
32 | 'rgb(0, 60, 149)',
33 | 'rgb(244, 232, 219)',
34 | 'rgb(0, 96, 68)',
35 | ];
36 | const swatchBorder = [
37 | 'rgb(242, 185, 158)',
38 | 'rgb(206, 177, 82)',
39 | 'rgb(128, 186, 76)',
40 | 'rgb(0, 168, 166)',
41 | 'rgb(0, 159, 222)',
42 | 'rgb(0, 134, 205)',
43 | 'rgb(165, 148, 198)',
44 | 'rgb(233, 155, 193)',
45 | 'rgb(230, 0, 54)',
46 | lightBlack,
47 | 'rgb(0, 137, 94)',
48 | 'rgb(0, 60, 149)',
49 | lightBlack,
50 | 'rgb(0, 96, 68)',
51 | ];
52 |
53 | function CriText({ cri }: { cri: ReturnType }) {
54 | return (
55 |
56 | {cri.R.map((r, i) => (
57 |
64 | ))}
65 |
66 | );
67 | }
68 |
69 | const criBarLabels = ['Ra', 'R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9', 'R10', 'R11', 'R12', 'R13', 'R14'];
70 |
71 | function CriBars({ cri, showAll }: { cri: ReturnType; showAll: boolean }) {
72 | return (
73 | (c.dataIndex == 0 ? lightBlack : swatch[c.dataIndex - 1]),
83 | data: showAll ? cri.R : cri.R.slice(0, 9),
84 | },
85 | ],
86 | }}
87 | options={{
88 | indexAxis: 'y',
89 | elements: {
90 | bar: {
91 | borderWidth: 2,
92 | },
93 | },
94 | scales: {
95 | x: {
96 | min: 0,
97 | max: 100,
98 | grid: {
99 | display: true,
100 | },
101 | },
102 | y: {
103 | grid: {
104 | display: true,
105 | },
106 | },
107 | },
108 | plugins: {
109 | legend: {
110 | display: false,
111 | },
112 | },
113 | }}
114 | />
115 | );
116 | }
117 |
118 | function CriChart({ cri, showAll }: { cri: ReturnType; showAll?: boolean }) {
119 | return (
120 | ({
125 | label: `R${i + 1}`,
126 | borderColor: swatchBorder[i],
127 | backgroundColor: swatch[i],
128 | datalabels: { display: false },
129 | pointRotation: (ctx) => pointRotationAuto(ctx),
130 | pointRadius: (ctx) => ctx.dataIndex != 0 && 3,
131 | showLine: true,
132 | data: [
133 | { x: ref[0], y: ref[1] },
134 | { x: test[0], y: test[1] },
135 | ],
136 | })),
137 | }}
138 | options={{
139 | plugins: {
140 | // @ts-ignore
141 | customCanvasBackgroundColor: {
142 | color: 'white',
143 | },
144 | tooltip: {
145 | enabled: true,
146 | callbacks: {
147 | title: (_tooltipItems) => `Difference`,
148 | beforeLabel: (tooltipItem) =>
149 | `R${tooltipItem.datasetIndex + 1} ${tooltipItem.dataIndex == 0 ? 'Ref' : 'Test'}`,
150 | label: (tooltipItem) =>
151 | `U*V*: ${tooltipItem.formattedValue} ${tooltipItem.dataIndex == 0 ? '' : `∆E: ${cri.DE[tooltipItem.datasetIndex].toFixed(3)}`}`,
152 | },
153 | },
154 | },
155 | scales: {
156 | x: {
157 | min: showAll ? -60 : -40,
158 | max: showAll ? 120 : 40,
159 | grid: {
160 | color: gridColorAuto,
161 | },
162 | },
163 | y: {
164 | min: showAll ? -50 : -40,
165 | max: showAll ? 60 : 40,
166 | grid: {
167 | color: gridColorAuto,
168 | },
169 | },
170 | },
171 | elements: {
172 | line: {
173 | borderWidth: 2,
174 | },
175 | point: {
176 | pointStyle: 'rect',
177 | borderWidth: 1,
178 | },
179 | },
180 | }}
181 | />
182 | );
183 | }
184 |
185 | export default function Cri() {
186 | const [meas] = useGlobalState('res_lm_measurement');
187 | const cri = useMemo(() => lm3CalcCRI(meas), [meas]);
188 | const [chartShowAll, setChartShowAll] = useState(false);
189 |
190 | return (
191 |
192 |
193 |
194 | OLM - CRI
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 | setChartShowAll(event.target.checked)} />}
204 | label="Show R9-R14"
205 | />
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 | );
219 | }
220 |
--------------------------------------------------------------------------------
/pages/flicker.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useState } from 'react';
2 | import Box from '@mui/system/Box';
3 | import Container from '@mui/material/Container';
4 | import IconButton from '@mui/material/IconButton';
5 | import ModelTrainingIcon from '@mui/icons-material/ModelTraining';
6 | import Paper from '@mui/material/Paper';
7 | import Slider from '@mui/material/Slider';
8 | import Stack from '@mui/material/Stack';
9 | import Table from '@mui/material/Table';
10 | import TableBody from '@mui/material/TableBody';
11 | import TableCell from '@mui/material/TableCell';
12 | import TableContainer from '@mui/material/TableContainer';
13 | import TableHead from '@mui/material/TableHead';
14 | import TableRow from '@mui/material/TableRow';
15 | import Typography from '@mui/material/Typography';
16 | import MyHead from 'components/MyHead';
17 | import Title from 'components/Title';
18 | import { Line } from 'components/Chart';
19 | import { useGlobalState } from 'lib/global';
20 | import { calcFft } from 'lib/flicker';
21 |
22 | const marks = [
23 | {
24 | value: 0,
25 | label: 'low',
26 | },
27 | {
28 | value: 1,
29 | label: 'medium',
30 | },
31 | {
32 | value: 2,
33 | label: 'high',
34 | },
35 | ];
36 |
37 | function DiscreteSliderValues({ onChange }: { onChange: (newValue: number) => void }) {
38 | return (
39 |
40 | {
49 | let n: number;
50 | switch (newValue) {
51 | case 0:
52 | n = 146;
53 | break;
54 | case 1:
55 | n = 25;
56 | break;
57 | case 2:
58 | n = 11;
59 | break;
60 | default:
61 | n = 25;
62 | }
63 | onChange(n);
64 | }}
65 | />
66 |
67 | );
68 | }
69 |
70 | function Control({ n }) {
71 | const [lm3] = useGlobalState('lm3');
72 | const [meas] = useGlobalState('res_lm_freq');
73 | const [working, setWorking] = useState(false);
74 |
75 | useEffect(() => {
76 | setWorking(!lm3);
77 | }, [meas, lm3]);
78 |
79 | return (
80 |
81 | {
84 | setWorking(true);
85 | lm3.readFreq(0, n);
86 | }}
87 | size="large"
88 | aria-label="start/pause measurements"
89 | color="inherit"
90 | >
91 | {}
92 |
93 |
94 | );
95 | }
96 |
97 | type TypedArray =
98 | | Int8Array
99 | | Uint8Array
100 | | Uint8ClampedArray
101 | | Int16Array
102 | | Uint16Array
103 | | Int32Array
104 | | Uint32Array
105 | | Float32Array
106 | | Float64Array;
107 | //| BigInt64Array
108 | //| BigUint64Array;
109 |
110 | function typedArrayMax(arr: TypedArray): number {
111 | let max: number = arr.length > 0 ? arr[0] : -Infinity;
112 |
113 | for (let v of arr) {
114 | if (v > max) {
115 | max = v;
116 | }
117 | }
118 |
119 | return max;
120 | }
121 |
122 | const FFT = ({ wave, freqDiv, setFc }: { wave: readonly number[]; freqDiv: number; setFc: (fc: number) => void }) => {
123 | const data: Float32Array = useMemo(() => calcFft(wave), [wave]);
124 | const unitMul = freqDiv > 26 ? 1 : 1 / 1000;
125 | useEffect(() => setFc((1e3 * data.indexOf(typedArrayMax(data.subarray(1)))) / freqDiv), [freqDiv, setFc, data]);
126 |
127 | return (
128 |
129 | i),
132 | datasets: [
133 | {
134 | label: 'Dataset',
135 | data: data,
136 | fill: false,
137 | borderColor: 'black',
138 | borderWidth: 1,
139 | pointRadius: 0,
140 | },
141 | ],
142 | }}
143 | options={{
144 | plugins: {
145 | legend: {
146 | display: false,
147 | },
148 | datalabels: {
149 | display: false,
150 | },
151 | },
152 | scales: {
153 | x: {
154 | display: true,
155 | grid: {
156 | display: true,
157 | },
158 | title: {
159 | display: true,
160 | text: unitMul === 1 ? 'Frequency [Hz]' : 'Frequency [kHz]',
161 | color: 'black',
162 | },
163 | ticks: {
164 | display: true,
165 | color: 'black',
166 | callback: (tickValue, index, ticks) =>
167 | unitMul === 1
168 | ? Math.round((1e3 * Number(tickValue)) / freqDiv)
169 | : (((1e3 * Number(tickValue)) / freqDiv) * unitMul).toFixed(2),
170 | },
171 | },
172 | y: {
173 | display: true,
174 | grid: {
175 | display: true,
176 | },
177 | title: {
178 | display: true,
179 | text: 'Mag [Lux]',
180 | color: 'black',
181 | },
182 | ticks: {
183 | display: true,
184 | color: 'black',
185 | },
186 | },
187 | },
188 | }}
189 | />
190 |
191 | );
192 | };
193 |
194 | function DataTable({ CCT, Lux, flickerIndex, fluDepth, fc }) {
195 | return (
196 |
197 |
198 |
199 |
200 | CCT
201 | Lux
202 | Flicker Index
203 | Modulation Depth [%]
204 | fc [Hz]
205 |
206 |
207 |
208 |
209 | {Math.round(CCT)}
210 | {Math.round(Lux)}
211 | {Math.round(flickerIndex)}
212 | {Math.round(fluDepth)}
213 | {Math.round(fc)}
214 |
215 |
216 |
217 |
218 | );
219 | }
220 |
221 | export default function Flicker() {
222 | const [data] = useGlobalState('res_lm_freq');
223 | const [n, setN] = useState(146);
224 | const [fc, setFc] = useState(0);
225 |
226 | return (
227 |
228 |
229 |
230 | OLM - Flicker
231 |
232 | Sampling
233 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
252 |
253 |
254 | );
255 | }
256 |
--------------------------------------------------------------------------------
/pages/text.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import Box from '@mui/system/Box';
3 | import Container from '@mui/material/Container';
4 | import Paper from '@mui/material/Paper';
5 | import { DataGrid, GridColDef, GridToolbar } from '@mui/x-data-grid';
6 | import { GridLogicOperator } from '@mui/x-data-grid';
7 | import Memory from 'components/Memory';
8 | import MyHead from 'components/MyHead';
9 | import Title from 'components/Title';
10 | import lm3CalcCRI from 'lib/lm3calc';
11 | import { XYZnD65, xy2XYZ, XYZ2Lab, LabHueSatChroma } from 'lib/CIEConv';
12 | import { calcCRI } from 'lib/cri';
13 | import { useGlobalState, useMemoryRecall } from 'lib/global';
14 |
15 | const rowFormatter: { [key: string]: (value: never) => string } = {
16 | CCT: (value: number) => `${Math.round(value)} K`,
17 | 'CCT [Mired]': (value: number) => `${Math.round(value)} MK⁻¹`,
18 | x: (value: number) => `${value.toFixed(4)}`,
19 | y: (value: number) => `${value.toFixed(4)}`,
20 | u: (value: number) => `${value.toFixed(4)}`,
21 | v: (value: number) => `${value.toFixed(4)}`,
22 | Duv: (value: number) => value.toFixed(3),
23 | Tint: (value: number) => value.toFixed(0),
24 | Hue: (value: number) => `${value} deg`,
25 | Sat: (value: number) => `${(100 * value).toFixed(0)} %`,
26 | Illuminance: (value: number) => `${Math.round(value)} lx`,
27 | 'Illuminance [fc]': (value: number) => `${Math.round(value)} ft⋅cd`,
28 | Ra: (value: number) => `${value.toFixed(0)}`,
29 | R0: (value: number) => `${value.toFixed(0)}`,
30 | R1: (value: number) => `${value.toFixed(0)}`,
31 | R2: (value: number) => `${value.toFixed(0)}`,
32 | R3: (value: number) => `${value.toFixed(0)}`,
33 | R4: (value: number) => `${value.toFixed(0)}`,
34 | R5: (value: number) => `${value.toFixed(0)}`,
35 | R6: (value: number) => `${value.toFixed(0)}`,
36 | R7: (value: number) => `${value.toFixed(0)}`,
37 | R8: (value: number) => `${value.toFixed(0)}`,
38 | R9: (value: number) => `${value.toFixed(0)}`,
39 | R10: (value: number) => `${value.toFixed(0)}`,
40 | R11: (value: number) => `${value.toFixed(0)}`,
41 | R12: (value: number) => `${value.toFixed(0)}`,
42 | R13: (value: number) => `${value.toFixed(0)}`,
43 | R14: (value: number) => `${value.toFixed(0)}`,
44 | Temperature: (value: number) => `${value.toFixed(2)} °C`,
45 | };
46 |
47 | const rowsSample = [{ id: 1, name: 'CCT', value: 5600 }];
48 |
49 | const columnsTemplate: GridColDef<(typeof rowsSample)[number]>[] = [
50 | { field: 'id', headerName: 'ID' },
51 | {
52 | field: 'name',
53 | headerName: 'Name',
54 | width: 120,
55 | hideable: false,
56 | },
57 | {
58 | field: 'value',
59 | headerName: 'Current',
60 | type: 'number',
61 | width: 120,
62 | sortable: false,
63 | hideable: false,
64 | getApplyQuickFilterFn: undefined,
65 | valueFormatter: (value, { name }) => rowFormatter[name](value || 0),
66 | },
67 | ];
68 |
69 | function DataArray({ rows, pageSize = 5, filter }: { rows: typeof rowsSample; pageSize?: number; filter?: any }) {
70 | const recall = useMemoryRecall();
71 | const columns = [
72 | ...columnsTemplate,
73 | ...recall.map((rvalue, i) => ({
74 | ...columnsTemplate[2],
75 | field: `recall${i}`,
76 | headerName: `${rvalue.name}`,
77 | hideable: true,
78 | })),
79 | ];
80 |
81 | return (
82 |
83 |
106 |
107 | );
108 | }
109 |
110 | function calcHueSat(x: number, y: number, Lux: number) {
111 | const [X, Y, Z] = xy2XYZ(x, y, Lux);
112 | const [L, a, b] = XYZ2Lab(X, Y, Z, XYZnD65);
113 |
114 | return LabHueSatChroma(L, a, b);
115 | }
116 |
117 | const KtoMK = (cct: number) => 1_000_000 / cct;
118 | const Lux2fc = (lux: number) => lux * 0.09293680297;
119 |
120 | function makeRecallCols(cols: number[]) {
121 | return cols.reduce((prev, cur, i) => ((prev[`recall${i}`] = cur), prev), {});
122 | }
123 |
124 | export default function Text() {
125 | const [meas] = useGlobalState('res_lm_measurement');
126 | const recall = useMemoryRecall();
127 | const rows = useMemo(() => {
128 | const { hab: hue, sat } = calcHueSat(meas.Ex, meas.Ey, meas.Lux);
129 | const cri = lm3CalcCRI(meas);
130 | const recallCri = recall.map(({ type: t, meas: rMeas }) =>
131 | // @ts-ignore
132 | t === 'LM3' ? lm3CalcCRI(rMeas) : t === 'ref' ? calcCRI(rMeas.CCT, rMeas.SPD) : null
133 | );
134 | const array = [
135 | { id: 0, name: 'CCT', value: meas.CCT, ...makeRecallCols(recall.map((item) => item.meas.CCT)) },
136 | {
137 | id: 0,
138 | name: 'CCT [Mired]',
139 | value: KtoMK(meas.CCT),
140 | ...makeRecallCols(recall.map((item) => KtoMK(item.meas.CCT))),
141 | },
142 | { id: 0, name: 'x', value: meas.Ex, ...makeRecallCols(recall.map((item) => item.meas.Ex)) },
143 | { id: 0, name: 'y', value: meas.Ey, ...makeRecallCols(recall.map((item) => item.meas.Ey)) },
144 | { id: 0, name: 'u', value: meas.Eu, ...makeRecallCols(recall.map((item) => item.meas.Eu)) },
145 | { id: 0, name: 'v', value: meas.Ev, ...makeRecallCols(recall.map((item) => item.meas.Ev)) },
146 | { id: 0, name: 'Duv', value: meas.Duv, ...makeRecallCols(recall.map((item) => item.meas.Duv)) },
147 | { id: 0, name: 'Tint', value: meas.tint, ...makeRecallCols(recall.map((item) => item.meas.tint)) },
148 | {
149 | id: 0,
150 | name: 'Hue',
151 | value: hue,
152 | ...makeRecallCols(recall.map((item) => calcHueSat(item.meas.Ex, item.meas.Ey, item.meas.Lux).hab)),
153 | },
154 | {
155 | id: 0,
156 | name: 'Sat',
157 | value: sat,
158 | ...makeRecallCols(recall.map((item) => calcHueSat(item.meas.Ex, item.meas.Ey, item.meas.Lux).sat)),
159 | },
160 | { id: 0, name: 'Illuminance', value: meas.Lux, ...makeRecallCols(recall.map((item) => item.meas.Lux)) },
161 | {
162 | id: 0,
163 | name: 'Illuminance [fc]',
164 | value: Lux2fc(meas.Lux),
165 | ...makeRecallCols(recall.map((item) => Lux2fc(item.meas.Lux))),
166 | },
167 | {
168 | id: 0,
169 | name: 'Ra',
170 | value: Math.round(cri.R[0]),
171 | ...makeRecallCols(recall.map((_, i) => recallCri[i].R[0])),
172 | },
173 | ...Array.from({ length: 14 }).map((_, i) => ({
174 | id: 0,
175 | name: `R${i + 1}`,
176 | value: Math.round(cri.R[i + 1]),
177 | ...makeRecallCols(recallCri.map((cri) => cri.R[i + 1])),
178 | })),
179 | {
180 | id: 0,
181 | name: 'Temperature',
182 | value: meas.temperature,
183 | // @ts-ignore
184 | ...makeRecallCols(recall.map((item) => item.meas?.temperature || 20)),
185 | },
186 | ];
187 | for (let i = 0; i < array.length; i++) array[i].id = i;
188 | return array;
189 | }, [meas, recall]);
190 |
191 | return (
192 |
193 |
194 |
195 | OLM - Text
196 |
197 |
198 |
199 |
200 |
201 |
202 |
213 |
214 |
215 |
216 | );
217 | }
218 |
--------------------------------------------------------------------------------
/components/CIE1931.tsx:
--------------------------------------------------------------------------------
1 | import { Scatter as cScatter } from 'react-chartjs-2';
2 | import Container from '@mui/material/Container';
3 | import { Scatter, ScatterDataset, makeChartTitle, pointRotationAuto } from './Chart';
4 | import planckianCalc_xy from 'lib/planckian';
5 | import calcCCT from 'lib/cct';
6 | import { useState } from 'react';
7 |
8 | const spectral = [
9 | [0.1741, 0.005],
10 | [0.174, 0.005],
11 | [0.1738, 0.0049],
12 | [0.1736, 0.0049],
13 | [0.1733, 0.0048],
14 | [0.173, 0.0048],
15 | [0.1726, 0.0048],
16 | [0.1721, 0.0048],
17 | [0.1714, 0.0051],
18 | [0.1703, 0.0058],
19 | [0.1689, 0.0069],
20 | [0.1669, 0.0086],
21 | [0.1644, 0.0109],
22 | [0.1611, 0.0138],
23 | [0.1566, 0.0177],
24 | [0.151, 0.0227],
25 | [0.144, 0.0297],
26 | [0.1355, 0.0399],
27 | [0.1241, 0.0578],
28 | [0.1096, 0.0868],
29 | [0.0913, 0.1327],
30 | [0.0687, 0.2007],
31 | [0.0454, 0.295],
32 | [0.0235, 0.4127],
33 | [0.0082, 0.5384],
34 | [0.0039, 0.6548],
35 | [0.0139, 0.7502],
36 | [0.0389, 0.812],
37 | [0.0743, 0.8338],
38 | [0.1142, 0.8262],
39 | [0.1547, 0.8059],
40 | [0.1929, 0.7816],
41 | [0.2296, 0.7543],
42 | [0.2658, 0.7243],
43 | [0.3016, 0.6923],
44 | [0.3373, 0.6589],
45 | [0.3731, 0.6245],
46 | [0.4087, 0.5896],
47 | [0.4441, 0.5547],
48 | [0.4788, 0.5202],
49 | [0.5125, 0.4866],
50 | [0.5448, 0.4544],
51 | [0.5752, 0.4242],
52 | [0.6029, 0.3965],
53 | [0.627, 0.3725],
54 | [0.6482, 0.3514],
55 | [0.6658, 0.334],
56 | [0.6801, 0.3197],
57 | [0.6915, 0.3083],
58 | [0.7006, 0.2993],
59 | [0.7079, 0.292],
60 | [0.714, 0.2859],
61 | [0.719, 0.2809],
62 | [0.723, 0.277],
63 | [0.726, 0.274],
64 | [0.7283, 0.2717],
65 | [0.73, 0.27],
66 | [0.7311, 0.2689],
67 | [0.732, 0.268],
68 | [0.7327, 0.2673],
69 | [0.7334, 0.2666],
70 | [0.734, 0.266],
71 | [0.7344, 0.2656],
72 | [0.7346, 0.2654],
73 | [0.7347, 0.2653],
74 | [0.7347, 0.2653],
75 | [0.7347, 0.2653],
76 | [0.7347, 0.2653],
77 | [0.7347, 0.2653],
78 | [0.7347, 0.2653],
79 | [0.7347, 0.2653],
80 | [0.7347, 0.2653],
81 | [0.7347, 0.2653],
82 | [0.7347, 0.2653],
83 | [0.737, 0.2653],
84 | [0.7347, 0.2653],
85 | [0.7347, 0.2653],
86 | [0.7347, 0.2653],
87 | [0.7347, 0.2653],
88 | [0.7347, 0.2653],
89 | [0.7347, 0.2653],
90 | [0.1741, 0.005],
91 | ].map(([x, y]) => ({ x, y }));
92 |
93 | const markers = [
94 | { x: 0.1738, y: 0.0049, wl: 390 },
95 | { x: 0.144, y: 0.0297, wl: 460 },
96 | { x: 0.1241, y: 0.0578, wl: 470 },
97 | { x: 0.0913, y: 0.1327, wl: 480 },
98 | { x: 0.0454, y: 0.295, wl: 490 },
99 | { x: 0.0082, y: 0.5384, wl: 500 },
100 | { x: 0.0139, y: 0.7502, wl: 510 },
101 | { x: 0.0743, y: 0.8338, wl: 520 },
102 | { x: 0.2296, y: 0.7543, wl: 540 },
103 | { x: 0.3731, y: 0.6245, wl: 560 },
104 | { x: 0.5125, y: 0.4866, wl: 580 },
105 | { x: 0.627, y: 0.3725, wl: 600 },
106 | { x: 0.6915, y: 0.3083, wl: 620 },
107 | { x: 0.7347, y: 0.2653, wl: 700 },
108 | ];
109 |
110 | function planckianXYT(T: number) {
111 | const [x, y] = planckianCalc_xy(T);
112 | return { x, y, T };
113 | }
114 |
115 | const CCTMarkers = [
116 | planckianXYT(1500),
117 | planckianXYT(2000),
118 | planckianXYT(2500),
119 | planckianXYT(3000),
120 | planckianXYT(4000),
121 | planckianXYT(6000),
122 | planckianXYT(10000),
123 | ];
124 |
125 | const CCT_MIN = 1000;
126 | const CCT_MAX = 25000;
127 | const CCT_STEP = 100;
128 |
129 | const locus = Array.from({ length: (CCT_MAX - CCT_MIN) / CCT_STEP }, (_, i) => CCT_MIN + i * CCT_STEP).map(
130 | planckianXYT
131 | );
132 |
133 | const locusDuv = (Tmin: number, Tmax: number, Duv: number) =>
134 | Array.from({ length: (Tmax - Tmin) / CCT_STEP }, (_, i) => Tmin + i * CCT_STEP).map((T) => {
135 | const [x, y] = planckianCalc_xy(T, Duv);
136 | return { x, y, T };
137 | });
138 |
139 | function toolTipTitle(datasetIndex: number, dataIndex: number, defaultLabel: any) {
140 | switch (datasetIndex) {
141 | case 0:
142 | return 'locus';
143 | case 1:
144 | return `${markers[dataIndex].wl} nm`;
145 | default:
146 | return `${defaultLabel}`;
147 | }
148 | }
149 |
150 | const datasetSpectralLocus: ScatterDataset = {
151 | label: 'Spectral locus',
152 | data: spectral,
153 | animation: false,
154 | tension: 0.3,
155 | showLine: true,
156 | borderColor: 'black',
157 | borderWidth: 1,
158 | pointRadius: 0,
159 | datalabels: { display: false },
160 | };
161 | const datasetSpectralMarkers: ScatterDataset = {
162 | data: markers as { x: number; y: number }[],
163 | animation: false,
164 | borderColor: 'black',
165 | pointStyle: 'line',
166 | borderWidth: 2,
167 | pointRadius: 5,
168 | pointRotation: (ctx) => pointRotationAuto(ctx),
169 | datalabels: {
170 | labels: {
171 | value: {
172 | align: 'top',
173 | formatter: (_value, context) => markers[context.dataIndex].wl,
174 | },
175 | },
176 | },
177 | };
178 | const datasetLocus: ScatterDataset = {
179 | label: 'Planckian locus',
180 | data: locus.map(({ x, y }) => ({ x, y })),
181 | animation: false,
182 | showLine: true,
183 | borderColor: 'black',
184 | borderWidth: 1,
185 | pointRadius: 0,
186 | datalabels: { display: false },
187 | // @ts-ignore
188 | tooltip: {
189 | callbacks: {
190 | beforeLabel: () => 'Planckian locus',
191 | label: (tooltipItem) =>
192 | `xy: ${tooltipItem.formattedValue} CCT: ${calcCCT(tooltipItem.parsed.x, tooltipItem.parsed.y).toFixed(0)} K`,
193 | },
194 | },
195 | };
196 |
197 | const makeTempLineDataset = (Tmin: number, Tmax: number, Duv: number): ScatterDataset => {
198 | return {
199 | label: 'Duv limit',
200 | data: locusDuv(Tmin, Tmax, Duv).map(({ x, y }) => ({ x, y })),
201 | animation: false,
202 | showLine: true,
203 | borderColor: 'grey',
204 | borderWidth: 1,
205 | pointRadius: 0,
206 | datalabels: { display: false },
207 | // @ts-ignore
208 | tooltip: {
209 | callbacks: {
210 | beforeLabel: () => `Duv ${Duv}`,
211 | label: (tooltipItem) =>
212 | `xy: ${tooltipItem.formattedValue} CCT: ${calcCCT(tooltipItem.parsed.x, tooltipItem.parsed.y).toFixed(0)} K`,
213 | },
214 | },
215 | };
216 | };
217 | const datasetTempLinesm2 = makeTempLineDataset(2600, CCT_MAX, -0.02);
218 | const datasetTempLines2 = makeTempLineDataset(CCT_MIN, CCT_MAX, 0.02);
219 |
220 | const defaultDatasets: Array = [
221 | [datasetSpectralLocus, datasetSpectralMarkers, datasetLocus, datasetTempLinesm2, datasetTempLines2],
222 | [datasetLocus, datasetTempLinesm2, datasetTempLines2],
223 | ];
224 |
225 | export default function CIE1931({
226 | Ex,
227 | Ey,
228 | CCT,
229 | Duv,
230 | secondaryPoints,
231 | }: {
232 | Ex: number;
233 | Ey: number;
234 | CCT: number;
235 | Duv: number;
236 | secondaryPoints?: { label: string; Ex: number; Ey: number; CCT: number; Duv: number }[];
237 | }) {
238 | const [zoom, setZoom] = useState(false);
239 | const datasets: ScatterDataset[] = [
240 | ...defaultDatasets[+zoom],
241 | {
242 | data: CCTMarkers.map(({ x, y }) => ({ x, y })),
243 | animation: false,
244 | borderColor: 'black',
245 | pointStyle: 'line',
246 | borderWidth: 2,
247 | pointRadius: 5,
248 | pointRotation: [115, 100, 80, 65, 60, 50, 45],
249 | datalabels: {
250 | labels: {
251 | value: {
252 | align: 'top',
253 | rotation: 45,
254 | offset: (context) => context.dataIndex * 2,
255 | formatter: (_value, context) => `${CCTMarkers[context.dataIndex].T} K`,
256 | },
257 | },
258 | },
259 | // @ts-ignore
260 | tooltip: {
261 | callbacks: {
262 | label: (tooltipItem) =>
263 | `xy: ${tooltipItem.formattedValue} CCT: ${CCTMarkers[tooltipItem.dataIndex].T} K`,
264 | },
265 | },
266 | },
267 | {
268 | label: 'current',
269 | data: [{ x: Ex, y: Ey }],
270 | borderColor: 'black',
271 | pointRadius: 2,
272 | datalabels: { display: false },
273 | // @ts-ignore
274 | tooltip: {
275 | callbacks: {
276 | beforeLabel: () => 'current',
277 | label: (tooltipItem) =>
278 | `xy: ${tooltipItem.formattedValue} CCT: ${CCT.toFixed(0)} K Duv: ${Duv.toFixed(3)}`,
279 | },
280 | },
281 | },
282 | ...(secondaryPoints || []).map((point) => ({
283 | label: point.label,
284 | data: [{ x: point.Ex, y: point.Ey }],
285 | borderColor: 'grey',
286 | pointRadius: 2,
287 | datalabels: { display: false },
288 | // @ts-ignore
289 | tooltip: {
290 | callbacks: {
291 | beforeLabel: () => point.label,
292 | label: (tooltipItem) =>
293 | `xy: ${tooltipItem.formattedValue} CCT: ${point.CCT.toFixed(0)} K Duv: ${point.Duv.toFixed(3)}`,
294 | },
295 | },
296 | })),
297 | ];
298 |
299 | return (
300 |
301 |
319 | toolTipTitle(
320 | tooltipItems[0].datasetIndex,
321 | tooltipItems[0].dataIndex,
322 | tooltipItems[0].label
323 | ),
324 | beforeLabel: (tooltipItem) => tooltipItem.dataset.label,
325 | label: (tooltipItem) => `xy: ${tooltipItem.formattedValue}`,
326 | },
327 | },
328 | },
329 | scales: {
330 | x: {
331 | min: zoom ? 0.2 : 0.0,
332 | max: zoom ? 0.55 : 0.8,
333 | display: true,
334 | grid: {
335 | display: true,
336 | },
337 | title: {
338 | display: true,
339 | text: 'y',
340 | color: 'black',
341 | },
342 | ticks: {
343 | display: true,
344 | },
345 | },
346 | y: {
347 | min: zoom ? 0.2 : 0.0,
348 | max: zoom ? 0.5 : 0.9,
349 | display: true,
350 | grid: {
351 | display: true,
352 | },
353 | title: {
354 | display: true,
355 | text: 'x',
356 | color: 'black',
357 | },
358 | ticks: {
359 | display: true,
360 | },
361 | },
362 | },
363 | onClick(event, elements, chart) {
364 | setZoom(!zoom);
365 | },
366 | }}
367 | />
368 |
369 | );
370 | }
371 |
--------------------------------------------------------------------------------
/components/Memory.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { SxProps, Theme } from '@mui/material/styles';
3 | import Autocomplete from '@mui/material/Autocomplete';
4 | import Box from '@mui/material/Box';
5 | import Button from '@mui/material/Button';
6 | import ButtonGroup from '@mui/material/ButtonGroup';
7 | import Card from '@mui/material/Card';
8 | import CardActions from '@mui/material/CardActions';
9 | import CardContent from '@mui/material/CardContent';
10 | import Checkbox from '@mui/material/Checkbox';
11 | import DeleteIcon from '@mui/icons-material/Delete';
12 | import IconButton from '@mui/material/IconButton';
13 | import InputAdornment from '@mui/material/InputAdornment';
14 | import List from '@mui/material/List';
15 | import ListItem from '@mui/material/ListItem';
16 | import ListItemIcon from '@mui/material/ListItemIcon';
17 | import ListItemText from '@mui/material/ListItemText';
18 | import MemoryIcon from '@mui/icons-material/Memory';
19 | import Modal from '@mui/material/Modal';
20 | import Paper from '@mui/material/Paper';
21 | import TextField from '@mui/material/TextField';
22 | import Typography from '@mui/material/Typography';
23 | import { SettingsCard, iconStyle, ActionButton } from 'components/settings/SettingsCard';
24 | import { MemoryItem, useGlobalState } from 'lib/global';
25 | import { getDateTime } from 'lib/locale';
26 | import * as std from 'lib/spdIlluminants';
27 | import { calcRefMeas } from 'lib/spd';
28 |
29 | const recallModalStyle = {
30 | position: 'absolute' as 'absolute',
31 | top: '50%',
32 | left: '50%',
33 | transform: 'translate(-50%, -50%)',
34 | width: 425,
35 | p: 4,
36 | };
37 |
38 | const addModalStyle = {
39 | position: 'absolute' as 'absolute',
40 | top: '50%',
41 | left: '50%',
42 | transform: 'translate(-50%, -50%)',
43 | width: 400,
44 | p: 4,
45 | };
46 |
47 | function MemoryList({ sx = [] }: { sx?: SxProps }) {
48 | const [memory, setMemory] = useGlobalState('memory');
49 | const [isClient, setIsClient] = useState(false);
50 |
51 | // Hack to avoid hydration errors.
52 | useEffect(() => {
53 | setIsClient(true);
54 | }, []);
55 |
56 | return (
57 |
58 |
59 | {!isClient
60 | ? ''
61 | : memory.map((item: MemoryItem, i: number) => {
62 | const labelId = `list-item-${i}-label`;
63 | const toggle = () =>
64 | setMemory(
65 | memory.map((m: MemoryItem) =>
66 | m == item ? { ...m, recall: !(m.recall ?? false) } : m
67 | )
68 | );
69 | const deleteItem = () => setMemory(memory.filter((m: MemoryItem) => m != item));
70 |
71 | return (
72 |
76 |
77 |
78 | }
79 | >
80 |
81 |
90 |
91 |
92 |
93 | );
94 | })}
95 |
96 |
97 | );
98 | }
99 |
100 | export default function Memory() {
101 | const [meas] = useGlobalState('res_lm_measurement');
102 | const [memory, setMemory] = useGlobalState('memory');
103 | const [name, setName] = useState('');
104 | const handleSave = () =>
105 | setMemory([
106 | ...memory,
107 | {
108 | name: name.length > 0 ? name : `${getDateTime(new Date())}: ${meas.CCT.toFixed(0)} K`,
109 | created: Date.now(),
110 | type: 'LM3',
111 | recall: false,
112 | meas: Object.freeze(meas),
113 | },
114 | ]);
115 | const [openRecallModal, setOpenRecallModal] = useState(false);
116 | const handleOpen = () => setOpenRecallModal(true);
117 | const handleClose = () => {
118 | setOpenRecallModal(false);
119 | };
120 |
121 | return (
122 |
123 | setName(e.currentTarget.value)} />
124 |
125 |
126 |
132 |
133 |
134 |
135 |
136 | Recall
137 |
138 |
139 | Select the measurement(s) to be recalled.
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | );
148 | }
149 |
150 | const SPDs: {
151 | label: string;
152 | desc: string;
153 | SPD: Float64Array | ((CCT: number) => Float64Array);
154 | }[] = [
155 | { label: 'Planck', desc: 'Planckian locus.', SPD: std.SPDofPlanck },
156 | {
157 | label: 'A',
158 | desc: 'CIE standard illuminant A is intended to represent typical 2856 K tungsten-filament lighting.',
159 | SPD: std.SPDofA,
160 | },
161 | { label: 'D', desc: 'CIE standard illuminant series D represents natural daylight locus.', SPD: std.SPDofD },
162 | { label: 'D50', desc: 'CIE horizon light.', SPD: std.SPDofD(5000) },
163 | { label: 'D55', desc: 'CIE mid-morning / mid-afternoon daylight.', SPD: std.SPDofD(5500) },
164 | { label: 'D60', desc: '6000 K illuminant D', SPD: std.SPDofD(6000) },
165 | { label: 'D65', desc: 'CIE noon daylight.', SPD: std.SPDofD(6500) },
166 | { label: 'D75', desc: 'CIE North sky daylight.', SPD: std.SPDofD(7500) },
167 | { label: 'D93', desc: 'high-efficiency blue phosphor monitors', SPD: std.SPDofD(9300) },
168 | {
169 | label: 'E',
170 | desc: 'Equal-energy radiator, an illuminant that gives equal weight to all wavelengths.',
171 | SPD: std.SPDofE,
172 | },
173 | { label: 'FL1', desc: 'CIE 6430 K daylight fluorescent.', SPD: std.SPDofFL1 },
174 | { label: 'FL2', desc: 'CIE 4230 K cool white fluorescent.', SPD: std.SPDofFL2 },
175 | { label: 'FL3', desc: 'CIE 3450 K white fluorescent.', SPD: std.SPDofFL3 },
176 | { label: 'FL4', desc: 'CIE 2940 K warm white fluorescent.', SPD: std.SPDofFL4 },
177 | { label: 'FL5', desc: 'CIE 6350 K daylight fluorescent.', SPD: std.SPDofFL5 },
178 | { label: 'FL6', desc: 'CIE 4150 K light white fluorescent.', SPD: std.SPDofFL6 },
179 | { label: 'FL7', desc: 'CIE broadband D65 simulator.', SPD: std.SPDofFL7 },
180 | { label: 'FL8', desc: 'CIE broadband D50 simulator.', SPD: std.SPDofFL8 },
181 | { label: 'FL9', desc: 'CIE broadband 4150 K cool white deluxe fluorescent.', SPD: std.SPDofFL9 },
182 | { label: 'FL10', desc: 'CIE 3-band 5000 K', SPD: std.SPDofFL10 },
183 | { label: 'FL11', desc: 'CIE 3-band 4000 K', SPD: std.SPDofFL11 },
184 | { label: 'FL12', desc: 'CIE 3-band 3000 K', SPD: std.SPDofFL12 },
185 | { label: 'FL3.1', desc: 'CIE 2932 K Standard halophosphate lamp', SPD: std.SPDofFL3_1 },
186 | { label: 'FL3.2', desc: 'CIE 3965 K Standard halophosphate lamp.', SPD: std.SPDofFL3_2 },
187 | { label: 'FL3.3', desc: 'CIE 6280 K Standard halophosphate lamp', SPD: std.SPDofFL3_3 },
188 | { label: 'FL3.4', desc: 'CIE 2904 K Standard DeLuxe type lamp', SPD: std.SPDofFL3_4 },
189 | { label: 'FL3.5', desc: 'CIE 4086 K Standard DeLuxe type lamp', SPD: std.SPDofFL3_5 },
190 | { label: 'FL3.6', desc: 'CIE 4894 K Standard DeLuxe type lamp', SPD: std.SPDofFL3_6 },
191 | { label: 'FL3.7', desc: 'CIE 2979 K 3 Band', SPD: std.SPDofFL3_7 },
192 | { label: 'FL3.8', desc: 'CIE 4006 K 3 Band', SPD: std.SPDofFL3_8 },
193 | { label: 'FL3.9', desc: 'CIE 4853 K 3 Band', SPD: std.SPDofFL3_9 },
194 | { label: 'FL3.10', desc: 'CIE 5000 K 3 Band', SPD: std.SPDofFL3_10 },
195 | { label: 'FL3.11', desc: 'CIE 5854 K 3 Band', SPD: std.SPDofFL3_11 },
196 | { label: 'FL3.12', desc: 'CIE 2984 K Multi Band', SPD: std.SPDofFL3_12 },
197 | { label: 'FL3.13', desc: 'CIE 3896 K Multi Band', SPD: std.SPDofFL3_13 },
198 | { label: 'FL3.14', desc: 'CIE 5045 K Multi Band', SPD: std.SPDofFL3_14 },
199 | { label: 'FL3.15', desc: 'CIE 6509 K Multi Band', SPD: std.SPDofFL3_15 },
200 | { label: 'Xenon', desc: '6044 K', SPD: std.SPDofXenon },
201 | { label: 'HMI 6002 K', desc: 'Hydrargyrum medium-arc iodide (HMI) lamp.', SPD: std.SPDofHMI1 },
202 | { label: 'HMI 5630 K', desc: 'Hydrargyrum medium-arc iodide (HMI) lamp.', SPD: std.SPDofHMI2 },
203 | { label: 'LED-B1', desc: '2733 K phosphor-converted blue.', SPD: std.SPDofLED_B1 },
204 | { label: 'LED-B2', desc: '2998 K phosphor-converted blue.', SPD: std.SPDofLED_B2 },
205 | { label: 'LED-B3', desc: '4103 K phosphor-converted blue.', SPD: std.SPDofLED_B3 },
206 | { label: 'LED-B4', desc: '5109 K phosphor-converted blue.', SPD: std.SPDofLED_B4 },
207 | { label: 'LED-B5', desc: '6598 K phosphor-converted blue.', SPD: std.SPDofLED_B5 },
208 | { label: 'LED-BH1', desc: '2851 K mixing of phosphor-converted blue LED and red LED.', SPD: std.SPDofLED_BH1 },
209 | { label: 'LED-RGB1', desc: '2840 K mixing of red, green, and blue LEDs.', SPD: std.SPDofLED_RGB1 },
210 | { label: 'LED-V1', desc: '2724 K violet-pumped phosphor-type LEDs.', SPD: std.SPDofLED_V1 },
211 | { label: 'LED-V2', desc: '4070 K violet-pumped phosphor-type LEDs.', SPD: std.SPDofLED_V2 },
212 | { label: 'HP1', desc: 'CIE 1959 K Standard high pressure sodium lamp', SPD: std.SPDofHP1 },
213 | { label: 'HP2', desc: 'CIE 2506 K Colour enhanced high pressure sodium lamp', SPD: std.SPDofHP2 },
214 | { label: 'HP3', desc: 'CIE 3144 K High pressure metal halide lamp', SPD: std.SPDofHP3 },
215 | { label: 'HP4', desc: 'CIE 4002 K High pressure metal halide lamp', SPD: std.SPDofHP4 },
216 | { label: 'HP5', desc: 'CIE 4039 K High pressure metal halide lamp', SPD: std.SPDofHP5 },
217 | ];
218 |
219 | type ArrayElement = ArrayType extends readonly (infer ElementType)[]
220 | ? ElementType
221 | : never;
222 | function SelectReferenceIlluminant({ onAdd }: { onAdd: (label: string, spd: Float64Array) => void }) {
223 | const [selected, setSelected] = useState>();
224 | const [CCT, setCCT] = useState(6500);
225 | const handleAdd = () => {
226 | if (typeof selected.SPD === 'function') {
227 | onAdd(`${selected.label} ${CCT.toFixed(0)} K`, selected.SPD(CCT));
228 | } else {
229 | onAdd(selected.label, selected.SPD);
230 | }
231 | };
232 |
233 | return (
234 |
235 |
236 |
237 |
238 | Add Illuminant
239 |
240 | }
247 | onChange={(_event, value, _reason) => setSelected(value)}
248 | />
249 | K,
255 | }}
256 | onChange={(e) => {
257 | const v = e.target.value;
258 | const num = Number(v);
259 | if (!Number.isNaN(num)) setCCT(num);
260 | }}
261 | />
262 |
263 |
264 | {selected?.desc}
265 |
266 |
267 |
268 |
269 |
272 |
273 |
274 |
275 | );
276 | }
277 |
278 | export function MemorySettings() {
279 | const [memory, setMemory] = useGlobalState('memory');
280 | const [openAddModal, setOpenAddModal] = useState(false);
281 | const handleOpen = () => setOpenAddModal(true);
282 | const handleClose = () => setOpenAddModal(false);
283 | const handleAdd = (label: string, spd: Float64Array) => {
284 | setMemory([
285 | ...memory,
286 | {
287 | name: label,
288 | created: Date.now(),
289 | type: 'ref',
290 | recall: false,
291 | meas: calcRefMeas(spd),
292 | },
293 | ]);
294 | }
295 |
296 | return (
297 | }
299 | title="Memory"
300 | actions={
301 |
302 | Add Illuminant
303 |
304 | }
305 | >
306 |
307 |
313 |
314 |
315 |
316 | );
317 | }
318 |
--------------------------------------------------------------------------------
/lib/cri.ts:
--------------------------------------------------------------------------------
1 | import { CIE1931_2DEG_CMF } from './CMF';
2 | import calcCCT from './cct';
3 | import { SPDofD, SPDofPlanck } from './spdIlluminants';
4 | import { spd2XYZ, normalizeSPD } from './spd';
5 | import { XYZ2xy, xy2uv, XYZ2UVW } from './CIEConv';
6 |
7 | //const nmIncrement = 5;
8 | const TCSSamples = Object.freeze([
9 | // 1: 7.5R6/4
10 | Float64Array.from([
11 | 0.219, 0.239, 0.252, 0.256, 0.256, 0.254, 0.252, 0.248, 0.244, 0.24, 0.237, 0.232, 0.23, 0.226, 0.225, 0.222,
12 | 0.22, 0.218, 0.216, 0.214, 0.214, 0.214, 0.216, 0.218, 0.223, 0.225, 0.226, 0.226, 0.225, 0.225, 0.227, 0.23,
13 | 0.236, 0.245, 0.253, 0.262, 0.272, 0.283, 0.298, 0.318, 0.341, 0.367, 0.39, 0.409, 0.424, 0.435, 0.442, 0.448,
14 | 0.45, 0.451, 0.451, 0.451, 0.451, 0.451, 0.45, 0.45, 0.451, 0.451, 0.453, 0.454, 0.455, 0.457, 0.458, 0.46,
15 | 0.462, 0.463, 0.464, 0.465, 0.466, 0.466, 0.466, 0.466, 0.467, 0.467, 0.467, 0.467, 0.467, 0.467, 0.467, 0.467,
16 | 0.467,
17 | ]),
18 | // 2: 5Y6/4
19 | Float64Array.from([
20 | 0.07, 0.079, 0.089, 0.101, 0.111, 0.116, 0.118, 0.12, 0.121, 0.122, 0.122, 0.122, 0.123, 0.124, 0.127, 0.128,
21 | 0.131, 0.134, 0.138, 0.143, 0.15, 0.159, 0.174, 0.19, 0.207, 0.225, 0.242, 0.253, 0.26, 0.264, 0.267, 0.269,
22 | 0.272, 0.276, 0.282, 0.289, 0.299, 0.309, 0.322, 0.329, 0.335, 0.339, 0.341, 0.341, 0.342, 0.342, 0.342, 0.341,
23 | 0.341, 0.339, 0.339, 0.338, 0.338, 0.337, 0.336, 0.335, 0.334, 0.332, 0.332, 0.331, 0.331, 0.33, 0.329, 0.328,
24 | 0.328, 0.327, 0.326, 0.325, 0.324, 0.324, 0.324, 0.323, 0.322, 0.321, 0.32, 0.318, 0.316, 0.315, 0.315, 0.314,
25 | 0.314,
26 | ]),
27 | // 3: 5GY6/8
28 | Float64Array.from([
29 | 0.065, 0.068, 0.07, 0.072, 0.073, 0.073, 0.074, 0.074, 0.074, 0.073, 0.073, 0.073, 0.073, 0.073, 0.074, 0.075,
30 | 0.077, 0.08, 0.085, 0.094, 0.109, 0.126, 0.148, 0.172, 0.198, 0.221, 0.241, 0.26, 0.278, 0.302, 0.339, 0.37,
31 | 0.392, 0.399, 0.4, 0.393, 0.38, 0.365, 0.349, 0.332, 0.315, 0.299, 0.285, 0.272, 0.264, 0.257, 0.252, 0.247,
32 | 0.241, 0.235, 0.229, 0.224, 0.22, 0.217, 0.216, 0.216, 0.219, 0.224, 0.23, 0.238, 0.251, 0.269, 0.288, 0.312,
33 | 0.34, 0.366, 0.39, 0.412, 0.431, 0.447, 0.46, 0.472, 0.481, 0.488, 0.493, 0.497, 0.5, 0.502, 0.505, 0.51, 0.516,
34 | ]),
35 | // 4: 2.5G6/6
36 | Float64Array.from([
37 | 0.074, 0.083, 0.093, 0.105, 0.116, 0.121, 0.124, 0.126, 0.128, 0.131, 0.135, 0.139, 0.144, 0.151, 0.161, 0.172,
38 | 0.186, 0.205, 0.229, 0.254, 0.281, 0.308, 0.332, 0.352, 0.37, 0.383, 0.39, 0.394, 0.395, 0.392, 0.385, 0.377,
39 | 0.367, 0.354, 0.341, 0.327, 0.312, 0.296, 0.28, 0.263, 0.247, 0.229, 0.214, 0.198, 0.185, 0.175, 0.169, 0.164,
40 | 0.16, 0.156, 0.154, 0.152, 0.151, 0.149, 0.148, 0.148, 0.148, 0.149, 0.151, 0.154, 0.158, 0.162, 0.165, 0.168,
41 | 0.17, 0.171, 0.17, 0.168, 0.166, 0.164, 0.164, 0.165, 0.168, 0.172, 0.177, 0.181, 0.185, 0.189, 0.192, 0.194,
42 | 0.197,
43 | ]),
44 | // 5: 10BG6/4
45 | Float64Array.from([
46 | 0.295, 0.306, 0.31, 0.312, 0.313, 0.315, 0.319, 0.322, 0.326, 0.33, 0.334, 0.339, 0.346, 0.352, 0.36, 0.369,
47 | 0.381, 0.394, 0.403, 0.41, 0.415, 0.418, 0.419, 0.417, 0.413, 0.409, 0.403, 0.396, 0.389, 0.381, 0.372, 0.363,
48 | 0.353, 0.342, 0.331, 0.32, 0.308, 0.296, 0.284, 0.271, 0.26, 0.247, 0.232, 0.22, 0.21, 0.2, 0.194, 0.189, 0.185,
49 | 0.183, 0.18, 0.177, 0.176, 0.175, 0.175, 0.175, 0.175, 0.177, 0.18, 0.183, 0.186, 0.189, 0.192, 0.195, 0.199,
50 | 0.2, 0.199, 0.198, 0.196, 0.195, 0.195, 0.196, 0.197, 0.2, 0.203, 0.205, 0.208, 0.212, 0.215, 0.217, 0.219,
51 | ]),
52 | // 6: 5PB6/8
53 | Float64Array.from([
54 | 0.151, 0.203, 0.265, 0.339, 0.41, 0.464, 0.492, 0.508, 0.517, 0.524, 0.531, 0.538, 0.544, 0.551, 0.556, 0.556,
55 | 0.554, 0.549, 0.541, 0.531, 0.519, 0.504, 0.488, 0.469, 0.45, 0.431, 0.414, 0.395, 0.377, 0.358, 0.341, 0.325,
56 | 0.309, 0.293, 0.279, 0.265, 0.253, 0.241, 0.234, 0.227, 0.225, 0.222, 0.221, 0.22, 0.22, 0.22, 0.22, 0.22,
57 | 0.223, 0.227, 0.233, 0.239, 0.244, 0.251, 0.258, 0.263, 0.268, 0.273, 0.278, 0.281, 0.283, 0.286, 0.291, 0.296,
58 | 0.302, 0.313, 0.325, 0.338, 0.351, 0.364, 0.376, 0.389, 0.401, 0.413, 0.425, 0.436, 0.447, 0.458, 0.469, 0.477,
59 | 0.485,
60 | ]),
61 | // 7: 2.5P6/8
62 | Float64Array.from([
63 | 0.378, 0.459, 0.524, 0.546, 0.551, 0.555, 0.559, 0.56, 0.561, 0.558, 0.556, 0.551, 0.544, 0.535, 0.522, 0.506,
64 | 0.488, 0.469, 0.448, 0.429, 0.408, 0.385, 0.363, 0.341, 0.324, 0.311, 0.301, 0.291, 0.283, 0.273, 0.265, 0.26,
65 | 0.257, 0.257, 0.259, 0.26, 0.26, 0.258, 0.256, 0.254, 0.254, 0.259, 0.27, 0.284, 0.302, 0.324, 0.344, 0.362,
66 | 0.377, 0.389, 0.4, 0.41, 0.42, 0.429, 0.438, 0.445, 0.452, 0.457, 0.462, 0.466, 0.468, 0.47, 0.473, 0.477,
67 | 0.483, 0.489, 0.496, 0.503, 0.511, 0.518, 0.525, 0.532, 0.539, 0.546, 0.553, 0.559, 0.565, 0.57, 0.575, 0.578,
68 | 0.581,
69 | ]),
70 | // 8: 10P6/8
71 | Float64Array.from([
72 | 0.104, 0.129, 0.17, 0.24, 0.319, 0.416, 0.462, 0.482, 0.49, 0.488, 0.482, 0.473, 0.462, 0.45, 0.439, 0.426,
73 | 0.413, 0.397, 0.382, 0.366, 0.352, 0.337, 0.325, 0.31, 0.299, 0.289, 0.283, 0.276, 0.27, 0.262, 0.256, 0.251,
74 | 0.25, 0.251, 0.254, 0.258, 0.264, 0.269, 0.272, 0.274, 0.278, 0.284, 0.295, 0.316, 0.348, 0.384, 0.434, 0.482,
75 | 0.528, 0.568, 0.604, 0.629, 0.648, 0.663, 0.676, 0.685, 0.693, 0.7, 0.705, 0.709, 0.712, 0.715, 0.717, 0.719,
76 | 0.721, 0.72, 0.719, 0.722, 0.725, 0.727, 0.729, 0.73, 0.73, 0.73, 0.73, 0.73, 0.73, 0.73, 0.73, 0.73, 0.73,
77 | ]),
78 | // 9: 4.5R4/13
79 | Float64Array.from([
80 | 0.066, 0.062, 0.058, 0.055, 0.052, 0.052, 0.051, 0.05, 0.05, 0.049, 0.048, 0.047, 0.046, 0.044, 0.042, 0.041,
81 | 0.038, 0.035, 0.033, 0.031, 0.03, 0.029, 0.028, 0.028, 0.028, 0.029, 0.03, 0.03, 0.031, 0.031, 0.032, 0.032,
82 | 0.033, 0.034, 0.035, 0.037, 0.041, 0.044, 0.048, 0.052, 0.06, 0.076, 0.102, 0.136, 0.19, 0.256, 0.336, 0.418,
83 | 0.505, 0.581, 0.641, 0.682, 0.717, 0.74, 0.758, 0.77, 0.781, 0.79, 0.797, 0.803, 0.809, 0.814, 0.819, 0.824,
84 | 0.828, 0.83, 0.831, 0.833, 0.835, 0.836, 0.836, 0.837, 0.838, 0.839, 0.839, 0.839, 0.839, 0.839, 0.839, 0.839,
85 | 0.839,
86 | ]),
87 | // 10: 5Y8/10
88 | Float64Array.from([
89 | 0.05, 0.054, 0.059, 0.063, 0.066, 0.067, 0.068, 0.069, 0.069, 0.07, 0.072, 0.073, 0.076, 0.078, 0.083, 0.088,
90 | 0.095, 0.103, 0.113, 0.125, 0.142, 0.162, 0.189, 0.219, 0.262, 0.305, 0.365, 0.416, 0.465, 0.509, 0.546, 0.581,
91 | 0.61, 0.634, 0.653, 0.666, 0.678, 0.687, 0.693, 0.698, 0.701, 0.704, 0.705, 0.705, 0.706, 0.707, 0.707, 0.707,
92 | 0.708, 0.708, 0.71, 0.711, 0.712, 0.714, 0.716, 0.718, 0.72, 0.722, 0.725, 0.729, 0.731, 0.735, 0.739, 0.742,
93 | 0.746, 0.748, 0.749, 0.751, 0.753, 0.754, 0.755, 0.755, 0.755, 0.755, 0.756, 0.757, 0.758, 0.759, 0.759, 0.759,
94 | 0.759,
95 | ]),
96 | // 11: 4.5G5/8
97 | Float64Array.from([
98 | 0.111, 0.121, 0.127, 0.129, 0.127, 0.121, 0.116, 0.112, 0.108, 0.105, 0.104, 0.104, 0.105, 0.106, 0.11, 0.115,
99 | 0.123, 0.134, 0.148, 0.167, 0.192, 0.219, 0.252, 0.291, 0.325, 0.347, 0.356, 0.353, 0.346, 0.333, 0.314, 0.294,
100 | 0.271, 0.248, 0.227, 0.206, 0.188, 0.17, 0.153, 0.138, 0.125, 0.114, 0.106, 0.1, 0.096, 0.092, 0.09, 0.087,
101 | 0.085, 0.082, 0.08, 0.079, 0.078, 0.078, 0.078, 0.078, 0.081, 0.083, 0.088, 0.093, 0.102, 0.112, 0.125, 0.141,
102 | 0.161, 0.182, 0.203, 0.223, 0.242, 0.257, 0.27, 0.282, 0.292, 0.302, 0.31, 0.314, 0.317, 0.323, 0.33, 0.334,
103 | 0.338,
104 | ]),
105 | // 12: 3PB3/11
106 | Float64Array.from([
107 | 0.12, 0.103, 0.09, 0.082, 0.076, 0.068, 0.064, 0.065, 0.075, 0.093, 0.123, 0.16, 0.207, 0.256, 0.3, 0.331,
108 | 0.346, 0.347, 0.341, 0.328, 0.307, 0.282, 0.257, 0.23, 0.204, 0.178, 0.154, 0.129, 0.109, 0.09, 0.075, 0.062,
109 | 0.051, 0.041, 0.035, 0.029, 0.025, 0.022, 0.019, 0.017, 0.017, 0.017, 0.016, 0.016, 0.016, 0.016, 0.016, 0.016,
110 | 0.016, 0.016, 0.018, 0.018, 0.018, 0.018, 0.019, 0.02, 0.023, 0.024, 0.026, 0.03, 0.035, 0.043, 0.056, 0.074,
111 | 0.097, 0.128, 0.166, 0.21, 0.257, 0.305, 0.354, 0.401, 0.446, 0.485, 0.52, 0.551, 0.577, 0.599, 0.618, 0.633,
112 | 0.645,
113 | ]),
114 | // 13: 5YR8/4
115 | Float64Array.from([
116 | 0.104, 0.127, 0.161, 0.211, 0.264, 0.313, 0.341, 0.352, 0.359, 0.361, 0.364, 0.365, 0.367, 0.369, 0.372, 0.374,
117 | 0.376, 0.379, 0.384, 0.389, 0.397, 0.405, 0.416, 0.429, 0.443, 0.454, 0.461, 0.466, 0.469, 0.471, 0.474, 0.476,
118 | 0.483, 0.49, 0.506, 0.526, 0.553, 0.582, 0.618, 0.651, 0.68, 0.701, 0.717, 0.729, 0.736, 0.742, 0.745, 0.747,
119 | 0.748, 0.748, 0.748, 0.748, 0.748, 0.748, 0.748, 0.748, 0.747, 0.747, 0.747, 0.747, 0.747, 0.747, 0.747, 0.746,
120 | 0.746, 0.746, 0.745, 0.744, 0.743, 0.744, 0.745, 0.748, 0.75, 0.75, 0.749, 0.748, 0.748, 0.747, 0.747, 0.747,
121 | 0.747,
122 | ]),
123 | // 14: 5GY4/4
124 | Float64Array.from([
125 | 0.036, 0.036, 0.037, 0.038, 0.039, 0.039, 0.04, 0.041, 0.042, 0.042, 0.043, 0.044, 0.044, 0.045, 0.045, 0.046,
126 | 0.047, 0.048, 0.05, 0.052, 0.055, 0.057, 0.062, 0.067, 0.075, 0.083, 0.092, 0.1, 0.108, 0.121, 0.133, 0.142,
127 | 0.15, 0.154, 0.155, 0.152, 0.147, 0.14, 0.133, 0.125, 0.118, 0.112, 0.106, 0.101, 0.098, 0.095, 0.093, 0.09,
128 | 0.089, 0.087, 0.086, 0.085, 0.084, 0.084, 0.084, 0.084, 0.085, 0.087, 0.092, 0.096, 0.102, 0.11, 0.123, 0.137,
129 | 0.152, 0.169, 0.188, 0.207, 0.226, 0.243, 0.26, 0.277, 0.294, 0.31, 0.325, 0.339, 0.353, 0.366, 0.379, 0.39,
130 | 0.399,
131 | ]),
132 | ]);
133 |
134 | function referenceIlluminant(CCT: number) {
135 | return CCT < 5000 ? SPDofPlanck(CCT) : SPDofD(CCT);
136 | }
137 |
138 | function calcTCS_XYZ(spd: Float64Array, cmf: Float64Array, TCSSample: number[] | Float64Array) {
139 | const CIExsum = spd.reduce((sum, v, i) => sum + v * TCSSample[i] * cmf[i * 3], 0);
140 | const CIEysum = spd.reduce((sum, v, i) => sum + v * TCSSample[i] * cmf[i * 3 + 1], 0);
141 | const CIEzsum = spd.reduce((sum, v, i) => sum + v * TCSSample[i] * cmf[i * 3 + 2], 0);
142 | const ysum = spd.reduce((sum, v, i) => sum + v * cmf[i * 3 + 1], 0);
143 |
144 | return [(100 * CIExsum) / ysum, (100 * CIEysum) / ysum, (100 * CIEzsum) / ysum];
145 | }
146 |
147 | function calcAllTCS_XYZ(spd: Float64Array, cmf: Float64Array) {
148 | return TCSSamples.map((sample) => calcTCS_XYZ(spd, cmf, sample));
149 | }
150 |
151 | function uv2cd(u: number, v: number) {
152 | const c = (4 - u - 10 * v) / v;
153 | const d = (1.708 * v + 0.404 - 1.481 * u) / v;
154 |
155 | return [c, d];
156 | }
157 |
158 | function calcRef(CCT: number) {
159 | // Normalize by index 36 that is 560 nm
160 | const ref = normalizeSPD(referenceIlluminant(CCT));
161 | const XYZ = spd2XYZ(ref, CIE1931_2DEG_CMF);
162 | const [x, y] = XYZ2xy(XYZ);
163 | const [u, v] = xy2uv(x, y);
164 | const [cr, dr] = uv2cd(u, v);
165 |
166 | return {
167 | data: ref,
168 | x,
169 | y,
170 | u,
171 | v,
172 | CCT: calcCCT(x, y),
173 | cr,
174 | dr,
175 | };
176 | }
177 |
178 | //const testIlluminant = [ 1.87, 2.36, 2.94, 3.47, 5.17, 19.49, 6.13, 6.24, 7.01, 7.79, 8.56, 43.67, 16.94, 10.72, 11.35, 11.89, 12.37, 12.75, 13.00, 13.15, 13.23, 13.17, 13.13, 12.85, 12.52, 12.20, 11.83, 11.50, 11.22, 11.05, 11.03, 11.18, 11.53, 27.74, 17.05, 13.55, 14.33, 15.01, 15.52, 18.29, 19.55, 15.48, 14.91, 14.15, 13.22, 12.19, 11.12, 10.03, 8.95, 7.96, 7.02, 6.20, 5.42, 4.73, 4.15, 3.64, 3.20, 2.81, 2.47, 2.18, 1.93, 1.72, 1.67, 1.43, 1.29, 1.19, 1.08, 0.96, 0.88, 0.81, 0.77, 0.75, 0.73, 0.68, 0.69, 0.64, 0.68, 0.69, 0.61, 0.52, 0.43 ];
179 | //const testIlluminantNorm = normalizeSPD(testIlluminant);
180 |
181 | function calcRa(R: number[]) {
182 | let sum = 0;
183 |
184 | for (let i = 0; i < 8; i++) {
185 | sum += R[i];
186 | }
187 |
188 | return sum / 8;
189 | }
190 |
191 | export function calcCRI(CCT: number, test: Float64Array) {
192 | // Reference
193 | const ref = calcRef(CCT);
194 | const TCSrefXYZ = calcAllTCS_XYZ(ref.data, CIE1931_2DEG_CMF);
195 | const TCSrefUVW = TCSrefXYZ.map((sref) => XYZ2UVW(sref, ref.u, ref.v));
196 |
197 | // Test
198 | const testIlluminantNorm = normalizeSPD(test);
199 | const TCStestIlluminantXYZ = calcAllTCS_XYZ(testIlluminantNorm, CIE1931_2DEG_CMF);
200 | const TCStestIlluminantuv = TCStestIlluminantXYZ.map((XYZ) => xy2uv(...XYZ2xy(XYZ)));
201 | const XYZtest = spd2XYZ(testIlluminantNorm, CIE1931_2DEG_CMF);
202 | const [ut, vt] = xy2uv(...XYZ2xy(XYZtest));
203 | const [ct, dt] = uv2cd(ut, vt);
204 | const uat =
205 | (10.872 + 0.404 * (ref.cr / ct) * ct - 4 * (ref.dr / dt) * dt) /
206 | (16.518 + 1.481 * (ref.cr / ct) * ct - (ref.dr / dt) * dt);
207 | const vat = 5.52 / (16.518 + 1.481 * (ref.cr / ct) * ct - (ref.dr / dt) * dt);
208 |
209 | // Adaptation Correction
210 | const adaptUVW = TCStestIlluminantXYZ.map((XYZ) => {
211 | const [u, v] = xy2uv(...XYZ2xy(XYZ));
212 | const [cs, ds] = uv2cd(u, v);
213 | const uas =
214 | (10.872 + 0.404 * (ref.cr / ct) * cs - 4 * (ref.dr / dt) * ds) /
215 | (16.518 + 1.481 * (ref.cr / ct) * cs - (ref.dr / dt) * ds);
216 | const vas = 5.52 / (16.518 + 1.481 * (ref.cr / ct) * cs - (ref.dr / dt) * ds);
217 |
218 | const W = 25 * XYZ[1] ** (1 / 3) - 17;
219 | const U = 13 * W * (uas - uat);
220 | const V = 13 * W * (vas - vat);
221 |
222 | return [U, V, W];
223 | });
224 |
225 | const DE = adaptUVW.map((UVW, i) => {
226 | const Uref = TCSrefUVW[i][0];
227 | const Vref = TCSrefUVW[i][1];
228 | const Wref = TCSrefUVW[i][2];
229 |
230 | return Math.sqrt((Uref - UVW[0]) ** 2 + (Vref - UVW[1]) ** 2 + (Wref - UVW[2]) ** 2);
231 | });
232 | const R = DE.map((delta) => 100 - 4.6 * delta);
233 | R.unshift(calcRa(R));
234 |
235 | return {
236 | UVPairs: TCSrefUVW.map((UVWref, i) => ({
237 | ref: [UVWref[0], UVWref[1]],
238 | test: [adaptUVW[i][0], adaptUVW[i][1]],
239 | })),
240 | DE,
241 | R,
242 | };
243 | }
244 |
--------------------------------------------------------------------------------
/lib/ble/lm3.ts:
--------------------------------------------------------------------------------
1 | import { getGlobalState, setGlobalState } from '../global';
2 | import calcCCT from 'lib/cct';
3 | import calcDuv from 'lib/duv';
4 | import calcTint from 'lib/tint';
5 | import { calcFlicker } from 'lib/flicker';
6 | import { XYZ2xy, xy2uv } from 'lib/CIEConv';
7 |
8 | export const BLE_SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
9 | const RX_CHARACTERISTIC_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';
10 | const TX_CHARACTERISTIC_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e';
11 |
12 | const PROTO_REQ_READ_M3 = 2564;
13 | const PROTO_RES_READ_M3 = 2565;
14 | const PROTO_REQ_MEAS = 2560;
15 | const PROTO_RES_MEAS = 2561;
16 | const PROTO_REQ_FREQ = 2570;
17 | const PROTO_RES_FREQ = 2571;
18 |
19 | const PROTO_MSG_SINGLE = 0;
20 | const PROTO_MSG_FFRAG = 0x80;
21 | const PROTO_MSG_MFRAG = 0xa0;
22 | const PROTO_MSG_LFRAG = 0xc0;
23 |
24 | enum LightMode {
25 | MONOCHROMATIC = 1,
26 | INCANDESCENT = 2,
27 | GENERAL = 3,
28 | }
29 |
30 | const tristimulusM = Object.freeze([
31 | Object.freeze([
32 | [0.06023, 0.00106, 0.02108, 0.03673, 0.1683, 0.02001, 0],
33 | [0.00652, 0.04478, 0.16998, -0.03268, 0.07425, 0.00739, 0],
34 | [0.33092, 0.12936, -0.15809, 0.19889, -0.0156, 0.00296, 0],
35 | ]),
36 | Object.freeze([
37 | Object.freeze([-0.43786, 0.53102, -0.1453, 0.2316, 0.36758, -0.09047, 0]),
38 | Object.freeze([-0.23226, 0.69225, -0.39786, 0.22539, 0.47947, -0.17614, 0]),
39 | Object.freeze([-0.11002, 1.21259, -0.56003, 0.14487, 0.35074, -0.30248, 0]),
40 | ]),
41 | Object.freeze([
42 | Object.freeze([-0.05825, -0.0896, 0.25859, 0.19518, 0.10893, 0.06724, 0]),
43 | Object.freeze([-0.19865, 0.01337, 0.40651, 0.29702, -0.06287, 0.03282, 0]),
44 | Object.freeze([0.58258, 0.11548, 0.21823, -0.00136, -0.10732, -0.00915, 0]),
45 | ]),
46 | ]);
47 |
48 | function getLightMode(V1: number, B1: number, G1: number, Y1: number, O1: number, R1: number, C1: number): LightMode {
49 | const a = (O1 + R1) / (V1 + B1 + G1 + Y1 + O1 + R1);
50 | const b = (R1 - Y1) / (V1 + B1 + G1 + Y1 + O1 + R1);
51 |
52 | if (Math.max(V1, B1, G1, Y1, O1, R1) / (V1 + B1 + G1 + Y1 + O1 + R1) >= 0.45) {
53 | return LightMode.MONOCHROMATIC;
54 | } else if (a >= 0.5 && a <= 0.55 && b >= 0 && b <= 0.05) {
55 | return LightMode.INCANDESCENT;
56 | } else {
57 | return LightMode.GENERAL;
58 | }
59 | }
60 |
61 | function calcResult(V1: number, B1: number, G1: number, Y1: number, O1: number, R1: number, C1: number) {
62 | let mode = getLightMode(V1, B1, G1, Y1, O1, R1, C1);
63 | const w = matMul(tristimulusM[mode - 1], 3, 7, [[V1], [B1], [G1], [Y1], [O1], [R1], [C1]], 7, 1);
64 |
65 | // Tristimulus
66 | let X: number;
67 | let Y: number;
68 | let Z: number;
69 | (X = w[0][0]) < 0 && (X = 0);
70 | (Y = w[1][0]) < 0 && (Y = 0);
71 | (Z = w[2][0]) < 0 && (Z = 0);
72 |
73 | let [x, y] = XYZ2xy([X, Y, Z]);
74 | const [Eu, Ev] = xy2uv(x, y);
75 |
76 | return (
77 | 0 == X && 0 == Y && 0 == Z && ((x = 0), (y = 0)),
78 | {
79 | Ey: y,
80 | Ex: x,
81 | Eu,
82 | Ev,
83 | CCT: calcCCT(x, y),
84 | Duv: calcDuv(Eu, Ev),
85 | tint: calcTint(x, y)[1],
86 | Lux: 1 * Y,
87 | mode: mode,
88 | }
89 | );
90 | }
91 |
92 | function calcEml(cct: number, v1: number, b1: number, g1: number, y1: number, o1: number, r1: number, mode: LightMode) {
93 | let eml: number;
94 |
95 | if (cct < 4e3) {
96 | if (cct < 3e3 && mode === LightMode.INCANDESCENT) {
97 | eml = -11.1321 * v1 + 10.088 * b1 + 10.5399 * g1 - 4.9714 * y1 - 4.2457 * o1 + 1.3921 * r1;
98 | } else {
99 | eml = 0.1157 * v1 + 0.543 * b1 + 0.1886 * g1 + 0.02516 * y1 - 0.0825 * o1 - 0.007316 * r1;
100 | }
101 | } else {
102 | eml = -0.005224 * v1 + 0.3113 * b1 + 0.3649 * g1 + 0.3632 * y1 - 0.4313 * o1 + 0.05123 * r1;
103 | }
104 |
105 | return eml < 0 ? 0 : eml;
106 | }
107 |
108 | function matMul(
109 | mat_a: readonly (readonly number[])[],
110 | m1: number,
111 | n1: number,
112 | mat_b: readonly (readonly number[])[],
113 | m2: number,
114 | n2: number
115 | ) {
116 | let mat_r = [
117 | [0, 0, 0],
118 | [0, 0, 0],
119 | [0, 0, 0],
120 | ];
121 |
122 | for (let i = 0; i < m1; i++)
123 | for (let j = 0; j < n2; j++) for (let k = 0; k < n1; k++) mat_r[i][j] = mat_r[i][j] + mat_a[i][k] * mat_b[k][j];
124 |
125 | return mat_r;
126 | }
127 |
128 | function lpfGetA(avg_period: number, sample_interval: number) {
129 | return Math.exp(-(sample_interval / avg_period));
130 | }
131 |
132 | /**
133 | * Calculate the next output value of the lpf.
134 | * @param prev is the previous output of this function.
135 | * @param a is the coefficient calculated by lpfGetA().
136 | * @param sample is the current sample.
137 | */
138 | function lpfCalcNext(a: number, prev: number, sample: number) {
139 | return a * prev + (1.0 - a) * sample;
140 | }
141 |
142 | function parseMeasurementData(
143 | data: Uint8Array,
144 | prevMeas: readonly number[],
145 | coeff_a: number,
146 | kSensor: readonly number[]
147 | ) {
148 | const V0 = (data[1] << 8) + data[2];
149 | const B0 = (data[3] << 8) + data[4];
150 | const G0 = (data[5] << 8) + data[6];
151 | const Y0 = (data[7] << 8) + data[8];
152 | const O0 = (data[9] << 8) + data[10];
153 | const R0 = (data[11] << 8) + data[12];
154 | const power = (data[13] << 8) + data[14];
155 | const C0 = data[15];
156 |
157 | // Correction
158 | const V1 = lpfCalcNext(coeff_a, prevMeas[0], V0 * kSensor[0]); // 450 nm
159 | const B1 = lpfCalcNext(coeff_a, prevMeas[1], B0 * kSensor[1]); // 500 nm
160 | const G1 = lpfCalcNext(coeff_a, prevMeas[2], G0 * kSensor[2]); // 550 nm
161 | const Y1 = lpfCalcNext(coeff_a, prevMeas[3], Y0 * kSensor[3]); // 570 nm
162 | const O1 = lpfCalcNext(coeff_a, prevMeas[4], O0 * kSensor[4]); // 600 nm
163 | const R1 = lpfCalcNext(coeff_a, prevMeas[5], R0 * kSensor[5]); // 650 nm
164 | const C1 = 1 * kSensor[6]; // But what's the relation of this to C0 that's the ambient temperature?
165 |
166 | const result = calcResult(V1, B1, G1, Y1, O1, R1, C1);
167 | // TODO
168 | //(V0 > 65e3 || B0 > 65e3 || G0 > 65e3 || Y0 > 65e3 || O0 > 65e3 || R0 > 65e3 || result.Lux > 5e4) && (result.Lux = 5e4, result.CCT = 0, result.Eu = 0, result.Ev = 0, result.Ey = 0, result.Ex = 0);
169 | //result.Lux < 3 && (result.Lux = 0);
170 |
171 | return {
172 | result: {
173 | V1,
174 | B1,
175 | G1,
176 | Y1,
177 | O1,
178 | R1,
179 | C1,
180 | ...result,
181 | temperature: C0,
182 | eml: calcEml(result.CCT, V1, B1, G1, Y1, O1, R1, result.mode),
183 | },
184 | power,
185 | };
186 | }
187 |
188 | function parseWave(data: Uint8Array, len: number): number[] {
189 | const x: number[] = [];
190 | for (let i = 0; i < len; i++) {
191 | let a = (data[16 + 6 * i + 0] << 8) + data[16 + 6 * i + 1];
192 | let b = (data[16 + 6 * i + 2] << 8) + data[16 + 6 * i + 3];
193 | let c = (data[16 + 6 * i + 4] << 8) + data[16 + 6 * i + 5];
194 | const xn0 = a >> 4;
195 | const xn1 = ((0xf & a) << 8) | (b >> 8);
196 | const xn2 = ((0xff & b) << 4) | (c >> 12);
197 | const xn3 = 0xfff & c;
198 |
199 | x.push(xn0, xn1, xn2, xn3);
200 | }
201 |
202 | return x;
203 | }
204 |
205 | const batU = Object.freeze([4080, 3985, 3894, 3838, 3773, 3725, 3710, 3688, 3656, 3594, 3455]);
206 | const batP = Object.freeze([100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 1]);
207 |
208 | function batteryLevel(voltage: number) {
209 | let level = 1;
210 |
211 | for (let i = 0; i < 9; i++) {
212 | if (voltage > batU[i + 1]) {
213 | level = ((voltage - batU[i + 1]) / (batU[i] - batU[i + 1])) * (batP[i] - batP[i + 1]) + batP[i + 1];
214 | break;
215 | }
216 | }
217 | return Math.min(level, 100);
218 | }
219 |
220 | export async function createLm3(server: BluetoothRemoteGATTServer) {
221 | const service = await server.getPrimaryService(BLE_SERVICE_UUID);
222 | //const rxCharacteristic = await service.getCharacteristic(RX_CHARACTERISTIC_UUID);
223 | const txCharacteristic = await service.getCharacteristic(TX_CHARACTERISTIC_UUID);
224 |
225 | const calData = {
226 | kSensor: [],
227 | power: 0,
228 | };
229 | let prevMeas = [0, 0, 0, 0, 0, 0]; // V1, B1, G1, Y1, O1, R1
230 | let coeff_a = 1; // Calc with lpfGetA()
231 |
232 | let inflight = false;
233 | let rxBuffer = null;
234 | let rxBufferLen = 0;
235 | txCharacteristic.addEventListener('characteristicvaluechanged', (event) => {
236 | // @ts-ignore
237 | const value = event.target.value;
238 | const buf = new Uint8Array(value.buffer);
239 |
240 | //console.log('event:', value);
241 |
242 | try {
243 | const msgType = value.getUint8(0) & 0xe0;
244 | let payload: Uint8Array;
245 | switch (msgType) {
246 | case PROTO_MSG_SINGLE: // Single fragment message
247 | //console.log(`Single message. len: ${value.length}`);
248 | parseMsg(buf.subarray(3));
249 | break;
250 | case PROTO_MSG_FFRAG: // First fragment of a message
251 | const totalLen = (value.getUint8(1) << 8) | value.getUint8(2);
252 | //const something = value.getUint8(3);
253 | //console.log(`First fragment. totalLen: ${totalLen}`);
254 | payload = buf.subarray(3);
255 | rxBuffer = new Uint8Array(totalLen);
256 | rxBuffer.set(payload, 0);
257 | rxBufferLen = payload.length;
258 | break;
259 | case PROTO_MSG_MFRAG: // Partial
260 | //console.log(`Fragment. len: ${value.length}`);
261 | payload = buf.subarray(1);
262 | rxBuffer.set(payload, rxBufferLen);
263 | rxBufferLen += payload.length;
264 | break;
265 | case PROTO_MSG_LFRAG:
266 | //console.log(`Last fragment. len: ${value.length}`);
267 | payload = buf.subarray(1);
268 | rxBuffer.set(payload, rxBufferLen);
269 | rxBufferLen += payload.length;
270 | //console.log('buf len', rxBuffer.length, rxBufferLen);
271 | parseMsg(rxBuffer.subarray(0, rxBufferLen));
272 | break;
273 | default:
274 | console.log(`Unknown message type: ${msgType}`);
275 | }
276 | } catch (e) {
277 | console.log('Msg parse failed:', e);
278 | }
279 | inflight = false;
280 | });
281 |
282 | let seqno = 0;
283 |
284 | function encapsulateData(data: number[]) {
285 | const nFragments = data.length < 17 ? 1 : Math.ceil((data.length - 17) / 19 + 1);
286 | const fragments = [];
287 |
288 | for (let c = 0; c < nFragments; c++) {
289 | let head = [];
290 | let l = [];
291 |
292 | if (0 == c) {
293 | let u = data.length + nFragments + 2;
294 |
295 | head = [nFragments > 1 ? PROTO_MSG_FFRAG : PROTO_MSG_SINGLE, (0xff00 & u) >> 8, 0xff & u];
296 | l = nFragments > 1 ? data.slice(0, 17) : data.slice(0);
297 | } else {
298 | if (c != nFragments - 1) {
299 | head = [PROTO_MSG_MFRAG | c];
300 | l = data.slice(17 + 19 * (c - 1), 17 + 19 * c);
301 | } else {
302 | head = [PROTO_MSG_LFRAG | c];
303 | l = data.slice(17 + 19 * (c - 1));
304 | }
305 | }
306 | fragments.push([...head, ...l]);
307 | }
308 |
309 | return fragments;
310 | }
311 |
312 | const sendCommand = async (opCode: number, n?: number[]) => {
313 | /* this.realWriteDataWithCommondAndBody */
314 | seqno = (seqno + 1) & 0xff;
315 |
316 | let h = [0, 0x13, 0, 0, seqno, 0, 0 + (n ? n.length : 0), 0, 0, (0xff00 & opCode) >> 8, 0xff & opCode];
317 |
318 | let v = h;
319 | n && (v = h.concat(n));
320 | let frames = encapsulateData(v);
321 | //console.log('sending:', frames);
322 | inflight = true;
323 | for (const frame of frames) {
324 | let buf = new ArrayBuffer(frame.length);
325 | let view = new DataView(buf);
326 |
327 | for (let i = 0; i < frame.length; i++) {
328 | view.setInt8(i, 0xff & frame[i]);
329 | }
330 |
331 | await txCharacteristic.writeValue(buf);
332 | }
333 | };
334 |
335 | const parseMsg = (data: Uint8Array) => {
336 | const code = ((255 & data[9]) << 8) + data[10];
337 | // We could theoretically implement a frame reassembly and callback
338 | // response system here, but it's totally unnecessary because we
339 | // are alway sending one command at time and there are only 3 possible
340 | // command responses that are only parsed in one way.
341 | //const trx = data[5];
342 | //console.log('msgType: ' + (255 & data[9]) + ', ' + ((0xff & data[9]) << 8) + ', ' + code + ',transNumber:' + trx);
343 |
344 | if (code === PROTO_RES_MEAS) {
345 | // response to singleMeasure?
346 | const res = parseMeasurementData(data.subarray(11), prevMeas, coeff_a, calData.kSensor);
347 | prevMeas[0] = res.result.V1;
348 | prevMeas[1] = res.result.B1;
349 | prevMeas[2] = res.result.G1;
350 | prevMeas[3] = res.result.Y1;
351 | prevMeas[4] = res.result.O1;
352 | prevMeas[5] = res.result.R1;
353 | //console.log('Read measurement:', res);
354 |
355 | setGlobalState('res_lm_measurement', res.result);
356 | setGlobalState('res_battery_level', batteryLevel(res.power));
357 | } else if (code === PROTO_RES_FREQ) {
358 | if (data[12] >= 4) {
359 | console.log('wat?');
360 | return;
361 | }
362 | const len = data[12] === 3 ? 61 : 65;
363 | const wave = parseWave(data.subarray(12), len);
364 |
365 | waveData.x.push(...wave);
366 | if (len === 61) {
367 | waveData.sRange = data[13];
368 | // Some of the values retrieved this way are totally incorrect but CCT and Lux are good.
369 | const { CCT, Lux } = parseMeasurementData(data.slice(11).slice(2), prevMeas, 0, calData.kSensor).result;
370 | calcFlicker(waveData, CCT, Lux, calData.kSensor);
371 | }
372 | } else if (code === PROTO_RES_READ_M3) {
373 | // response to readM3: M3 Matrix
374 | const arr2float = (v) => new DataView(new Uint8Array(v).buffer).getFloat32(0);
375 |
376 | let s = [];
377 | for (let i = 0; i < 7; i++) {
378 | const j = 4 * i;
379 | const l = data.slice(12 + j, 12 + j + 4);
380 | s.push(arr2float(l.reverse()));
381 | }
382 | calData.kSensor = s;
383 | calData.power = (data[40] << 8) + data[41];
384 | }
385 | };
386 |
387 | const singleMeasure = async () => await sendCommand(PROTO_REQ_MEAS);
388 |
389 | let measTim: ReturnType = null;
390 | const startMeasuring = (ms: number, avgPeriod: number) => {
391 | if (measTim) {
392 | throw new Error('Already measuring');
393 | }
394 |
395 | // TODO It would be good to clear all the previous measurement data here
396 |
397 | coeff_a = lpfGetA(avgPeriod, ms / 1000);
398 | measTim = setInterval(() => {
399 | if (inflight || !getGlobalState('running')) {
400 | return;
401 | }
402 |
403 | singleMeasure().catch((e) => {
404 | console.log(e);
405 | clearInterval(measTim);
406 | });
407 | }, ms);
408 | };
409 |
410 | let waveData: {
411 | n: number;
412 | sRange: number; // Stroboscopic range
413 | x: number[];
414 | } = { n: 0, sRange: 0, x: [] };
415 |
416 | const readFreq = async (a: number, n: number) => {
417 | if (measTim && getGlobalState('running')) {
418 | throw new Error('The measurement loop must be paused first');
419 | }
420 |
421 | waveData = { n, sRange: NaN, x: [] };
422 | await sendCommand(PROTO_REQ_FREQ, [a, (n & 0xff00) >> 8, n & 0xff]);
423 | };
424 |
425 | return {
426 | startNotifications: () => txCharacteristic.startNotifications(),
427 | readCal: async () => await sendCommand(PROTO_REQ_READ_M3),
428 | singleMeasure,
429 | startMeasuring,
430 | readFreq,
431 | };
432 | }
433 |
--------------------------------------------------------------------------------
/lib/spdIlluminants.ts:
--------------------------------------------------------------------------------
1 | import wlMap from './wlmap';
2 |
3 | const nmIncrement = 5;
4 |
5 | /**
6 | * Black body radiant.
7 | */
8 | export function SPDofPlanck(CCT: number) {
9 | return Float64Array.from(
10 | wlMap((wl) => {
11 | const wlp = wl * 10 ** -9;
12 | return 1.191027e-16 / (wlp ** 5 * (Math.exp(0.0143876 / (wlp * CCT)) - 1)); // Planck
13 | }, nmIncrement)
14 | );
15 | }
16 |
17 | export const SPDofA = Float64Array.from(
18 | wlMap((wl) => {
19 | return (100 * (560 / wl) ** 5 * (Math.exp(1.435e7 / (2848 * 560)) - 1)) / (Math.exp(1.435e7 / (2848 * wl)) - 1);
20 | }, nmIncrement)
21 | );
22 |
23 | // 380 - 780
24 | // S0(L), S1(L), S2(L)
25 | // step: 5 nm
26 | const CIE_DIlluminant = Float64Array.from([
27 | 63.4, 38.5, 3.0, 64.6, 36.75, 2.1, 65.8, 35.0, 1.2, 80.3, 39.2, 0.05, 94.8, 43.4, -1.1, 99.8, 44.85, -0.8, 104.8,
28 | 46.3, -0.5, 105.35, 45.1, -0.6, 105.9, 43.9, -0.7, 101.35, 40.5, -0.95, 96.8, 37.1, -1.2, 105.35, 36.9, -1.9, 113.9,
29 | 36.7, -2.6, 119.75, 36.3, -2.75, 125.6, 35.9, -2.9, 125.55, 34.25, -2.85, 125.5, 32.6, -2.8, 123.4, 30.25, -2.7,
30 | 121.3, 27.9, -2.6, 121.3, 26.1, -2.6, 121.3, 24.3, -2.6, 117.4, 22.2, -2.2, 113.5, 20.1, -1.8, 113.3, 18.15, -1.65,
31 | 113.1, 16.2, -1.5, 111.95, 14.7, -1.4, 110.8, 13.2, -1.3, 108.65, 10.9, -1.25, 106.5, 8.6, -1.2, 107.65, 7.35, -1.1,
32 | 108.8, 6.1, -1.0, 107.05, 5.15, -0.75, 105.3, 4.2, -0.5, 104.85, 3.05, -0.4, 104.4, 1.9, -0.3, 102.2, 0.95, -0.15,
33 | 100.0, 0.0, 0.0, 98.0, -0.8, 0.1, 96.0, -1.6, 0.2, 95.55, -2.55, 0.35, 95.1, -3.5, 0.5, 92.1, -3.5, 1.3, 89.1, -3.5,
34 | 2.1, 89.8, -4.65, 2.65, 90.5, -5.8, 3.2, 90.4, -6.5, 3.65, 90.3, -7.2, 4.1, 89.35, -7.9, 4.4, 88.4, -8.6, 4.7, 86.2,
35 | -9.05, 4.9, 84.0, -9.5, 5.1, 84.55, -10.2, 5.9, 85.1, -10.9, 6.7, 83.5, -10.8, 7.0, 81.9, -10.7, 7.3, 82.25, -11.35,
36 | 7.95, 82.6, -12.0, 8.6, 83.75, -13.0, 9.2, 84.9, -14.0, 9.8, 83.1, -13.8, 10.0, 81.3, -13.6, 10.2, 76.6, -12.8,
37 | 9.25, 71.9, -12.0, 8.3, 73.1, -12.65, 8.95, 74.3, -13.3, 9.6, 75.35, -13.1, 9.05, 76.4, -12.9, 8.5, 69.85, -11.75,
38 | 7.75, 63.3, -10.6, 7.0, 67.5, -11.1, 7.3, 71.7, -11.6, 7.6, 74.35, -11.9, 7.8, 77.0, -12.2, 8.0, 71.1, -11.2, 7.35,
39 | 65.2, -10.2, 6.7, 56.45, -9.0, 5.95, 47.7, -7.8, 5.2, 58.15, -9.5, 6.3, 68.6, -11.2, 7.4, 66.8, -10.8, 7.1, 65.0,
40 | -10.4, 6.8,
41 | ]);
42 |
43 | export function SPDofD(CCT: number) {
44 | const xlo = -4.607e9 / CCT ** 3 + 2.9678e6 / CCT ** 2 + 0.09911e3 / CCT + 0.244063; // 4000 - 7000 K
45 | const xhi = 2.0064e9 / CCT ** 3 + 1.9018e6 / CCT ** 2 + 0.24748e3 / CCT + 0.23704; // 7001 - 25000 K
46 | const xd = CCT < 7000 ? xlo : xhi;
47 | const yd = -3 * xd ** 2 + 2.87 * xd - 0.275;
48 |
49 | return Float64Array.from(
50 | wlMap((_, i) => {
51 | const M1 = (-1.3515 - 1.7703 * xd + 5.9114 * yd) / (0.0241 + 0.2562 * xd - 0.7341 * yd);
52 | const M2 = (0.03 - 31.4424 * xd + 30.0717 * yd) / (0.0241 + 0.2562 * xd - 0.7341 * yd);
53 | return CIE_DIlluminant[i * 3] + M1 * CIE_DIlluminant[i * 3 + 1] + M2 * CIE_DIlluminant[i * 3 + 2]; // D illuminant
54 | }, nmIncrement)
55 | );
56 | }
57 |
58 | export const SPDofE = Float64Array.from(wlMap((_wl) => 100, nmIncrement));
59 |
60 | /**
61 | * Fluorescent Std FL1.
62 | * 6430 K
63 | */
64 | export const SPDofFL1 = Float64Array.from([
65 | 1.87, 2.36, 2.94, 3.47, 5.17, 19.49, 6.13, 6.24, 7.01, 7.79, 8.56, 43.67, 16.94, 10.72, 11.35, 11.89, 12.37, 12.75,
66 | 13.0, 13.15, 13.23, 13.17, 13.13, 12.85, 12.52, 12.2, 11.83, 11.5, 11.22, 11.05, 11.03, 11.18, 11.53, 27.74, 17.05,
67 | 13.55, 14.33, 15.01, 15.52, 18.29, 19.55, 15.48, 14.91, 14.15, 13.22, 12.19, 11.12, 10.03, 8.95, 7.96, 7.02, 6.2,
68 | 5.42, 4.73, 4.15, 3.64, 3.2, 2.81, 2.47, 2.18, 1.93, 1.72, 1.67, 1.43, 1.29, 1.19, 1.08, 0.96, 0.88, 0.81, 0.77,
69 | 0.75, 0.73, 0.68, 0.69, 0.64, 0.68, 0.69, 0.61, 0.52, 0.43,
70 | ]);
71 |
72 | /**
73 | * Fluorescent Std FL2.
74 | * 4230 K
75 | */
76 | export const SPDofFL2 = Float64Array.from([
77 | 1.18, 1.48, 1.84, 2.15, 3.44, 15.69, 3.85, 3.74, 4.19, 4.62, 5.06, 34.98, 11.81, 6.27, 6.63, 6.93, 7.19, 7.4, 7.54,
78 | 7.62, 7.65, 7.62, 7.62, 7.45, 7.28, 7.15, 7.05, 7.04, 7.16, 7.47, 8.04, 8.88, 10.01, 24.88, 16.64, 14.59, 16.16,
79 | 17.56, 18.62, 21.47, 22.79, 19.29, 18.66, 17.73, 16.54, 15.21, 13.8, 12.36, 10.95, 9.65, 8.4, 7.32, 6.31, 5.43,
80 | 4.68, 4.02, 3.45, 2.96, 2.55, 2.19, 1.89, 1.64, 1.53, 1.27, 1.1, 0.99, 0.88, 0.76, 0.68, 0.61, 0.56, 0.54, 0.51,
81 | 0.47, 0.47, 0.43, 0.46, 0.47, 0.4, 0.33, 0.27,
82 | ]);
83 |
84 | /**
85 | * Fluorescent Std FL3.
86 | * 3450 K
87 | */
88 | export const SPDofFL3 = Float64Array.from([
89 | 0.82, 1.02, 1.26, 1.44, 2.57, 14.36, 2.7, 2.45, 2.73, 3.0, 3.28, 31.85, 9.47, 4.02, 4.25, 4.44, 4.59, 4.72, 4.8,
90 | 4.86, 4.87, 4.85, 4.88, 4.77, 4.67, 4.62, 4.62, 4.73, 4.99, 5.48, 6.25, 7.34, 8.78, 23.82, 16.14, 14.59, 16.63,
91 | 18.49, 19.95, 23.11, 24.69, 21.41, 20.85, 19.93, 18.67, 17.22, 15.65, 14.04, 12.45, 10.95, 9.51, 8.27, 7.11, 6.09,
92 | 5.22, 4.45, 3.8, 3.23, 2.75, 2.33, 1.99, 1.7, 1.55, 1.27, 1.09, 0.96, 0.83, 0.71, 0.62, 0.54, 0.49, 0.46, 0.43,
93 | 0.39, 0.39, 0.35, 0.38, 0.39, 0.33, 0.28, 0.21,
94 | ]);
95 |
96 | /**
97 | * Fluorescent Std FL4.
98 | * 2940 K
99 | */
100 | export const SPDofFL4 = Float64Array.from([
101 | 0.57, 0.7, 0.87, 0.98, 2.01, 13.75, 1.95, 1.59, 1.76, 1.93, 2.1, 30.28, 8.03, 2.55, 2.7, 2.82, 2.91, 2.99, 3.04,
102 | 3.08, 3.09, 3.09, 3.14, 3.06, 3.0, 2.98, 3.01, 3.14, 3.41, 3.9, 4.69, 5.81, 7.32, 22.59, 15.11, 13.88, 16.33, 18.68,
103 | 20.64, 24.28, 26.26, 23.28, 22.94, 22.14, 20.91, 19.43, 17.74, 16.0, 14.42, 12.56, 10.93, 9.52, 8.18, 7.01, 6.0,
104 | 5.11, 4.36, 3.69, 3.13, 2.64, 2.24, 1.91, 1.7, 1.39, 1.18, 1.03, 0.88, 0.74, 0.64, 0.54, 0.49, 0.46, 0.42, 0.37,
105 | 0.37, 0.33, 0.35, 0.36, 0.31, 0.26, 0.19,
106 | ]);
107 |
108 | /**
109 | * Fluorescent Std FL5.
110 | * 6350 K
111 | */
112 | export const SPDofFL5 = Float64Array.from([
113 | 1.87, 2.35, 2.92, 3.45, 5.1, 18.91, 6.0, 6.11, 6.85, 7.58, 8.31, 40.76, 16.06, 10.32, 10.91, 11.4, 11.83, 12.17,
114 | 12.4, 12.54, 12.58, 12.52, 12.47, 12.2, 11.89, 11.61, 11.33, 11.1, 10.96, 10.97, 11.16, 11.54, 12.12, 27.78, 17.73,
115 | 14.47, 15.2, 15.77, 16.1, 18.54, 19.5, 15.39, 14.64, 13.72, 12.69, 11.57, 10.45, 9.35, 8.29, 7.32, 6.41, 5.63, 4.9,
116 | 4.26, 3.72, 3.25, 2.83, 2.49, 2.19, 1.93, 1.71, 1.52, 1.48, 1.26, 1.13, 1.05, 0.96, 0.85, 0.78, 0.72, 0.68, 0.67,
117 | 0.65, 0.61, 0.62, 0.59, 0.62, 0.64, 0.55, 0.47, 0.4,
118 | ]);
119 |
120 | /**
121 | * Fluorescent Std FL6.
122 | * 4150 K
123 | */
124 | export const SPDofFL6 = Float64Array.from([
125 | 1.05, 1.31, 1.63, 1.9, 3.11, 14.8, 3.43, 3.3, 3.68, 4.07, 4.45, 32.61, 10.74, 5.48, 5.78, 6.03, 6.25, 6.41, 6.52,
126 | 6.58, 6.59, 6.56, 6.56, 6.42, 6.28, 6.2, 6.19, 6.3, 6.6, 7.12, 7.94, 9.07, 10.49, 25.22, 17.46, 15.63, 17.22, 18.53,
127 | 19.43, 21.97, 23.01, 19.41, 18.56, 17.42, 16.09, 14.64, 13.15, 11.68, 10.25, 8.95, 7.74, 6.69, 5.71, 4.87, 4.16,
128 | 3.55, 3.02, 2.57, 2.2, 1.87, 1.6, 1.37, 1.29, 1.05, 0.91, 0.81, 0.71, 0.61, 0.54, 0.48, 0.44, 0.43, 0.4, 0.37, 0.38,
129 | 0.35, 0.39, 0.41, 0.33, 0.26, 0.21,
130 | ]);
131 |
132 | /**
133 | * Fluorescent Broad-band FL7.
134 | * 6500 K
135 | */
136 | export const SPDofFL7 = Float64Array.from([
137 | 2.56, 3.18, 3.84, 4.53, 6.15, 19.37, 7.37, 7.05, 7.71, 8.41, 9.15, 44.14, 17.52, 11.35, 12.0, 12.58, 13.08, 13.45,
138 | 13.71, 13.88, 13.95, 13.93, 13.82, 13.64, 13.43, 13.25, 13.08, 12.93, 12.78, 12.6, 12.44, 12.33, 12.26, 29.52,
139 | 17.05, 12.44, 12.58, 12.72, 12.83, 15.46, 16.75, 12.83, 12.67, 12.45, 12.19, 11.89, 11.6, 11.35, 11.12, 10.95,
140 | 10.76, 10.42, 10.11, 10.04, 10.02, 10.11, 9.87, 8.65, 7.27, 6.44, 5.83, 5.41, 5.04, 4.57, 4.12, 3.77, 3.46, 3.08,
141 | 2.73, 2.47, 2.25, 2.06, 1.9, 1.75, 1.62, 1.54, 1.45, 1.32, 1.17, 0.99, 0.81,
142 | ]);
143 |
144 | /**
145 | * Fluorescent Broad-band FL8.
146 | * 5000 K
147 | */
148 | export const SPDofFL8 = Float64Array.from([
149 | 1.21, 1.5, 1.81, 2.13, 3.17, 13.08, 3.83, 3.45, 3.86, 4.42, 5.09, 34.1, 12.42, 7.68, 8.6, 9.46, 10.24, 10.84, 11.33,
150 | 11.71, 11.98, 12.17, 12.28, 12.32, 12.35, 12.44, 12.55, 12.68, 12.77, 12.72, 12.6, 12.43, 12.22, 28.96, 16.51,
151 | 11.79, 11.76, 11.77, 11.84, 14.61, 16.11, 12.34, 12.53, 12.72, 12.92, 13.12, 13.34, 13.61, 13.87, 14.07, 14.2,
152 | 14.16, 14.13, 14.34, 14.5, 14.46, 14.0, 12.58, 10.99, 9.98, 9.22, 8.62, 8.07, 7.39, 6.71, 6.16, 5.63, 5.03, 4.46,
153 | 4.02, 3.66, 3.36, 3.09, 2.85, 2.65, 2.51, 2.37, 2.15, 1.89, 1.61, 1.32,
154 | ]);
155 |
156 | /**
157 | * Fluorescent Broad-band FL9.
158 | * 4150 K
159 | */
160 | export const SPDofFL9 = Float64Array.from([
161 | 0.9, 1.12, 1.36, 1.6, 2.59, 12.8, 3.05, 2.56, 2.86, 3.3, 3.82, 32.62, 10.77, 5.84, 6.57, 7.25, 7.86, 8.35, 8.75,
162 | 9.06, 9.31, 9.48, 9.61, 9.68, 9.74, 9.88, 10.04, 10.26, 10.48, 10.63, 10.78, 10.96, 11.18, 27.71, 16.29, 12.28,
163 | 12.74, 13.21, 13.65, 16.57, 18.14, 14.55, 14.65, 14.66, 14.61, 14.5, 14.39, 14.4, 14.47, 14.62, 14.72, 14.55, 14.4,
164 | 14.58, 14.88, 15.51, 15.47, 13.2, 10.57, 9.18, 8.25, 7.57, 7.03, 6.35, 5.72, 5.25, 4.8, 4.29, 3.8, 3.43, 3.12, 2.86,
165 | 2.64, 2.43, 2.26, 2.14, 2.02, 1.83, 1.61, 1.38, 1.12,
166 | ]);
167 |
168 | /**
169 | * Fluorescent 3 Band FL10.
170 | * 5000 K
171 | */
172 | export const SPDofFL10 = Float64Array.from([
173 | 1.11, 0.8, 0.62, 0.57, 1.48, 12.16, 2.12, 2.7, 3.74, 5.14, 6.75, 34.39, 14.86, 10.4, 10.76, 10.67, 10.11, 9.27,
174 | 8.29, 7.29, 7.91, 16.64, 16.73, 10.44, 5.94, 3.34, 2.35, 1.88, 1.59, 1.47, 1.8, 5.71, 40.98, 73.69, 33.61, 8.24,
175 | 3.38, 2.47, 2.14, 4.86, 11.45, 14.79, 12.16, 8.97, 6.52, 8.31, 44.12, 34.55, 12.09, 12.15, 10.52, 4.43, 1.95, 2.19,
176 | 3.19, 2.77, 2.29, 2.0, 1.52, 1.35, 1.47, 1.79, 1.74, 1.02, 1.14, 3.32, 4.49, 2.05, 0.49, 0.24, 0.21, 0.21, 0.24,
177 | 0.24, 0.21, 0.17, 0.21, 0.22, 0.17, 0.12, 0.09,
178 | ]);
179 |
180 | /**
181 | * Fluorescent 3 Band FL11.
182 | * 4000 K
183 | */
184 | export const SPDofFL11 = Float64Array.from([
185 | 0.91, 0.63, 0.46, 0.37, 1.29, 12.68, 1.59, 1.79, 2.46, 3.33, 4.49, 33.94, 12.13, 6.95, 7.19, 7.12, 6.72, 6.13, 5.46,
186 | 4.79, 5.66, 14.29, 14.96, 8.97, 4.72, 2.33, 1.47, 1.1, 0.89, 0.83, 1.18, 4.9, 39.59, 72.84, 32.61, 7.52, 2.83, 1.96,
187 | 1.67, 4.43, 11.28, 14.76, 12.73, 9.74, 7.33, 9.72, 55.27, 42.58, 13.18, 13.16, 12.26, 5.11, 2.07, 2.34, 3.58, 3.01,
188 | 2.48, 2.14, 1.54, 1.33, 1.46, 1.94, 2.0, 1.2, 1.35, 4.1, 5.58, 2.51, 0.57, 0.27, 0.23, 0.21, 0.24, 0.24, 0.2, 0.24,
189 | 0.32, 0.26, 0.16, 0.12, 0.09,
190 | ]);
191 |
192 | /**
193 | * Fluorescent 3 Band FL12.
194 | * 3000 K
195 | */
196 | export const SPDofFL12 = Float64Array.from([
197 | 0.96, 0.64, 0.45, 0.33, 1.19, 12.48, 1.12, 0.94, 1.08, 1.37, 1.78, 29.05, 7.9, 2.65, 2.71, 2.65, 2.49, 2.33, 2.1,
198 | 1.91, 3.01, 10.83, 11.88, 6.88, 3.43, 1.49, 0.92, 0.71, 0.6, 0.63, 1.1, 4.56, 34.4, 65.4, 29.48, 7.16, 3.08, 2.47,
199 | 2.27, 5.09, 11.96, 15.32, 14.27, 11.86, 9.28, 12.31, 68.53, 53.02, 14.67, 14.38, 14.71, 6.46, 2.57, 2.75, 4.18,
200 | 3.44, 2.81, 2.42, 1.64, 1.36, 1.49, 2.14, 2.34, 1.42, 1.61, 5.04, 6.98, 3.19, 0.71, 0.3, 0.26, 0.23, 0.28, 0.28,
201 | 0.21, 0.17, 0.21, 0.19, 0.15, 0.1, 0.05,
202 | ]);
203 |
204 | /**
205 | * Fluorescent 3.1: Standard halophosphate lamp.
206 | */
207 | export const SPDofFL3_1 = Float64Array.from([
208 | 2.39, 2.93, 3.82, 4.23, 4.97, 86.3, 11.65, 7.09, 7.84, 8.59, 9.44, 196.54, 10.94, 11.38, 11.89, 12.37, 12.81, 13.15,
209 | 13.39, 13.56, 13.59, 13.56, 14.07, 13.39, 13.29, 13.25, 13.53, 14.24, 15.74, 18.26, 22.28, 27.97, 35.7, 148.98,
210 | 56.55, 68.68, 79.99, 91.47, 101.32, 123.16, 129.53, 115.05, 113.48, 110.08, 104.28, 97.98, 89.6, 80.74, 71.92, 63.5,
211 | 55.46, 47.97, 41.39, 35.5, 30.32, 25.79, 21.84, 18.53, 15.67, 13.22, 11.14, 9.4, 8.65, 6.75, 5.69, 4.87, 4.29, 3.54,
212 | 3.03, 2.62, 2.28, 1.94, 1.7, 1.5, 1.36, 1.16, 4.91, 0.95, 1.5, 0.89, 0.68,
213 | ]);
214 |
215 | /**
216 | * Fluorescent 3.2: Standard halophosphate lamp.
217 | */
218 | export const SPDofFL3_2 = Float64Array.from([
219 | 5.8, 6.99, 8.7, 9.89, 11.59, 94.53, 20.8, 16.52, 18.3, 20.33, 22.0, 231.9, 25.81, 27.63, 29.1, 30.61, 31.92, 33.11,
220 | 33.83, 34.7, 35.02, 35.22, 35.81, 35.14, 35.14, 34.9, 34.7, 35.02, 36.13, 37.92, 40.62, 44.7, 49.63, 154.16, 62.21,
221 | 68.92, 75.83, 81.95, 86.95, 103.54, 109.94, 91.95, 89.85, 87.15, 83.26, 78.93, 73.93, 68.84, 63.44, 58.84, 53.84,
222 | 49.43, 45.54, 41.53, 38.31, 34.62, 31.8, 29.02, 26.72, 24.22, 22.19, 20.41, 19.1, 16.79, 15.13, 13.82, 12.63, 11.39,
223 | 10.32, 9.21, 8.89, 7.5, 6.71, 6.11, 5.4, 4.8, 8.7, 4.01, 4.09, 3.3, 2.82,
224 | ]);
225 |
226 | /**
227 | * Fluorescent 3.3: Standard halophosphate lamp.
228 | */
229 | export const SPDofFL3_3 = Float64Array.from([
230 | 8.94, 11.21, 14.08, 16.48, 19.63, 116.33, 32.07, 29.72, 33.39, 36.94, 40.33, 262.66, 46.87, 49.79, 52.46, 54.81,
231 | 56.81, 58.44, 59.52, 60.12, 60.24, 59.88, 59.88, 58.6, 57.85, 56.29, 54.81, 53.42, 52.7, 52.5, 53.3, 54.89, 57.61,
232 | 182.75, 65.27, 69.41, 73.28, 76.56, 78.67, 95.74, 97.22, 76.79, 73.36, 69.33, 64.23, 58.92, 53.38, 47.91, 42.61,
233 | 37.74, 33.11, 29.04, 25.29, 22.1, 19.31, 16.84, 14.68, 12.89, 11.37, 9.97, 8.82, 7.86, 7.78, 6.3, 5.67, 5.15, 4.91,
234 | 4.31, 3.99, 3.67, 3.43, 3.19, 2.95, 2.75, 2.63, 2.43, 7.14, 2.19, 2.71, 2.0, 1.8,
235 | ]);
236 |
237 | /**
238 | * Fluorescent 3.4: Standard DeLuxe type lamp.
239 | */
240 | export const SPDofFL3_4 = Float64Array.from([
241 | 3.46, 3.86, 4.41, 4.51, 4.86, 71.22, 8.72, 5.36, 5.61, 5.91, 6.42, 192.77, 7.77, 8.37, 9.22, 10.18, 11.18, 12.28,
242 | 13.38, 14.54, 15.74, 17.09, 19.6, 21.05, 23.96, 27.77, 32.68, 38.29, 43.76, 47.72, 50.27, 51.78, 52.68, 167.36,
243 | 55.29, 56.94, 59.3, 62.15, 65.26, 84.26, 89.22, 75.79, 79.19, 82.8, 85.76, 88.62, 91.12, 93.43, 96.89, 101.45,
244 | 103.65, 100.3, 97.89, 96.59, 106.21, 109.97, 117.49, 96.04, 80.15, 70.42, 65.01, 60.15, 56.04, 50.92, 46.26, 42.6,
245 | 38.85, 35.09, 31.73, 28.77, 25.76, 32.16, 21.3, 18.55, 17.74, 14.74, 12.93, 13.63, 10.43, 9.97, 8.07,
246 | ]);
247 |
248 | /**
249 | * Fluorescent 3.5: Standard DeLuxe type lamp.
250 | */
251 | export const SPDofFL3_5 = Float64Array.from([
252 | 4.72, 5.82, 7.18, 8.39, 9.96, 58.86, 15.78, 15.1, 17.3, 19.66, 22.43, 176.0, 28.67, 31.92, 35.38, 38.73, 41.98,
253 | 44.92, 47.49, 49.58, 51.21, 52.36, 53.99, 53.78, 54.04, 53.88, 53.62, 53.25, 53.09, 52.88, 52.99, 53.15, 53.67,
254 | 167.93, 55.61, 56.82, 58.39, 60.22, 62.21, 81.45, 84.96, 68.71, 70.7, 73.01, 74.69, 76.26, 77.68, 78.67, 80.14,
255 | 81.71, 82.08, 79.98, 78.15, 76.52, 79.2, 79.51, 81.08, 70.76, 62.58, 56.87, 52.83, 49.11, 46.28, 42.24, 38.58,
256 | 35.59, 32.76, 29.61, 26.89, 24.53, 22.17, 20.02, 18.45, 16.09, 15.62, 13.1, 11.69, 12.42, 9.43, 8.96, 7.39,
257 | ]);
258 |
259 | /**
260 | * Fluorescent 3.6: Standard DeLuxe type lamp.
261 | */
262 | export const SPDofFL3_6 = Float64Array.from([
263 | 5.53, 6.63, 8.07, 9.45, 11.28, 61.47, 17.8, 17.47, 20.12, 23.05, 26.37, 186.01, 33.94, 37.98, 42.12, 46.38, 50.3,
264 | 53.95, 56.94, 59.48, 61.36, 62.68, 64.34, 63.9, 63.85, 63.24, 62.46, 61.41, 60.47, 59.48, 58.65, 57.93, 57.49,
265 | 175.17, 57.27, 57.49, 57.99, 58.76, 59.64, 78.77, 81.26, 63.18, 64.29, 65.78, 66.77, 67.77, 68.6, 69.1, 70.15,
266 | 71.69, 71.97, 69.81, 68.05, 66.66, 69.7, 70.37, 72.47, 62.3, 54.45, 49.2, 45.6, 42.4, 40.02, 36.48, 33.28, 30.84,
267 | 28.3, 25.65, 23.33, 21.23, 19.29, 17.41, 16.31, 14.21, 14.04, 11.55, 10.39, 11.28, 8.51, 8.24, 7.02,
268 | ]);
269 |
270 | /**
271 | * Fluorescent 3.7: 3 Band.
272 | */
273 | export const SPDofFL3_7 = Float64Array.from([
274 | 3.79, 2.56, 1.91, 1.42, 1.51, 73.64, 7.37, 4.69, 5.33, 6.75, 8.51, 181.81, 11.71, 11.96, 12.18, 11.9, 11.16, 11.22,
275 | 9.83, 8.94, 12.08, 52.56, 55.42, 31.69, 16.03, 6.72, 4.59, 3.67, 3.02, 3.21, 4.9, 19.05, 177.64, 347.34, 116.8,
276 | 31.87, 16.37, 14.92, 14.12, 29.5, 61.4, 85.05, 64.86, 65.01, 53.17, 34.22, 427.27, 201.1, 58.63, 72.01, 88.19,
277 | 20.07, 13.1, 12.92, 24.54, 15.94, 13.56, 13.38, 8.42, 6.57, 7.18, 9.9, 11.47, 8.88, 3.05, 22.04, 42.79, 14.4, 1.88,
278 | 1.6, 1.42, 1.05, 1.23, 1.76, 0.74, 0.52, 4.1, 0.46, 0.99, 0.43, 0.0,
279 | ]);
280 |
281 | /**
282 | * Fluorescent 3.8: 3 Band.
283 | */
284 | export const SPDofFL3_8 = Float64Array.from([
285 | 4.18, 2.93, 2.29, 1.98, 2.44, 70.7, 10.19, 9.79, 13.21, 17.79, 22.98, 191.43, 31.76, 33.35, 33.87, 32.89, 30.6,
286 | 28.28, 24.81, 21.6, 23.4, 68.99, 70.85, 42.29, 22.67, 11.08, 7.66, 6.07, 5.07, 4.88, 6.26, 20.29, 204.67, 390.25,
287 | 135.69, 34.57, 15.71, 12.6, 11.05, 25.05, 54.98, 82.84, 58.22, 53.06, 41.44, 25.26, 329.89, 161.29, 54.19, 66.3,
288 | 71.43, 15.74, 10.22, 10.68, 20.32, 14.13, 11.72, 11.75, 7.87, 6.38, 7.23, 8.94, 9.79, 7.26, 2.59, 17.03, 33.69,
289 | 12.02, 1.68, 1.5, 1.31, 1.01, 1.16, 1.59, 0.79, 0.67, 4.82, 0.61, 1.25, 0.79, 0.58,
290 | ]);
291 |
292 | /**
293 | * Fluorescent 3.9: 3 Band.
294 | */
295 | export const SPDofFL3_9 = Float64Array.from([
296 | 3.77, 2.64, 2.06, 1.87, 2.55, 71.68, 12.05, 13.57, 19.6, 27.33, 35.39, 211.82, 49.02, 51.83, 52.5, 50.73, 46.93,
297 | 42.42, 37.16, 31.84, 31.94, 77.74, 79.45, 47.93, 26.24, 13.15, 8.8, 6.7, 5.38, 4.93, 6.06, 19.76, 215.94, 412.13,
298 | 34.74, 14.76, 10.99, 9.25, 23.5, 53.05, 81.9, 54.92, 47.8, 36.65, 21.82, 285.69, 139.94, 53.37, 64.3, 64.04, 13.79,
299 | 9.06, 9.83, 18.6, 13.38, 10.99, 10.77, 7.57, 6.19, 7.09, 8.54, 8.77, 6.41, 2.26, 15.02, 29.39, 10.22, 1.42, 1.23,
300 | 1.1, 0.84, 0.97, 1.35, 0.65, 0.13, 4.22, 0.1, 0.68, 0.16, 0.0,
301 | ]);
302 |
303 | /**
304 | * Fluorescent 3.10: 3 Band.
305 | */
306 | export const SPDofFL3_10 = Float64Array.from([
307 | 0.25, 0.0, 0.0, 0.0, 0.69, 21.24, 2.18, 1.86, 3.1, 5.0, 7.03, 45.08, 16.78, 12.28, 13.31, 13.66, 13.69, 13.13,
308 | 12.28, 11.42, 11.66, 22.04, 26.17, 18.57, 11.36, 6.83, 5.58, 4.88, 4.31, 3.76, 3.61, 5.62, 38.59, 100, 36.54, 10.57,
309 | 2.98, 2.05, 1.84, 6.09, 17.27, 21.77, 18.72, 10.15, 7.26, 5.17, 56.66, 49.39, 18.57, 14.21, 14.01, 5.99, 2.68, 3.14,
310 | 6.25, 5.78, 6.75, 5.16, 3.03, 1.57, 1.72, 1.54, 1.71, 1.1, 0.28, 3.65, 7.54, 2.34, 0.05, 0.04, 0.04, 0.03, 0.03,
311 | 0.02, 0.02, 0.01, 0.01, 0.0, 0.0, 0.0, 0.0,
312 | ]);
313 |
314 | /**
315 | * Fluorescent 3.11: 3 Band.
316 | */
317 | export const SPDofFL3_11 = Float64Array.from([
318 | 3.85, 2.91, 2.56, 2.59, 3.63, 74.54, 14.69, 17.22, 24.99, 34.4, 44.57, 228.08, 61.53, 65.31, 66.35, 64.37, 59.81,
319 | 54.24, 47.42, 41.1, 40.04, 85.54, 86.55, 53.47, 30.91, 17.41, 12.56, 10.1, 8.48, 7.74, 8.58, 21.39, 220.12, 417.35,
320 | 146.13, 36.67, 16.51, 12.56, 10.81, 25.31, 53.31, 80.75, 53.56, 44.02, 33.05, 20.26, 233.61, 118.2, 51.66, 61.27,
321 | 55.15, 12.95, 8.93, 9.77, 17.12, 13.01, 10.45, 10.33, 7.7, 6.34, 7.35, 8.22, 7.93, 5.7, 2.23, 12.43, 24.24, 8.74,
322 | 1.39, 1.23, 1.1, 0.84, 0.94, 1.23, 0.68, 0.52, 4.6, 0.45, 1.04, 0.45, 0.0,
323 | ]);
324 |
325 | /**
326 | * Fluorescent 3.12: Multi Band.
327 | */
328 | export const SPDofFL3_12 = Float64Array.from([
329 | 1.62, 2.06, 2.71, 3.11, 3.67, 74.6, 8.88, 4.77, 4.72, 4.72, 4.94, 150.29, 6.08, 7.13, 9.1, 11.76, 14.96, 18.54,
330 | 22.48, 26.76, 31.66, 40.93, 45.83, 46.0, 45.26, 43.16, 41.63, 39.75, 37.83, 36.16, 35.25, 37.04, 59.86, 183.53,
331 | 59.03, 47.93, 48.67, 52.69, 57.24, 77.75, 87.81, 80.55, 84.83, 86.84, 91.44, 96.51, 105.25, 106.74, 108.53, 106.92,
332 | 101.54, 95.2, 89.34, 82.95, 75.78, 68.65, 61.7, 55.23, 48.58, 42.9, 37.74, 32.93, 29.65, 25.19, 21.69, 19.28, 17.36,
333 | 14.74, 12.86, 11.28, 9.97, 8.88, 7.78, 7.04, 6.3, 5.55, 10.15, 4.5, 4.81, 3.72, 3.28,
334 | ]);
335 |
336 | /**
337 | * Fluorescent 3.13: Multi Band.
338 | */
339 | export const SPDofFL3_13 = Float64Array.from([
340 | 2.23, 2.92, 3.91, 4.55, 5.46, 77.4, 11.25, 7.69, 8.29, 8.98, 10.01, 204.45, 3.75, 16.88, 21.73, 27.96, 34.92, 41.96,
341 | 48.62, 54.33, 59.49, 67.91, 70.01, 66.4, 62.07, 56.95, 52.7, 48.54, 44.8, 41.75, 39.77, 40.5, 59.27, 184.09, 59.06,
342 | 49.95, 50.9, 54.51, 58.33, 77.49, 85.78, 76.2, 78.73, 78.95, 81.48, 84.57, 87.75, 89.56, 91.36, 89.0, 83.67, 78.26,
343 | 73.19, 67.61, 61.42, 55.49, 49.78, 44.46, 39.13, 34.45, 30.28, 26.37, 23.88, 20.1, 17.4, 15.29, 13.62, 11.68, 10.31,
344 | 9.11, 8.03, 7.13, 6.31, 5.67, 5.11, 4.55, 9.06, 3.74, 4.04, 3.14, 2.75,
345 | ]);
346 |
347 | /**
348 | * Fluorescent 3.14: Multi Band.
349 | */
350 | export const SPDofFL3_14 = Float64Array.from([
351 | 2.87, 3.69, 4.87, 5.82, 7.17, 72.21, 13.69, 11.12, 12.43, 13.9, 15.82, 200.99, 21.72, 26.33, 32.85, 40.8, 49.23,
352 | 57.39, 65.26, 71.99, 78.25, 88.85, 91.67, 86.81, 80.42, 73.82, 69.12, 63.69, 58.44, 53.57, 49.66, 48.44, 72.56,
353 | 200.42, 65.0, 47.49, 44.14, 44.71, 46.01, 63.52, 71.73, 63.52, 64.13, 63.74, 66.82, 70.65, 79.29, 80.77, 83.59,
354 | 82.59, 77.6, 72.47, 68.34, 63.82, 58.57, 53.18, 47.97, 43.14, 38.19, 33.85, 29.94, 26.24, 23.9, 20.33, 17.42, 15.64,
355 | 14.34, 12.21, 10.65, 9.43, 8.34, 7.52, 6.73, 6.08, 5.52, 5.0, 9.47, 4.08, 4.43, 3.39, 3.17,
356 | ]);
357 |
358 | /**
359 | * Fluorescent 3.15: Multi Band.
360 | */
361 | export const SPDofFL3_15 = Float64Array.from([
362 | 300.0, 286.0, 268.0, 244.0, 304.0, 581.0, 225.0, 155.0, 152.0, 170.0, 295.0, 1417.0, 607.0, 343.0, 386.0, 430.0,
363 | 469.0, 502.0, 531.0, 552.0, 567.0, 572.0, 575.0, 561.0, 548.0, 527.0, 507.0, 482.0, 461.0, 438.0, 418.0, 404.0,
364 | 429.0, 1016.0, 581.0, 370.0, 368.0, 371.0, 377.0, 490.0, 525.0, 402.0, 404.0, 412.0, 418.0, 425.0, 428.0, 432.0,
365 | 433.0, 431.0, 427.0, 420.0, 410.0, 399.0, 385.0, 370.0, 352.0, 336.0, 317.0, 298.0, 277.0, 260.0, 242.0, 223.0,
366 | 202.0, 187.0, 167.0, 152.0, 136.0, 125.0, 113.0, 103.0, 93.0, 84.0, 75.0, 66.0, 58.0, 51.0, 46.0, 41.0, 37.0,
367 | ]);
368 |
369 | /**
370 | * Xenon.
371 | * 6044 K
372 | */
373 | export const SPDofXenon = Float64Array.from([
374 | 93.03, 94.59, 96.33, 100.56, 102.81, 100.1, 100.84, 101.45, 102.57, 101.94, 101.29, 101.54, 103.74, 103.67, 110.3,
375 | 112.78, 116.52, 129.56, 141.07, 126.45, 115.94, 118.42, 111.75, 113.67, 105.17, 103.88, 102.9, 102.71, 102.29,
376 | 101.9, 101.54, 101.47, 101.12, 101.43, 100.98, 100.75, 100.44, 100.28, 100.19, 100.0, 99.86, 100.47, 100.33, 99.04,
377 | 97.17, 96.65, 96.84, 98.5, 100.21, 99.91, 97.8, 96.84, 98.92, 99.18, 101.99, 98.78, 97.14, 98.43, 100.3, 101.29,
378 | 101.97, 109.55, 110.88, 102.08, 94.49, 93.05, 98.1, 106.2, 98.24, 95.69, 101.73, 108.39, 102.62, 100.28, 97.33,
379 | 97.45, 101.71, 137.21, 105.22, 84.41, 80.55,
380 | ]);
381 |
382 | /**
383 | * HMI 1.
384 | * 6002 K
385 | */
386 | export const SPDofHMI1 = Float64Array.from([
387 | 116.39, 114.92, 445.62, 107.07, 108.19, 123.74, 125.42, 123.88, 125.56, 116.39, 89.57, 122.69, 120.03, 71.29, 65.93,
388 | 73.25, 80.53, 72.48, 66.65, 67.82, 66.6, 59.4, 60.43, 62.92, 64.03, 68.89, 57.96, 57.28, 54.24, 50.74, 52.25, 53.26,
389 | 56.32, 114.85, 119.19, 59.24, 56.91, 62.96, 65.18, 100.0, 158.96, 81.58, 82.42, 72.41, 74.44, 62.19, 61.75, 61.0,
390 | 60.76, 62.91, 55.78, 50.61, 52.97, 49.83, 46.82, 47.96, 56.13, 53.38, 67.89, 73.25, 47.32, 50.08, 44.96, 56.34,
391 | 69.68, 36.16, 28.41, 27.15, 28.21, 34.95, 34.94, 27.68, 24.19, 22.61, 21.79, 30.47, 33.75, 36.16, 32.39, 26.55,
392 | 24.38,
393 | ]);
394 |
395 | /**
396 | * HMI 2.
397 | * 5630 K
398 | */
399 | export const SPDofHMI2 = Float64Array.from([
400 | 25.54, 37.72, 50.54, 70.58, 85.75, 127.05, 183.25, 290.31, 400.39, 462.06, 470.93, 678.6, 527.99, 424.0, 441.57,
401 | 462.96, 488.59, 452.99, 444.71, 439.61, 453.62, 416.76, 434.63, 399.82, 399.07, 425.56, 377.81, 370.65, 332.78,
402 | 326.62, 348.29, 315.82, 332.37, 903.57, 498.77, 346.72, 335.4, 412.55, 400.25, 723.26, 819.15, 406.53, 599.9,
403 | 439.39, 448.26, 356.58, 458.39, 377.63, 356.1, 380.23, 299.38, 302.49, 317.13, 314.58, 283.14, 290.45, 305.33,
404 | 326.97, 573.24, 353.87, 280.1, 352.53, 262.44, 277.25, 283.94, 191.55, 168.42, 157.05, 168.39, 181.89, 162.78,
405 | 148.76, 166.76, 138.57, 141.64, 220.97, 185.33, 346.98, 401.23, 184.98, 171.06,
406 | ]);
407 |
408 | /**
409 | * LED-B1.
410 | * Phosphor-type LEDs in different CCT categories.
411 | * 2733 K
412 | */
413 | export const SPDofLED_B1 = Float64Array.from([
414 | 0, 0.01, 0.01, 0.02, 0.04, 0.07, 0.15, 0.28, 0.53, 0.92, 1.54, 2.52, 4.16, 6.39, 7.89, 7.57, 6.25, 5.16, 4.37, 3.84,
415 | 3.7, 3.88, 4.26, 4.79, 5.41, 6.05, 6.7, 7.34, 7.98, 8.64, 9.31, 10.02, 10.76, 11.53, 12.38, 13.28, 14.23, 15.2,
416 | 16.2, 17.19, 18.14, 19.02, 19.78, 20.38, 20.8, 21, 20.97, 20.71, 20.23, 19.56, 18.76, 17.82, 16.78, 15.66, 14.49,
417 | 13.31, 12.15, 11.02, 9.95, 8.93, 7.98, 7.1, 6.3, 5.57, 4.89, 4.26, 3.72, 3.25, 2.83, 2.47, 2.15, 1.87, 1.63, 1.42,
418 | 1.25, 1.1, 0.97, 0.86, 0.76, 0.68, 0.61,
419 | ]);
420 |
421 | /**
422 | * LED-B2.
423 | * Phosphor-type LEDs in different CCT categories.
424 | * 2998 K
425 | */
426 | export const SPDofLED_B2 = Float64Array.from([
427 | 0, 0, 0, 0.01, 0.02, 0.05, 0.11, 0.24, 0.5, 0.96, 1.71, 2.82, 4.71, 7.87, 10.56, 10.24, 7.94, 6.25, 5.23, 4.36, 4,
428 | 4.17, 4.59, 5.21, 5.96, 6.72, 7.44, 8.11, 8.74, 9.39, 10.06, 10.75, 11.49, 12.25, 13.05, 13.86, 14.7, 15.5, 16.28,
429 | 17, 17.69, 18.31, 18.83, 19.24, 19.51, 19.59, 19.48, 19.19, 18.72, 18.1, 17.34, 16.47, 15.51, 14.49, 13.42, 12.33,
430 | 11.26, 10.24, 9.25, 8.3, 7.42, 6.6, 5.85, 5.17, 4.55, 3.99, 3.49, 3.04, 2.65, 2.31, 2.01, 1.75, 1.52, 1.32, 1.15, 1,
431 | 0.87, 0.76, 0.67, 0.59, 0.52,
432 | ]);
433 |
434 | /**
435 | * LED-B3.
436 | * Phosphor-type LEDs in different CCT categories.
437 | * 4103 K
438 | */
439 | export const SPDofLED_B3 = Float64Array.from([
440 | 0, 0, 0.01, 0.02, 0.05, 0.11, 0.25, 0.56, 1.2, 2.37, 4.24, 7.17, 12.11, 17.7, 18.87, 14.58, 9.9, 7.53, 6.02, 5.11,
441 | 5.17, 5.82, 6.77, 7.88, 8.94, 9.82, 10.52, 11.06, 11.51, 11.93, 12.37, 12.83, 13.31, 13.79, 14.26, 14.68, 15.08,
442 | 15.42, 15.72, 15.97, 16.18, 16.37, 16.5, 16.55, 16.5, 16.32, 16, 15.54, 14.96, 14.27, 13.48, 12.62, 11.71, 10.78,
443 | 9.85, 8.93, 8.04, 7.2, 6.42, 5.69, 5.02, 4.42, 3.88, 3.39, 2.95, 2.56, 2.22, 1.92, 1.65, 1.43, 1.23, 1.06, 0.91,
444 | 0.78, 0.68, 0.58, 0.5, 0.43, 0.37, 0.32, 0.28,
445 | ]);
446 |
447 | /**
448 | * LED-B4.
449 | * Phosphor-type LEDs in different CCT categories.
450 | * 5109 K
451 | */
452 | export const SPDofLED_B4 = Float64Array.from([
453 | 0, 0, 0, 0.01, 0.03, 0.08, 0.21, 0.54, 1.31, 2.75, 5.04, 8.31, 13.37, 20.61, 25.91, 24.12, 17.47, 11.9, 8.62, 6.34,
454 | 4.89, 4.25, 4.19, 4.7, 5.77, 7.27, 8.94, 10.6, 12.11, 13.36, 14.3, 15.02, 15.54, 15.9, 16.15, 16.33, 16.42, 16.44,
455 | 16.36, 16.2, 15.98, 15.7, 15.36, 14.93, 14.44, 13.86, 13.24, 12.57, 11.84, 11.12, 10.38, 9.65, 8.91, 8.19, 7.51,
456 | 6.86, 6.21, 5.6, 5.05, 4.54, 4.05, 3.62, 3.23, 2.87, 2.53, 2.24, 1.99, 1.76, 1.55, 1.37, 1.21, 1.07, 0.95, 0.85,
457 | 0.75, 0.67, 0.61, 0.55, 0.5, 0.45, 0.42,
458 | ]);
459 |
460 | /**
461 | * LED-B5
462 | * Phosphor-type LEDs in different CCT categories.
463 | * 6598 K
464 | */
465 | export const SPDofLED_B5 = Float64Array.from([
466 | 0, 0.01, 0.01, 0.03, 0.07, 0.16, 0.35, 0.78, 1.67, 3.3, 5.94, 9.92, 16.15, 25.15, 32.34, 31.18, 23.46, 16.39, 12.08,
467 | 9.08, 7.21, 6.48, 6.49, 7.05, 8.1, 9.45, 10.88, 12.22, 13.38, 14.29, 14.98, 15.48, 15.81, 16.01, 16.1, 16.13, 16.1,
468 | 15.98, 15.77, 15.5, 15.15, 14.76, 14.31, 13.79, 13.22, 12.59, 11.91, 11.2, 10.47, 9.73, 9, 8.27, 7.57, 6.9, 6.25,
469 | 5.65, 5.08, 4.55, 4.06, 3.61, 3.22, 2.86, 2.54, 2.26, 2, 1.75, 1.54, 1.36, 1.2, 1.05, 0.93, 0.82, 0.72, 0.64, 0.56,
470 | 0.49, 0.44, 0.39, 0.35, 0.31, 0.27,
471 | ]);
472 |
473 | /**
474 | * LED-BH1
475 | * Hybrid-type.
476 | * 2851 K
477 | */
478 | export const SPDofLED_BH1 = Float64Array.from([
479 | 0, 0.01, 0.02, 0.04, 0.08, 0.17, 0.41, 0.93, 1.84, 3.05, 4.29, 5.69, 7.06, 7.91, 7.74, 6.62, 5.13, 3.85, 2.94, 2.34,
480 | 1.99, 1.92, 2.19, 2.87, 4.03, 5.64, 7.49, 9.25, 10.82, 12.11, 13.08, 13.66, 14.01, 14.19, 14.28, 14.27, 14.19,
481 | 14.03, 13.82, 13.59, 13.35, 13.2, 13.19, 13.46, 14.31, 16.15, 19.49, 25.07, 33.87, 40.75, 35.59, 21.59, 11.11, 7.27,
482 | 5.58, 4.64, 3.98, 3.48, 3.07, 2.73, 2.42, 2.15, 1.91, 1.7, 1.5, 1.33, 1.18, 1.05, 0.93, 0.83, 0.74, 0.65, 0.57,
483 | 0.51, 0.46, 0.41, 0.37, 0.34, 0.3, 0.28, 0.25,
484 | ]);
485 |
486 | /**
487 | * LED-RGB1.
488 | * spectral distribution.
489 | * 2840 K
490 | */
491 | export const SPDofLED_RGB1 = Float64Array.from([
492 | 0, 0, 0, 0.01, 0.02, 0.05, 0.11, 0.24, 0.5, 0.94, 1.6, 2.43, 3.38, 4.59, 5.84, 5.56, 4, 3.14, 2.8, 2.68, 3.03, 3.98,
493 | 5.53, 7.84, 10.96, 14.61, 18.01, 20.24, 20.57, 19.22, 16.93, 14.58, 12.61, 11.09, 9.93, 9.11, 8.6, 8.31, 8.21, 8.24,
494 | 8.42, 8.74, 9.25, 10, 11.1, 12.74, 15.35, 19.43, 26.01, 36.47, 51.1, 62.69, 52.07, 28.62, 15.5, 9.58, 6.54, 4.95,
495 | 4.01, 3.37, 2.89, 2.51, 2.18, 1.92, 1.67, 1.44, 1.25, 1.09, 0.93, 0.82, 0.71, 0.61, 0.53, 0.45, 0.4, 0.35, 0.3,
496 | 0.27, 0.24, 0.21, 0.17,
497 | ]);
498 |
499 | /**
500 | * LED-V1.
501 | * violet-pumped phosphor-type LEDs.
502 | * 2724 K
503 | */
504 | export const SPDofLED_V1 = Float64Array.from([
505 | 0.01, 0.03, 0.14, 0.61, 2.62, 8.43, 16.51, 17.9, 11.26, 5.94, 3.31, 2.25, 2.07, 2.29, 2.68, 3.15, 3.64, 4.18, 4.83,
506 | 5.57, 6.4, 7.27, 8.08, 8.73, 9.21, 9.54, 9.75, 9.91, 10.06, 10.26, 10.51, 10.82, 11.17, 11.55, 11.92, 12.3, 12.68,
507 | 13.07, 13.52, 14.06, 14.7, 15.46, 16.38, 17.39, 18.5, 19.51, 20.52, 21.52, 22.38, 23.07, 23.51, 23.71, 23.67, 23.35,
508 | 22.81, 22.01, 21.03, 19.91, 18.69, 17.39, 16.05, 14.69, 13.36, 12.04, 10.8, 9.62, 8.54, 7.57, 6.68, 5.86, 5.12,
509 | 4.46, 3.88, 3.37, 2.94, 2.54, 2.21, 1.9, 1.65, 1.44, 1.22,
510 | ]);
511 |
512 | /**
513 | * LED-V2.
514 | * violet-pumped phosphor-type LEDs.
515 | * 4070 K
516 | */
517 | export const SPDofLED_V2 = Float64Array.from([
518 | 0.01, 0.06, 0.26, 1.15, 4.47, 11.98, 19.89, 20.24, 14.39, 8.95, 5.78, 4.65, 4.86, 5.78, 6.97, 8.1, 9.05, 9.82,
519 | 10.52, 11.21, 11.87, 12.5, 12.98, 13.23, 13.3, 13.21, 13.05, 12.88, 12.74, 12.68, 12.7, 12.77, 12.91, 13.04, 13.16,
520 | 13.26, 13.35, 13.42, 13.52, 13.64, 13.83, 14.07, 14.42, 14.82, 15.26, 15.64, 16.03, 16.4, 16.67, 16.88, 16.93,
521 | 16.85, 16.62, 16.23, 15.72, 15.07, 14.31, 13.45, 12.57, 11.64, 10.69, 9.78, 8.87, 8, 7.17, 6.37, 5.65, 5.01, 4.42,
522 | 3.88, 3.39, 2.95, 2.58, 2.24, 1.95, 1.7, 1.47, 1.3, 1.1, 0.96, 0.84,
523 | ]);
524 |
525 | /**
526 | * High Pressure discharge lamp: Standard high pressure sodium lamp.
527 | */
528 | export const SPDofHP1 = Float64Array.from([
529 | 1.9, 2.2, 2.5, 2.7, 3.1, 4.3, 3.8, 4.2, 4.8, 5.19, 5.89, 7.39, 7.89, 5.69, 12.89, 6.69, 4.3, 20.78, 12.99, 6.69,
530 | 1.4, 1.5, 3.2, 18.18, 56.24, 2.9, 2.1, 13.39, 2.1, 2.0, 2.2, 2.3, 2.6, 5.1, 11.39, 15.48, 20.78, 55.64, 254.03,
531 | 56.14, 111.78, 297.98, 142.55, 334.84, 189.4, 117.78, 79.92, 108.09, 46.85, 38.16, 32.47, 28.37, 25.37, 22.98,
532 | 20.38, 19.78, 17.78, 16.78, 19.18, 17.98, 13.69, 9.99, 8.19, 7.59, 6.99, 6.79, 6.49, 6.39, 6.09, 5.99, 5.79, 5.79,
533 | 5.79, 5.79, 6.39, 5.99, 5.59, 31.97, 27.87, 5.89, 6.69,
534 | ]);
535 |
536 | /**
537 | * High Pressure discharge lamp: Colour enhanced high pressure sodium lamp.
538 | */
539 | export const SPDofHP2 = Float64Array.from([
540 | 2.64, 2.77, 3.42, 3.68, 4.33, 5.5, 5.94, 7.2, 9.02, 10.27, 12.48, 16.82, 16.04, 15.26, 22.58, 20.07, 15.13, 25.27,
541 | 28.04, 15.99, 10.4, 11.1, 13.44, 22.62, 49.71, 17.21, 17.12, 27.26, 20.02, 21.54, 23.36, 25.66, 29.69, 43.12, 98.3,
542 | 125.6, 134.57, 149.7, 166.12, 98.77, 30.47, 1.17, 0.39, 1.65, 21.41, 76.11, 126.16, 161.96, 160.06, 158.19, 153.69,
543 | 147.4, 140.6, 134.92, 127.59, 124.65, 118.02, 113.94, 118.1, 115.16, 102.85, 90.54, 83.34, 79.44, 76.97, 74.85,
544 | 73.12, 71.51, 70.13, 69.04, 67.48, 66.7, 66.31, 65.14, 65.7, 64.79, 64.1, 83.04, 86.25, 63.93, 64.92,
545 | ]);
546 |
547 | /**
548 | * High Pressure discharge lamp: High pressure metal halide lamp.
549 | */
550 | export const SPDofHP3 = Float64Array.from([
551 | 3.15, 7.49, 10.87, 12.57, 12.97, 21.29, 26.29, 30.18, 43.06, 29.58, 23.18, 35.28, 26.29, 24.29, 22.91, 26.2, 29.31,
552 | 25.3, 28.14, 24.05, 21.82, 20.51, 23.05, 26.98, 30.96, 30.72, 27.13, 29.55, 34.22, 29.98, 41.21, 173.14, 141.37,
553 | 64.98, 33.83, 34.26, 33.32, 52.8, 74.29, 47.97, 49.2, 96.07, 85.41, 175.18, 153.73, 120.22, 98.9, 90.22, 70.07,
554 | 66.84, 57.61, 53.03, 49.85, 48.16, 42.76, 50.64, 48.42, 41.27, 43.44, 40.48, 35.16, 34.94, 24.68, 24.7, 21.49,
555 | 19.49, 18.48, 17.55, 17.36, 17.09, 16.32, 16.07, 16.58, 15.78, 17.66, 20.46, 16.59, 17.81, 16.07, 14.83, 14.61,
556 | ]);
557 |
558 | /**
559 | * High Pressure discharge lamp: High pressure metal halide lamp.
560 | */
561 | export const SPDofHP4 = Float64Array.from([
562 | 9.8, 13.3, 19.97, 25.81, 24.69, 47.66, 54.44, 63.82, 85.52, 60.54, 38.37, 88.2, 44.94, 35.64, 30.75, 33.77, 40.81,
563 | 33.77, 35.28, 32.55, 29.44, 26.16, 29.96, 32.83, 33.58, 41.16, 32.93, 32.13, 34.45, 30.12, 41.13, 187.1, 101.37,
564 | 123.96, 42.47, 34.73, 31.82, 54.67, 57.45, 70.43, 69.5, 49.37, 183.35, 162.15, 109.35, 72.38, 70.6, 58.08, 44.13,
565 | 50.2, 40.8, 37.91, 36.71, 38.3, 31.24, 35.31, 45.62, 35.82, 89.91, 36.01, 32.57, 39.26, 23.27, 25.3, 20.02, 17.54,
566 | 16.25, 15.2, 15.15, 15.22, 14.26, 12.63, 14.75, 13.19, 17.63, 23.38, 16.02, 24.46, 22.05, 16.11, 12.91,
567 | ]);
568 |
569 | /**
570 | * High Pressure discharge lamp: High pressure metal halide lamp.
571 | */
572 | export const SPDofHP5 = Float64Array.from([
573 | 0.34, 7.11, 11.49, 14.97, 14.95, 29.14, 38.08, 51.56, 62.56, 55.61, 41.98, 50.02, 42.14, 39.04, 40.52, 45.29, 51.01,
574 | 49.18, 49.05, 46.12, 45.73, 39.46, 44.39, 46.14, 49.54, 59.76, 48.47, 48.38, 48.7, 44.25, 54.42, 128.93, 81.26,
575 | 67.36, 48.48, 51.41, 48.88, 68.52, 80.85, 65.96, 59.43, 67.57, 128.34, 131.85, 101.7, 77.05, 66.27, 77.09, 60.51,
576 | 65.23, 57.86, 56.2, 54.32, 56.34, 45.74, 50.79, 56.66, 51.99, 84.31, 47.48, 47.46, 61.78, 34.51, 38.74, 30.98,
577 | 25.45, 22.88, 20.82, 21.05, 20.81, 18.69, 17.54, 19.58, 16.42, 23.77, 35.39, 21.37, 34.58, 30.21, 19.71, 15.61,
578 | ]);
579 |
--------------------------------------------------------------------------------