Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Контекстный компилятор цветовых токенов для дизайн-систем.

`LCS` означает **Labpics Colors Space**, `LPC` — **Labpics Perceptual Contrast**.
Это собственные концепции Labpics; текущие CAM16/Oklab/APCA-shaped компоненты не
являются их полным определением и не доказывают перцептуальное превосходство.

Comment thread
lemone112 marked this conversation as resolved.
Lab Colors принимает конфиг клиента, компилирует его в `NamedRoleTable` и решает всю таблицу для одного локального фона и темы. Зависимые роли, уже представленные специальными рецептами, используют фактически полученные композиты. Браузерные помощники применяют результат, перепроверяют его при изменении окружения и при необходимости запускают новый resolve.

```text
Expand Down Expand Up @@ -225,7 +229,7 @@ anchors

- Нормативный floor применяется только там, где его требует контракт клиента или компонента.
- Core не определяет размер текста, essentialness, disabled/decorative status по имени роли.
- Экспериментальный LPC/APCA-shaped или appearance-результат не меняет WCAG pass/fail.
- Экспериментальный компонент формы APCA текущего LPC или результат модели внешнего вида не меняет WCAG pass/fail.
- Для финальной пары sRGB8 новый `wcag22-srgb8-contrast-v1` принимает явно объявленный критерий и возвращает строгий `Pass | Fail`; профиль, Q55-артефакт и full-domain proof входят в релиз.
- Старое поле `wcagRatio` остаётся compatibility-диагностикой текущего resolver/runtime и не может автоматически рекламироваться как результат нового evaluator-а.
- Цвет не должен быть единственным носителем смысла; текст, иконка и форма принадлежат компоненту.
Expand Down
6 changes: 3 additions & 3 deletions crates/labcolors-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ offline-операции. Все поверхности используют о
вычислителя или компилятора возвращаются типизированными ошибками; частичного
или запасного результата нет.

Полные проверки формул `C×E`, случая `E=0`, повторов ID, ресурсных отказов и
запрета частичного результата перечислены в разделе «Конечная компиляция
выполнимости WCAG 2.2» [карты верификации](../../docs/verification-map.md).
Формулы `C×E`, случай `E=0`, повторы ID, ресурсные отказы и запрет частичного
результата исполняются непосредственно в `src/wcag22_feasibility_tests.rs`,
`tests/wcag22_feasibility.rs` и `tests/wcag22_explicit_feasibility.rs`.

```rust
# #[cfg(feature = "wcag22-feasibility")]
Expand Down
6 changes: 3 additions & 3 deletions crates/labcolors-core/src/golden_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,9 @@ fn cam16_matches_colour_science_dim_surround() {
/// console.log(APCAcontrast(y(0),y(255)));" // 106.04066682868873
/// ```
///
/// Grey-on-grey isolates the Helmholtz-Kohlrausch term out of the metric
/// (luminance is fed directly), so this validates the curve alone.
/// Константы: APCA SAPC-8 версии 0.0.98G-4g; метрика называется LPC, не APCA.
/// Grey-on-grey bypasses the Helmholtz-Kohlrausch path because luminance is fed
/// directly. This validates the frozen candidate curve arithmetic only; it does
/// not validate APCA conformance, complete LPC or readability.
type ContrastGolden = (f64, f64, f64);
const ACHROMATIC_CONTRAST: [ContrastGolden; 13] = [
(0.0, 1.0, 106.04066682868873), // #000000 on #ffffff (BoW max)
Expand Down
28 changes: 18 additions & 10 deletions crates/labcolors-core/src/lcs.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
//! Current point representation used while **Labpics Colors Space (LCS)** is
//! being reduced to one context-bound coordinate contract. The stored
//! CAM16-UCS/Oklab views are implementation inputs, not independent editable
//! definitions of LCS and not a claim of uniform perceptual attributes.

use crate::spaces::srgb::{hex_from_srgb, srgb_from_hex, srgb_to_xyz, xyz_to_srgb};
use crate::spaces::{cam16, cat16, oklab, vc::ViewingConditions};

Expand All @@ -9,8 +14,9 @@ pub struct LcsColor {
pub h_ok: f64,
/// Internal reparameterisation of CAM16-UCS colourfulness `M′`:
/// `s = M′ / (J′ + 1)`. The `+ 1` is a regulariser against division by zero
/// as `J′ → 0`; it is lossless — `LcsColor::mp` recovers `M′` exactly as
/// `s · (J′ + 1)`. This is NOT the CAM16 saturation correlate.
/// as `J′ → 0`; `LcsColor::mp` applies the analytical inverse
/// `s · (J′ + 1)`, subject to ordinary binary64 round-off. This is NOT the
/// CAM16 saturation correlate.
pub s: f64,
h_cam: f64,
}
Expand All @@ -23,9 +29,9 @@ impl LcsColor {

/// Parse from hex using the given viewing conditions.
///
/// The resulting J', saturation, and CAM16 hue reflect perception under
/// the provided VC (e.g. [`ViewingConditions::dim_surround`] for dark
/// themes).
/// The stored CAM16-UCS/Oklab coordinates are evaluated under the provided
/// VC (e.g. [`ViewingConditions::dim_surround`] for dark themes). They are
/// implementation inputs, not universal perceptual-attribute scales.
pub fn from_hex_with_vc(hex: &str, vc: &ViewingConditions) -> Result<Self, String> {
let rgb = srgb_from_hex(hex)?;
let xyz = srgb_to_xyz(rgb);
Expand Down Expand Up @@ -54,8 +60,8 @@ impl LcsColor {
Self { jp, h_ok, s, h_cam }
}

/// CAM16-UCS colourfulness `M'`, recovered losslessly from the stored
/// reparameterisation (see the `s` field doc).
/// CAM16-UCS colourfulness correlate `M'`, recovered through the analytical
/// inverse of the stored reparameterisation (see the `s` field doc).
pub(crate) fn mp(&self) -> f64 {
self.s * (self.jp + 1.0)
}
Expand Down Expand Up @@ -92,9 +98,11 @@ impl LcsColor {
/// caller that already ran [`cam16::forward`] (e.g. [`crate::solve`]'s
/// `finish`) reuses that result instead of recomputing it.
pub(crate) fn from_cam16(j: f64, m: f64, h_cam: f64, h_ok: f64) -> Self {
// CAM16-UCS rescaling (Li et al. 2017, DOI 10.1002/col.22131): maps raw
// CIECAM16 J/M onto perceptually uniform J'/M' (J'=50 reads as
// half-lightness). Inverse in `to_xyz` via the same helpers.
// CAM16-UCS rescaling (Li et al. 2017, DOI 10.1002/col.22131). This is
// an analytically invertible coordinate transform used for
// colour-difference work; binary64 round-off is covered by the shared
// tolerance tests. No individual J'/M' value is assigned a universal
// attribute meaning here. Inverse in `to_xyz` uses the same helpers.
let jp = cam16::ucs_j(j);
let mp = cam16::ucs_m(m);
let s = mp / (jp + 1.0);
Expand Down
10 changes: 5 additions & 5 deletions crates/labcolors-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ mod pair_label_tests;
#[cfg(test)]
mod r3_byte_identity_tests;

// External published reference vectors for the deepest colour-science layers
// (sRGB EOTF & matrices, Ottosson Oklab, CAT16/CIECAM16 adapt, Hellwig-2022 H-K,
// WCAG linearise). These reach `pub(crate)` transforms an integration test in
// `tests/` cannot see; the public-API-reachable vectors live in
// `tests/reference_vectors.rs`. See `docs/verification-map.md`.
// Reference checks for the deepest colour-science layers (sRGB EOTF & matrices,
// Ottosson Oklab, CAT16/CIECAM16 adapt, Hellwig-2022 H-K, WCAG linearise). These
// reach `pub(crate)` transforms an integration test in `tests/` cannot see; the
// public-API-reachable checks live in `tests/reference_vectors.rs`, with source
// and oracle scope beside each test.
#[cfg(test)]
mod reference_vectors_deep;

Expand Down
50 changes: 22 additions & 28 deletions crates/labcolors-core/src/lpc.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
//! Candidate components of **Labpics Perceptual Contrast (LPC)**.
//!
//! The current APCA-shaped and H-K paths are characterized implementation
//! components, not the complete LPC definition and not evidence that LPC
//! outperforms APCA. Readability admission is owned by a versioned evaluator
//! profile with declared typography, observer and context applicability.

use crate::spaces::srgb::{D65_WHITE, srgb_from_hex, srgb_to_xyz};
use crate::spaces::{cam16, cat16, vc::ViewingConditions};

Expand Down Expand Up @@ -174,15 +181,11 @@ fn y_hk_bisect(j_hk: f64, vc: &ViewingConditions) -> f64 {
(lo + hi) * 0.5
}

// Канонические константы перцептивного контраста из опубликованной формулы
// версии 0.0.98G-4g («4g»-набор SAPC-8). Имена в комментариях воспроизводят
// исходные идентификаторы, чтобы маппинг был аудируемым.
//
// Правовая позиция: Copyright (17 U.S.C. § 102(b)) не охраняет формулы и константы,
// только конкретное кодовое выражение. Данная реализация написана независимо;
// файлы репозиториев Myndex не копировались. Метрика называется LPC — не APCA,
// не APCA-совместима и не одобрена Myndex Research или Andrew Somers.
// Товарный знак «APCA» в публичных API-символах и названии метрики не используется.
// Константы candidate-кривой транскрибированы из опубликованного набора
// SAPC-8 0.0.98G-4g. Имена в комментариях воспроизводят исходные
// идентификаторы, чтобы маппинг был аудируемым. Эта транскрипция сама по себе не
// является APCA conformance, complete LPC или evidence читаемости; комментарий
// также не делает правового вывода о допустимости дальнейшего распространения.
//
// Это ЕДИНСТВЕННЫЙ ИСТОЧНИК ИСТИНЫ для кривой контраста: и прямой `contrast_core`,
// и обратный решатель (`crate::solve`) читают значения здесь.
Expand Down Expand Up @@ -308,27 +311,18 @@ pub(crate) fn soft_clamp_inv(clamped: f64) -> Option<f64> {
Some(y)
}

/// Perceptual-contrast core curve (asymmetric power contrast on luminance).
/// Candidate asymmetric power curve over a luminance-shaped scalar.
///
/// Faithful port of the published generic perceptual-contrast math — soft
/// black clamp, polarity-dependent power exponents, the minimum-luminance
/// gate, the low-contrast clip, and the polarity offsets. Fed the *same* input
/// luminance, the curve reproduces the reference; the absolute numbers agree
/// with the published APCA only at the endpoints (Y = 0 and Y = 1, e.g. black
/// on white ≈ `106.04`). For interior greys the luminance fed here is
/// `Y_hk`, not the reference's `Ys`, so LPC departs from the published APCA
/// on those: measured against `apca-w3` on the 8-bit grey axis the departure
/// stays within ~2.3 Lc (grey-on-grey pairs ≤ ~0.6 Lc, endpoints exact). On
/// near-neutrals the H-K term itself is ≈0 (M ≲ 1), so the interior departure
/// is dominated by the CAM16 lightness reconstruction inside `Y_hk`. A
/// deliberate, declared difference of the metric (the same `Y_hk` substitution
/// that makes LPC diverge from the reference on chromatic colours), not a
/// porting error.
/// The branches and constants mirror the frozen SAPC-8 0.0.98G-4g candidate:
/// soft black clamp, polarity-dependent exponents, a minimum-luminance gate,
/// low-contrast clipping and polarity offsets. Current call sites feed more
/// than one luminance definition, including an H-K/CAM16-derived scalar. That
/// composition is characterized implementation behavior, not APCA conformance,
/// complete LPC or evidence of glyph readability.
///
/// Константы: формула APCA SAPC-8 версии 0.0.98G-4g; метрика называется LPC,
/// не APCA, не одобрена Myndex Research. The achromatic alignment is locked by
/// `golden_tests::contrast_core_matches_reference_on_grey_axis`. The curve is
/// inverted by `crate::solve` to recover a foreground luminance from a target.
/// `golden_tests::contrast_core_matches_reference_on_grey_axis` pins only the
/// scalar curve arithmetic. The curve is inverted by `crate::solve` to recover
/// a foreground scalar from a target.
pub(crate) fn contrast_core(y_fg: f64, y_bg: f64) -> f64 {
let fg = soft_clamp(y_fg);
let bg = soft_clamp(y_bg);
Expand Down
5 changes: 3 additions & 2 deletions crates/labcolors-core/src/pair_label_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,9 @@ fn pair_label_spec(
/// отгруженным фоном светлых labui-тем (классы совпадают в этой дизайн-
/// системе по факту фикстуры);
/// * `#101012` — отгруженный фон тёмных labui-тем (фикстура);
/// * `#767676` — опубликованная WCAG-граница серого (≈4.54:1 к белому,
/// см. `docs/verification-map.md`);
/// * `#767676` — опубликованная WCAG-граница серого (≈4.54:1 к белому),
/// напрямую закреплённая
/// `tests/reference_vectors.rs::wcag_published_ratios_via_public_api`;
/// * `#FFF4E0` — хроматический светлый witness: точная warning-поверхность
/// из graph-тестов (`appearance_graph_tests`);
/// * `#0000FF` — насыщенный хроматический угол куба (sRGB primary).
Expand Down
17 changes: 9 additions & 8 deletions crates/labcolors-core/src/reference_vectors_deep.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
//! External published reference vectors for the deepest colour-science layers.
//! Reference checks for the deepest colour-science layers.
//!
//! These pin the crate's transforms to CONTROL POINTS AND VECTORS PUBLISHED IN
//! STANDARDS / PEER-REVIEWED SOURCES, not to the crate's own output. They live
//! in-crate (not `tests/`) because the transforms they touch are `pub(crate)`
//! and invisible to an integration test. Public-API-reachable vectors are in
//! `tests/reference_vectors.rs`; the full map is `docs/verification-map.md`.
//! The checks combine published control points, independent transcriptions and
//! explicit identities. They live in-crate (not `tests/`) because the transforms
//! they touch are `pub(crate)` and invisible to an integration test.
//! Public-API-reachable checks are in `tests/reference_vectors.rs`; each test
//! below carries its own source and applicability boundary beside the assertion
//! it protects.
//!
//! Sources cited per test:
//! * IEC 61966-2-1:1999 — sRGB EOTF/OETF and primaries; also W3C CSS Color 4.
Expand Down Expand Up @@ -297,8 +298,8 @@ fn cam16_ucs_constants() {
"ucs_m not invertible at {m}"
);
}
// J'=50 reads as half-lightness only if the 1.7/0.007 pair is intact:
// published sanity value ucs_j(43.30..) ≈ 55.6 is a monotone lift, not 1:1.
// The rescale is not the identity: J=50 maps above 50. This checks only the
// published coordinate transform, not a human meaning for either number.
assert!(ucs_j(50.0) > 50.0, "UCS lightness lift must raise J");
}

Expand Down
10 changes: 5 additions & 5 deletions crates/labcolors-core/src/semantic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,11 @@
//! The default undertone policy is [`RoleChroma::Curve`] (v2), derived from three
//! computable mechanisms rather than a flat ratio of the gamut:
//!
//! 1. **Constant perceptual colorfulness** — the chroma at each role's resolved
//! lightness is solved to a *constant* CAM16-UCS `M'` (`TINT_TARGET_MP`), not
//! a fixed fraction of the gamut maximum. Because UCS is perceptually uniform,
//! one constant holds the chroma in the lights and moderates it in the middle
//! fixing v1's inverted envelope (over-saturated middle, starved light end).
//! 1. **Constant CAM16-UCS coordinate** — the chroma at each role's resolved
//! lightness is solved to a constant `M'` (`TINT_TARGET_MP`), not a fixed
//! fraction of the gamut maximum. This is a characterized design policy that
//! holds chroma in the lights and moderates it in the middle; it does not turn
//! `M'` into a universal perceptual-colorfulness scale.
Comment thread
lemone112 marked this conversation as resolved.
//! 2. **Cusp-attracted hue** — the hue at each lightness is pulled toward the
//! local chroma cusp of the sRGB gamut, penalised for leaving the canonical
//! 286° (`cusp_attracted_hue`). The drift emerges from geometry; it is *not*
Expand Down
9 changes: 6 additions & 3 deletions crates/labcolors-core/src/spaces/cam16.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,12 @@ fn forward_compute(xyz: [f64; 3], vc: &ViewingConditions) -> (f64, f64, f64) {
//
// J' = 1.7·J / (1 + 0.007·J), M' = ln(1 + 0.0228·M) / 0.0228.
//
// Maps raw CIECAM16 J/M onto perceptually uniform J'/M' (J'=50 reads as
// half-lightness). These four helpers are the SINGLE SOURCE OF TRUTH for the
// rescale: `lcs` stores J'/M', `lpc` decompresses back to raw J/M, and the
// These four helpers are the SINGLE SOURCE OF TRUTH for the CAM16-UCS
// coordinate rescale. The forward and inverse formulae are analytically mutual
// inverses; binary64 round-trips are validated within the `1e-12` tolerance in
// `ucs_rescale_round_trips`, not claimed bit-exact. They do not assign universal
// perceptual-attribute meaning to an individual J'/M' value: `lcs` stores the
// coordinates, `lpc` transforms them back to raw J/M, and the
// constants (`1.7`, `0.007`, `0.0228`) must never be re-typed inline anywhere
// else (previously duplicated across `lcs::from_xyz_with_hok`, `lcs::to_xyz`,
// and `lpc::y_hk_from_lcs`).
Expand Down
12 changes: 7 additions & 5 deletions crates/labcolors-core/tests/reference_vectors.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
//! External published reference vectors reachable through the PUBLIC API.
//! Reference checks reachable through the PUBLIC API.
//!
//! Companion to the crate-internal `reference_vectors_deep` (which reaches
//! `pub(crate)` transforms). Every vector here is a control point or worked
//! value from a STANDARD or PEER-REVIEWED SOURCE, asserted end-to-end through
//! the shipped surface. Full map: `docs/verification-map.md`.
//! `pub(crate)` transforms). The checks combine published control points,
//! independently transcribed formulae and explicit cross-boundary identities.
//! Each assertion owns its source and oracle boundary; crate-private companion
//! checks live in `src/reference_vectors_deep.rs`.
//!
//! Sources:
//! * W3C WCAG 2.1 §1.4.3 / §1.4.11 — relative luminance & contrast ratio.
Expand Down Expand Up @@ -262,7 +263,8 @@ fn dim_surround_shifts_lpc() {
// (byte-exact round-trip, proven for the core by `oklch::round_trip_is_byte_exact`).
// This file OWNS the seed set; the committed fixture is the artifact the JS test
// reads; `oklch_core_vectors_fixture_is_fresh` keeps it in lock-step with the
// live emitter. See `docs/verification-map.md`.
// live emitter, while `packages/colors/test/reference-vectors.test.mjs`
// independently decodes the committed strings back to the seed bytes.
// ═════════════════════════════════════════════════════════════════════════════

const FIXTURE_REL: &str = "/../../packages/colors/test/data/oklch-core-vectors.txt";
Expand Down
Loading
Loading