From f5a82152394fde4d8f740f664ab7737b82861618 Mon Sep 17 00:00:00 2001 From: cashmere Date: Tue, 28 Jul 2026 19:41:16 +0200 Subject: feat: GPUI desktop port foundation - scene crate: full project/scene/element model, serde round-trip proven against the web SerializedProject shape - transfer crate: .ocp zip container, cross-compat with web fflate archives proven in both directions - storage crate: filesystem project store replacing IndexedDB, .ocp import/export - media crate: ffmpeg-next 8 decoder (frames, audio, thumbnails, waveform summaries) - playback crate: wall-clock anchored, frame-rounded playback clock - gpu crate: native texture readback; compositor renders headless offscreen on Vulkan (spike test) - flake devshell: rust toolchain, GPUI native deps, ffmpeg dev, bindgen clang args; GPUI hello-world builds - port plan in notes/gpui-port.md --- rust/crates/animation/src/channel_layout.rs | 124 ++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 rust/crates/animation/src/channel_layout.rs (limited to 'rust/crates/animation/src/channel_layout.rs') diff --git a/rust/crates/animation/src/channel_layout.rs b/rust/crates/animation/src/channel_layout.rs new file mode 100644 index 00000000..59bbc3f6 --- /dev/null +++ b/rust/crates/animation/src/channel_layout.rs @@ -0,0 +1,124 @@ +use scene::ParamValue; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +use crate::color::{LinearRgba, parse_color_to_linear_rgba}; +use crate::types::AnimationInterpolation; + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum ChannelValueKind { + Scalar, + Discrete, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ChannelComponentDefinition { + pub key: String, + pub value_kind: ChannelValueKind, + pub default_interpolation: AnimationInterpolation, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ChannelLayout { + Leaf(ChannelComponentDefinition), + Composite(Vec), +} + +impl ChannelLayout { + pub fn components(&self) -> &[ChannelComponentDefinition] { + match self { + ChannelLayout::Leaf(component) => std::slice::from_ref(component), + ChannelLayout::Composite(components) => components, + } + } + + pub fn decompose(&self, value: &ParamValue) -> Option> { + match self { + ChannelLayout::Leaf(component) => { + let mut values = BTreeMap::new(); + values.insert(component.key.clone(), value.clone()); + Some(values) + } + ChannelLayout::Composite(_) => { + let ParamValue::String(color) = value else { + return None; + }; + let LinearRgba { r, g, b, a } = parse_color_to_linear_rgba(color)?; + let mut values = BTreeMap::new(); + values.insert("r".to_string(), ParamValue::Number(r)); + values.insert("g".to_string(), ParamValue::Number(g)); + values.insert("b".to_string(), ParamValue::Number(b)); + values.insert("a".to_string(), ParamValue::Number(a)); + Some(values) + } + } + } +} + +pub fn number_channel_layout() -> ChannelLayout { + ChannelLayout::Leaf(ChannelComponentDefinition { + key: "value".to_string(), + value_kind: ChannelValueKind::Scalar, + default_interpolation: AnimationInterpolation::Linear, + }) +} + +pub fn boolean_channel_layout() -> ChannelLayout { + ChannelLayout::Leaf(ChannelComponentDefinition { + key: "value".to_string(), + value_kind: ChannelValueKind::Discrete, + default_interpolation: AnimationInterpolation::Hold, + }) +} + +pub fn string_channel_layout() -> ChannelLayout { + ChannelLayout::Leaf(ChannelComponentDefinition { + key: "value".to_string(), + value_kind: ChannelValueKind::Discrete, + default_interpolation: AnimationInterpolation::Hold, + }) +} + +pub fn color_channel_layout() -> ChannelLayout { + let component = |key: &str| ChannelComponentDefinition { + key: key.to_string(), + value_kind: ChannelValueKind::Scalar, + default_interpolation: AnimationInterpolation::Linear, + }; + ChannelLayout::Composite(vec![ + component("r"), + component("g"), + component("b"), + component("a"), + ]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn leaf_decompose_wraps_value_under_component_key() { + let layout = number_channel_layout(); + let values = layout.decompose(&ParamValue::Number(3.0)).unwrap(); + assert_eq!(values.get("value"), Some(&ParamValue::Number(3.0))); + } + + #[test] + fn color_decompose_parses_hex_into_linear_components() { + let layout = color_channel_layout(); + let values = layout + .decompose(&ParamValue::String("#ff0000".to_string())) + .unwrap(); + assert_eq!(values.get("r"), Some(&ParamValue::Number(1.0))); + assert_eq!(values.get("g"), Some(&ParamValue::Number(0.0))); + assert_eq!(values.get("a"), Some(&ParamValue::Number(1.0))); + assert!( + layout + .decompose(&ParamValue::Number(1.0)) + .is_none() + ); + } +} -- cgit v1.3.1