use std::fmt;
use bevy::color::Srgba;
use bevy::math::Vec2;
use serde::Deserialize;
use serde::de::{self, Deserializer, Visitor};
use crate::canvas::parse_css_color;
use crate::protocol::{animatable::Animatable, decode_warn};
mod path;
#[cfg(test)]
mod tests;
pub use path::{PathData, PathSeg};
#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct ShapeAttrs {
pub x: Option<Animatable<f32>>,
pub y: Option<Animatable<f32>>,
pub width: Option<Animatable<f32>>,
pub height: Option<Animatable<f32>>,
pub cx: Option<Animatable<f32>>,
pub cy: Option<Animatable<f32>>,
pub r: Option<Animatable<f32>>,
pub rx: Option<Animatable<f32>>,
pub ry: Option<Animatable<f32>>,
pub x1: Option<Animatable<f32>>,
pub y1: Option<Animatable<f32>>,
pub x2: Option<Animatable<f32>>,
pub y2: Option<Animatable<f32>>,
#[serde(deserialize_with = "de_points")]
pub points: Option<Vec<Vec2>>,
#[serde(deserialize_with = "de_path")]
pub d: Option<PathData>,
#[serde(deserialize_with = "de_paint")]
pub fill: Option<ShapePaint>,
#[serde(deserialize_with = "de_paint")]
pub stroke: Option<ShapePaint>,
pub stroke_width: Option<Animatable<f32>>,
pub opacity: Option<Animatable<f32>>,
#[serde(deserialize_with = "de_fill_rule")]
pub fill_rule: Option<FillRuleKind>,
#[serde(deserialize_with = "de_linecap")]
pub stroke_linecap: Option<LinecapKind>,
#[serde(deserialize_with = "de_linejoin")]
pub stroke_linejoin: Option<LinejoinKind>,
#[serde(deserialize_with = "de_transform")]
pub transform: Option<ShapeTransform>,
#[serde(deserialize_with = "de_transition")]
pub transition: Option<Box<ShapeTransitionSpec>>,
}
pub(crate) type NumericAttrAccessor = fn(&ShapeAttrs) -> &Option<Animatable<f32>>;
pub(crate) type NumericAttrAccessorMut = fn(&mut ShapeAttrs) -> &mut Option<Animatable<f32>>;
pub(crate) const NUMERIC_ATTR_COUNT: usize = 15;
pub(crate) const NUMERIC_ATTRS: [(&str, NumericAttrAccessor, NumericAttrAccessorMut);
NUMERIC_ATTR_COUNT] = [
("x", |a| &a.x, |a| &mut a.x),
("y", |a| &a.y, |a| &mut a.y),
("width", |a| &a.width, |a| &mut a.width),
("height", |a| &a.height, |a| &mut a.height),
("cx", |a| &a.cx, |a| &mut a.cx),
("cy", |a| &a.cy, |a| &mut a.cy),
("r", |a| &a.r, |a| &mut a.r),
("rx", |a| &a.rx, |a| &mut a.rx),
("ry", |a| &a.ry, |a| &mut a.ry),
("x1", |a| &a.x1, |a| &mut a.x1),
("y1", |a| &a.y1, |a| &mut a.y1),
("x2", |a| &a.x2, |a| &mut a.x2),
("y2", |a| &a.y2, |a| &mut a.y2),
("strokeWidth", |a| &a.stroke_width, |a| &mut a.stroke_width),
("opacity", |a| &a.opacity, |a| &mut a.opacity),
];
pub(crate) fn numeric_attr_mut<'a>(
attrs: &'a mut ShapeAttrs,
name: &str,
) -> Option<&'a mut Option<Animatable<f32>>> {
NUMERIC_ATTRS
.iter()
.find(|(n, _, _)| *n == name)
.map(|(_, _, m)| m(attrs))
}
pub(crate) fn numeric_attr<'a>(
attrs: &'a ShapeAttrs,
name: &str,
) -> Option<&'a Option<Animatable<f32>>> {
NUMERIC_ATTRS
.iter()
.find(|(n, _, _)| *n == name)
.map(|(_, r, _)| r(attrs))
}
#[cfg(test)]
pub(crate) fn st(v: f32) -> Option<Animatable<f32>> {
Some(Animatable::Static(v))
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ShapeTransitionSpec {
entries: [Option<crate::transition::ChannelTransition>; NUMERIC_ATTR_COUNT],
}
impl ShapeTransitionSpec {
pub fn for_attr(&self, name: &str) -> Option<&crate::transition::ChannelTransition> {
NUMERIC_ATTRS
.iter()
.position(|(n, _, _)| *n == name)
.and_then(|i| self.entries[i].as_ref())
}
pub(crate) fn at(&self, index: usize) -> Option<&crate::transition::ChannelTransition> {
self.entries[index].as_ref()
}
}
fn de_transition<'de, D: Deserializer<'de>>(
d: D,
) -> Result<Option<Box<ShapeTransitionSpec>>, D::Error> {
let Some(value) = Option::<serde_json::Value>::deserialize(d)? else {
return Ok(None);
};
let serde_json::Value::Object(map) = value else {
if !value.is_null() {
decode_warn(
"shapeTransition",
&value.to_string(),
"transition takes an object of per-attr timing specs; dropping",
);
}
return Ok(None);
};
let mut spec = ShapeTransitionSpec::default();
for (key, entry) in map {
let Some(i) = NUMERIC_ATTRS.iter().position(|(n, _, _)| *n == key) else {
decode_warn(
"shapeTransition",
&key,
&format!("`{key}` is not a numeric shape attr (only those ease); dropping"),
);
continue;
};
match serde_json::from_value(entry) {
Ok(timing) => spec.entries[i] = Some(timing),
Err(e) => {
decode_warn(
"shapeTransition",
&key,
&format!("invalid transition spec for `{key}`: {e}; dropping"),
);
}
}
}
Ok(Some(Box::new(spec)))
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ShapePaint {
None,
Color(Srgba),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FillRuleKind {
NonZero,
EvenOdd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinecapKind {
Butt,
Round,
Square,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinejoinKind {
Miter,
Round,
Bevel,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ShapeTransform(pub [f32; 6]);
impl Default for ShapeTransform {
fn default() -> Self {
ShapeTransform([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])
}
}
const IDENTITY: [f64; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
fn mul(a: [f64; 6], b: [f64; 6]) -> [f64; 6] {
[
a[0] * b[0] + a[2] * b[1],
a[1] * b[0] + a[3] * b[1],
a[0] * b[2] + a[2] * b[3],
a[1] * b[2] + a[3] * b[3],
a[0] * b[4] + a[2] * b[5] + a[4],
a[1] * b[4] + a[3] * b[5] + a[5],
]
}
impl ShapeTransform {
pub(crate) fn parse(s: &str) -> Result<ShapeTransform, String> {
use svgtypes::{TransformListParser, TransformListToken as T};
let mut m = IDENTITY;
for token in TransformListParser::from(s) {
let token = token.map_err(|e| format!("invalid transform {s:?}: {e}"))?;
let t = match token {
T::Translate { tx, ty } => [1.0, 0.0, 0.0, 1.0, tx, ty],
T::Scale { sx, sy } => [sx, 0.0, 0.0, sy, 0.0, 0.0],
T::Rotate { angle } => {
let (sin, cos) = angle.to_radians().sin_cos();
[cos, sin, -sin, cos, 0.0, 0.0]
}
T::Matrix { .. } | T::SkewX { .. } | T::SkewY { .. } => {
return Err(format!(
"unsupported transform function in {s:?} \
(v1 supports translate/scale/rotate)"
));
}
};
m = mul(m, t);
}
Ok(ShapeTransform(m.map(|v| v as f32)))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ViewBox {
pub min: Vec2,
pub size: Vec2,
}
impl ViewBox {
pub(crate) fn parse(s: &str) -> Result<ViewBox, String> {
let vb: svgtypes::ViewBox = s
.parse()
.map_err(|e| format!("invalid viewBox {s:?}: {e}"))?;
Ok(ViewBox {
min: Vec2::new(vb.x as f32, vb.y as f32),
size: Vec2::new(vb.w as f32, vb.h as f32),
})
}
}
pub(crate) fn de_view_box<'de, D: Deserializer<'de>>(d: D) -> Result<Option<ViewBox>, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<ViewBox>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a viewBox string \"minX minY width height\"")
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
Ok(match ViewBox::parse(s) {
Ok(vb) => Some(vb),
Err(e) => {
decode_warn("viewBox", s, &e);
None
}
})
}
fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
warn_object_dropped(map, "viewBox").map(|()| None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
d.deserialize_any(V)
}
fn warn_object_dropped<'de, A: de::MapAccess<'de>>(
map: A,
kind: &'static str,
) -> Result<(), A::Error> {
let v = serde_json::Value::deserialize(de::value::MapAccessDeserializer::new(map))?;
let hint = if v.get("animated").is_some() {
" (only numeric shape attrs accept { animated } bindings)"
} else {
""
};
decode_warn(
kind,
&v.to_string(),
&format!("unexpected object value{hint}; dropping"),
);
Ok(())
}
fn de_path<'de, D: Deserializer<'de>>(d: D) -> Result<Option<PathData>, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<PathData>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("an SVG path data string")
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
Ok(match PathData::parse(s) {
Ok(p) => Some(p),
Err(e) => {
decode_warn("shapePath", s, &e);
None
}
})
}
fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
warn_object_dropped(map, "shapePath").map(|()| None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
d.deserialize_any(V)
}
fn de_points<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<Vec2>>, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<Vec<Vec2>>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a flat number array [x0, y0, x1, y1, …]")
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut nums = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(n) = seq.next_element::<f32>()? {
nums.push(n);
}
if nums.len() % 2 != 0 {
decode_warn(
"shapePoints",
&format!("[{} numbers]", nums.len()),
&format!(
"points needs an even number of coordinates, got {}; dropping",
nums.len()
),
);
return Ok(None);
}
Ok(Some(
nums.chunks_exact(2)
.map(|p| Vec2::new(p[0], p[1]))
.collect(),
))
}
fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
warn_object_dropped(map, "shapePoints").map(|()| None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
d.deserialize_any(V)
}
fn de_paint<'de, D: Deserializer<'de>>(d: D) -> Result<Option<ShapePaint>, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<ShapePaint>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a CSS color string or the keyword \"none\"")
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
if s == "none" {
return Ok(Some(ShapePaint::None));
}
Ok(match parse_css_color(s) {
Some(c) => Some(ShapePaint::Color(c)),
None => {
decode_warn("shapePaint", s, &format!("unrecognized paint {s:?}"));
None
}
})
}
fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
warn_object_dropped(map, "shapePaint").map(|()| None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
d.deserialize_any(V)
}
fn de_transform<'de, D: Deserializer<'de>>(d: D) -> Result<Option<ShapeTransform>, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<ShapeTransform>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("an SVG transform list string")
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
Ok(match ShapeTransform::parse(s) {
Ok(t) => Some(t),
Err(e) => {
decode_warn("shapeTransform", s, &e);
None
}
})
}
fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
warn_object_dropped(map, "shapeTransform").map(|()| None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
d.deserialize_any(V)
}
macro_rules! shape_keywords {
($( fn $fn_name:ident($ty:ident) { $($kw:literal => $variant:ident),+ $(,)? } )+) => { $(
fn $fn_name<'de, D: Deserializer<'de>>(d: D) -> Result<Option<$ty>, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<$ty>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(concat!("a `", stringify!($ty), "` keyword"))
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
Ok(match s {
$( $kw => Some(<$ty>::$variant), )+
_ => {
decode_warn(
"shapeEnum",
s,
&format!(
concat!("unrecognized ", stringify!($ty), " keyword {:?}"),
s
),
);
None
}
})
}
fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
warn_object_dropped(map, "shapeEnum").map(|()| None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
d.deserialize_any(V)
}
)+ };
}
shape_keywords! {
fn de_fill_rule(FillRuleKind) {
"nonzero" => NonZero, "evenodd" => EvenOdd,
}
fn de_linecap(LinecapKind) {
"butt" => Butt, "round" => Round, "square" => Square,
}
fn de_linejoin(LinejoinKind) {
"miter" => Miter, "round" => Round, "bevel" => Bevel,
}
}