use crate::base::{Rect, Rgba};
use super::cell::Cell;
use super::surface::Surface;
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum Blend {
#[default]
Normal,
Additive,
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub enum ColorTransform {
#[default]
None,
Dim(f32),
Tint(Rgba, f32),
Grayscale(f32),
}
impl ColorTransform {
fn is_identity(&self) -> bool {
match *self {
ColorTransform::None => true,
ColorTransform::Dim(f) => f >= 1.0,
ColorTransform::Tint(_, s) | ColorTransform::Grayscale(s) => s <= 0.0,
}
}
fn apply(&self, c: Rgba) -> Rgba {
if c.is_transparent() {
return c;
}
match *self {
ColorTransform::None => c,
ColorTransform::Dim(f) => {
let f = f.clamp(0.0, 1.0);
let mul = |v: u8| (v as f32 * f).round() as u8;
Rgba::new(mul(c.r), mul(c.g), mul(c.b), c.a)
}
ColorTransform::Tint(color, strength) => {
let s = strength.clamp(0.0, 1.0);
let mixed = c.lerp(color, s);
Rgba::new(mixed.r, mixed.g, mixed.b, c.a)
}
ColorTransform::Grayscale(strength) => {
let s = strength.clamp(0.0, 1.0);
let luma =
((2126 * c.r as u32 + 7152 * c.g as u32 + 722 * c.b as u32) / 10000) as u8;
let gray = Rgba::new(luma, luma, luma, c.a);
c.lerp(gray, s)
}
}
}
}
pub trait CellShader {
fn shade(&self, x: i32, y: i32, t: f32, cell: Cell) -> Cell;
fn changed_region(&self, t0: f32, t1: f32, bounds: Rect) -> Option<Rect> {
let _ = (t0, t1, bounds);
None
}
}
pub(crate) type BoxedShader = Box<dyn CellShader>;
pub struct Layer {
surface: Surface,
origin: crate::base::Point,
z: i32,
opacity: f32,
visible: bool,
blend: Blend,
transform: ColorTransform,
shader: Option<BoxedShader>,
shader_t: f32,
frame_damage: Vec<Rect>,
}
impl Layer {
pub fn new(surface: Surface, origin: crate::base::Point, z: i32) -> Layer {
Layer {
surface,
origin,
z,
opacity: 1.0,
visible: true,
blend: Blend::Normal,
transform: ColorTransform::None,
shader: None,
shader_t: 0.0,
frame_damage: Vec::new(),
}
}
pub fn surface(&self) -> &Surface {
&self.surface
}
pub fn surface_mut(&mut self) -> &mut Surface {
&mut self.surface
}
pub fn origin(&self) -> crate::base::Point {
self.origin
}
pub fn z(&self) -> i32 {
self.z
}
pub fn opacity(&self) -> f32 {
self.opacity
}
pub fn visible(&self) -> bool {
self.visible
}
pub fn blend(&self) -> Blend {
self.blend
}
pub fn color_transform(&self) -> ColorTransform {
self.transform
}
pub fn shader_t(&self) -> f32 {
self.shader_t
}
pub fn has_shader(&self) -> bool {
self.shader.is_some()
}
pub fn bounds(&self) -> Rect {
Rect::new(
self.origin.x,
self.origin.y,
self.surface.width(),
self.surface.height(),
)
}
pub fn set_origin(&mut self, origin: crate::base::Point) {
if origin == self.origin {
return;
}
self.frame_damage.push(self.bounds());
self.origin = origin;
self.frame_damage.push(self.bounds());
}
pub fn set_z(&mut self, z: i32) {
if z != self.z {
self.z = z;
self.frame_damage.push(self.bounds());
}
}
pub fn set_opacity(&mut self, opacity: f32) {
let opacity = opacity.clamp(0.0, 1.0);
if (opacity - self.opacity).abs() > f32::EPSILON {
self.opacity = opacity;
self.frame_damage.push(self.bounds());
}
}
pub fn set_visible(&mut self, visible: bool) {
if visible != self.visible {
self.visible = visible;
self.frame_damage.push(self.bounds());
}
}
pub fn set_blend(&mut self, blend: Blend) {
if blend != self.blend {
self.blend = blend;
self.frame_damage.push(self.bounds());
}
}
pub fn set_color_transform(&mut self, transform: ColorTransform) {
if transform != self.transform {
self.transform = transform;
self.frame_damage.push(self.bounds());
}
}
pub fn set_shader(&mut self, shader: Option<BoxedShader>) {
self.shader = shader;
self.frame_damage.push(self.bounds());
}
pub fn set_shader_t(&mut self, t: f32) {
if (t - self.shader_t).abs() <= f32::EPSILON {
return;
}
let t0 = self.shader_t;
self.shader_t = t;
let Some(shader) = self.shader.as_deref() else {
return;
};
let bounds = self.bounds();
let region = match shader.changed_region(t0, t, bounds) {
Some(hint) => hint.intersect(bounds),
None => bounds,
};
if !region.is_empty() {
self.frame_damage.push(region);
}
}
pub(crate) fn is_dirty(&self) -> bool {
!self.frame_damage.is_empty() || (self.visible && self.surface.has_damage())
}
pub(crate) fn take_frame_damage(&mut self, out: &mut Vec<Rect>) {
out.append(&mut self.frame_damage);
}
pub(crate) fn contribution(&self, x: i32, y: i32) -> Option<Cell> {
let lx = x - self.origin.x;
let ly = y - self.origin.y;
let mut cell = *self.surface.get(lx, ly)?;
if let Some(shader) = self.shader.as_deref() {
cell = shader.shade(x, y, self.shader_t, cell);
}
if !self.transform.is_identity() {
cell.fg = self.transform.apply(cell.fg);
cell.bg = self.transform.apply(cell.bg);
cell.ul = self.transform.apply(cell.ul);
}
Some(cell)
}
}
pub(crate) fn add_saturating(acc: Rgba, src: Rgba) -> Rgba {
if src.is_transparent() {
return acc;
}
let pre = |c: u8| ((c as u16 * src.a as u16 + 127) / 255) as u8;
Rgba::new(
acc.r.saturating_add(pre(src.r)),
acc.g.saturating_add(pre(src.g)),
acc.b.saturating_add(pre(src.b)),
acc.a.saturating_add(src.a),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::{Point, Size};
#[test]
fn transform_setters_record_damage_and_noop_sets_are_free() {
let mut l = Layer::new(Surface::new(Size::new(4, 2), Cell::EMPTY), Point::ZERO, 0);
let mut sink = Vec::new();
l.take_frame_damage(&mut sink);
sink.clear();
l.set_color_transform(ColorTransform::Dim(0.5));
l.set_blend(Blend::Additive);
l.take_frame_damage(&mut sink);
assert_eq!(sink.len(), 2, "each grade change damages the bounds");
sink.clear();
l.set_color_transform(ColorTransform::Dim(0.5));
l.set_blend(Blend::Additive);
l.take_frame_damage(&mut sink);
assert!(sink.is_empty(), "no-op sets are free");
}
#[test]
fn shader_clock_damages_only_with_a_shader_installed() {
struct Id;
impl CellShader for Id {
fn shade(&self, _x: i32, _y: i32, _t: f32, cell: Cell) -> Cell {
cell
}
}
let mut l = Layer::new(Surface::new(Size::new(4, 2), Cell::EMPTY), Point::ZERO, 0);
let mut sink = Vec::new();
l.take_frame_damage(&mut sink);
sink.clear();
l.set_shader_t(0.5);
l.take_frame_damage(&mut sink);
assert!(sink.is_empty(), "no shader, no animation, no damage");
l.set_shader(Some(Box::new(Id)));
l.set_shader_t(1.0);
l.take_frame_damage(&mut sink);
assert_eq!(sink.len(), 2, "install + tick each damage the bounds");
}
#[test]
fn dim_tint_grayscale_math() {
let c = Rgba::rgb(200, 100, 50);
assert_eq!(ColorTransform::None.apply(c), c);
let dim = ColorTransform::Dim(0.5).apply(c);
assert_eq!((dim.r, dim.g, dim.b), (100, 50, 25));
let full_tint = ColorTransform::Tint(Rgba::rgb(0, 0, 255), 1.0).apply(c);
assert_eq!((full_tint.r, full_tint.g, full_tint.b), (0, 0, 255));
let half_tint = ColorTransform::Tint(Rgba::rgb(0, 0, 255), 0.5).apply(c);
assert!(half_tint.b > c.b && half_tint.r < c.r);
let gray = ColorTransform::Grayscale(1.0).apply(c);
assert_eq!(gray.r, gray.g);
assert_eq!(gray.g, gray.b);
assert_eq!(
ColorTransform::Dim(0.1).apply(Rgba::TRANSPARENT),
Rgba::TRANSPARENT
);
let translucent = Rgba::new(100, 100, 100, 128);
assert_eq!(ColorTransform::Grayscale(1.0).apply(translucent).a, 128);
}
#[test]
fn additive_primitive_saturates_and_premultiplies() {
let acc = Rgba::rgb(200, 200, 200);
let out = add_saturating(acc, Rgba::rgb(100, 10, 0));
assert_eq!((out.r, out.g, out.b), (255, 210, 200));
let dim = add_saturating(Rgba::rgb(0, 0, 0), Rgba::new(100, 100, 100, 128));
assert!((dim.r as i32 - 50).abs() <= 1, "{dim:?}");
assert_eq!(add_saturating(acc, Rgba::TRANSPARENT), acc);
}
}