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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ ThemeConfig клиента
сертификат; Glow требует явный decision profile и может завершиться
типизированным `Indeterminate` без CSS fallback.
- **Непрерывные семейства.** `ColorCurve` и реализации `NeutralCurve`/`AccentCurve` доступны как низкоуровневые вычислительные примитивы.
- **Браузерное применение.** `applyTheme`, `watchTheme`, `adaptTheme` и `effectiveBackground` связывают результат WASM с локальной областью DOM.
- **Браузерное применение.** Публичные `applyTheme`, `watchTheme` и `adaptTheme`
связывают результат WASM с локальной областью DOM; обход подложки остаётся
внутренней частью runtime и не является отдельным API.

## Что не следует приписывать текущей реализации

Expand Down
4 changes: 2 additions & 2 deletions crates/labcolors-core/src/alpha.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ pub fn composite_over_encoded(
/// half-tie: `(250/255)·0.122·255` может стать `30.499…`, хотя эталонная
/// byte-reference `250·0.122` равен `30.5` и по round-half-up даёт байт 31.
/// Binary64-операции выполняются как монотонная affine-форма
/// `bg + alpha*(tint-bg)` — ровно тот же порядок использует официальный JS-
/// потребитель на непрозрачной подложке. Expanded-форма запрещена: на ULP-швах
/// `bg + alpha*(tint-bg)` — официальный JS-runtime вызывает этот Core-профиль,
/// а не воспроизводит формулу отдельно. Expanded-форма запрещена: на ULP-швах
/// она способна дать последовательность PASS→FAIL→PASS при росте alpha.
///
/// # Errors
Expand Down
119 changes: 47 additions & 72 deletions crates/labcolors-core/src/appearance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use std::collections::BTreeSet;

use crate::Srgb8;
pub(crate) use crate::composition::CompositionProfileV1;

/// Непрозрачный handle цветового входа. Число — только идентичность.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
Expand Down Expand Up @@ -84,15 +85,6 @@ impl OccurrenceId {
}
}

/// Версионированная identity математической операции композиции.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CompositionProfileV1 {
/// Straight-alpha source-over в encoded-sRGB8: по одному округлению
/// финального канала. Это exact-профиль Lab Colors, не обещание о
/// произвольном браузерном или HDR pipeline.
EncodedSrgb8SourceOverV1,
}

/// Структурная identity статической физической программы. Она описывает
/// topology/opcode/profile, а не числовые handles декларации или client ID.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -895,9 +887,10 @@ impl PointOpacityOverSurfaceV1 {
opacity: f64,
backdrop: [u8; 3],
) -> Result<ResolvedOccurrence, PointOpacityError> {
crate::composition::validate_alpha(opacity)
.map_err(|message| PointOpacityError { message })?;
let opacity = if opacity == 0.0 { 0.0 } else { opacity };
let opacity =
crate::composition::AdmittedOpacityV1::new(opacity).map_err(|_| PointOpacityError {
message: format!("alpha вне конечного [0,1]: {opacity}"),
})?;
Ok(Self::evaluate_value(source, opacity, backdrop))
}

Expand All @@ -908,33 +901,35 @@ impl PointOpacityOverSurfaceV1 {
opacity: crate::composition::AdmittedOpacityV1,
backdrop: [u8; 3],
) -> ResolvedOccurrence {
Self::evaluate_value(source, opacity.value(), backdrop)
Self::evaluate_value(source, opacity, backdrop)
}

fn evaluate_value(source: [u8; 3], opacity: f64, backdrop: [u8; 3]) -> ResolvedOccurrence {
fn evaluate_value(
source: [u8; 3],
opacity: crate::composition::AdmittedOpacityV1,
backdrop: [u8; 3],
) -> ResolvedOccurrence {
let mut paints = [None; 2];
let mut surfaces = [None; 2];
let mut occurrences = [None; 1];
POINT_OPACITY_OVER_SURFACE_V1.execute_into(
|id| match id {
POINT_SOURCE => Srgb8::new(source),
_ => unreachable!("sealed point program has one ColorInput port"),
|id| {
debug_assert_eq!(id, POINT_SOURCE);
Srgb8::new(source)
},
|id| match id {
POINT_CONTEXT => Srgb8::new(backdrop),
_ => unreachable!("sealed point program has one SurfaceInput port"),
|id| {
debug_assert_eq!(id, POINT_CONTEXT);
Srgb8::new(backdrop)
},
|id| match id {
POINT_OPACITY => opacity,
_ => unreachable!("sealed point program has one OpacityInput port"),
|id| {
debug_assert_eq!(id, POINT_OPACITY);
opacity
},
&mut paints,
&mut surfaces,
&mut occurrences,
);
occurrences[0].unwrap_or_else(|| {
unreachable!("compiler-verified point program materializes its Occurrence")
})
occurrences[0].unwrap_or_else(|| unreachable!())
}
}

Expand Down Expand Up @@ -1006,13 +1001,13 @@ impl AppearanceBindings {
}
}

/// Материализованный point Paint. Значение alpha хранится битами, чтобы
/// equality/certificate не теряли binary64 representation.
/// Материализованный point Paint. Тип alpha делает повторную admission перед
/// каждым occurrence невозможной; его биты без потерь переходят в certificate.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ResolvedPaint {
id: PaintId,
rgb: Srgb8,
opacity_bits: u64,
opacity: crate::composition::AdmittedOpacityV1,
}

impl ResolvedPaint {
Expand All @@ -1023,7 +1018,7 @@ impl ResolvedPaint {

#[cfg(test)]
pub(crate) fn opacity_bits(&self) -> u64 {
self.opacity_bits
self.opacity.bits()
}
}

Expand All @@ -1033,23 +1028,16 @@ impl ResolvedPaint {
pub(crate) struct SourceOverCertificateV1 {
profile: CompositionProfileV1,
subject_rgb: [u8; 3],
subject_opacity_bits: u64,
subject_opacity: crate::composition::AdmittedOpacityV1,
backdrop_rgb: [u8; 3],
output_rgb: [u8; 3],
}

impl SourceOverCertificateV1 {
#[cfg(test)]
pub(crate) fn replay(&self) -> Result<[u8; 3], String> {
match self.profile {
CompositionProfileV1::EncodedSrgb8SourceOverV1 => {
crate::composition::source_over_srgb8(
self.subject_rgb,
f64::from_bits(self.subject_opacity_bits),
self.backdrop_rgb,
)
}
}
pub(crate) fn replay(&self) -> [u8; 3] {
self.profile
.composite(self.subject_rgb, self.subject_opacity, self.backdrop_rgb)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

#[cfg(test)]
Expand All @@ -1062,7 +1050,7 @@ impl SourceOverCertificateV1 {
}

pub(crate) fn subject_opacity_bits(&self) -> u64 {
self.subject_opacity_bits
self.subject_opacity.bits()
}

pub(crate) fn backdrop_rgb(&self) -> [u8; 3] {
Expand Down Expand Up @@ -1336,14 +1324,12 @@ impl CompiledAppearanceProgram<'_> {
.unwrap_or_else(|_| unreachable!("bindings were matched before evaluation"));
surfaces[index].1
};
let opacity_value = |id: OpacityInputId| -> f64 {
let opacity_value = |id: OpacityInputId| -> crate::composition::AdmittedOpacityV1 {
let index = opacities
.binary_search_by_key(&id, |(bound, _)| *bound)
.unwrap_or_else(|_| unreachable!("bindings were matched before evaluation"));
let alpha = opacities[index].1;
// Straight alpha — не signed quantity: ±0 описывают один
// физический state и не должны минтить разные certificate bits.
if alpha == 0.0 { 0.0 } else { alpha }
crate::composition::AdmittedOpacityV1::new(opacities[index].1)
.unwrap_or_else(|_| unreachable!("opacity bindings were admitted before execution"))
};

let mut resolved_paints: Vec<Option<ResolvedPaint>> = vec![None; self.paints.len()];
Expand Down Expand Up @@ -1402,7 +1388,7 @@ impl CompiledAppearanceProgram<'_> {
) where
C: Fn(ColorInputId) -> Srgb8,
S: Fn(SurfaceInputPortId) -> Srgb8,
O: Fn(OpacityInputId) -> f64,
O: Fn(OpacityInputId) -> crate::composition::AdmittedOpacityV1,
{
debug_assert_eq!(resolved_paints.len(), self.paints.len());
debug_assert_eq!(resolved_surfaces.len(), self.surfaces.len());
Expand All @@ -1413,21 +1399,18 @@ impl CompiledAppearanceProgram<'_> {
CompiledPaintSpec::Solid { id, color } => ResolvedPaint {
id,
rgb: color_value(color),
opacity_bits: 1.0f64.to_bits(),
opacity: crate::composition::AdmittedOpacityV1::OPAQUE,
},
CompiledPaintSpec::Opacity {
id,
source,
opacity,
} => {
let source = resolved_paints[source]
.unwrap_or_else(|| unreachable!("Paint dependency precedes its consumer"));
let effective_alpha =
f64::from_bits(source.opacity_bits) * opacity_value(opacity);
let source = resolved_paints[source].unwrap_or_else(|| unreachable!());
ResolvedPaint {
id,
rgb: source.rgb,
opacity_bits: effective_alpha.to_bits(),
opacity: source.opacity.multiply(opacity_value(opacity)),
}
}
};
Expand All @@ -1441,34 +1424,26 @@ impl CompiledAppearanceProgram<'_> {
CompiledSurfaceSpec::Input { port, .. } => surface_value(port),
CompiledSurfaceSpec::FromOccurrence { occurrence, .. } => Srgb8::new(
resolved_occurrences[occurrence]
.unwrap_or_else(|| {
unreachable!("occurrence precedes surfaceFrom in render topo")
})
.unwrap_or_else(|| unreachable!())
.visible(),
),
};
resolved_surfaces[index] = Some(value);
}
RenderNode::Occurrence(index) => {
let spec = &self.occurrences[index];
let subject = resolved_paints[spec.subject]
.unwrap_or_else(|| unreachable!("Paint DAG is evaluated first"));
let backdrop = resolved_surfaces[spec.against].unwrap_or_else(|| {
unreachable!("backdrop precedes occurrence in render topo")
});
let visible = match spec.profile {
CompositionProfileV1::EncodedSrgb8SourceOverV1 => {
crate::composition::source_over_srgb8_validated(
subject.rgb.bytes(),
f64::from_bits(subject.opacity_bits),
backdrop.bytes(),
)
}
};
let subject = resolved_paints[spec.subject].unwrap_or_else(|| unreachable!());
let backdrop =
resolved_surfaces[spec.against].unwrap_or_else(|| unreachable!());
let visible = spec.profile.composite(
subject.rgb.bytes(),
subject.opacity,
backdrop.bytes(),
);
let certificate = SourceOverCertificateV1 {
profile: spec.profile,
subject_rgb: subject.rgb.bytes(),
subject_opacity_bits: subject.opacity_bits,
subject_opacity: subject.opacity,
backdrop_rgb: backdrop.bytes(),
output_rgb: visible,
};
Expand Down
18 changes: 9 additions & 9 deletions crates/labcolors-core/src/appearance_graph_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ fn canonical_paint_occurrence_surface_chain_evaluates_exactly() {
.unwrap()
.certificate()
.replay(),
Ok([0xFF, 0xF4, 0xE0])
[0xFF, 0xF4, 0xE0]
);
}

Expand Down Expand Up @@ -348,7 +348,7 @@ fn nested_opacity_materializes_once_by_multiplying_opacity() {
.unwrap()
.certificate()
.replay(),
Ok(rendered.occurrence(FILL_OCCURRENCE).unwrap().visible())
rendered.occurrence(FILL_OCCURRENCE).unwrap().visible()
);
}

Expand Down Expand Up @@ -415,7 +415,7 @@ fn nested_opacity_preserves_subnormal_and_rounds_underflow_to_positive_zero() {
expected_bits
);
assert_eq!(occurrence.visible(), [0; 3]);
assert_eq!(occurrence.certificate().replay(), Ok([0; 3]));
assert_eq!(occurrence.certificate().replay(), [0; 3]);
}
}

Expand Down Expand Up @@ -556,8 +556,8 @@ fn one_paint_is_surface_agnostic_across_two_occurrences() {
assert_eq!(first.subject(), FILL_PAINT);
assert_eq!(second.subject(), FILL_PAINT);
assert_ne!(first.visible(), second.visible());
assert_eq!(first.certificate().replay(), Ok(first.visible()));
assert_eq!(second.certificate().replay(), Ok(second.visible()));
assert_eq!(first.certificate().replay(), first.visible());
assert_eq!(second.certificate().replay(), second.visible());
}

#[test]
Expand Down Expand Up @@ -666,11 +666,11 @@ fn surface_from_reuses_visible_result_without_recompositing() {
assert_ne!(second_occurrence.visible(), [255, 128, 128]);
assert_eq!(
first_occurrence.certificate().replay(),
Ok(first_occurrence.visible())
first_occurrence.visible()
);
assert_eq!(
second_occurrence.certificate().replay(),
Ok(second_occurrence.visible())
second_occurrence.visible()
);
}

Expand Down Expand Up @@ -718,7 +718,7 @@ proptest! {
prop_assert_eq!(program_occurrence.backdrop_surface(), CONTEXT_SURFACE);
prop_assert_eq!(certificate.backdrop_rgb(), context);
prop_assert_eq!(certificate.output_rgb(), rendered.occurrence(FILL_OCCURRENCE).unwrap().visible());
prop_assert_eq!(certificate.replay(), Ok(certificate.output_rgb()));
prop_assert_eq!(certificate.replay(), certificate.output_rgb());
}

#[test]
Expand Down Expand Up @@ -778,7 +778,7 @@ proptest! {
let oracle = crate::alpha::composite_over_srgb8(source, effective, context).unwrap();
prop_assert_eq!(rendered.paint(subject).unwrap().opacity_bits(), effective.to_bits());
prop_assert_eq!(occurrence.visible(), oracle);
prop_assert_eq!(occurrence.certificate().replay(), Ok(oracle));
prop_assert_eq!(occurrence.certificate().replay(), oracle);
}

#[test]
Expand Down
Loading
Loading