use bevy::color::Srgba;
use bevy::math::Vec2;
use tiny_skia::{FillRule, LineCap, LineJoin, Paint, Path, PathBuilder, Pixmap, Stroke, Transform};
use super::{
FillRuleKind, LinecapKind, LinejoinKind, PathData, PathSeg, ShapeAttrs, ShapeKind, ShapePaint,
ShapeTransform, ViewBox,
};
use crate::protocol::{animatable::Animatable, animatable::AnimatableField};
#[cfg(test)]
mod tests;
pub(super) const KAPPA: f32 = 0.552_284_8;
impl From<&ShapeTransform> for Transform {
fn from(t: &ShapeTransform) -> Self {
let ShapeTransform([a, b, c, d, e, f]) = *t;
Transform::from_row(a, b, c, d, e, f)
}
}
pub(crate) fn meet_transform(min: Vec2, size: Vec2, w: u32, h: u32) -> Transform {
let scale = (w as f32 / size.x).min(h as f32 / size.y);
let tx = (w as f32 - size.x * scale) * 0.5 - min.x * scale;
let ty = (h as f32 - size.y * scale) * 0.5 - min.y * scale;
Transform::from_row(scale, 0.0, 0.0, scale, tx, ty)
}
pub(crate) fn view_box_transform(
view_box: Option<&ViewBox>,
w_px: u32,
h_px: u32,
scale_factor: f32,
) -> Transform {
match view_box {
Some(vb) if vb.size.x > 0.0 && vb.size.y > 0.0 => {
meet_transform(vb.min, vb.size, w_px, h_px)
}
_ => Transform::from_scale(scale_factor, scale_factor),
}
}
pub(crate) fn shape_path(kind: ShapeKind, attrs: &ShapeAttrs) -> Option<Path> {
let g = |v: &Option<Animatable<f32>>| v.static_or_seed().unwrap_or(0.0);
match kind {
ShapeKind::Rect => rect_path(attrs),
ShapeKind::Circle => {
let r = g(&attrs.r);
if !(r.is_finite() && r > 0.0) {
return None;
}
ellipse_path(g(&attrs.cx), g(&attrs.cy), r, r)
}
ShapeKind::Ellipse => {
let rx = valid_radius(attrs.rx.static_or_seed())
.or(valid_radius(attrs.ry.static_or_seed()))
.unwrap_or(0.0);
let ry = valid_radius(attrs.ry.static_or_seed())
.or(valid_radius(attrs.rx.static_or_seed()))
.unwrap_or(0.0);
if rx <= 0.0 || ry <= 0.0 {
return None;
}
ellipse_path(g(&attrs.cx), g(&attrs.cy), rx, ry)
}
ShapeKind::Line => {
let mut pb = PathBuilder::new();
pb.move_to(g(&attrs.x1), g(&attrs.y1));
pb.line_to(g(&attrs.x2), g(&attrs.y2));
pb.finish()
}
ShapeKind::Polyline => poly_path(attrs.points.as_deref(), false),
ShapeKind::Polygon => poly_path(attrs.points.as_deref(), true),
ShapeKind::Path => replay_path(attrs.d.as_ref()?),
ShapeKind::Group => None,
}
}
pub(crate) fn paint_shape(
pixmap: &mut Pixmap,
kind: ShapeKind,
attrs: &ShapeAttrs,
transform: Transform,
inherited_opacity: f32,
) {
let Some(path) = shape_path(kind, attrs) else {
return;
};
let full = match &attrs.transform {
Some(t) => transform.pre_concat(t.into()),
None => transform,
};
let opacity = attrs
.opacity
.static_or_seed()
.unwrap_or(1.0)
.clamp(0.0, 1.0)
* inherited_opacity.clamp(0.0, 1.0);
if kind != ShapeKind::Line {
match attrs.fill.unwrap_or(ShapePaint::Color(Srgba::BLACK)) {
ShapePaint::Color(c) => {
let rule = match attrs.fill_rule.unwrap_or(FillRuleKind::NonZero) {
FillRuleKind::NonZero => FillRule::Winding,
FillRuleKind::EvenOdd => FillRule::EvenOdd,
};
pixmap.fill_path(&path, &solid(c, opacity), rule, full, None);
}
ShapePaint::None => {}
}
}
match attrs.stroke {
Some(ShapePaint::Color(c)) => {
let width = attrs.stroke_width.static_or_seed().unwrap_or(1.0);
let b = path.bounds();
if width.is_finite() && width > 0.0 && (b.width() > 0.0 || b.height() > 0.0) {
let stroke = Stroke {
width,
line_cap: match attrs.stroke_linecap.unwrap_or(LinecapKind::Butt) {
LinecapKind::Butt => LineCap::Butt,
LinecapKind::Round => LineCap::Round,
LinecapKind::Square => LineCap::Square,
},
line_join: match attrs.stroke_linejoin.unwrap_or(LinejoinKind::Miter) {
LinejoinKind::Miter => LineJoin::Miter,
LinejoinKind::Round => LineJoin::Round,
LinejoinKind::Bevel => LineJoin::Bevel,
},
..Stroke::default()
};
pixmap.stroke_path(&path, &solid(c, opacity), &stroke, full, None);
}
}
Some(ShapePaint::None) | None => {}
}
}
pub(super) fn valid_radius(v: Option<f32>) -> Option<f32> {
v.filter(|v| v.is_finite() && *v >= 0.0)
}
fn rect_path(attrs: &ShapeAttrs) -> Option<Path> {
let (w, h) = (
attrs.width.static_or_seed().unwrap_or(0.0),
attrs.height.static_or_seed().unwrap_or(0.0),
);
if !(w.is_finite() && w > 0.0 && h.is_finite() && h > 0.0) {
return None;
}
let (x, y) = (
attrs.x.static_or_seed().unwrap_or(0.0),
attrs.y.static_or_seed().unwrap_or(0.0),
);
let rx = valid_radius(attrs.rx.static_or_seed())
.or(valid_radius(attrs.ry.static_or_seed()))
.unwrap_or(0.0);
let ry = valid_radius(attrs.ry.static_or_seed())
.or(valid_radius(attrs.rx.static_or_seed()))
.unwrap_or(0.0);
let (rx, ry) = (rx.min(w * 0.5), ry.min(h * 0.5));
let mut pb = PathBuilder::new();
if rx <= 0.0 || ry <= 0.0 {
pb.push_rect(tiny_skia::Rect::from_xywh(x, y, w, h)?);
return pb.finish();
}
let (kx, ky) = (rx * KAPPA, ry * KAPPA);
let (r, b) = (x + w, y + h); pb.move_to(x + rx, y);
pb.line_to(r - rx, y);
pb.cubic_to(r - rx + kx, y, r, y + ry - ky, r, y + ry);
pb.line_to(r, b - ry);
pb.cubic_to(r, b - ry + ky, r - rx + kx, b, r - rx, b);
pb.line_to(x + rx, b);
pb.cubic_to(x + rx - kx, b, x, b - ry + ky, x, b - ry);
pb.line_to(x, y + ry);
pb.cubic_to(x, y + ry - ky, x + rx - kx, y, x + rx, y);
pb.close();
pb.finish()
}
fn ellipse_path(cx: f32, cy: f32, rx: f32, ry: f32) -> Option<Path> {
let (kx, ky) = (rx * KAPPA, ry * KAPPA);
let mut pb = PathBuilder::new();
pb.move_to(cx + rx, cy);
pb.cubic_to(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry);
pb.cubic_to(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy);
pb.cubic_to(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry);
pb.cubic_to(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy);
pb.close();
pb.finish()
}
fn poly_path(points: Option<&[Vec2]>, close: bool) -> Option<Path> {
let pts = points.unwrap_or(&[]);
if pts.len() < 2 {
return None;
}
let mut pb = PathBuilder::new();
pb.move_to(pts[0].x, pts[0].y);
for p in &pts[1..] {
pb.line_to(p.x, p.y);
}
if close {
pb.close();
}
pb.finish()
}
fn replay_path(d: &PathData) -> Option<Path> {
let mut pb = PathBuilder::new();
for seg in &d.0 {
match *seg {
PathSeg::MoveTo { x, y } => pb.move_to(x, y),
PathSeg::LineTo { x, y } => pb.line_to(x, y),
PathSeg::QuadTo { c1x, c1y, x, y } => pb.quad_to(c1x, c1y, x, y),
PathSeg::CubicTo {
c1x,
c1y,
c2x,
c2y,
x,
y,
} => pb.cubic_to(c1x, c1y, c2x, c2y, x, y),
PathSeg::Close => pb.close(),
}
}
pb.finish()
}
fn solid(c: Srgba, opacity: f32) -> Paint<'static> {
let mut paint = Paint {
anti_alias: true,
..Paint::default()
};
let color = tiny_skia::Color::from_rgba(
c.red.clamp(0.0, 1.0),
c.green.clamp(0.0, 1.0),
c.blue.clamp(0.0, 1.0),
(c.alpha * opacity).clamp(0.0, 1.0),
)
.unwrap_or(tiny_skia::Color::BLACK);
paint.set_color(color);
paint
}