Lightningbeam/lightningbeam-ui/lightningbeam-core/src/raster_layer.rs

205 lines
7.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Raster (pixel-buffer) layer for Lightningbeam
//!
//! Each keyframe holds a PNG-compressed pixel buffer stored in the .beam ZIP
//! under `media/raster/<uuid>.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.01.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<StrokePoint>,
}
/// 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/<uuid>.png"`
pub media_path: String,
/// Stroke history (for potential replay / future non-destructive editing)
pub stroke_log: Vec<StrokeRecord>,
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<u8>,
}
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<RasterKeyframe>,
}
impl RasterLayer {
/// Create a new raster layer with the given name
pub fn new(name: impl Into<String>) -> 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<usize> {
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; }
}