blob: df4807e8089b87c102aadc3c7d552fa91621403f [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::{collections::HashMap, path::Path, time::Duration};
use forma::{
BlendMode, Composition, Fill, FillRule, Func, Gradient, GradientBuilder, GradientType, Order,
Point, Props, Style,
};
use svg::{
node::{element::tag, Value},
parser::Event,
};
use svgtypes::{Color, PathParser, PathSegment, StyleParser, Transform};
use crate::{App, Keyboard};
fn reflect(point: Point, against: Point) -> Point {
Point::new(against.x * 2.0 - point.x, against.y * 2.0 - point.y)
}
struct EllipticalArc {
cx: f32,
cy: f32,
rx: f32,
ry: f32,
x_axis_rotation: f32,
angle: f32,
angle_delta: f32,
}
#[allow(clippy::too_many_arguments)]
fn convert_to_center(
rx: f32,
ry: f32,
x_axis_rotation: f32,
large_arc: bool,
sweep: bool,
x0: f32,
y0: f32,
x1: f32,
y1: f32,
) -> Option<EllipticalArc> {
if (x0 - x1).abs() < f32::EPSILON && (y0 - y1).abs() < f32::EPSILON {
return None;
}
let rx = rx.abs();
let ry = ry.abs();
if rx == 0.0 || ry == 0.0 {
return None;
}
let cos_phi = x_axis_rotation.cos();
let sin_phi = x_axis_rotation.sin();
let x0 = (x0 * cos_phi + y0 * sin_phi) / rx;
let y0 = (-x0 * sin_phi + y0 * cos_phi) / ry;
let x1 = (x1 * cos_phi + y1 * sin_phi) / rx;
let y1 = (-x1 * sin_phi + y1 * cos_phi) / ry;
let lx = (x0 - x1) * 0.5;
let ly = (y0 - y1) * 0.5;
let mut cx = (x0 + x1) * 0.5;
let mut cy = (y0 + y1) * 0.5;
let len_squared = lx * lx + ly * ly;
if len_squared < 1.0 {
let mut radicand = ((1.0 - len_squared) / len_squared).sqrt();
if large_arc != sweep {
radicand = -radicand;
}
cx += -ly * radicand;
cy += lx * radicand;
}
let theta = (y0 - cy).atan2(x0 - cx);
let mut delta_theta = (y1 - cy).atan2(x1 - cx) - theta;
let cxs = cx * rx;
let cys = cy * ry;
cx = cxs * cos_phi - cys * sin_phi;
cy = cxs * sin_phi + cys * cos_phi;
if sweep {
if delta_theta < 0.0 {
delta_theta += std::f32::consts::PI * 2.0;
}
} else if delta_theta > 0.0 {
delta_theta -= std::f32::consts::PI * 2.0;
}
Some(EllipticalArc { cx, cy, rx, ry, x_axis_rotation, angle: theta, angle_delta: delta_theta })
}
fn parse_fill_rule(attrs: &HashMap<String, Value>) -> FillRule {
if let Some(fill_rule) = attrs.get("fill-rule") {
if &fill_rule.to_string() == "evenodd" {
return FillRule::EvenOdd;
}
}
FillRule::NonZero
}
fn parse_color(attrs: &HashMap<String, Value>) -> Option<Color> {
attrs.get("fill").or_else(|| attrs.get("stop-color")).and_then(|fill| fill.parse().ok())
}
fn parse_opacity(attrs: &HashMap<String, Value>) -> Option<f32> {
attrs
.get("opacity")
.or_else(|| attrs.get("stop-opacity"))
.and_then(|opacity| opacity.parse().ok())
.or_else(|| attrs.get("fill-opacity").and_then(|opacity| opacity.parse().ok()))
}
fn parse_blend_mode(attrs: &HashMap<String, Value>) -> Option<BlendMode> {
attrs.get("style").and_then(|style| {
StyleParser::from(style.as_ref()).find_map(|pair| {
pair.ok().and_then(|pair| {
(pair.0 == "mix-blend-mode").then_some(match pair.1 {
"normal" => BlendMode::Over,
"multiply" => BlendMode::Multiply,
"screen" => BlendMode::Screen,
"overlay" => BlendMode::Overlay,
"darken" => BlendMode::Darken,
"lighten" => BlendMode::Lighten,
"color-dodge" => BlendMode::ColorDodge,
"color-burn" => BlendMode::ColorBurn,
"hard-light" => BlendMode::HardLight,
"soft-light" => BlendMode::SoftLight,
"difference" => BlendMode::Difference,
"exclusion" => BlendMode::Exclusion,
"hue" => BlendMode::Hue,
"saturation" => BlendMode::Saturation,
"color" => BlendMode::Color,
"luminosity" => BlendMode::Luminosity,
_ => BlendMode::Over,
})
})
})
})
}
#[derive(Debug, Default)]
struct Group {
transform: Option<Transform>,
fill: Option<Color>,
opacity: Option<f32>,
}
#[derive(Debug)]
pub struct Svg {
groups: Vec<Group>,
paths: Vec<(forma::Path, FillRule, Fill, BlendMode)>,
gradient_builder: Option<(String, GradientBuilder)>,
gradients: HashMap<String, Gradient>,
needs_composition: bool,
}
impl Svg {
pub fn new<P: AsRef<Path>>(path: P, scale: f32) -> Self {
let mut result = Self {
groups: vec![],
paths: vec![],
gradient_builder: None,
gradients: HashMap::new(),
needs_composition: true,
};
result.open(path);
let transform = &[scale, 0.0, 0.0, 0.0, scale, 0.0, 0.0, 0.0, 1.0];
for (path, ..) in &mut result.paths {
*path = path.transform(transform);
}
result
}
fn group_transform(&self) -> Option<&Transform> {
self.groups.iter().rev().find_map(|group| group.transform.as_ref())
}
fn group_fill(&self) -> Option<Color> {
self.groups.iter().rev().find_map(|group| group.fill)
}
fn groups_opacity(&self) -> f32 {
self.groups.iter().filter_map(|group| group.opacity).product()
}
fn t(&self, point: Point) -> Point {
match self.group_transform() {
None => point,
Some(t) => {
let mut x = f64::from(point.x);
let mut y = f64::from(point.y);
t.apply_to(&mut x, &mut y);
Point::new(x as f32, y as f32)
}
}
}
fn parse_fill(&self, attrs: &HashMap<String, Value>) -> Fill {
if let Some(gradient) = attrs.get("fill").and_then(|fill| {
let fill = fill.to_string();
fill.strip_prefix("url(#")
.and_then(|fill| fill.strip_suffix(')'))
.and_then(|id| self.gradients.get(id))
}) {
return Fill::Gradient(gradient.clone());
}
let color: Option<Color> =
parse_color(attrs).or_else(|| self.group_fill()).or_else(|| Some(Color::black()));
let opacity: f32 = parse_opacity(attrs).unwrap_or_else(|| self.groups_opacity());
let color = color.map_or(forma::Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, |color| {
forma::Color { a: opacity, ..crate::to_linear([color.red, color.green, color.blue]) }
});
Fill::Solid(color)
}
fn push_rationals_from_arc(
&self,
builder: &mut forma::PathBuilder,
arc: &EllipticalArc,
mut end_point: Point,
) -> Point {
let mut angle = arc.angle;
let mut angle_delta = arc.angle_delta;
let cos_phi = arc.x_axis_rotation.cos();
let sin_phi = arc.x_axis_rotation.sin();
let angle_sweep = std::f32::consts::PI / 2.0;
let angle_incr = if angle_delta > 0.0 { angle_sweep } else { -angle_sweep };
while angle_delta != 0.0 {
let theta = angle;
let sweep = if angle_delta.abs() <= angle_sweep { angle_delta } else { angle_incr };
angle += sweep;
angle_delta -= sweep;
let half_sweep = sweep * 0.5;
let w = half_sweep.cos();
let mut p1 = Point::new((theta + half_sweep).cos() / w, (theta + half_sweep).sin() / w);
let mut p2 = Point::new((theta + sweep).cos(), (theta + sweep).sin());
p1.x *= arc.rx;
p1.y *= arc.ry;
p2.x *= arc.rx;
p2.y *= arc.ry;
let p1 = Point::new(
(arc.cx + p1.x * cos_phi - p1.y * sin_phi) as f32,
(arc.cy + p1.x * sin_phi + p1.y * cos_phi) as f32,
);
let p2 = Point::new(
(arc.cx + p2.x * cos_phi - p2.y * sin_phi) as f32,
(arc.cy + p2.x * sin_phi + p2.y * cos_phi) as f32,
);
builder.rat_quad_to(self.t(p1), self.t(p2), w as f32);
end_point = p2;
}
end_point
}
pub fn open(&mut self, path: impl AsRef<Path>) {
for event in svg::open(path).unwrap() {
match event {
Event::Tag(tag::Group, tag::Type::Start, attrs) => {
self.groups.push(Group {
transform: attrs
.get("transform")
.and_then(|transform| transform.parse().ok()),
fill: parse_color(&attrs),
opacity: parse_opacity(&attrs),
});
}
Event::Tag(tag::Group, tag::Type::End, _) => {
self.groups.pop();
}
Event::Tag(tag::Path, _, attrs) => {
if attrs.get("stroke").filter(|val| val.to_string() != "none").is_some() {
continue;
}
let mut builder = forma::PathBuilder::new();
let data = match attrs.get("d") {
Some(data) => data,
None => continue,
};
let mut start_point = None;
let mut end_point = Point::new(0.0, 0.0);
let mut quad_control_point = None;
let mut cubic_control_point = None;
let add_diff = |end_point: Point, x, y| {
Point::new(end_point.x + x as f32, end_point.y + y as f32)
};
for segment in PathParser::from(data.to_string().as_str()) {
match segment.unwrap() {
PathSegment::MoveTo { abs: true, x, y } => {
let p = Point::new(x as f32, y as f32);
builder.move_to(self.t(p));
start_point.take();
end_point = p;
quad_control_point = None;
cubic_control_point = None;
}
PathSegment::MoveTo { abs: false, x, y } => {
let p = add_diff(end_point, x, y);
builder.move_to(self.t(p));
start_point.take();
end_point = p;
quad_control_point = None;
cubic_control_point = None;
}
PathSegment::LineTo { abs: true, x, y } => {
let p0 = Point::new(x as f32, y as f32);
builder.line_to(self.t(p0));
start_point.get_or_insert(end_point);
end_point = p0;
quad_control_point = None;
cubic_control_point = None;
}
PathSegment::LineTo { abs: false, x, y } => {
let p0 = add_diff(end_point, x, y);
builder.line_to(self.t(p0));
start_point.get_or_insert(end_point);
end_point = p0;
quad_control_point = None;
cubic_control_point = None;
}
PathSegment::HorizontalLineTo { abs: true, x } => {
let p0 = Point::new(x as f32, end_point.y);
builder.line_to(self.t(p0));
start_point.get_or_insert(end_point);
end_point = p0;
quad_control_point = None;
cubic_control_point = None;
}
PathSegment::HorizontalLineTo { abs: false, x } => {
let p0 = add_diff(end_point, x, 0.0);
builder.line_to(self.t(p0));
start_point.get_or_insert(end_point);
end_point = p0;
quad_control_point = None;
cubic_control_point = None;
}
PathSegment::VerticalLineTo { abs: true, y } => {
let p0 = Point::new(end_point.x, y as f32);
builder.line_to(self.t(p0));
start_point.get_or_insert(end_point);
end_point = p0;
quad_control_point = None;
cubic_control_point = None;
}
PathSegment::VerticalLineTo { abs: false, y } => {
let p0 = add_diff(end_point, 0.0, y);
builder.line_to(self.t(p0));
start_point.get_or_insert(end_point);
end_point = p0;
quad_control_point = None;
cubic_control_point = None;
}
PathSegment::Quadratic { abs: true, x1, y1, x, y } => {
let p0 = Point::new(x1 as f32, y1 as f32);
let p1 = Point::new(x as f32, y as f32);
let control_point = p0;
builder.quad_to(self.t(control_point), self.t(p1));
start_point.get_or_insert(end_point);
end_point = p1;
quad_control_point = Some(control_point);
cubic_control_point = None;
}
PathSegment::Quadratic { abs: false, x1, y1, x, y } => {
let p0 = add_diff(end_point, x1, y1);
let p1 = add_diff(end_point, x, y);
let control_point = p0;
builder.quad_to(self.t(control_point), self.t(p1));
start_point.get_or_insert(end_point);
end_point = p1;
quad_control_point = Some(control_point);
cubic_control_point = None;
}
PathSegment::CurveTo { abs: true, x1, y1, x2, y2, x, y } => {
let p0 = Point::new(x1 as f32, y1 as f32);
let p1 = Point::new(x2 as f32, y2 as f32);
let p2 = Point::new(x as f32, y as f32);
let control_point = p1;
builder.cubic_to(self.t(p0), self.t(control_point), self.t(p2));
start_point.get_or_insert(end_point);
end_point = p2;
quad_control_point = None;
cubic_control_point = Some(control_point);
}
PathSegment::CurveTo { abs: false, x1, y1, x2, y2, x, y } => {
let p0 = add_diff(end_point, x1, y1);
let p1 = add_diff(end_point, x2, y2);
let p2 = add_diff(end_point, x, y);
let control_point = p1;
builder.cubic_to(self.t(p0), self.t(control_point), self.t(p2));
start_point.get_or_insert(end_point);
end_point = p2;
quad_control_point = None;
cubic_control_point = Some(control_point);
}
PathSegment::SmoothQuadratic { abs: true, x, y } => {
let p1 = Point::new(x as f32, y as f32);
let control_point =
reflect(quad_control_point.unwrap_or(end_point), end_point);
builder.quad_to(self.t(control_point), self.t(p1));
start_point.get_or_insert(end_point);
end_point = p1;
quad_control_point = Some(control_point);
cubic_control_point = None;
}
PathSegment::SmoothQuadratic { abs: false, x, y } => {
let p1 = add_diff(end_point, x, y);
let control_point =
reflect(quad_control_point.unwrap_or(end_point), end_point);
builder.quad_to(self.t(control_point), self.t(p1));
start_point.get_or_insert(end_point);
end_point = p1;
quad_control_point = Some(control_point);
cubic_control_point = None;
}
PathSegment::SmoothCurveTo { abs: true, x2, y2, x, y } => {
let p1 = Point::new(x2 as f32, y2 as f32);
let p2 = Point::new(x as f32, y as f32);
let control_point =
reflect(cubic_control_point.unwrap_or(end_point), end_point);
builder.cubic_to(self.t(control_point), self.t(p1), self.t(p2));
start_point.get_or_insert(end_point);
end_point = p2;
quad_control_point = None;
cubic_control_point = Some(control_point);
}
PathSegment::SmoothCurveTo { abs: false, x2, y2, x, y } => {
let p1 = add_diff(end_point, x2, y2);
let p2 = add_diff(end_point, x, y);
let control_point =
reflect(cubic_control_point.unwrap_or(end_point), end_point);
builder.cubic_to(self.t(control_point), self.t(p1), self.t(p2));
start_point.get_or_insert(end_point);
end_point = p2;
quad_control_point = None;
cubic_control_point = Some(control_point);
}
PathSegment::EllipticalArc {
abs: true,
rx,
ry,
x_axis_rotation,
large_arc,
sweep,
x,
y,
} => {
let arc = convert_to_center(
rx as f32,
ry as f32,
x_axis_rotation as f32,
large_arc,
sweep,
end_point.x as f32,
end_point.y as f32,
x as f32,
y as f32,
);
if let Some(arc) = arc {
let new_end_point =
self.push_rationals_from_arc(&mut builder, &arc, end_point);
start_point.get_or_insert(end_point);
end_point = new_end_point;
}
quad_control_point = None;
cubic_control_point = None;
}
PathSegment::EllipticalArc {
abs: false,
rx,
ry,
x_axis_rotation,
large_arc,
sweep,
x,
y,
} => {
let p0 = add_diff(end_point, x, y);
let arc = convert_to_center(
rx as f32,
ry as f32,
x_axis_rotation as f32,
large_arc,
sweep,
end_point.x as f32,
end_point.y as f32,
p0.x as f32,
p0.y as f32,
);
if let Some(arc) = arc {
let new_end_point =
self.push_rationals_from_arc(&mut builder, &arc, end_point);
start_point.get_or_insert(end_point);
end_point = new_end_point;
}
quad_control_point = None;
cubic_control_point = None;
}
PathSegment::ClosePath { .. } => {
if let Some(start_point) = start_point.take() {
end_point = start_point;
quad_control_point = None;
cubic_control_point = None;
}
}
}
}
let fill_command = parse_fill_rule(&attrs);
let blend_mode = parse_blend_mode(&attrs).unwrap_or_default();
self.paths.push((
builder.build(),
fill_command,
self.parse_fill(&attrs),
blend_mode,
));
}
Event::Tag(tag::Rectangle, tag::Type::Start | tag::Type::Empty, attrs) => {
if attrs.get("stroke").filter(|val| val.to_string() != "none").is_some() {
continue;
}
let mut builder = forma::PathBuilder::new();
let x: f32 =
attrs.get("x").and_then(|opacity| opacity.parse().ok()).unwrap_or_default();
let y: f32 =
attrs.get("y").and_then(|opacity| opacity.parse().ok()).unwrap_or_default();
let width: f32 =
attrs.get("width").expect("rect missing width").parse().unwrap();
let height: f32 =
attrs.get("height").expect("rect missing height").parse().unwrap();
builder.move_to(Point::new(x, y));
builder.line_to(Point::new(x, y + height));
builder.line_to(Point::new(x + width, y + height));
builder.line_to(Point::new(x + width, y));
builder.line_to(Point::new(x, y));
let fill_command = parse_fill_rule(&attrs);
let blend_mode = parse_blend_mode(&attrs).unwrap_or_default();
self.paths.push((
builder.build(),
fill_command,
self.parse_fill(&attrs),
blend_mode,
));
}
Event::Tag(tag::LinearGradient, tag::Type::Start, attrs) => {
if !attrs
.get("gradientUnits")
.map(|value| &**value == "userSpaceOnUse")
.unwrap_or_default()
{
continue;
}
let id = attrs.get("id").expect("linearGradient missing id").to_string();
let x1: f32 = attrs
.get("x1")
.and_then(|opacity| opacity.parse().ok())
.expect("linearGradient missing x1");
let y1: f32 = attrs
.get("y1")
.and_then(|opacity| opacity.parse().ok())
.expect("linearGradient missing y1");
let x2: f32 = attrs
.get("x2")
.and_then(|opacity| opacity.parse().ok())
.expect("linearGradient missing x2");
let y2: f32 = attrs
.get("y2")
.and_then(|opacity| opacity.parse().ok())
.expect("linearGradient missing y2");
let mut gradient_builder =
GradientBuilder::new(Point::new(x1, y1), Point::new(x2, y2));
gradient_builder.r#type(GradientType::Linear);
self.gradient_builder = Some((id, gradient_builder));
}
Event::Tag(tag::LinearGradient, tag::Type::End, _) => {
let (id, gradient_builder) =
self.gradient_builder.take().expect("linearGradient missing start tag");
self.gradients.insert(
id,
gradient_builder.build().expect("linearGradient requires at least 2 stops"),
);
}
Event::Tag(tag::RadialGradient, tag::Type::Start, attrs) => {
if !attrs
.get("gradientUnits")
.map(|value| &**value == "userSpaceOnUse")
.unwrap_or_default()
{
continue;
}
let id = attrs.get("id").expect("radialGradient missing id").to_string();
let cx: f32 = attrs
.get("cx")
.and_then(|opacity| opacity.parse().ok())
.expect("radialGradient missing cx");
let cy: f32 = attrs
.get("cy")
.and_then(|opacity| opacity.parse().ok())
.expect("radialGradient missing cy");
let r: f32 = attrs
.get("r")
.and_then(|opacity| opacity.parse().ok())
.expect("radialGradient missing r");
let mut gradient_builder =
GradientBuilder::new(Point::new(cx, cy), Point::new(cx + r, cy));
gradient_builder.r#type(GradientType::Radial);
self.gradient_builder = Some((id, gradient_builder));
}
Event::Tag(tag::RadialGradient, tag::Type::End, _) => {
let (id, gradient_builder) =
self.gradient_builder.take().expect("radialGradient missing start tag");
self.gradients.insert(
id,
gradient_builder.build().expect("radialGradient requires at least 2 stops"),
);
}
Event::Tag(tag::Stop, _, attrs) => {
let fill: Option<Color> = parse_color(&attrs).or_else(|| Some(Color::black()));
let opacity: f32 = parse_opacity(&attrs).unwrap_or(1.0);
let fill = fill
.map(|fill| forma::Color {
a: opacity,
..crate::to_linear([fill.red, fill.green, fill.blue])
})
.expect("stop missing stop-fill");
let stop: f32 = attrs
.get("offset")
.and_then(|stop| {
let stop = stop.to_string();
stop[..stop.len() - 1].parse().ok()
})
.expect("stop missing offset");
let gradient_builder = &mut self
.gradient_builder
.as_mut()
.expect("stop missing gradient start tag")
.1;
gradient_builder.color_with_stop(fill, stop / 100.0);
}
_ => (),
}
}
}
}
impl App for Svg {
fn width(&self) -> usize {
1000
}
fn height(&self) -> usize {
1000
}
fn set_width(&mut self, _width: usize) {}
fn set_height(&mut self, _height: usize) {}
fn compose(&mut self, composition: &mut Composition, _: Duration, _: &Keyboard) {
if !self.needs_composition {
return;
}
for (order, (path, fill_rule, fill, blend_mode)) in self.paths.iter().enumerate() {
let mut layer = composition.create_layer();
layer.insert(path).set_props(Props {
fill_rule: *fill_rule,
func: Func::Draw(Style {
fill: fill.clone(),
blend_mode: *blend_mode,
..Default::default()
}),
});
composition.insert(Order::new(order as u32).unwrap(), layer);
}
self.needs_composition = false;
}
}