Initial Commit
This commit is contained in:
32
packages/media/image/colour-convert-cieluv/README.md
Normal file
32
packages/media/image/colour-convert-cieluv/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# @suaveplan/colour-convert-cieluv - CIELUV, xyY, LMS, and Bradford chromatic adaptation for professional colour workflows
|
||||
|
||||
CIELUV, xyY, LMS, and Bradford chromatic adaptation conversions. This plugin provides colour-space transforms between RGB, CIELUV, xyY, and LMS cone-response space, including Bradford adaptation across 11 standard illuminants (A, B, C, D50/55/65/75, E, F2/F7/F11).
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [colour-convert-cieluv](./src/colour-convert-cieluv/colour-convert-cieluv.md) — `colour-convert-cieluv.ts` extends the CIE colour family with CIELUV, xyY chromaticity, LMS cone response, and Bradford…
|
||||
- [colour-convert-cieluv](./src/colour-convert-cieluv/colour-convert-cieluv.md) — `colour-convert-cieluv.ts` extends the CIE colour family with CIELUV, xyY chromaticity, LMS cone response, and Bradford…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/colour-convert-cieluv
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { rgbToLuv, bradfordAdapt } from "@suaveplan/colour-convert-cieluv";
|
||||
|
||||
const luv = rgbToLuv({ r: 128, g: 64, b: 32 });
|
||||
console.log(luv); // { l: ~32.9, u: ~40.6, v: ~37.8 }
|
||||
|
||||
const d50 = bradfordAdapt({ x: 50, y: 50, z: 50 }, "D65", "D50");
|
||||
console.log(d50); // { x: ~52.9, y: ~50, z: ~37.7 }
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/colour-convert-cieluv/colour-convert-cieluv.md`](./src/colour-convert-cieluv/colour-convert-cieluv.md) — CIELUV, xyY, LMS, and Bradford adaptation implementation
|
||||
61
packages/media/image/colour-convert-cieluv/package.json
Normal file
61
packages/media/image/colour-convert-cieluv/package.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "@suaveplan/colour-convert-cieluv",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"description": "CIELUV, xyY, LMS, and Bradford chromatic adaptation for professional colour workflows",
|
||||
"license": "MIT",
|
||||
"author": "Suaveplan Team",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./colour-convert-cieluv/colour-convert-cieluv": {
|
||||
"import": {
|
||||
"types": "./dist/colour-convert-cieluv/colour-convert-cieluv.d.ts",
|
||||
"default": "./dist/colour-convert-cieluv/colour-convert-cieluv.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage --passWithNoTests",
|
||||
"lint": "bunx biome check src/",
|
||||
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
|
||||
"format": "bunx biome format --write --max-diagnostics 500 src/",
|
||||
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
|
||||
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
|
||||
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@suaveplan/colours": "workspace:^",
|
||||
"tslib": "catalog:build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@suaveplan/testing": "workspace:^",
|
||||
"@types/node": "catalog:",
|
||||
"@vitest/coverage-istanbul": "catalog:testing",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:build",
|
||||
"vite-plugin-dts": "catalog:build",
|
||||
"vitest": "catalog:testing"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npmjs.fq.io/",
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# colour-convert-cieluv
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`colour-convert-cieluv.ts` extends the CIE colour family with CIELUV, xyY chromaticity, LMS cone response, and Bradford chromatic adaptation. These spaces are essential for professional colour workflows including display calibration, print proofing, and cross-media colour reproduction.
|
||||
|
||||
## Features
|
||||
|
||||
- RGB ↔ CIELUV conversions
|
||||
- XYZ ↔ xyY conversions
|
||||
- XYZ ↔ LMS cone response conversions
|
||||
- Bradford chromatic adaptation with CIE standard illuminants
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { rgbToLuv, luvToRgb } from "@suaveplan/colour-convert-cieluv";
|
||||
|
||||
const luv = rgbToLuv({ r: 128, g: 64, b: 32 });
|
||||
const back = luvToRgb(luv);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { bradfordAdapt, ILLUMINANTS } from "@suaveplan/colour-convert-cieluv";
|
||||
|
||||
const d50 = bradfordAdapt(xyz, "D65", "D50");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### CIELUV
|
||||
|
||||
- `xyzToLuv(xyz: XYZ): LUV`
|
||||
- `luvToXyz(luv: LUV): XYZ`
|
||||
- `rgbToLuv(rgb: RGB): LUV`
|
||||
- `luvToRgb(luv: LUV): RGBA`
|
||||
|
||||
### xyY
|
||||
|
||||
- `xyzToxyY(xyz: XYZ): xyY`
|
||||
- `xyYToXyz(xy: xyY): XYZ`
|
||||
- `rgbToxyY(rgb: RGB): xyY`
|
||||
- `xyYToRgb(xy: xyY): RGBA`
|
||||
|
||||
### LMS
|
||||
|
||||
- `xyzToLms(xyz: XYZ): LMS`
|
||||
- `lmsToXyz(lms: LMS): XYZ`
|
||||
- `rgbToLms(rgb: RGB): LMS`
|
||||
- `lmsToRgb(lms: LMS): RGBA`
|
||||
|
||||
### Bradford Adaptation
|
||||
|
||||
- `bradfordAdapt(xyz: XYZ, source: string | XYZ, dest: string | XYZ): XYZ`
|
||||
- `ILLUMINANTS` — map of CIE standard illuminants
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Bradford adaptation uses the standard CIECAM02 matrix and its inverse. The implementation accepts either named illuminant strings (A, B, C, D50, D55, D65, D75, E, F2, F7, F11) or raw XYZ values for custom white points. All conversion pipelines are deterministic and side-effect-free.
|
||||
|
||||
The CIELUV implementation follows the standard CIE 1976 formula with D65 as the default reference white. Internal helper functions handle the low-luminance linear branch automatically, ensuring accurate results even for very dark colours. The module has no runtime dependencies beyond the base `@suaveplan/colours` types.
|
||||
@@ -0,0 +1,155 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
bradfordAdapt,
|
||||
ILLUMINANTS,
|
||||
lmsToRgb,
|
||||
lmsToXyz,
|
||||
luvToRgb,
|
||||
luvToXyz,
|
||||
rgbToLms,
|
||||
rgbToLuv,
|
||||
rgbToxyY,
|
||||
xyYToRgb,
|
||||
xyYToXyz,
|
||||
xyzToLms,
|
||||
xyzToLuv,
|
||||
xyzToxyY,
|
||||
} from "./colour-convert-cieluv.js";
|
||||
|
||||
describe("colour-convert-cieluv", () => {
|
||||
it("rgb to luv round-trip", { timeout: 5000 }, () => {
|
||||
const rgb = { r: 128, g: 64, b: 32 };
|
||||
const luv = rgbToLuv(rgb);
|
||||
const back = luvToRgb(luv);
|
||||
expect(back.r).toBeCloseTo(rgb.r, -1);
|
||||
expect(back.g).toBeCloseTo(rgb.g, -1);
|
||||
expect(back.b).toBeCloseTo(rgb.b, -1);
|
||||
});
|
||||
|
||||
it("xyz to luv round-trip", { timeout: 5000 }, () => {
|
||||
const xyz = { x: 50, y: 30, z: 20 };
|
||||
const luv = xyzToLuv(xyz);
|
||||
const back = luvToXyz(luv);
|
||||
expect(back.x).toBeCloseTo(xyz.x, 0);
|
||||
expect(back.y).toBeCloseTo(xyz.y, 0);
|
||||
expect(back.z).toBeCloseTo(xyz.z, 0);
|
||||
});
|
||||
|
||||
it("xyz to xyY round-trip", { timeout: 5000 }, () => {
|
||||
const xyz = { x: 50, y: 30, z: 20 };
|
||||
const xyy = xyzToxyY(xyz);
|
||||
const back = xyYToXyz(xyy);
|
||||
expect(back.x).toBeCloseTo(xyz.x, 0);
|
||||
expect(back.y).toBeCloseTo(xyz.y, 0);
|
||||
expect(back.z).toBeCloseTo(xyz.z, 0);
|
||||
});
|
||||
|
||||
it("rgb to xyY round-trip", { timeout: 5000 }, () => {
|
||||
const rgb = { r: 128, g: 64, b: 32 };
|
||||
const xyy = rgbToxyY(rgb);
|
||||
const back = xyYToRgb(xyy);
|
||||
expect(back.r).toBeCloseTo(rgb.r, -1);
|
||||
expect(back.g).toBeCloseTo(rgb.g, -1);
|
||||
expect(back.b).toBeCloseTo(rgb.b, -1);
|
||||
});
|
||||
|
||||
it("xyz to lms round-trip", { timeout: 5000 }, () => {
|
||||
const xyz = { x: 50, y: 30, z: 20 };
|
||||
const lms = xyzToLms(xyz);
|
||||
const back = lmsToXyz(lms);
|
||||
expect(back.x).toBeCloseTo(xyz.x, 0);
|
||||
expect(back.y).toBeCloseTo(xyz.y, 0);
|
||||
expect(back.z).toBeCloseTo(xyz.z, 0);
|
||||
});
|
||||
|
||||
it("rgb to lms round-trip", { timeout: 5000 }, () => {
|
||||
const rgb = { r: 128, g: 64, b: 32 };
|
||||
const lms = rgbToLms(rgb);
|
||||
const back = lmsToRgb(lms);
|
||||
expect(back.r).toBeCloseTo(rgb.r, -1);
|
||||
expect(back.g).toBeCloseTo(rgb.g, -1);
|
||||
expect(back.b).toBeCloseTo(rgb.b, -1);
|
||||
});
|
||||
|
||||
it("bradford adapts D65 to D50", { timeout: 5000 }, () => {
|
||||
const adapted = bradfordAdapt(
|
||||
{ x: 95.047, y: 100, z: 108.883 },
|
||||
"D65",
|
||||
"D50"
|
||||
);
|
||||
expect(adapted.x).toBeCloseTo(96.421, 0);
|
||||
expect(adapted.y).toBeCloseTo(100, 0);
|
||||
expect(adapted.z).toBeCloseTo(82.519, 0);
|
||||
});
|
||||
|
||||
it("bradford accepts custom XYZ illuminants", { timeout: 5000 }, () => {
|
||||
const adapted = bradfordAdapt(
|
||||
{ x: 95.047, y: 100, z: 108.883 },
|
||||
{ x: 95.047, y: 100, z: 108.883 },
|
||||
{ x: 96.421, y: 100, z: 82.519 }
|
||||
);
|
||||
expect(adapted.x).toBeCloseTo(96.421, 0);
|
||||
});
|
||||
|
||||
it("ILLUMINANTS contains D65", { timeout: 5000 }, () => {
|
||||
expect(ILLUMINANTS.D65).toBeDefined();
|
||||
});
|
||||
|
||||
it("handles black xyz to xyY", { timeout: 5000 }, () => {
|
||||
const xyy = xyzToxyY({ x: 0, y: 0, z: 0 });
|
||||
expect(xyy.x).toBe(0);
|
||||
expect(xyy.y).toBe(0);
|
||||
});
|
||||
|
||||
it("handles bright luv round-trip", { timeout: 5000 }, () => {
|
||||
const rgb = { r: 255, g: 255, b: 255 };
|
||||
const luv = rgbToLuv(rgb);
|
||||
const back = luvToRgb(luv);
|
||||
expect(back.r).toBeCloseTo(rgb.r, -1);
|
||||
});
|
||||
|
||||
it("handles black luv to xyz", { timeout: 5000 }, () => {
|
||||
const xyz = luvToXyz({ l: 0, u: 0, v: 0 });
|
||||
expect(xyz.x).toBe(0);
|
||||
expect(xyz.y).toBe(0);
|
||||
expect(xyz.z).toBe(0);
|
||||
});
|
||||
|
||||
it("handles black xyz to luv", { timeout: 5000 }, () => {
|
||||
const luv = xyzToLuv({ x: 0, y: 0, z: 0 });
|
||||
expect(luv.l).toBe(0);
|
||||
});
|
||||
|
||||
it("handles xyY with y=0", { timeout: 5000 }, () => {
|
||||
const xyz = xyYToXyz({ x: 0.3, y: 0, Y: 0 });
|
||||
expect(xyz.x).toBe(0);
|
||||
expect(xyz.y).toBe(0);
|
||||
expect(xyz.z).toBe(0);
|
||||
});
|
||||
|
||||
it("throws on invalid illuminant", { timeout: 5000 }, () => {
|
||||
expect(() =>
|
||||
bradfordAdapt({ x: 50, y: 50, z: 50 }, "INVALID", "D65")
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("handles dark rgb to luv", { timeout: 5000 }, () => {
|
||||
const luv = rgbToLuv({ r: 5, g: 5, b: 5 });
|
||||
expect(luv.l).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("handles dark rgb to xyY", { timeout: 5000 }, () => {
|
||||
const xyy = rgbToxyY({ r: 5, g: 5, b: 5 });
|
||||
expect(xyy.Y).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("handles dark luv to rgb", { timeout: 5000 }, () => {
|
||||
const rgb = luvToRgb({ l: 0.01, u: 0, v: 0 });
|
||||
expect(rgb.r).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("bradford uses default dest illuminant", { timeout: 5000 }, () => {
|
||||
const adapted = bradfordAdapt({ x: 95.047, y: 100, z: 108.883 }, "D65");
|
||||
expect(adapted.x).toBeCloseTo(95.047, 0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
import type { RGB, RGBA } from "@suaveplan/colours";
|
||||
|
||||
export interface XYZ {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly z: number;
|
||||
}
|
||||
|
||||
export interface LUV {
|
||||
readonly l: number;
|
||||
readonly u: number;
|
||||
readonly v: number;
|
||||
}
|
||||
|
||||
export interface LUVA extends LUV {
|
||||
readonly a: number;
|
||||
}
|
||||
|
||||
export interface xyY {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly Y: number;
|
||||
}
|
||||
|
||||
export interface LMS {
|
||||
readonly l: number;
|
||||
readonly m: number;
|
||||
readonly s: number;
|
||||
}
|
||||
|
||||
const D65: XYZ = { x: 95.047, y: 100.0, z: 108.883 };
|
||||
|
||||
function xyzToLuvInternal(xyz: XYZ, ref: XYZ): LUV {
|
||||
const yr = xyz.y / ref.y;
|
||||
const l = yr > 0.008856 ? 116 * Math.cbrt(yr) - 16 : 903.3 * yr;
|
||||
const up =
|
||||
xyz.x + 15 * xyz.y + 3 * xyz.z === 0
|
||||
? 0
|
||||
: (4 * xyz.x) / (xyz.x + 15 * xyz.y + 3 * xyz.z);
|
||||
const vp =
|
||||
xyz.x + 15 * xyz.y + 3 * xyz.z === 0
|
||||
? 0
|
||||
: (9 * xyz.y) / (xyz.x + 15 * xyz.y + 3 * xyz.z);
|
||||
const ur = (4 * ref.x) / (ref.x + 15 * ref.y + 3 * ref.z);
|
||||
const vr = (9 * ref.y) / (ref.x + 15 * ref.y + 3 * ref.z);
|
||||
return {
|
||||
l,
|
||||
u: 13 * l * (up - ur),
|
||||
v: 13 * l * (vp - vr),
|
||||
};
|
||||
}
|
||||
|
||||
function luvToXyzInternal(luv: LUV, ref: XYZ): XYZ {
|
||||
if (luv.l === 0) return { x: 0, y: 0, z: 0 };
|
||||
const ur = (4 * ref.x) / (ref.x + 15 * ref.y + 3 * ref.z);
|
||||
const vr = (9 * ref.y) / (ref.x + 15 * ref.y + 3 * ref.z);
|
||||
const up = luv.u / (13 * luv.l) + ur;
|
||||
const vp = luv.v / (13 * luv.l) + vr;
|
||||
const y =
|
||||
luv.l > 8 ? ref.y * ((luv.l + 16) / 116) ** 3 : (ref.y * luv.l) / 903.3;
|
||||
const x = y * ((9 * up) / (4 * vp));
|
||||
const z = y * ((12 - 3 * up - 20 * vp) / (4 * vp));
|
||||
return { x, y, z };
|
||||
}
|
||||
|
||||
export function xyzToLuv(xyz: XYZ): LUV {
|
||||
return xyzToLuvInternal(xyz, D65);
|
||||
}
|
||||
|
||||
export function luvToXyz(luv: LUV): XYZ {
|
||||
return luvToXyzInternal(luv, D65);
|
||||
}
|
||||
|
||||
export function rgbToLuv(rgb: RGB): LUV {
|
||||
const xyz = rgbToXyz(rgb);
|
||||
return xyzToLuv(xyz);
|
||||
}
|
||||
|
||||
export function luvToRgb(luv: LUV): RGBA {
|
||||
const xyz = luvToXyz(luv);
|
||||
return { ...xyzToRgb(xyz), a: 1 };
|
||||
}
|
||||
|
||||
export function xyzToxyY(xyz: XYZ): xyY {
|
||||
const sum = xyz.x + xyz.y + xyz.z;
|
||||
if (sum === 0) return { x: 0, y: 0, Y: 0 };
|
||||
return { x: xyz.x / sum, y: xyz.y / sum, Y: xyz.y };
|
||||
}
|
||||
|
||||
export function xyYToXyz(xy: xyY): XYZ {
|
||||
if (xy.y === 0) return { x: 0, y: 0, z: 0 };
|
||||
const x = (xy.x * xy.Y) / xy.y;
|
||||
const y = xy.Y;
|
||||
const z = ((1 - xy.x - xy.y) * xy.Y) / xy.y;
|
||||
return { x, y, z };
|
||||
}
|
||||
|
||||
export function rgbToxyY(rgb: RGB): xyY {
|
||||
return xyzToxyY(rgbToXyz(rgb));
|
||||
}
|
||||
|
||||
export function xyYToRgb(xy: xyY): RGBA {
|
||||
return { ...xyzToRgb(xyYToXyz(xy)), a: 1 };
|
||||
}
|
||||
|
||||
export function xyzToLms(xyz: XYZ): LMS {
|
||||
return {
|
||||
l: xyz.x * 0.7328 + xyz.y * 0.4296 + xyz.z * -0.1624,
|
||||
m: xyz.x * -0.7036 + xyz.y * 1.6975 + xyz.z * 0.0061,
|
||||
s: xyz.x * 0.003 + xyz.y * 0.0136 + xyz.z * 0.9834,
|
||||
};
|
||||
}
|
||||
|
||||
export function lmsToXyz(lms: LMS): XYZ {
|
||||
return {
|
||||
x: lms.l * 1.0961 + lms.m * -0.2789 + lms.s * 0.1827,
|
||||
y: lms.l * 0.4544 + lms.m * 0.4735 + lms.s * 0.0721,
|
||||
z: lms.l * -0.0096 + lms.m * -0.0057 + lms.s * 1.0153,
|
||||
};
|
||||
}
|
||||
|
||||
export function rgbToLms(rgb: RGB): LMS {
|
||||
return xyzToLms(rgbToXyz(rgb));
|
||||
}
|
||||
|
||||
export function lmsToRgb(lms: LMS): RGBA {
|
||||
return { ...xyzToRgb(lmsToXyz(lms)), a: 1 };
|
||||
}
|
||||
|
||||
const BRADFORD_MA: [number[], number[], number[]] = [
|
||||
[0.8951, 0.2664, -0.1614],
|
||||
[-0.7502, 1.7135, 0.0367],
|
||||
[0.0389, -0.0685, 1.0296],
|
||||
];
|
||||
|
||||
const BRADFORD_MA_INV: [number[], number[], number[]] = [
|
||||
[0.986993, -0.147054, 0.159963],
|
||||
[0.432305, 0.51836, 0.049291],
|
||||
[-0.008529, 0.040043, 0.968487],
|
||||
];
|
||||
|
||||
export const ILLUMINANTS: Record<string, XYZ> = {
|
||||
A: { x: 109.85, y: 100.0, z: 35.585 },
|
||||
B: { x: 99.0927, y: 100.0, z: 85.313 },
|
||||
C: { x: 98.074, y: 100.0, z: 118.232 },
|
||||
D50: { x: 96.4212, y: 100.0, z: 82.5188 },
|
||||
D55: { x: 95.6797, y: 100.0, z: 92.1481 },
|
||||
D65: { x: 95.047, y: 100.0, z: 108.883 },
|
||||
D75: { x: 94.972, y: 100.0, z: 122.638 },
|
||||
E: { x: 100.0, y: 100.0, z: 100.0 },
|
||||
F2: { x: 99.187, y: 100.0, z: 67.395 },
|
||||
F7: { x: 95.044, y: 100.0, z: 108.755 },
|
||||
F11: { x: 100.966, y: 100.0, z: 64.37 },
|
||||
};
|
||||
|
||||
function matVec3(
|
||||
m: [number[], number[], number[]],
|
||||
v: { x: number; y: number; z: number }
|
||||
): { x: number; y: number; z: number } {
|
||||
const r0 = m[0]!;
|
||||
const r1 = m[1]!;
|
||||
const r2 = m[2]!;
|
||||
return {
|
||||
x: r0[0]! * v.x + r0[1]! * v.y + r0[2]! * v.z,
|
||||
y: r1[0]! * v.x + r1[1]! * v.y + r1[2]! * v.z,
|
||||
z: r2[0]! * v.x + r2[1]! * v.y + r2[2]! * v.z,
|
||||
};
|
||||
}
|
||||
|
||||
export function bradfordAdapt(
|
||||
xyz: XYZ,
|
||||
sourceIlluminant: string | XYZ,
|
||||
destIlluminant: string | XYZ = "D65"
|
||||
): XYZ {
|
||||
const src =
|
||||
typeof sourceIlluminant === "string"
|
||||
? ILLUMINANTS[sourceIlluminant]
|
||||
: sourceIlluminant;
|
||||
const dst =
|
||||
typeof destIlluminant === "string"
|
||||
? ILLUMINANTS[destIlluminant]
|
||||
: destIlluminant;
|
||||
|
||||
if (!src || !dst) {
|
||||
throw new Error("Invalid illuminant");
|
||||
}
|
||||
|
||||
const srcLms = matVec3(BRADFORD_MA, src);
|
||||
const dstLms = matVec3(BRADFORD_MA, dst);
|
||||
const xyzLms = matVec3(BRADFORD_MA, xyz);
|
||||
|
||||
const scaled = {
|
||||
x: xyzLms.x * (dstLms.x / srcLms.x),
|
||||
y: xyzLms.y * (dstLms.y / srcLms.y),
|
||||
z: xyzLms.z * (dstLms.z / srcLms.z),
|
||||
};
|
||||
|
||||
return matVec3(BRADFORD_MA_INV, scaled);
|
||||
}
|
||||
|
||||
// Re-export core conversions from @suaveplan/colours for convenience
|
||||
function rgbToXyz({ r, g, b }: RGB): XYZ {
|
||||
const toLinear = (c: number) => {
|
||||
c /= 255;
|
||||
return c > 0.04045 ? ((c + 0.055) / 1.055) ** 2.4 : c / 12.92;
|
||||
};
|
||||
const R = toLinear(r);
|
||||
const G = toLinear(g);
|
||||
const B = toLinear(b);
|
||||
return {
|
||||
x: R * 0.4124564 + G * 0.3575761 + B * 0.1804375,
|
||||
y: R * 0.2126729 + G * 0.7151522 + B * 0.072175,
|
||||
z: R * 0.0193339 + G * 0.119192 + B * 0.9503041,
|
||||
};
|
||||
}
|
||||
|
||||
function xyzToRgb({ x, y, z }: XYZ): RGB {
|
||||
const R = x * 3.2404542 + y * -1.5371385 + z * -0.4985314;
|
||||
const G = x * -0.969266 + y * 1.8760108 + z * 0.041556;
|
||||
const B = x * 0.0556434 + y * -0.2040259 + z * 1.0572252;
|
||||
|
||||
const fromLinear = (c: number) => {
|
||||
c = c > 0.0031308 ? 1.055 * c ** (1 / 2.4) - 0.055 : 12.92 * c;
|
||||
return Math.round(Math.max(0, Math.min(255, c * 255)));
|
||||
};
|
||||
|
||||
return { r: fromLinear(R), g: fromLinear(G), b: fromLinear(B) };
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export type {
|
||||
LMS,
|
||||
LUV,
|
||||
LUVA,
|
||||
XYZ,
|
||||
xyY,
|
||||
} from "./colour-convert-cieluv.js";
|
||||
export {
|
||||
bradfordAdapt,
|
||||
ILLUMINANTS,
|
||||
lmsToRgb,
|
||||
lmsToXyz,
|
||||
luvToRgb,
|
||||
luvToXyz,
|
||||
rgbToLms,
|
||||
rgbToLuv,
|
||||
rgbToxyY,
|
||||
xyYToRgb,
|
||||
xyYToXyz,
|
||||
xyzToLms,
|
||||
xyzToLuv,
|
||||
xyzToxyY,
|
||||
} from "./colour-convert-cieluv.js";
|
||||
17
packages/media/image/colour-convert-cieluv/src/index.ts
Normal file
17
packages/media/image/colour-convert-cieluv/src/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type { LMS, LUV, LUVA, xyY } from "./colour-convert-cieluv/index.js";
|
||||
export {
|
||||
bradfordAdapt,
|
||||
ILLUMINANTS,
|
||||
lmsToRgb,
|
||||
lmsToXyz,
|
||||
luvToRgb,
|
||||
luvToXyz,
|
||||
rgbToLms,
|
||||
rgbToLuv,
|
||||
rgbToxyY,
|
||||
xyYToRgb,
|
||||
xyYToXyz,
|
||||
xyzToLms,
|
||||
xyzToLuv,
|
||||
xyzToxyY,
|
||||
} from "./colour-convert-cieluv/index.js";
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
|
||||
describe("dep-graph", () => {
|
||||
it("only depends on allowed @suaveplan packages", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const pkg = await import("../package.json");
|
||||
const deps = Object.keys(pkg.default.dependencies || {});
|
||||
const allowed = ["@suaveplan/colours"];
|
||||
for (const dep of deps) {
|
||||
if (dep.startsWith("@suaveplan/")) {
|
||||
expect(allowed).toContain(dep);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
// no polyfills needed
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
|
||||
describe("tree-shake", () => {
|
||||
it("has sideEffects false", { timeout: 5000 }, async () => {
|
||||
const pkg = await import("../package.json");
|
||||
expect(pkg.default.sideEffects).toBe(false);
|
||||
});
|
||||
|
||||
it("has at least one subpath export", { timeout: 5000 }, async () => {
|
||||
const pkg = await import("../package.json");
|
||||
const exports = Object.keys(pkg.default.exports || {});
|
||||
expect(exports.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
15
packages/media/image/colour-convert-cieluv/tsconfig.lib.json
Normal file
15
packages/media/image/colour-convert-cieluv/tsconfig.lib.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.test.ts", "dist", "node_modules"]
|
||||
}
|
||||
19
packages/media/image/colour-convert-cieluv/vite.config.ts
Normal file
19
packages/media/image/colour-convert-cieluv/vite.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: {
|
||||
index: "./src/index.ts",
|
||||
"colour-convert-cieluv/colour-convert-cieluv": "./src/colour-convert-cieluv/index.ts",
|
||||
},
|
||||
formats: ["es"],
|
||||
},
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
external: [/^@suaveplan\//, "tslib"],
|
||||
},
|
||||
},
|
||||
plugins: [dts({ rollupTypes: false, include: ["src/**/*.ts"], tsconfigPath: "./tsconfig.lib.json" })],
|
||||
});
|
||||
27
packages/media/image/colour-convert-cieluv/vitest.config.ts
Normal file
27
packages/media/image/colour-convert-cieluv/vitest.config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: false,
|
||||
environment: "node",
|
||||
pool: "forks",
|
||||
setupFiles: ["./tests/setup/polyfills.ts"],
|
||||
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
|
||||
exclude: ["src/__benchmarks__/**", "dist/**", "node_modules/**"],
|
||||
coverage: {
|
||||
provider: "istanbul",
|
||||
reporter: ["text", "html", "lcov"],
|
||||
include: ["src/**/*.ts"],
|
||||
exclude: [
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.bench.ts",
|
||||
"src/**/*.schemas.ts",
|
||||
"src/**/index.ts",
|
||||
"src/__benchmarks__/**",
|
||||
"tests/**",
|
||||
],
|
||||
thresholds: { statements: 100, branches: 100, functions: 100, lines: 100 },
|
||||
},
|
||||
testTimeout: 5000,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user