//! Raster (pixel-buffer) layer for Lightningbeam //! //! Each keyframe holds a PNG-compressed pixel buffer stored in the .beam ZIP //! under `media/raster/.png`. A brush engine renders dabs along strokes //! and the resulting RGBA image is composited into the Vello scene. use crate::brush_settings::BrushSettings; use crate::layer::{Layer, LayerTrait, LayerType}; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// How a raster stroke blends onto the layer buffer #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum RasterBlendMode { /// Normal alpha-compositing (paint over) Normal, /// Erase pixels (reduce alpha) Erase, /// Smudge / blend surrounding pixels Smudge, /// Clone stamp: copy pixels from a source region CloneStamp, } impl Default for RasterBlendMode { fn default() -> Self { Self::Normal } } /// A single point along a stroke #[derive(Clone, Debug, Serialize, Deserialize)] pub struct StrokePoint { pub x: f32, pub y: f32, /// Pen/tablet pressure 0.0–1.0 (mouse uses 0.5) pub pressure: f32, /// Pen tilt X in radians pub tilt_x: f32, /// Pen tilt Y in radians pub tilt_y: f32, /// Seconds elapsed since start of this stroke pub timestamp: f64, } /// Record of a single brush stroke applied to a keyframe #[derive(Clone, Debug, Serialize, Deserialize)] pub struct StrokeRecord { pub brush_settings: BrushSettings, /// RGBA linear color [r, g, b, a] pub color: [f32; 4], pub blend_mode: RasterBlendMode, /// Clone stamp source offset: (source_x - drag_start_x, source_y - drag_start_y). /// For each dab at canvas position D, the source pixel is sampled from D + offset. /// None for all non-clone-stamp blend modes. #[serde(default)] pub clone_src_offset: Option<(f32, f32)>, pub points: Vec, } /// Specifies how the raster content transitions to the next keyframe #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum TweenType { /// Hold the pixel buffer until the next keyframe Hold, } impl Default for TweenType { fn default() -> Self { Self::Hold } } /// A single keyframe of a raster layer #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RasterKeyframe { /// Unique ID for this keyframe (used as pixel-cache key) pub id: Uuid, /// Time position in seconds pub time: f64, pub width: u32, pub height: u32, /// ZIP-relative path: `"media/raster/.png"` pub media_path: String, /// Stroke history (for potential replay / future non-destructive editing) pub stroke_log: Vec, pub tween_after: TweenType, /// Raw RGBA pixel buffer (width × height × 4 bytes). /// /// This is the working in-memory representation used by the brush engine and renderer. /// NOT serialized to the document JSON — populated from the ZIP's PNG on load, /// and encoded back to PNG on save. An empty Vec means the canvas is blank (transparent). #[serde(skip)] pub raw_pixels: Vec, } impl RasterKeyframe { /// Returns true when the pixel buffer has been initialised (non-blank). pub fn has_pixels(&self) -> bool { !self.raw_pixels.is_empty() } pub fn new(time: f64, width: u32, height: u32) -> Self { let id = Uuid::new_v4(); let media_path = format!("media/raster/{}.png", id); Self { id, time, width, height, media_path, stroke_log: Vec::new(), tween_after: TweenType::Hold, raw_pixels: Vec::new(), } } } /// A pixel-buffer painting layer #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RasterLayer { /// Base layer properties (id, name, opacity, visibility, …) pub layer: Layer, /// Keyframes sorted by time pub keyframes: Vec, } impl RasterLayer { /// Create a new raster layer with the given name pub fn new(name: impl Into) -> Self { Self { layer: Layer::new(LayerType::Raster, name), keyframes: Vec::new(), } } // === Keyframe accessors === /// Get the active keyframe at-or-before `time` pub fn keyframe_at(&self, time: f64) -> Option<&RasterKeyframe> { let idx = self.keyframes.partition_point(|kf| kf.time <= time); if idx > 0 { Some(&self.keyframes[idx - 1]) } else { None } } /// Get a mutable reference to the active keyframe at-or-before `time` pub fn keyframe_at_mut(&mut self, time: f64) -> Option<&mut RasterKeyframe> { let idx = self.keyframes.partition_point(|kf| kf.time <= time); if idx > 0 { Some(&mut self.keyframes[idx - 1]) } else { None } } /// Find the index of a keyframe at exactly `time` (within tolerance) fn keyframe_index_at_exact(&self, time: f64, tolerance: f64) -> Option { self.keyframes.iter().position(|kf| (kf.time - time).abs() < tolerance) } /// Ensure a keyframe exists at `time`; create one (with given dimensions) if not. /// /// If `width`/`height` are 0 the new keyframe inherits dimensions from the /// previous active keyframe, falling back to 1920×1080. pub fn ensure_keyframe_at(&mut self, time: f64, width: u32, height: u32) -> &mut RasterKeyframe { let tolerance = 0.001; if let Some(idx) = self.keyframe_index_at_exact(time, tolerance) { return &mut self.keyframes[idx]; } let (w, h) = if width == 0 || height == 0 { self.keyframe_at(time) .map(|kf| (kf.width, kf.height)) .unwrap_or((1920, 1080)) } else { (width, height) }; let insert_idx = self.keyframes.partition_point(|kf| kf.time < time); self.keyframes.insert(insert_idx, RasterKeyframe::new(time, w, h)); &mut self.keyframes[insert_idx] } /// Return the ZIP-relative PNG path for the active keyframe at `time`, or `None`. pub fn buffer_path_at_time(&self, time: f64) -> Option<&str> { self.keyframe_at(time).map(|kf| kf.media_path.as_str()) } } // Delegate all LayerTrait methods to self.layer impl LayerTrait for RasterLayer { fn id(&self) -> Uuid { self.layer.id } fn name(&self) -> &str { &self.layer.name } fn set_name(&mut self, name: String) { self.layer.name = name; } fn has_custom_name(&self) -> bool { self.layer.has_custom_name } fn set_has_custom_name(&mut self, custom: bool) { self.layer.has_custom_name = custom; } fn visible(&self) -> bool { self.layer.visible } fn set_visible(&mut self, visible: bool) { self.layer.visible = visible; } fn opacity(&self) -> f64 { self.layer.opacity } fn set_opacity(&mut self, opacity: f64) { self.layer.opacity = opacity; } fn volume(&self) -> f64 { self.layer.volume } fn set_volume(&mut self, volume: f64) { self.layer.volume = volume; } fn muted(&self) -> bool { self.layer.muted } fn set_muted(&mut self, muted: bool) { self.layer.muted = muted; } fn soloed(&self) -> bool { self.layer.soloed } fn set_soloed(&mut self, soloed: bool) { self.layer.soloed = soloed; } fn locked(&self) -> bool { self.layer.locked } fn set_locked(&mut self, locked: bool) { self.layer.locked = locked; } }