|
|
|
|
|
use crate::Color; |
|
pub use crate::gradient::*; |
|
use dyn_any::DynAny; |
|
use glam::DAffine2; |
|
|
|
|
|
|
|
|
|
|
|
|
|
#[repr(C)] |
|
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)] |
|
pub enum Fill { |
|
#[default] |
|
None, |
|
Solid(Color), |
|
Gradient(Gradient), |
|
} |
|
|
|
impl std::fmt::Display for Fill { |
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
|
match self { |
|
Self::None => write!(f, "None"), |
|
Self::Solid(color) => write!(f, "#{} (Alpha: {}%)", color.to_rgb_hex_srgb(), color.a() * 100.), |
|
Self::Gradient(gradient) => write!(f, "{}", gradient), |
|
} |
|
} |
|
} |
|
|
|
impl Fill { |
|
|
|
pub fn solid(color: Color) -> Self { |
|
Self::Solid(color) |
|
} |
|
|
|
|
|
pub fn solid_or_none(color: Option<Color>) -> Self { |
|
match color { |
|
Some(color) => Self::Solid(color), |
|
None => Self::None, |
|
} |
|
} |
|
|
|
|
|
pub fn color(&self) -> Color { |
|
match self { |
|
Self::None => Color::BLACK, |
|
Self::Solid(color) => *color, |
|
|
|
Self::Gradient(Gradient { stops, .. }) => stops.0[0].1, |
|
} |
|
} |
|
|
|
pub fn lerp(&self, other: &Self, time: f64) -> Self { |
|
let transparent = Self::solid(Color::TRANSPARENT); |
|
let a = if *self == Self::None { &transparent } else { self }; |
|
let b = if *other == Self::None { &transparent } else { other }; |
|
|
|
match (a, b) { |
|
(Self::Solid(a), Self::Solid(b)) => Self::Solid(a.lerp(b, time as f32)), |
|
(Self::Solid(a), Self::Gradient(b)) => { |
|
let mut solid_to_gradient = b.clone(); |
|
solid_to_gradient.stops.0.iter_mut().for_each(|(_, color)| *color = *a); |
|
let a = &solid_to_gradient; |
|
Self::Gradient(a.lerp(b, time)) |
|
} |
|
(Self::Gradient(a), Self::Solid(b)) => { |
|
let mut gradient_to_solid = a.clone(); |
|
gradient_to_solid.stops.0.iter_mut().for_each(|(_, color)| *color = *b); |
|
let b = &gradient_to_solid; |
|
Self::Gradient(a.lerp(b, time)) |
|
} |
|
(Self::Gradient(a), Self::Gradient(b)) => Self::Gradient(a.lerp(b, time)), |
|
_ => Self::None, |
|
} |
|
} |
|
|
|
|
|
pub fn as_gradient(&self) -> Option<&Gradient> { |
|
match self { |
|
Self::Gradient(gradient) => Some(gradient), |
|
_ => None, |
|
} |
|
} |
|
|
|
|
|
pub fn as_solid(&self) -> Option<Color> { |
|
match self { |
|
Self::Solid(color) => Some(*color), |
|
_ => None, |
|
} |
|
} |
|
|
|
|
|
pub fn is_opaque(&self) -> bool { |
|
match self { |
|
Fill::Solid(color) => color.is_opaque(), |
|
Fill::Gradient(gradient) => gradient.stops.iter().all(|(_, color)| color.is_opaque()), |
|
Fill::None => true, |
|
} |
|
} |
|
|
|
|
|
pub fn is_none(&self) -> bool { |
|
*self == Self::None |
|
} |
|
} |
|
|
|
impl From<Color> for Fill { |
|
fn from(color: Color) -> Fill { |
|
Fill::Solid(color) |
|
} |
|
} |
|
|
|
impl From<Option<Color>> for Fill { |
|
fn from(color: Option<Color>) -> Fill { |
|
Fill::solid_or_none(color) |
|
} |
|
} |
|
|
|
impl From<Gradient> for Fill { |
|
fn from(gradient: Gradient) -> Fill { |
|
Fill::Gradient(gradient) |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
#[repr(C)] |
|
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)] |
|
pub enum FillChoice { |
|
#[default] |
|
None, |
|
|
|
Solid(Color), |
|
|
|
Gradient(GradientStops), |
|
} |
|
|
|
impl FillChoice { |
|
pub fn as_solid(&self) -> Option<Color> { |
|
let Self::Solid(color) = self else { return None }; |
|
Some(*color) |
|
} |
|
|
|
pub fn as_gradient(&self) -> Option<&GradientStops> { |
|
let Self::Gradient(gradient) = self else { return None }; |
|
Some(gradient) |
|
} |
|
|
|
|
|
|
|
pub fn to_fill(&self, existing_gradient: Option<&Gradient>) -> Fill { |
|
match self { |
|
Self::None => Fill::None, |
|
Self::Solid(color) => Fill::Solid(*color), |
|
Self::Gradient(stops) => { |
|
let mut fill = existing_gradient.cloned().unwrap_or_default(); |
|
fill.stops = stops.clone(); |
|
Fill::Gradient(fill) |
|
} |
|
} |
|
} |
|
} |
|
|
|
impl From<Fill> for FillChoice { |
|
fn from(fill: Fill) -> Self { |
|
match fill { |
|
Fill::None => FillChoice::None, |
|
Fill::Solid(color) => FillChoice::Solid(color), |
|
Fill::Gradient(gradient) => FillChoice::Gradient(gradient.stops), |
|
} |
|
} |
|
} |
|
|
|
|
|
#[repr(C)] |
|
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type, node_macro::ChoiceType)] |
|
#[widget(Radio)] |
|
pub enum FillType { |
|
#[default] |
|
Solid, |
|
Gradient, |
|
} |
|
|
|
|
|
#[repr(C)] |
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] |
|
#[widget(Radio)] |
|
pub enum StrokeCap { |
|
#[default] |
|
Butt, |
|
Round, |
|
Square, |
|
} |
|
|
|
impl StrokeCap { |
|
pub fn svg_name(&self) -> &'static str { |
|
match self { |
|
StrokeCap::Butt => "butt", |
|
StrokeCap::Round => "round", |
|
StrokeCap::Square => "square", |
|
} |
|
} |
|
} |
|
|
|
#[repr(C)] |
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] |
|
#[widget(Radio)] |
|
pub enum StrokeJoin { |
|
#[default] |
|
Miter, |
|
Bevel, |
|
Round, |
|
} |
|
|
|
impl StrokeJoin { |
|
pub fn svg_name(&self) -> &'static str { |
|
match self { |
|
StrokeJoin::Bevel => "bevel", |
|
StrokeJoin::Miter => "miter", |
|
StrokeJoin::Round => "round", |
|
} |
|
} |
|
} |
|
|
|
#[repr(C)] |
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] |
|
#[widget(Radio)] |
|
pub enum StrokeAlign { |
|
#[default] |
|
Center, |
|
Inside, |
|
Outside, |
|
} |
|
|
|
impl StrokeAlign { |
|
pub fn is_not_centered(self) -> bool { |
|
self != Self::Center |
|
} |
|
} |
|
|
|
#[repr(C)] |
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] |
|
#[widget(Radio)] |
|
pub enum PaintOrder { |
|
#[default] |
|
StrokeAbove, |
|
StrokeBelow, |
|
} |
|
|
|
impl PaintOrder { |
|
pub fn is_default(self) -> bool { |
|
self == Self::default() |
|
} |
|
} |
|
|
|
fn daffine2_identity() -> DAffine2 { |
|
DAffine2::IDENTITY |
|
} |
|
|
|
#[repr(C)] |
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] |
|
#[serde(default)] |
|
pub struct Stroke { |
|
|
|
pub color: Option<Color>, |
|
|
|
pub weight: f64, |
|
pub dash_lengths: Vec<f64>, |
|
pub dash_offset: f64, |
|
#[serde(alias = "line_cap")] |
|
pub cap: StrokeCap, |
|
#[serde(alias = "line_join")] |
|
pub join: StrokeJoin, |
|
#[serde(alias = "line_join_miter_limit")] |
|
pub join_miter_limit: f64, |
|
#[serde(default)] |
|
pub align: StrokeAlign, |
|
#[serde(default = "daffine2_identity")] |
|
pub transform: DAffine2, |
|
#[serde(default)] |
|
pub non_scaling: bool, |
|
#[serde(default)] |
|
pub paint_order: PaintOrder, |
|
} |
|
|
|
impl std::hash::Hash for Stroke { |
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { |
|
self.color.hash(state); |
|
self.weight.to_bits().hash(state); |
|
{ |
|
self.dash_lengths.len().hash(state); |
|
self.dash_lengths.iter().for_each(|length| length.to_bits().hash(state)); |
|
} |
|
self.dash_offset.to_bits().hash(state); |
|
self.cap.hash(state); |
|
self.join.hash(state); |
|
self.join_miter_limit.to_bits().hash(state); |
|
self.align.hash(state); |
|
self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)); |
|
self.non_scaling.hash(state); |
|
self.paint_order.hash(state); |
|
} |
|
} |
|
|
|
impl From<Color> for Stroke { |
|
fn from(color: Color) -> Self { |
|
Self::new(Some(color), 1.) |
|
} |
|
} |
|
impl From<Option<Color>> for Stroke { |
|
fn from(color: Option<Color>) -> Self { |
|
Self::new(color, 1.) |
|
} |
|
} |
|
|
|
impl Stroke { |
|
pub const fn new(color: Option<Color>, weight: f64) -> Self { |
|
Self { |
|
color, |
|
weight, |
|
dash_lengths: Vec::new(), |
|
dash_offset: 0., |
|
cap: StrokeCap::Butt, |
|
join: StrokeJoin::Miter, |
|
join_miter_limit: 4., |
|
align: StrokeAlign::Center, |
|
transform: DAffine2::IDENTITY, |
|
non_scaling: false, |
|
paint_order: PaintOrder::StrokeAbove, |
|
} |
|
} |
|
|
|
pub fn lerp(&self, other: &Self, time: f64) -> Self { |
|
Self { |
|
color: self.color.map(|color| color.lerp(&other.color.unwrap_or(color), time as f32)), |
|
weight: self.weight + (other.weight - self.weight) * time, |
|
dash_lengths: self.dash_lengths.iter().zip(other.dash_lengths.iter()).map(|(a, b)| a + (b - a) * time).collect(), |
|
dash_offset: self.dash_offset + (other.dash_offset - self.dash_offset) * time, |
|
cap: if time < 0.5 { self.cap } else { other.cap }, |
|
join: if time < 0.5 { self.join } else { other.join }, |
|
join_miter_limit: self.join_miter_limit + (other.join_miter_limit - self.join_miter_limit) * time, |
|
align: if time < 0.5 { self.align } else { other.align }, |
|
transform: DAffine2::from_mat2_translation( |
|
time * self.transform.matrix2 + (1. - time) * other.transform.matrix2, |
|
self.transform.translation * time + other.transform.translation * (1. - time), |
|
), |
|
non_scaling: if time < 0.5 { self.non_scaling } else { other.non_scaling }, |
|
paint_order: if time < 0.5 { self.paint_order } else { other.paint_order }, |
|
} |
|
} |
|
|
|
|
|
pub fn color(&self) -> Option<Color> { |
|
self.color |
|
} |
|
|
|
|
|
pub fn weight(&self) -> f64 { |
|
self.weight |
|
} |
|
|
|
pub fn dash_lengths(&self) -> String { |
|
if self.dash_lengths.is_empty() { |
|
"none".to_string() |
|
} else { |
|
self.dash_lengths.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", ") |
|
} |
|
} |
|
|
|
pub fn dash_offset(&self) -> f64 { |
|
self.dash_offset |
|
} |
|
|
|
pub fn cap_index(&self) -> u32 { |
|
self.cap as u32 |
|
} |
|
|
|
pub fn join_index(&self) -> u32 { |
|
self.join as u32 |
|
} |
|
|
|
pub fn join_miter_limit(&self) -> f32 { |
|
self.join_miter_limit as f32 |
|
} |
|
|
|
pub fn with_color(mut self, color: &Option<Color>) -> Option<Self> { |
|
self.color = *color; |
|
|
|
Some(self) |
|
} |
|
|
|
pub fn with_weight(mut self, weight: f64) -> Self { |
|
self.weight = weight; |
|
self |
|
} |
|
|
|
pub fn with_dash_lengths(mut self, dash_lengths: &str) -> Option<Self> { |
|
dash_lengths |
|
.split(&[',', ' ']) |
|
.filter(|x| !x.is_empty()) |
|
.map(str::parse::<f64>) |
|
.collect::<Result<Vec<_>, _>>() |
|
.ok() |
|
.map(|lengths| { |
|
self.dash_lengths = lengths; |
|
self |
|
}) |
|
} |
|
|
|
pub fn with_dash_offset(mut self, dash_offset: f64) -> Self { |
|
self.dash_offset = dash_offset; |
|
self |
|
} |
|
|
|
pub fn with_stroke_cap(mut self, stroke_cap: StrokeCap) -> Self { |
|
self.cap = stroke_cap; |
|
self |
|
} |
|
|
|
pub fn with_stroke_join(mut self, stroke_join: StrokeJoin) -> Self { |
|
self.join = stroke_join; |
|
self |
|
} |
|
|
|
pub fn with_stroke_join_miter_limit(mut self, limit: f64) -> Self { |
|
self.join_miter_limit = limit; |
|
self |
|
} |
|
|
|
pub fn with_stroke_align(mut self, stroke_align: StrokeAlign) -> Self { |
|
self.align = stroke_align; |
|
self |
|
} |
|
|
|
pub fn with_non_scaling(mut self, non_scaling: bool) -> Self { |
|
self.non_scaling = non_scaling; |
|
self |
|
} |
|
|
|
pub fn has_renderable_stroke(&self) -> bool { |
|
self.weight > 0. && self.color.is_some_and(|color| color.a() != 0.) |
|
} |
|
} |
|
|
|
|
|
impl Default for Stroke { |
|
fn default() -> Self { |
|
Self { |
|
weight: 0., |
|
color: Some(Color::from_rgba8_srgb(0, 0, 0, 255)), |
|
dash_lengths: Vec::new(), |
|
dash_offset: 0., |
|
cap: StrokeCap::Butt, |
|
join: StrokeJoin::Miter, |
|
join_miter_limit: 4., |
|
align: StrokeAlign::Center, |
|
transform: DAffine2::IDENTITY, |
|
non_scaling: false, |
|
paint_order: PaintOrder::default(), |
|
} |
|
} |
|
} |
|
|
|
#[repr(C)] |
|
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] |
|
pub struct PathStyle { |
|
pub stroke: Option<Stroke>, |
|
pub fill: Fill, |
|
} |
|
|
|
impl std::hash::Hash for PathStyle { |
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { |
|
self.stroke.hash(state); |
|
self.fill.hash(state); |
|
} |
|
} |
|
|
|
impl std::fmt::Display for PathStyle { |
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
|
let fill = &self.fill; |
|
|
|
let stroke = match &self.stroke { |
|
Some(stroke) => format!("#{} (Weight: {} px)", stroke.color.map_or("None".to_string(), |c| c.to_rgba_hex_srgb()), stroke.weight), |
|
None => "None".to_string(), |
|
}; |
|
|
|
write!(f, "Fill: {fill}\nStroke: {stroke}") |
|
} |
|
} |
|
|
|
impl PathStyle { |
|
pub const fn new(stroke: Option<Stroke>, fill: Fill) -> Self { |
|
Self { stroke, fill } |
|
} |
|
|
|
pub fn lerp(&self, other: &Self, time: f64) -> Self { |
|
Self { |
|
fill: self.fill.lerp(&other.fill, time), |
|
stroke: match (self.stroke.as_ref(), other.stroke.as_ref()) { |
|
(Some(a), Some(b)) => Some(a.lerp(b, time)), |
|
(Some(a), None) => { |
|
if time < 0.5 { |
|
Some(a.clone()) |
|
} else { |
|
None |
|
} |
|
} |
|
(None, Some(b)) => { |
|
if time < 0.5 { |
|
Some(b.clone()) |
|
} else { |
|
None |
|
} |
|
} |
|
(None, None) => None, |
|
}, |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn fill(&self) -> &Fill { |
|
&self.fill |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn stroke(&self) -> Option<Stroke> { |
|
self.stroke.clone() |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn set_fill(&mut self, fill: Fill) { |
|
self.fill = fill; |
|
} |
|
|
|
pub fn set_stroke_transform(&mut self, transform: DAffine2) { |
|
if let Some(stroke) = &mut self.stroke { |
|
stroke.transform = transform; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn set_stroke(&mut self, stroke: Stroke) { |
|
self.stroke = Some(stroke); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn clear_fill(&mut self) { |
|
self.fill = Fill::None; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn clear_stroke(&mut self) { |
|
self.stroke = None; |
|
} |
|
} |
|
|
|
|
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] |
|
pub enum ViewMode { |
|
|
|
#[default] |
|
Normal, |
|
|
|
Outline, |
|
|
|
Pixels, |
|
} |
|
|