Add round corners and change arguments

This commit is contained in:
Martin Haug 2022-04-30 21:59:34 +02:00
parent f9e115daf5
commit 5f1499d380
9 changed files with 473 additions and 96 deletions

View File

@ -16,9 +16,9 @@ use ttf_parser::{name_id, GlyphId, Tag};
use super::subset::subset;
use crate::font::{find_name, FaceId, FontStore};
use crate::frame::{Element, Frame, Geometry, Group, Shape, Text};
use crate::frame::{rect_path, rect_paths, Element, Frame, Geometry, Group, Shape, Text};
use crate::geom::{
self, Color, Em, Length, Numeric, Paint, Point, Size, Stroke, Transform,
self, Color, Em, Length, Numeric, Paint, Point, Sides, Size, Stroke, Transform,
};
use crate::image::{Image, ImageId, ImageStore, RasterImage};
use crate::Context;
@ -499,16 +499,16 @@ impl<'a> PageExporter<'a> {
}
fn write_shape(&mut self, x: f32, y: f32, shape: &Shape) {
if shape.fill.is_none() && shape.stroke.is_none() {
if shape.fill.is_none() && shape.stroke.iter().all(Option::is_none) {
return;
}
match shape.geometry {
Geometry::Rect(size) => {
Geometry::Rect(size, radius) => {
let w = size.x.to_f32();
let h = size.y.to_f32();
if w > 0.0 && h > 0.0 {
self.content.rect(x, y, w, h);
self.write_path(x, y, &rect_path(size, radius));
}
}
Geometry::Ellipse(size) => {
@ -530,16 +530,37 @@ impl<'a> PageExporter<'a> {
self.set_fill(fill);
}
if let Some(stroke) = shape.stroke {
self.set_stroke(stroke);
// The stroke does not exist or is non-uniform.
let mut use_stroke = false;
if shape.stroke.is_uniform() || !matches!(shape.geometry, Geometry::Rect(_, _)) {
if let Some(stroke) = shape.stroke.top {
self.set_stroke(stroke);
use_stroke = true;
}
}
match (shape.fill, shape.stroke) {
(None, None) => unreachable!(),
(Some(_), None) => self.content.fill_nonzero(),
(None, Some(_)) => self.content.stroke(),
(Some(_), Some(_)) => self.content.fill_nonzero_and_stroke(),
match (shape.fill, use_stroke) {
(None, false) => self.content.end_path(),
(Some(_), false) => self.content.fill_nonzero(),
(None, true) => self.content.stroke(),
(Some(_), true) => self.content.fill_nonzero_and_stroke(),
};
if let Geometry::Rect(size, radius) = shape.geometry {
if !use_stroke {
for (path, stroke) in rect_paths(size, radius, Some(shape.stroke)) {
if let Some(stroke) = stroke {
self.write_shape(x, y, &Shape {
geometry: Geometry::Path(path),
fill: None,
stroke: Sides::splat(Some(stroke)),
});
} else {
continue;
}
}
}
}
}
fn write_path(&mut self, x: f32, y: f32, path: &geom::Path) {

View File

@ -8,8 +8,9 @@ use ttf_parser::{GlyphId, OutlineBuilder};
use usvg::FitTo;
use crate::frame::{Element, Frame, Geometry, Group, Shape, Text};
use crate::geom::{self, Length, Paint, PathElement, Size, Stroke, Transform};
use crate::geom::{self, Length, Paint, PathElement, Sides, Size, Stroke, Transform};
use crate::image::{Image, RasterImage, Svg};
use crate::library::prelude::{rect_path, rect_paths};
use crate::Context;
/// Export a frame into a rendered image.
@ -298,12 +299,7 @@ fn render_shape(
shape: &Shape,
) -> Option<()> {
let path = match shape.geometry {
Geometry::Rect(size) => {
let w = size.x.to_f32();
let h = size.y.to_f32();
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h)?;
sk::PathBuilder::from_rect(rect)
}
Geometry::Rect(size, radius) => convert_path(&rect_path(size, radius))?,
Geometry::Ellipse(size) => convert_path(&geom::Path::ellipse(size))?,
Geometry::Line(target) => {
let mut builder = sk::PathBuilder::new();
@ -315,7 +311,7 @@ fn render_shape(
if let Some(fill) = shape.fill {
let mut paint: sk::Paint = fill.into();
if matches!(shape.geometry, Geometry::Rect(_)) {
if matches!(shape.geometry, Geometry::Rect(_, _)) {
paint.anti_alias = false;
}
@ -323,11 +319,27 @@ fn render_shape(
canvas.fill_path(&path, &paint, rule, ts, mask);
}
if let Some(Stroke { paint, thickness }) = shape.stroke {
let paint = paint.into();
let mut stroke = sk::Stroke::default();
stroke.width = thickness.to_f32();
canvas.stroke_path(&path, &paint, &stroke, ts, mask);
if shape.stroke.is_uniform() || !matches!(shape.geometry, Geometry::Rect(_, _)) {
if let Some(Stroke { paint, thickness }) = shape.stroke.top {
let paint = paint.into();
let mut stroke = sk::Stroke::default();
stroke.width = thickness.to_f32();
canvas.stroke_path(&path, &paint, &stroke, ts, mask);
}
} else {
if let Geometry::Rect(size, radius) = shape.geometry {
for (path, stroke) in rect_paths(size, radius, Some(shape.stroke)) {
if let Some(stroke) = stroke {
render_shape(canvas, ts, mask, &Shape {
geometry: Geometry::Path(path),
fill: None,
stroke: Sides::splat(Some(stroke)),
})?;
} else {
continue;
}
}
}
}
Some(())

View File

@ -1,11 +1,13 @@
//! Finished layouts.
use std::fmt::{self, Debug, Formatter, Write};
use std::mem;
use std::sync::Arc;
use crate::font::FaceId;
use crate::geom::{
Align, Em, Length, Numeric, Paint, Path, Point, Size, Spec, Stroke, Transform,
Align, Angle, Em, Get, Length, Numeric, Paint, Path, Point, Side, Sides, Size, Spec,
Stroke, Transform,
};
use crate::image::ImageId;
use crate::util::{EcoString, MaybeShared};
@ -306,7 +308,7 @@ pub struct Shape {
/// The shape's background fill.
pub fill: Option<Paint>,
/// The shape's border stroke.
pub stroke: Option<Stroke>,
pub stroke: Sides<Option<Stroke>>,
}
/// A shape's geometry.
@ -314,8 +316,8 @@ pub struct Shape {
pub enum Geometry {
/// A line to a point (relative to its position).
Line(Point),
/// A rectangle with its origin in the topleft corner.
Rect(Size),
/// A rectangle with its origin in the topleft corner and a border radius.
Rect(Size, Sides<Length>),
/// A ellipse with its origin in the topleft corner.
Ellipse(Size),
/// A bezier path.
@ -328,7 +330,7 @@ impl Geometry {
Shape {
geometry: self,
fill: Some(fill),
stroke: None,
stroke: Sides::splat(None),
}
}
@ -337,7 +339,170 @@ impl Geometry {
Shape {
geometry: self,
fill: None,
stroke: Some(stroke),
stroke: Sides::splat(Some(stroke)),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Connection {
None,
Left,
Right,
Both,
}
impl Connection {
pub fn advance(self, right: bool) -> Self {
match self {
Self::Right | Self::Both => {
if right {
Self::Both
} else {
Self::Left
}
}
Self::Left | Self::None => {
if right {
Self::Right
} else {
Self::None
}
}
}
}
fn left(self) -> bool {
matches!(self, Self::Left | Self::Both)
}
fn right(self) -> bool {
matches!(self, Self::Right | Self::Both)
}
}
/// Draws one side of the rounded rectangle. Will always draw the left arc. The
/// right arc will be drawn halfway iff there is no connection.
fn draw_side(
path: &mut Path,
side: Side,
size: Size,
radius_left: Length,
radius_right: Length,
connection: Connection,
) {
let reversed = |angle: Angle, radius, rotate, mirror_x, mirror_y| {
let [a, b, c, d] = angle.bezier_arc(radius, rotate, mirror_x, mirror_y);
[d, c, b, a]
};
let angle_left = Angle::deg(if connection.left() { 90.0 } else { 45.0 });
let angle_right = Angle::deg(if connection.right() { 90.0 } else { 45.0 });
let (arc1, arc2) = match side {
Side::Top => {
let arc1 = reversed(angle_left, radius_left, true, true, false)
.map(|x| x + Point::with_x(radius_left));
let arc2 = (-angle_right)
.bezier_arc(radius_right, true, true, false)
.map(|x| x + Point::with_x(size.x - radius_right));
(arc1, arc2)
}
Side::Right => {
let arc1 = reversed(-angle_left, radius_left, false, false, false)
.map(|x| x + Point::new(size.x, radius_left));
let arc2 = angle_right
.bezier_arc(radius_right, false, false, false)
.map(|x| x + Point::new(size.x, size.y - radius_right));
(arc1, arc2)
}
Side::Bottom => {
let arc1 = reversed(-angle_left, radius_left, true, false, false)
.map(|x| x + Point::new(size.x - radius_left, size.y));
let arc2 = angle_right
.bezier_arc(radius_right, true, false, false)
.map(|x| x + Point::new(radius_right, size.y));
(arc1, arc2)
}
Side::Left => {
let arc1 = reversed(angle_left, radius_left, false, false, true)
.map(|x| x + Point::with_y(size.y - radius_left));
let arc2 = (-angle_right)
.bezier_arc(radius_right, false, false, true)
.map(|x| x + Point::with_y(radius_right));
(arc1, arc2)
}
};
if !connection.left() {
path.move_to(if radius_left.is_zero() { arc1[3] } else { arc1[0] });
}
if !radius_left.is_zero() {
path.cubic_to(arc1[1], arc1[2], arc1[3]);
}
path.line_to(arc2[0]);
if !connection.right() && !radius_right.is_zero() {
path.cubic_to(arc2[1], arc2[2], arc2[3]);
}
}
pub fn rect_paths(
size: Size,
radius: Sides<Length>,
strokes: Option<Sides<Option<Stroke>>>,
) -> Vec<(Path, Option<Stroke>)> {
let strokes = strokes.unwrap_or_else(|| Sides::splat(None));
let mut res = vec![];
let mut connection = Connection::None;
let mut path = Path::new();
let sides = [Side::Top, Side::Right, Side::Bottom, Side::Left];
let mut always_continuous = true;
let radius = [
radius.left,
radius.top,
radius.right,
radius.bottom,
radius.left,
];
for (side, radius) in sides.into_iter().zip(radius.windows(2)) {
let stroke_continuity = strokes.get(side) == strokes.get(side.clockwise());
connection = connection.advance(stroke_continuity && side != Side::Left);
always_continuous &= stroke_continuity;
draw_side(&mut path, side, size, radius[0], radius[1], connection);
if !stroke_continuity {
res.push((mem::take(&mut path), strokes.get(side)));
}
}
if always_continuous {
path.close_path();
}
if !path.0.is_empty() {
res.push((path, strokes.left));
}
res
}
pub fn rect_path(size: Size, radius: Sides<Length>) -> Path {
let mut paths = rect_paths(size, radius, None);
assert_eq!(paths.len(), 1);
paths.pop().unwrap().0
}

View File

@ -64,6 +64,51 @@ impl Angle {
pub fn cos(self) -> f64 {
self.to_rad().cos()
}
/// Get the control points for a bezier curve that describes a circular arc
/// of this angle with the given radius.
pub fn bezier_arc(
self,
radius: Length,
rotate: bool,
mirror_x: bool,
mirror_y: bool,
) -> [Point; 4] {
let end = Point::new(self.cos() * radius - radius, self.sin() * radius);
let center = Point::new(-radius, Length::zero());
let mut ts = if mirror_y {
Transform::mirror_y()
} else {
Transform::identity()
};
if mirror_x {
ts = ts.pre_concat(Transform::mirror_x());
}
if rotate {
ts = ts.pre_concat(Transform::rotate(Angle::deg(90.0)));
}
let a = center * -1.0;
let b = end - center;
let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw();
let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw();
let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2)
/ (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw());
let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x);
let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x);
[
Point::zero(),
control_1.transform(ts),
control_2.transform(ts),
end.transform(ts),
]
}
}
impl Numeric for Angle {

View File

@ -31,6 +31,32 @@ impl<T> Sides<T> {
bottom: value,
}
}
/// Maps the individual fields with `f`.
pub fn map<F, U>(self, mut f: F) -> Sides<U>
where
F: FnMut(T) -> U,
{
Sides {
left: f(self.left),
top: f(self.top),
right: f(self.right),
bottom: f(self.bottom),
}
}
/// Returns an iterator over the sides.
pub fn iter(&self) -> impl Iterator<Item = &T> {
[&self.left, &self.top, &self.right, &self.bottom].into_iter()
}
/// Returns whether all sides are equal.
pub fn is_uniform(&self) -> bool
where
T: PartialEq,
{
self.left == self.top && self.top == self.right && self.right == self.bottom
}
}
impl<T> Sides<T>
@ -100,4 +126,24 @@ impl Side {
Self::Bottom => Self::Top,
}
}
/// The next side, clockwise.
pub fn clockwise(self) -> Self {
match self {
Self::Left => Self::Top,
Self::Top => Self::Right,
Self::Right => Self::Bottom,
Self::Bottom => Self::Left,
}
}
/// The next side, counter-clockwise.
pub fn counter_clockwise(self) -> Self {
match self {
Self::Left => Self::Bottom,
Self::Top => Self::Left,
Self::Right => Self::Top,
Self::Bottom => Self::Right,
}
}
}

View File

@ -24,6 +24,30 @@ impl Transform {
}
}
/// Transform by mirroring along the x-axis.
pub fn mirror_x() -> Self {
Self {
sx: Ratio::one(),
ky: Ratio::zero(),
kx: Ratio::zero(),
sy: -Ratio::one(),
tx: Length::zero(),
ty: Length::zero(),
}
}
/// Transform by mirroring along the y-axis.
pub fn mirror_y() -> Self {
Self {
sx: -Ratio::one(),
ky: Ratio::zero(),
kx: Ratio::zero(),
sy: Ratio::one(),
tx: Length::zero(),
ty: Length::zero(),
}
}
/// A translate transform.
pub const fn translate(tx: Length, ty: Length) -> Self {
Self { tx, ty, ..Self::identity() }

View File

@ -5,36 +5,46 @@ use crate::library::text::TextNode;
/// Place a node into a sizable and fillable shape.
#[derive(Debug, Hash)]
pub struct ShapeNode<const S: ShapeKind>(pub Option<LayoutNode>);
pub struct AngularNode<const S: ShapeKind>(pub Option<LayoutNode>);
/// Place a node into a square.
pub type SquareNode = ShapeNode<SQUARE>;
pub type SquareNode = AngularNode<SQUARE>;
/// Place a node into a rectangle.
pub type RectNode = ShapeNode<RECT>;
pub type RectNode = AngularNode<RECT>;
/// Place a node into a circle.
pub type CircleNode = ShapeNode<CIRCLE>;
// /// Place a node into a sizable and fillable shape.
// #[derive(Debug, Hash)]
// pub struct RoundNode<const S: ShapeKind>(pub Option<LayoutNode>);
/// Place a node into an ellipse.
pub type EllipseNode = ShapeNode<ELLIPSE>;
// /// Place a node into a circle.
// pub type CircleNode = RoundNode<CIRCLE>;
// /// Place a node into an ellipse.
// pub type EllipseNode = RoundNode<ELLIPSE>;
#[node]
impl<const S: ShapeKind> ShapeNode<S> {
impl<const S: ShapeKind> AngularNode<S> {
/// How to fill the shape.
pub const FILL: Option<Paint> = None;
/// How to stroke the shape.
#[property(resolve, fold)]
pub const STROKE: Smart<Option<RawStroke>> = Smart::Auto;
pub const STROKE: Smart<Sides<Option<RawStroke>>> = Smart::Auto;
/// How much to pad the shape's content.
pub const PADDING: Relative<RawLength> = Relative::zero();
#[property(resolve, fold)]
pub const INSET: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
/// How much to extend the shape's dimensions beyond the allocated space.
#[property(resolve, fold)]
pub const OUTSET: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
/// How much to round the shape's corners.
#[property(resolve, fold)]
pub const RADIUS: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let size = match S {
SQUARE => args.named::<RawLength>("size")?.map(Relative::from),
CIRCLE => args.named::<RawLength>("radius")?.map(|r| 2.0 * Relative::from(r)),
_ => None,
};
let size = args.named::<RawLength>("size")?.map(Relative::from);
let width = match size {
None => args.named("width")?,
@ -52,7 +62,50 @@ impl<const S: ShapeKind> ShapeNode<S> {
}
}
impl<const S: ShapeKind> Layout for ShapeNode<S> {
castable! {
Sides<Option<RawStroke>>,
Expected: "stroke, dictionary with strokes for each side",
Value::None => {
Sides::splat(None)
},
Value::Dict(values) => {
let get = |name: &str| values.get(name.into()).and_then(|v| v.clone().cast()).unwrap_or(None);
Sides {
top: get("top"),
right: get("right"),
bottom: get("bottom"),
left: get("left"),
}
},
Value::Length(thickness) => Sides::splat(Some(RawStroke {
paint: Smart::Auto,
thickness: Smart::Custom(thickness),
})),
Value::Color(color) => Sides::splat(Some(RawStroke {
paint: Smart::Custom(color.into()),
thickness: Smart::Auto,
})),
@stroke: RawStroke => Sides::splat(Some(*stroke)),
}
castable! {
Sides<Option<Relative<RawLength>>>,
Expected: "length or dictionary of lengths for each side",
Value::None => Sides::splat(None),
Value::Dict(values) => {
let get = |name: &str| values.get(name.into()).and_then(|v| v.clone().cast()).unwrap_or(None);
Sides {
top: get("top"),
right: get("right"),
bottom: get("bottom"),
left: get("left"),
}
},
Value::Length(l) => Sides::splat(Some(l.into())),
Value::Relative(r) => Sides::splat(Some(r)),
}
impl<const S: ShapeKind> Layout for AngularNode<S> {
fn layout(
&self,
ctx: &mut Context,
@ -61,50 +114,43 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
) -> TypResult<Vec<Arc<Frame>>> {
let mut frames;
if let Some(child) = &self.0 {
let mut padding = styles.get(Self::PADDING);
if is_round(S) {
padding.rel += Ratio::new(0.5 - SQRT_2 / 4.0);
}
let inset = styles.get(Self::INSET);
// Pad the child.
let child = child.clone().padded(Sides::splat(padding));
let child = child
.clone()
.padded(inset.map(|side| side.map(|abs| RawLength::from(abs))));
let mut pod = Regions::one(regions.first, regions.base, regions.expand);
frames = child.layout(ctx, &pod, styles)?;
// Relayout with full expansion into square region to make sure
// the result is really a square or circle.
if is_quadratic(S) {
let length = if regions.expand.x || regions.expand.y {
let target = regions.expand.select(regions.first, Size::zero());
target.x.max(target.y)
} else {
let size = frames[0].size;
let desired = size.x.max(size.y);
desired.min(regions.first.x).min(regions.first.y)
};
let length = if regions.expand.x || regions.expand.y {
let target = regions.expand.select(regions.first, Size::zero());
target.x.max(target.y)
} else {
let size = frames[0].size;
let desired = size.x.max(size.y);
desired.min(regions.first.x).min(regions.first.y)
};
pod.first = Size::splat(length);
pod.expand = Spec::splat(true);
frames = child.layout(ctx, &pod, styles)?;
}
pod.first = Size::splat(length);
pod.expand = Spec::splat(true);
frames = child.layout(ctx, &pod, styles)?;
} else {
// The default size that a shape takes on if it has no child and
// enough space.
let mut size =
Size::new(Length::pt(45.0), Length::pt(30.0)).min(regions.first);
if is_quadratic(S) {
let length = if regions.expand.x || regions.expand.y {
let target = regions.expand.select(regions.first, Size::zero());
target.x.max(target.y)
} else {
size.x.min(size.y)
};
size = Size::splat(length);
let length = if regions.expand.x || regions.expand.y {
let target = regions.expand.select(regions.first, Size::zero());
target.x.max(target.y)
} else {
size = regions.expand.select(regions.first, size);
}
size.x.min(size.y)
};
size = Size::splat(length);
frames = vec![Arc::new(Frame::new(size))];
}
@ -114,18 +160,28 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
// Add fill and/or stroke.
let fill = styles.get(Self::FILL);
let stroke = match styles.get(Self::STROKE) {
Smart::Auto => fill.is_none().then(Stroke::default),
Smart::Custom(stroke) => stroke.map(RawStroke::unwrap_or_default),
Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())),
Smart::Auto => Sides::splat(None),
Smart::Custom(strokes) => strokes.map(|s| Some(s.unwrap_or_default())),
};
if fill.is_some() || stroke.is_some() {
let geometry = if is_round(S) {
Geometry::Ellipse(frame.size)
} else {
Geometry::Rect(frame.size)
};
let radius = {
let radius = styles.get(Self::RADIUS);
let shape = Shape { geometry, fill, stroke };
Sides {
left: radius.left.relative_to(frame.size.x / 2.0),
top: radius.top.relative_to(frame.size.y / 2.0),
right: radius.right.relative_to(frame.size.x / 2.0),
bottom: radius.bottom.relative_to(frame.size.y / 2.0),
}
};
if fill.is_some() || stroke.iter().any(Option::is_some) {
let shape = Shape {
geometry: Geometry::Rect(frame.size, radius),
fill,
stroke,
};
frame.prepend(Point::zero(), Element::Shape(shape));
}
@ -152,13 +208,3 @@ const CIRCLE: ShapeKind = 2;
/// A curve around two focal points.
const ELLIPSE: ShapeKind = 3;
/// Whether a shape kind is curvy.
fn is_round(kind: ShapeKind) -> bool {
matches!(kind, CIRCLE | ELLIPSE)
}
/// Whether a shape kind has equal side length.
fn is_quadratic(kind: ShapeKind) -> bool {
matches!(kind, SQUARE | CIRCLE)
}

View File

@ -353,7 +353,8 @@ impl Layout for FillNode {
) -> TypResult<Vec<Arc<Frame>>> {
let mut frames = self.child.layout(ctx, regions, styles)?;
for frame in &mut frames {
let shape = Geometry::Rect(frame.size).filled(self.fill);
let shape = Geometry::Rect(frame.size, Sides::splat(Length::zero()))
.filled(self.fill);
Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
}
Ok(frames)
@ -378,7 +379,8 @@ impl Layout for StrokeNode {
) -> TypResult<Vec<Arc<Frame>>> {
let mut frames = self.child.layout(ctx, regions, styles)?;
for frame in &mut frames {
let shape = Geometry::Rect(frame.size).stroked(self.stroke);
let shape = Geometry::Rect(frame.size, Sides::splat(Length::zero()))
.stroked(self.stroke);
Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
}
Ok(frames)

View File

@ -459,6 +459,22 @@ where
}
}
impl<T> Fold for Sides<Option<T>>
where
T: Default,
{
type Output = Sides<T>;
fn fold(self, outer: Self::Output) -> Self::Output {
Sides {
left: self.left.unwrap_or(outer.left),
right: self.right.unwrap_or(outer.right),
top: self.top.unwrap_or(outer.top),
bottom: self.bottom.unwrap_or(outer.bottom),
}
}
}
/// A scoped property barrier.
///
/// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style