diff --git a/docs_build/dev/BUILD_PR.md b/docs_build/dev/BUILD_PR.md index cc725fb0f..8e003cf3d 100644 --- a/docs_build/dev/BUILD_PR.md +++ b/docs_build/dev/BUILD_PR.md @@ -1,23 +1,23 @@ -# PR_26177_003-shared-geometry-foundation +# PR_26177_004-shared-color-foundation ## Purpose -Add a small shared geometry foundation. +Add a small shared color foundation. ## Source Of Truth -This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth for `PR_26177_003-shared-geometry-foundation`. +This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth for `PR_26177_004-shared-color-foundation`. ## Stack -- Base branch: `PR_26177_002-shared-noise-foundation` +- Base branch: `PR_26177_003-shared-geometry-foundation` ## Exact Scope -- Add `src/shared/geometry/` foundation. -- Include small reusable primitives/helpers such as vectors, rectangles, bounds, distance, clamp/intersection basics. -- Add targeted tests for the shared geometry area. -- No engine refactor. +- Add `src/shared/color/` foundation. +- Include hex/rgb/hsl conversion, clamp, lerp/blend helpers, luminance/contrast basics. +- Add targeted tests for the shared color area. +- No page styling changes. - Create required Codex reports under `docs_build/dev/reports/`. - Create repo-structured delta ZIP under `tmp/`. @@ -25,19 +25,19 @@ This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth f - `docs_build/dev/PLAN_PR.md` - `docs_build/dev/BUILD_PR.md` -- `src/shared/geometry/geometry.js` -- `tests/shared/GeometryFoundation.test.mjs` -- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation.md` -- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_branch-validation.md` -- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_requirement-checklist.md` -- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_validation-lane.md` -- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_manual-validation-notes.md` +- `src/shared/color/color.js` +- `tests/shared/ColorFoundation.test.mjs` +- `docs_build/dev/reports/PR_26177_004-shared-color-foundation.md` +- `docs_build/dev/reports/PR_26177_004-shared-color-foundation_branch-validation.md` +- `docs_build/dev/reports/PR_26177_004-shared-color-foundation_requirement-checklist.md` +- `docs_build/dev/reports/PR_26177_004-shared-color-foundation_validation-lane.md` +- `docs_build/dev/reports/PR_26177_004-shared-color-foundation_manual-validation-notes.md` - `docs_build/dev/reports/codex_review.diff` - `docs_build/dev/reports/codex_changed_files.txt` ## Out Of Scope -- No engine refactor. +- No page styling changes. - No browser-owned product data. - No runtime UI changes. - No browser storage changes. @@ -51,9 +51,9 @@ This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth f Run exactly: ```powershell -node ./scripts/run-node-test-files.mjs tests/shared/GeometryFoundation.test.mjs -node --check src/shared/geometry/geometry.js -node --check tests/shared/GeometryFoundation.test.mjs +node ./scripts/run-node-test-files.mjs tests/shared/ColorFoundation.test.mjs +node --check src/shared/color/color.js +node --check tests/shared/ColorFoundation.test.mjs git diff --check ``` @@ -62,5 +62,5 @@ git diff --check Create repo-structured delta ZIP: ```text -tmp/PR_26177_003-shared-geometry-foundation_delta.zip +tmp/PR_26177_004-shared-color-foundation_delta.zip ``` diff --git a/docs_build/dev/PLAN_PR.md b/docs_build/dev/PLAN_PR.md index b9bb1a6d3..62670625e 100644 --- a/docs_build/dev/PLAN_PR.md +++ b/docs_build/dev/PLAN_PR.md @@ -1,22 +1,23 @@ -# PLAN_PR: PR_26177_003-shared-geometry-foundation +# PLAN_PR: PR_26177_004-shared-color-foundation ## Purpose -Add a small shared geometry foundation. +Add a small shared color foundation. ## Scope -- Add `src/shared/geometry/` foundation. -- Include reusable primitives/helpers for vectors, rectangles, bounds, distance, clamp, containment, and intersection basics. +- Add `src/shared/color/` foundation. +- Include hex/rgb/hsl conversion helpers. +- Include clamp, lerp/blend helpers, luminance, and contrast basics. - Add targeted tests. -- No engine refactor. +- No page styling changes. - No browser-owned product data. - No runtime UI changes. - No unrelated cleanup. ## Implementation Plan -1. Add `src/shared/geometry/geometry.js`. -2. Add `tests/shared/GeometryFoundation.test.mjs`. -3. Validate vector, rectangle, bounds, distance, clamp, containment, and intersection behavior. +1. Add `src/shared/color/color.js`. +2. Add `tests/shared/ColorFoundation.test.mjs`. +3. Validate conversion, blending, luminance, and contrast helpers. 4. Produce required Codex reports and repo-structured ZIP. diff --git a/docs_build/dev/reports/PR_26177_004-shared-color-foundation.md b/docs_build/dev/reports/PR_26177_004-shared-color-foundation.md new file mode 100644 index 000000000..976ea2c1f --- /dev/null +++ b/docs_build/dev/reports/PR_26177_004-shared-color-foundation.md @@ -0,0 +1,23 @@ +# PR_26177_004-shared-color-foundation + +Date: 2026-06-26 +Scope: Shared color foundation +Status: PASS + +## Summary + +- Added `src/shared/color/color.js`. +- Added hex/rgb/hsl conversion, clamp, lerp/blend, luminance, and contrast helpers. +- Added targeted tests in `tests/shared/ColorFoundation.test.mjs`. +- No page styling, runtime UI, API, database, or unrelated cleanup changes were made. + +## Validation + +- PASS: `node ./scripts/run-node-test-files.mjs tests/shared/ColorFoundation.test.mjs`. +- PASS: `node --check src/shared/color/color.js`. +- PASS: `node --check tests/shared/ColorFoundation.test.mjs`. +- PASS: `git diff --check`. + +## Artifact + +- `tmp/PR_26177_004-shared-color-foundation_delta.zip` diff --git a/docs_build/dev/reports/PR_26177_004-shared-color-foundation_branch-validation.md b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_branch-validation.md new file mode 100644 index 000000000..d4699efd9 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_branch-validation.md @@ -0,0 +1,8 @@ +# PR_26177_004-shared-color-foundation Branch Validation + +Status: PASS + +- PASS: Branch `PR_26177_004-shared-color-foundation` was created from `PR_26177_003-shared-geometry-foundation`. +- PASS: Stack base was clean before branch creation. +- PASS: One PR purpose only: shared color foundation. +- PASS: No `start_of_day` files changed. diff --git a/docs_build/dev/reports/PR_26177_004-shared-color-foundation_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_manual-validation-notes.md new file mode 100644 index 000000000..7c1b6d1be --- /dev/null +++ b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_manual-validation-notes.md @@ -0,0 +1,5 @@ +# PR_26177_004-shared-color-foundation Manual Validation Notes + +Status: PASS + +Manual review confirmed this PR only adds shared color helpers and targeted tests. It does not change page styling, runtime UI, browser product-data ownership, API, or database behavior. diff --git a/docs_build/dev/reports/PR_26177_004-shared-color-foundation_requirement-checklist.md b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_requirement-checklist.md new file mode 100644 index 000000000..48c379881 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_requirement-checklist.md @@ -0,0 +1,11 @@ +# PR_26177_004-shared-color-foundation Requirement Checklist + +| Requirement | Status | Notes | +|---|---:|---| +| Add `src/shared/color/` foundation | PASS | Added `src/shared/color/color.js`. | +| Include hex/rgb/hsl conversion | PASS | Added conversion helpers. | +| Include clamp helpers | PASS | Added `clamp01` and `clampByte`. | +| Include lerp/blend helpers | PASS | Added `lerpColor` and `blendColors`. | +| Include luminance/contrast basics | PASS | Added relative luminance and contrast ratio helpers. | +| No page styling changes | PASS | No CSS or page styling files changed. | +| Add targeted tests | PASS | Added `tests/shared/ColorFoundation.test.mjs`. | diff --git a/docs_build/dev/reports/PR_26177_004-shared-color-foundation_validation-lane.md b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_validation-lane.md new file mode 100644 index 000000000..dd80db48b --- /dev/null +++ b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_validation-lane.md @@ -0,0 +1,22 @@ +# PR_26177_004-shared-color-foundation Validation Lane + +Status: PASS + +## Commands + +```powershell +node ./scripts/run-node-test-files.mjs tests/shared/ColorFoundation.test.mjs +node --check src/shared/color/color.js +node --check tests/shared/ColorFoundation.test.mjs +git diff --check +``` + +## Results + +- PASS: Targeted color foundation test. +- PASS: Changed JS syntax checks. +- PASS: `git diff --check`. + +## Not Run + +- Full samples smoke was not run by default. diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index a23d9473b..1b00743a4 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,11 +1,11 @@ docs_build/dev/BUILD_PR.md docs_build/dev/PLAN_PR.md -docs_build/dev/reports/PR_26177_003-shared-geometry-foundation.md -docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_branch-validation.md -docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_manual-validation-notes.md -docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_requirement-checklist.md -docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_validation-lane.md +docs_build/dev/reports/PR_26177_004-shared-color-foundation.md +docs_build/dev/reports/PR_26177_004-shared-color-foundation_branch-validation.md +docs_build/dev/reports/PR_26177_004-shared-color-foundation_manual-validation-notes.md +docs_build/dev/reports/PR_26177_004-shared-color-foundation_requirement-checklist.md +docs_build/dev/reports/PR_26177_004-shared-color-foundation_validation-lane.md docs_build/dev/reports/codex_changed_files.txt docs_build/dev/reports/codex_review.diff -src/shared/geometry/geometry.js -tests/shared/GeometryFoundation.test.mjs +src/shared/color/color.js +tests/shared/ColorFoundation.test.mjs diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index dd281464a..e85e64434 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,406 +1,439 @@ diff --git a/docs_build/dev/BUILD_PR.md b/docs_build/dev/BUILD_PR.md -index 747703cbf..cc725fb0f 100644 +index cc725fb0f..8e003cf3d 100644 --- a/docs_build/dev/BUILD_PR.md +++ b/docs_build/dev/BUILD_PR.md -@@ -1,25 +1,23 @@ --# PR_26177_002-shared-noise-foundation -+# PR_26177_003-shared-geometry-foundation +@@ -1,23 +1,23 @@ +-# PR_26177_003-shared-geometry-foundation ++# PR_26177_004-shared-color-foundation ## Purpose --Add a small shared deterministic noise foundation. -+Add a small shared geometry foundation. +-Add a small shared geometry foundation. ++Add a small shared color foundation. ## Source Of Truth --This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth for `PR_26177_002-shared-noise-foundation`. -+This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth for `PR_26177_003-shared-geometry-foundation`. +-This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth for `PR_26177_003-shared-geometry-foundation`. ++This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth for `PR_26177_004-shared-color-foundation`. ## Stack --- Base branch: `PR_26177_001-shared-hash-foundation` --- This PR builds on PR_001 hash utilities. -+- Base branch: `PR_26177_002-shared-noise-foundation` +-- Base branch: `PR_26177_002-shared-noise-foundation` ++- Base branch: `PR_26177_003-shared-geometry-foundation` ## Exact Scope --- Add `src/shared/noise/` foundation. --- Build on existing `Random`/`RandomSeed` and PR_001 hash utilities. --- Include deterministic Value, Perlin-style, Simplex-style, and Fractal-style helpers only where practical. --- Keep API small and documented. --- Add targeted tests for the shared noise area. -+- Add `src/shared/geometry/` foundation. -+- Include small reusable primitives/helpers such as vectors, rectangles, bounds, distance, clamp/intersection basics. -+- Add targeted tests for the shared geometry area. -+- No engine refactor. +-- Add `src/shared/geometry/` foundation. +-- Include small reusable primitives/helpers such as vectors, rectangles, bounds, distance, clamp/intersection basics. +-- Add targeted tests for the shared geometry area. +-- No engine refactor. ++- Add `src/shared/color/` foundation. ++- Include hex/rgb/hsl conversion, clamp, lerp/blend helpers, luminance/contrast basics. ++- Add targeted tests for the shared color area. ++- No page styling changes. - Create required Codex reports under `docs_build/dev/reports/`. - Create repo-structured delta ZIP under `tmp/`. -@@ -27,23 +25,23 @@ This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth f +@@ -25,19 +25,19 @@ This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth f - `docs_build/dev/PLAN_PR.md` - `docs_build/dev/BUILD_PR.md` --- `src/shared/noise/noise.js` --- `tests/shared/NoiseFoundation.test.mjs` --- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation.md` --- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation_branch-validation.md` --- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation_requirement-checklist.md` --- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation_validation-lane.md` --- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation_manual-validation-notes.md` -+- `src/shared/geometry/geometry.js` -+- `tests/shared/GeometryFoundation.test.mjs` -+- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation.md` -+- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_branch-validation.md` -+- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_requirement-checklist.md` -+- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_validation-lane.md` -+- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_manual-validation-notes.md` +-- `src/shared/geometry/geometry.js` +-- `tests/shared/GeometryFoundation.test.mjs` +-- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation.md` +-- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_branch-validation.md` +-- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_requirement-checklist.md` +-- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_validation-lane.md` +-- `docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_manual-validation-notes.md` ++- `src/shared/color/color.js` ++- `tests/shared/ColorFoundation.test.mjs` ++- `docs_build/dev/reports/PR_26177_004-shared-color-foundation.md` ++- `docs_build/dev/reports/PR_26177_004-shared-color-foundation_branch-validation.md` ++- `docs_build/dev/reports/PR_26177_004-shared-color-foundation_requirement-checklist.md` ++- `docs_build/dev/reports/PR_26177_004-shared-color-foundation_validation-lane.md` ++- `docs_build/dev/reports/PR_26177_004-shared-color-foundation_manual-validation-notes.md` - `docs_build/dev/reports/codex_review.diff` - `docs_build/dev/reports/codex_changed_files.txt` ## Out Of Scope -+- No engine refactor. +-- No engine refactor. ++- No page styling changes. - No browser-owned product data. - No runtime UI changes. - No browser storage changes. - - No API/database changes. --- No engine refactor. - - No `start_of_day` folder changes. - - No unrelated cleanup. - - No full samples smoke by default. -@@ -53,9 +51,9 @@ This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth f +@@ -51,9 +51,9 @@ This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth f Run exactly: ```powershell --node ./scripts/run-node-test-files.mjs tests/shared/NoiseFoundation.test.mjs tests/shared/HashFoundation.test.mjs --node --check src/shared/noise/noise.js --node --check tests/shared/NoiseFoundation.test.mjs -+node ./scripts/run-node-test-files.mjs tests/shared/GeometryFoundation.test.mjs -+node --check src/shared/geometry/geometry.js -+node --check tests/shared/GeometryFoundation.test.mjs +-node ./scripts/run-node-test-files.mjs tests/shared/GeometryFoundation.test.mjs +-node --check src/shared/geometry/geometry.js +-node --check tests/shared/GeometryFoundation.test.mjs ++node ./scripts/run-node-test-files.mjs tests/shared/ColorFoundation.test.mjs ++node --check src/shared/color/color.js ++node --check tests/shared/ColorFoundation.test.mjs git diff --check ``` -@@ -64,5 +62,5 @@ git diff --check +@@ -62,5 +62,5 @@ git diff --check Create repo-structured delta ZIP: ```text --tmp/PR_26177_002-shared-noise-foundation_delta.zip -+tmp/PR_26177_003-shared-geometry-foundation_delta.zip +-tmp/PR_26177_003-shared-geometry-foundation_delta.zip ++tmp/PR_26177_004-shared-color-foundation_delta.zip ``` diff --git a/docs_build/dev/PLAN_PR.md b/docs_build/dev/PLAN_PR.md -index 9a9a80336..b9bb1a6d3 100644 +index b9bb1a6d3..62670625e 100644 --- a/docs_build/dev/PLAN_PR.md +++ b/docs_build/dev/PLAN_PR.md -@@ -1,23 +1,22 @@ --# PLAN_PR: PR_26177_002-shared-noise-foundation -+# PLAN_PR: PR_26177_003-shared-geometry-foundation +@@ -1,22 +1,23 @@ +-# PLAN_PR: PR_26177_003-shared-geometry-foundation ++# PLAN_PR: PR_26177_004-shared-color-foundation ## Purpose --Add a small shared deterministic noise foundation. -+Add a small shared geometry foundation. +-Add a small shared geometry foundation. ++Add a small shared color foundation. ## Scope --- Add `src/shared/noise/` foundation. --- Build on existing `RandomSeed` and PR_001 hash utilities. --- Include deterministic Value, Perlin-style, Simplex-style, and Fractal-style helpers where practical. --- Keep API small and documented. -+- Add `src/shared/geometry/` foundation. -+- Include reusable primitives/helpers for vectors, rectangles, bounds, distance, clamp, containment, and intersection basics. +-- Add `src/shared/geometry/` foundation. +-- Include reusable primitives/helpers for vectors, rectangles, bounds, distance, clamp, containment, and intersection basics. ++- Add `src/shared/color/` foundation. ++- Include hex/rgb/hsl conversion helpers. ++- Include clamp, lerp/blend helpers, luminance, and contrast basics. - Add targeted tests. -+- No engine refactor. +-- No engine refactor. ++- No page styling changes. - No browser-owned product data. - No runtime UI changes. - No unrelated cleanup. ## Implementation Plan --1. Add `src/shared/noise/noise.js`. --2. Add `tests/shared/NoiseFoundation.test.mjs`. --3. Validate deterministic output, seed variation, practical ranges, and permutation determinism. -+1. Add `src/shared/geometry/geometry.js`. -+2. Add `tests/shared/GeometryFoundation.test.mjs`. -+3. Validate vector, rectangle, bounds, distance, clamp, containment, and intersection behavior. +-1. Add `src/shared/geometry/geometry.js`. +-2. Add `tests/shared/GeometryFoundation.test.mjs`. +-3. Validate vector, rectangle, bounds, distance, clamp, containment, and intersection behavior. ++1. Add `src/shared/color/color.js`. ++2. Add `tests/shared/ColorFoundation.test.mjs`. ++3. Validate conversion, blending, luminance, and contrast helpers. 4. Produce required Codex reports and repo-structured ZIP. -diff --git a/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation.md b/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation.md +diff --git a/docs_build/dev/reports/PR_26177_004-shared-color-foundation.md b/docs_build/dev/reports/PR_26177_004-shared-color-foundation.md new file mode 100644 -index 000000000..7c6562688 +index 000000000..976ea2c1f --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation.md ++++ b/docs_build/dev/reports/PR_26177_004-shared-color-foundation.md @@ -0,0 +1,23 @@ -+# PR_26177_003-shared-geometry-foundation ++# PR_26177_004-shared-color-foundation + +Date: 2026-06-26 -+Scope: Shared geometry foundation ++Scope: Shared color foundation +Status: PASS + +## Summary + -+- Added `src/shared/geometry/geometry.js`. -+- Added small vector, rectangle, bounds, distance, clamp, containment, and intersection helpers. -+- Added targeted tests in `tests/shared/GeometryFoundation.test.mjs`. -+- No engine refactor, runtime UI, API, database, or unrelated cleanup changes were made. ++- Added `src/shared/color/color.js`. ++- Added hex/rgb/hsl conversion, clamp, lerp/blend, luminance, and contrast helpers. ++- Added targeted tests in `tests/shared/ColorFoundation.test.mjs`. ++- No page styling, runtime UI, API, database, or unrelated cleanup changes were made. + +## Validation + -+- PASS: `node ./scripts/run-node-test-files.mjs tests/shared/GeometryFoundation.test.mjs`. -+- PASS: `node --check src/shared/geometry/geometry.js`. -+- PASS: `node --check tests/shared/GeometryFoundation.test.mjs`. ++- PASS: `node ./scripts/run-node-test-files.mjs tests/shared/ColorFoundation.test.mjs`. ++- PASS: `node --check src/shared/color/color.js`. ++- PASS: `node --check tests/shared/ColorFoundation.test.mjs`. +- PASS: `git diff --check`. + +## Artifact + -+- `tmp/PR_26177_003-shared-geometry-foundation_delta.zip` -diff --git a/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_branch-validation.md b/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_branch-validation.md ++- `tmp/PR_26177_004-shared-color-foundation_delta.zip` +diff --git a/docs_build/dev/reports/PR_26177_004-shared-color-foundation_branch-validation.md b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_branch-validation.md new file mode 100644 -index 000000000..795d42b33 +index 000000000..d4699efd9 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_branch-validation.md ++++ b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_branch-validation.md @@ -0,0 +1,8 @@ -+# PR_26177_003-shared-geometry-foundation Branch Validation ++# PR_26177_004-shared-color-foundation Branch Validation + +Status: PASS + -+- PASS: Branch `PR_26177_003-shared-geometry-foundation` was created from `PR_26177_002-shared-noise-foundation`. ++- PASS: Branch `PR_26177_004-shared-color-foundation` was created from `PR_26177_003-shared-geometry-foundation`. +- PASS: Stack base was clean before branch creation. -+- PASS: One PR purpose only: shared geometry foundation. ++- PASS: One PR purpose only: shared color foundation. +- PASS: No `start_of_day` files changed. -diff --git a/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_manual-validation-notes.md +diff --git a/docs_build/dev/reports/PR_26177_004-shared-color-foundation_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_manual-validation-notes.md new file mode 100644 -index 000000000..cf721901a +index 000000000..7c1b6d1be --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_manual-validation-notes.md ++++ b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_manual-validation-notes.md @@ -0,0 +1,5 @@ -+# PR_26177_003-shared-geometry-foundation Manual Validation Notes ++# PR_26177_004-shared-color-foundation Manual Validation Notes + +Status: PASS + -+Manual review confirmed this PR only adds shared geometry primitives/helpers and targeted tests. It does not refactor engine code or change runtime UI behavior. -diff --git a/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_requirement-checklist.md b/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_requirement-checklist.md ++Manual review confirmed this PR only adds shared color helpers and targeted tests. It does not change page styling, runtime UI, browser product-data ownership, API, or database behavior. +diff --git a/docs_build/dev/reports/PR_26177_004-shared-color-foundation_requirement-checklist.md b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_requirement-checklist.md new file mode 100644 -index 000000000..1710b7abe +index 000000000..48c379881 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_requirement-checklist.md -@@ -0,0 +1,12 @@ -+# PR_26177_003-shared-geometry-foundation Requirement Checklist ++++ b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_requirement-checklist.md +@@ -0,0 +1,11 @@ ++# PR_26177_004-shared-color-foundation Requirement Checklist + +| Requirement | Status | Notes | +|---|---:|---| -+| Add `src/shared/geometry/` foundation | PASS | Added `src/shared/geometry/geometry.js`. | -+| Include vector helpers | PASS | Added vector creation and vector arithmetic helpers. | -+| Include rectangles and bounds helpers | PASS | Added rectangle and bounds helpers. | -+| Include distance helper | PASS | Added `distance`. | -+| Include clamp/intersection basics | PASS | Added `clamp`, containment, intersection checks, and intersection rectangle. | -+| No engine refactor | PASS | No engine files changed. | -+| Add targeted tests | PASS | Added `tests/shared/GeometryFoundation.test.mjs`. | -+| No runtime UI changes | PASS | No UI files changed. | -diff --git a/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_validation-lane.md b/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_validation-lane.md ++| Add `src/shared/color/` foundation | PASS | Added `src/shared/color/color.js`. | ++| Include hex/rgb/hsl conversion | PASS | Added conversion helpers. | ++| Include clamp helpers | PASS | Added `clamp01` and `clampByte`. | ++| Include lerp/blend helpers | PASS | Added `lerpColor` and `blendColors`. | ++| Include luminance/contrast basics | PASS | Added relative luminance and contrast ratio helpers. | ++| No page styling changes | PASS | No CSS or page styling files changed. | ++| Add targeted tests | PASS | Added `tests/shared/ColorFoundation.test.mjs`. | +diff --git a/docs_build/dev/reports/PR_26177_004-shared-color-foundation_validation-lane.md b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_validation-lane.md new file mode 100644 -index 000000000..554ef4700 +index 000000000..dd80db48b --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_003-shared-geometry-foundation_validation-lane.md ++++ b/docs_build/dev/reports/PR_26177_004-shared-color-foundation_validation-lane.md @@ -0,0 +1,22 @@ -+# PR_26177_003-shared-geometry-foundation Validation Lane ++# PR_26177_004-shared-color-foundation Validation Lane + +Status: PASS + +## Commands + +```powershell -+node ./scripts/run-node-test-files.mjs tests/shared/GeometryFoundation.test.mjs -+node --check src/shared/geometry/geometry.js -+node --check tests/shared/GeometryFoundation.test.mjs ++node ./scripts/run-node-test-files.mjs tests/shared/ColorFoundation.test.mjs ++node --check src/shared/color/color.js ++node --check tests/shared/ColorFoundation.test.mjs +git diff --check +``` + +## Results + -+- PASS: Targeted geometry foundation test. ++- PASS: Targeted color foundation test. +- PASS: Changed JS syntax checks. +- PASS: `git diff --check`. + +## Not Run + +- Full samples smoke was not run by default. -diff --git a/src/shared/geometry/geometry.js b/src/shared/geometry/geometry.js +diff --git a/src/shared/color/color.js b/src/shared/color/color.js new file mode 100644 -index 000000000..af02ed464 +index 000000000..61bdbfd5e --- /dev/null -+++ b/src/shared/geometry/geometry.js -@@ -0,0 +1,112 @@ ++++ b/src/shared/color/color.js +@@ -0,0 +1,160 @@ +function finiteOrZero(value) { + const number = Number(value); + return Number.isFinite(number) ? number : 0; +} + -+/** -+ * Clamps a number between inclusive bounds. -+ * -+ * @param {number} value Value to clamp. -+ * @param {number} min Inclusive minimum. -+ * @param {number} max Inclusive maximum. -+ * @returns {number} Clamped value. -+ */ -+export function clamp(value, min, max) { -+ const low = Math.min(finiteOrZero(min), finiteOrZero(max)); -+ const high = Math.max(finiteOrZero(min), finiteOrZero(max)); -+ return Math.max(low, Math.min(finiteOrZero(value), high)); ++export function clamp01(value) { ++ return Math.max(0, Math.min(1, finiteOrZero(value))); +} + -+/** -+ * Creates a 2D vector. -+ * -+ * @param {number} x X coordinate. -+ * @param {number} y Y coordinate. -+ * @returns {{x: number, y: number}} Vector. -+ */ -+export function vector2(x = 0, y = 0) { -+ return { x: finiteOrZero(x), y: finiteOrZero(y) }; ++export function clampByte(value) { ++ return Math.round(Math.max(0, Math.min(255, finiteOrZero(value)))); +} + -+export function addVectors(a, b) { -+ return vector2(finiteOrZero(a?.x) + finiteOrZero(b?.x), finiteOrZero(a?.y) + finiteOrZero(b?.y)); ++function normalizeRgb(color) { ++ return { ++ r: clampByte(color?.r), ++ g: clampByte(color?.g), ++ b: clampByte(color?.b), ++ }; +} + -+export function subtractVectors(a, b) { -+ return vector2(finiteOrZero(a?.x) - finiteOrZero(b?.x), finiteOrZero(a?.y) - finiteOrZero(b?.y)); ++function normalizeHue(value) { ++ return ((finiteOrZero(value) % 360) + 360) % 360; +} + -+export function scaleVector(vector, scale = 1) { -+ return vector2(finiteOrZero(vector?.x) * finiteOrZero(scale), finiteOrZero(vector?.y) * finiteOrZero(scale)); ++function percentToUnit(value) { ++ const number = finiteOrZero(value); ++ return clamp01(number > 1 ? number / 100 : number); +} + -+export function distance(a, b) { -+ const dx = finiteOrZero(a?.x) - finiteOrZero(b?.x); -+ const dy = finiteOrZero(a?.y) - finiteOrZero(b?.y); -+ return Math.sqrt(dx * dx + dy * dy); ++function componentToHex(value) { ++ return clampByte(value).toString(16).padStart(2, "0"); +} + +/** -+ * Creates a normalized rectangle with non-negative dimensions. ++ * Converts a hex color string to RGB components. + * -+ * @param {number} x Left coordinate. -+ * @param {number} y Top coordinate. -+ * @param {number} width Width. -+ * @param {number} height Height. -+ * @returns {{x: number, y: number, width: number, height: number, right: number, bottom: number}} Rectangle. ++ * @param {string} hex Hex color such as "#336699" or "#369". ++ * @returns {{r: number, g: number, b: number}} RGB color. + */ -+export function rectangle(x = 0, y = 0, width = 0, height = 0) { -+ const startX = finiteOrZero(x); -+ const startY = finiteOrZero(y); -+ const resolvedWidth = finiteOrZero(width); -+ const resolvedHeight = finiteOrZero(height); -+ const left = resolvedWidth < 0 ? startX + resolvedWidth : startX; -+ const top = resolvedHeight < 0 ? startY + resolvedHeight : startY; -+ const normalizedWidth = Math.abs(resolvedWidth); -+ const normalizedHeight = Math.abs(resolvedHeight); ++export function hexToRgb(hex) { ++ const text = String(hex || "").trim().replace(/^#/, ""); ++ const expanded = text.length === 3 ++ ? text.split("").map((character) => `${character}${character}`).join("") ++ : text; ++ ++ if (!/^[\da-f]{6}$/i.test(expanded)) { ++ throw new TypeError("hex must be a 3 or 6 digit color string."); ++ } ++ + return { -+ x: left, -+ y: top, -+ width: normalizedWidth, -+ height: normalizedHeight, -+ right: left + normalizedWidth, -+ bottom: top + normalizedHeight, ++ r: Number.parseInt(expanded.slice(0, 2), 16), ++ g: Number.parseInt(expanded.slice(2, 4), 16), ++ b: Number.parseInt(expanded.slice(4, 6), 16), + }; +} + -+export function containsPoint(rect, point) { -+ const box = rectangle(rect?.x, rect?.y, rect?.width, rect?.height); -+ const x = finiteOrZero(point?.x); -+ const y = finiteOrZero(point?.y); -+ return x >= box.x && x <= box.right && y >= box.y && y <= box.bottom; ++export function rgbToHex(color) { ++ const rgb = normalizeRgb(color); ++ return `#${componentToHex(rgb.r)}${componentToHex(rgb.g)}${componentToHex(rgb.b)}`; +} + -+export function rectanglesIntersect(a, b) { -+ const first = rectangle(a?.x, a?.y, a?.width, a?.height); -+ const second = rectangle(b?.x, b?.y, b?.width, b?.height); -+ return first.x <= second.right && first.right >= second.x && first.y <= second.bottom && first.bottom >= second.y; -+} ++export function rgbToHsl(color) { ++ const rgb = normalizeRgb(color); ++ const r = rgb.r / 255; ++ const g = rgb.g / 255; ++ const b = rgb.b / 255; ++ const max = Math.max(r, g, b); ++ const min = Math.min(r, g, b); ++ const lightness = (max + min) / 2; ++ const delta = max - min; ++ ++ if (delta === 0) { ++ return { h: 0, s: 0, l: Number((lightness * 100).toFixed(6)) }; ++ } + -+export function rectangleIntersection(a, b) { -+ if (!rectanglesIntersect(a, b)) { -+ return null; ++ const saturation = delta / (1 - Math.abs(2 * lightness - 1)); ++ let hue = 0; ++ if (max === r) { ++ hue = 60 * (((g - b) / delta) % 6); ++ } else if (max === g) { ++ hue = 60 * ((b - r) / delta + 2); ++ } else { ++ hue = 60 * ((r - g) / delta + 4); + } + -+ const first = rectangle(a?.x, a?.y, a?.width, a?.height); -+ const second = rectangle(b?.x, b?.y, b?.width, b?.height); -+ const x = Math.max(first.x, second.x); -+ const y = Math.max(first.y, second.y); -+ return rectangle(x, y, Math.min(first.right, second.right) - x, Math.min(first.bottom, second.bottom) - y); ++ return { ++ h: Number(normalizeHue(hue).toFixed(6)), ++ s: Number((saturation * 100).toFixed(6)), ++ l: Number((lightness * 100).toFixed(6)), ++ }; +} + -+export function boundsFromPoints(points) { -+ if (!Array.isArray(points) || points.length === 0) { -+ return rectangle(0, 0, 0, 0); ++export function hslToRgb(color) { ++ const h = normalizeHue(color?.h); ++ const s = percentToUnit(color?.s); ++ const l = percentToUnit(color?.l); ++ const chroma = (1 - Math.abs(2 * l - 1)) * s; ++ const x = chroma * (1 - Math.abs(((h / 60) % 2) - 1)); ++ const m = l - chroma / 2; ++ let r = 0; ++ let g = 0; ++ let b = 0; ++ ++ if (h < 60) { ++ [r, g, b] = [chroma, x, 0]; ++ } else if (h < 120) { ++ [r, g, b] = [x, chroma, 0]; ++ } else if (h < 180) { ++ [r, g, b] = [0, chroma, x]; ++ } else if (h < 240) { ++ [r, g, b] = [0, x, chroma]; ++ } else if (h < 300) { ++ [r, g, b] = [x, 0, chroma]; ++ } else { ++ [r, g, b] = [chroma, 0, x]; + } + -+ const xs = points.map((point) => finiteOrZero(point?.x)); -+ const ys = points.map((point) => finiteOrZero(point?.y)); -+ const minX = Math.min(...xs); -+ const minY = Math.min(...ys); -+ return rectangle(minX, minY, Math.max(...xs) - minX, Math.max(...ys) - minY); ++ return normalizeRgb({ ++ r: (r + m) * 255, ++ g: (g + m) * 255, ++ b: (b + m) * 255, ++ }); ++} ++ ++export function lerp(start, end, amount) { ++ return finiteOrZero(start) + (finiteOrZero(end) - finiteOrZero(start)) * clamp01(amount); ++} ++ ++export function lerpColor(start, end, amount) { ++ const a = normalizeRgb(start); ++ const b = normalizeRgb(end); ++ return normalizeRgb({ ++ r: lerp(a.r, b.r, amount), ++ g: lerp(a.g, b.g, amount), ++ b: lerp(a.b, b.b, amount), ++ }); ++} ++ ++export function blendColors(start, end, amount = 0.5) { ++ return rgbToHex(lerpColor(start, end, amount)); ++} ++ ++function linearizeChannel(value) { ++ const channel = clampByte(value) / 255; ++ return channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4; ++} ++ ++export function relativeLuminance(color) { ++ const rgb = normalizeRgb(color); ++ return 0.2126 * linearizeChannel(rgb.r) + 0.7152 * linearizeChannel(rgb.g) + 0.0722 * linearizeChannel(rgb.b); ++} ++ ++export function contrastRatio(first, second) { ++ const a = relativeLuminance(first); ++ const b = relativeLuminance(second); ++ const lighter = Math.max(a, b); ++ const darker = Math.min(a, b); ++ return Number(((lighter + 0.05) / (darker + 0.05)).toFixed(6)); +} -diff --git a/tests/shared/GeometryFoundation.test.mjs b/tests/shared/GeometryFoundation.test.mjs +diff --git a/tests/shared/ColorFoundation.test.mjs b/tests/shared/ColorFoundation.test.mjs new file mode 100644 -index 000000000..86ac41c92 +index 000000000..4a96d3a35 --- /dev/null -+++ b/tests/shared/GeometryFoundation.test.mjs -@@ -0,0 +1,58 @@ ++++ b/tests/shared/ColorFoundation.test.mjs +@@ -0,0 +1,50 @@ +/* +Toolbox Aid +David Quesenberry +06/26/2026 -+GeometryFoundation.test.mjs ++ColorFoundation.test.mjs +*/ +import assert from "node:assert/strict"; +import { -+ addVectors, -+ boundsFromPoints, -+ clamp, -+ containsPoint, -+ distance, -+ rectangle, -+ rectangleIntersection, -+ rectanglesIntersect, -+ scaleVector, -+ subtractVectors, -+ vector2, -+} from "../../src/shared/geometry/geometry.js"; ++ blendColors, ++ clamp01, ++ clampByte, ++ contrastRatio, ++ hexToRgb, ++ hslToRgb, ++ lerpColor, ++ relativeLuminance, ++ rgbToHex, ++ rgbToHsl, ++} from "../../src/shared/color/color.js"; + +export function run() { -+ assert.equal(clamp(12, 0, 10), 10); -+ assert.equal(clamp(-2, 0, 10), 0); -+ assert.equal(clamp(4, 10, 0), 4); -+ -+ assert.deepEqual(vector2("2", 3), { x: 2, y: 3 }); -+ assert.deepEqual(addVectors({ x: 1, y: 2 }, { x: 3, y: 4 }), { x: 4, y: 6 }); -+ assert.deepEqual(subtractVectors({ x: 5, y: 6 }, { x: 2, y: 3 }), { x: 3, y: 3 }); -+ assert.deepEqual(scaleVector({ x: 2, y: -3 }, 4), { x: 8, y: -12 }); -+ assert.equal(distance({ x: 0, y: 0 }, { x: 3, y: 4 }), 5); -+ -+ assert.deepEqual(rectangle(10, 20, -5, -10), { -+ x: 5, -+ y: 10, -+ width: 5, -+ height: 10, -+ right: 10, -+ bottom: 20, ++ assert.equal(clamp01(2), 1); ++ assert.equal(clamp01(-1), 0); ++ assert.equal(clampByte(260), 255); ++ assert.equal(clampByte(12.4), 12); ++ ++ assert.deepEqual(hexToRgb("#369"), { r: 51, g: 102, b: 153 }); ++ assert.deepEqual(hexToRgb("#336699"), { r: 51, g: 102, b: 153 }); ++ assert.equal(rgbToHex({ r: 51, g: 102, b: 153 }), "#336699"); ++ assert.throws(() => hexToRgb("not-a-color"), TypeError); ++ ++ assert.deepEqual(rgbToHsl({ r: 255, g: 0, b: 0 }), { h: 0, s: 100, l: 50 }); ++ assert.deepEqual(hslToRgb({ h: 120, s: 100, l: 50 }), { r: 0, g: 255, b: 0 }); ++ assert.equal(rgbToHex(hslToRgb(rgbToHsl({ r: 51, g: 102, b: 153 }))), "#336699"); ++ ++ assert.deepEqual(lerpColor({ r: 0, g: 0, b: 0 }, { r: 255, g: 255, b: 255 }, 0.5), { ++ r: 128, ++ g: 128, ++ b: 128, + }); ++ assert.equal(blendColors({ r: 255, g: 0, b: 0 }, { r: 0, g: 0, b: 255 }, 0.5), "#800080"); + -+ const a = rectangle(0, 0, 10, 10); -+ const b = rectangle(5, 5, 10, 10); -+ const c = rectangle(20, 20, 5, 5); -+ assert.equal(containsPoint(a, { x: 10, y: 10 }), true); -+ assert.equal(containsPoint(a, { x: 11, y: 10 }), false); -+ assert.equal(rectanglesIntersect(a, b), true); -+ assert.equal(rectanglesIntersect(a, c), false); -+ assert.deepEqual(rectangleIntersection(a, b), rectangle(5, 5, 5, 5)); -+ assert.equal(rectangleIntersection(a, c), null); -+ -+ assert.deepEqual(boundsFromPoints([{ x: 2, y: 3 }, { x: -1, y: 8 }, { x: 4, y: 1 }]), rectangle(-1, 1, 5, 7)); -+ assert.deepEqual(boundsFromPoints([]), rectangle(0, 0, 0, 0)); ++ assert.equal(relativeLuminance({ r: 0, g: 0, b: 0 }), 0); ++ assert.equal(Number(relativeLuminance({ r: 255, g: 255, b: 255 }).toFixed(6)), 1); ++ assert.equal(contrastRatio({ r: 0, g: 0, b: 0 }, { r: 255, g: 255, b: 255 }), 21); +} + +if (import.meta.url === `file://${process.argv[1]}`) { diff --git a/src/shared/color/color.js b/src/shared/color/color.js new file mode 100644 index 000000000..61bdbfd5e --- /dev/null +++ b/src/shared/color/color.js @@ -0,0 +1,160 @@ +function finiteOrZero(value) { + const number = Number(value); + return Number.isFinite(number) ? number : 0; +} + +export function clamp01(value) { + return Math.max(0, Math.min(1, finiteOrZero(value))); +} + +export function clampByte(value) { + return Math.round(Math.max(0, Math.min(255, finiteOrZero(value)))); +} + +function normalizeRgb(color) { + return { + r: clampByte(color?.r), + g: clampByte(color?.g), + b: clampByte(color?.b), + }; +} + +function normalizeHue(value) { + return ((finiteOrZero(value) % 360) + 360) % 360; +} + +function percentToUnit(value) { + const number = finiteOrZero(value); + return clamp01(number > 1 ? number / 100 : number); +} + +function componentToHex(value) { + return clampByte(value).toString(16).padStart(2, "0"); +} + +/** + * Converts a hex color string to RGB components. + * + * @param {string} hex Hex color such as "#336699" or "#369". + * @returns {{r: number, g: number, b: number}} RGB color. + */ +export function hexToRgb(hex) { + const text = String(hex || "").trim().replace(/^#/, ""); + const expanded = text.length === 3 + ? text.split("").map((character) => `${character}${character}`).join("") + : text; + + if (!/^[\da-f]{6}$/i.test(expanded)) { + throw new TypeError("hex must be a 3 or 6 digit color string."); + } + + return { + r: Number.parseInt(expanded.slice(0, 2), 16), + g: Number.parseInt(expanded.slice(2, 4), 16), + b: Number.parseInt(expanded.slice(4, 6), 16), + }; +} + +export function rgbToHex(color) { + const rgb = normalizeRgb(color); + return `#${componentToHex(rgb.r)}${componentToHex(rgb.g)}${componentToHex(rgb.b)}`; +} + +export function rgbToHsl(color) { + const rgb = normalizeRgb(color); + const r = rgb.r / 255; + const g = rgb.g / 255; + const b = rgb.b / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const lightness = (max + min) / 2; + const delta = max - min; + + if (delta === 0) { + return { h: 0, s: 0, l: Number((lightness * 100).toFixed(6)) }; + } + + const saturation = delta / (1 - Math.abs(2 * lightness - 1)); + let hue = 0; + if (max === r) { + hue = 60 * (((g - b) / delta) % 6); + } else if (max === g) { + hue = 60 * ((b - r) / delta + 2); + } else { + hue = 60 * ((r - g) / delta + 4); + } + + return { + h: Number(normalizeHue(hue).toFixed(6)), + s: Number((saturation * 100).toFixed(6)), + l: Number((lightness * 100).toFixed(6)), + }; +} + +export function hslToRgb(color) { + const h = normalizeHue(color?.h); + const s = percentToUnit(color?.s); + const l = percentToUnit(color?.l); + const chroma = (1 - Math.abs(2 * l - 1)) * s; + const x = chroma * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - chroma / 2; + let r = 0; + let g = 0; + let b = 0; + + if (h < 60) { + [r, g, b] = [chroma, x, 0]; + } else if (h < 120) { + [r, g, b] = [x, chroma, 0]; + } else if (h < 180) { + [r, g, b] = [0, chroma, x]; + } else if (h < 240) { + [r, g, b] = [0, x, chroma]; + } else if (h < 300) { + [r, g, b] = [x, 0, chroma]; + } else { + [r, g, b] = [chroma, 0, x]; + } + + return normalizeRgb({ + r: (r + m) * 255, + g: (g + m) * 255, + b: (b + m) * 255, + }); +} + +export function lerp(start, end, amount) { + return finiteOrZero(start) + (finiteOrZero(end) - finiteOrZero(start)) * clamp01(amount); +} + +export function lerpColor(start, end, amount) { + const a = normalizeRgb(start); + const b = normalizeRgb(end); + return normalizeRgb({ + r: lerp(a.r, b.r, amount), + g: lerp(a.g, b.g, amount), + b: lerp(a.b, b.b, amount), + }); +} + +export function blendColors(start, end, amount = 0.5) { + return rgbToHex(lerpColor(start, end, amount)); +} + +function linearizeChannel(value) { + const channel = clampByte(value) / 255; + return channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4; +} + +export function relativeLuminance(color) { + const rgb = normalizeRgb(color); + return 0.2126 * linearizeChannel(rgb.r) + 0.7152 * linearizeChannel(rgb.g) + 0.0722 * linearizeChannel(rgb.b); +} + +export function contrastRatio(first, second) { + const a = relativeLuminance(first); + const b = relativeLuminance(second); + const lighter = Math.max(a, b); + const darker = Math.min(a, b); + return Number(((lighter + 0.05) / (darker + 0.05)).toFixed(6)); +} diff --git a/tests/shared/ColorFoundation.test.mjs b/tests/shared/ColorFoundation.test.mjs new file mode 100644 index 000000000..4a96d3a35 --- /dev/null +++ b/tests/shared/ColorFoundation.test.mjs @@ -0,0 +1,50 @@ +/* +Toolbox Aid +David Quesenberry +06/26/2026 +ColorFoundation.test.mjs +*/ +import assert from "node:assert/strict"; +import { + blendColors, + clamp01, + clampByte, + contrastRatio, + hexToRgb, + hslToRgb, + lerpColor, + relativeLuminance, + rgbToHex, + rgbToHsl, +} from "../../src/shared/color/color.js"; + +export function run() { + assert.equal(clamp01(2), 1); + assert.equal(clamp01(-1), 0); + assert.equal(clampByte(260), 255); + assert.equal(clampByte(12.4), 12); + + assert.deepEqual(hexToRgb("#369"), { r: 51, g: 102, b: 153 }); + assert.deepEqual(hexToRgb("#336699"), { r: 51, g: 102, b: 153 }); + assert.equal(rgbToHex({ r: 51, g: 102, b: 153 }), "#336699"); + assert.throws(() => hexToRgb("not-a-color"), TypeError); + + assert.deepEqual(rgbToHsl({ r: 255, g: 0, b: 0 }), { h: 0, s: 100, l: 50 }); + assert.deepEqual(hslToRgb({ h: 120, s: 100, l: 50 }), { r: 0, g: 255, b: 0 }); + assert.equal(rgbToHex(hslToRgb(rgbToHsl({ r: 51, g: 102, b: 153 }))), "#336699"); + + assert.deepEqual(lerpColor({ r: 0, g: 0, b: 0 }, { r: 255, g: 255, b: 255 }, 0.5), { + r: 128, + g: 128, + b: 128, + }); + assert.equal(blendColors({ r: 255, g: 0, b: 0 }, { r: 0, g: 0, b: 255 }, 0.5), "#800080"); + + assert.equal(relativeLuminance({ r: 0, g: 0, b: 0 }), 0); + assert.equal(Number(relativeLuminance({ r: 255, g: 255, b: 255 }).toFixed(6)), 1); + assert.equal(contrastRatio({ r: 0, g: 0, b: 0 }, { r: 255, g: 255, b: 255 }), 21); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + run(); +}