Better rect edges (#1956)

This commit is contained in:
Anton Wetzel 2023-09-25 13:54:58 +02:00 committed by GitHub
parent 44bbfded5f
commit 98e5d97509
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 685 additions and 195 deletions

View File

@ -12,7 +12,7 @@ use crate::eval::{cast, dict, ty, Dict, Value};
use crate::export::PdfPageLabel;
use crate::font::Font;
use crate::geom::{
self, rounded_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke,
self, styled_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke,
Geometry, Length, Numeric, Paint, Point, Rel, Shape, Sides, Size, Transform,
};
use crate::image::Image;
@ -301,9 +301,8 @@ impl Frame {
let outset = outset.relative_to(self.size());
let size = self.size() + outset.sum_by_axis();
let pos = Point::new(-outset.left, -outset.top);
let radius = radius.map(|side| side.relative_to(size.x.min(size.y) / 2.0));
self.prepend_multiple(
rounded_rect(size, radius, fill, stroke)
styled_rect(size, radius, fill, stroke)
.into_iter()
.map(|x| (pos, FrameItem::Shape(x, span))),
)

View File

@ -110,6 +110,48 @@ pub enum Corner {
BottomLeft,
}
impl Corner {
/// The next corner, clockwise.
pub fn next_cw(self) -> Self {
match self {
Self::TopLeft => Self::TopRight,
Self::TopRight => Self::BottomRight,
Self::BottomRight => Self::BottomLeft,
Self::BottomLeft => Self::TopLeft,
}
}
/// The next corner, counter-clockwise.
pub fn next_ccw(self) -> Self {
match self {
Self::TopLeft => Self::BottomLeft,
Self::TopRight => Self::TopLeft,
Self::BottomRight => Self::TopRight,
Self::BottomLeft => Self::BottomRight,
}
}
/// The next side, clockwise.
pub fn side_cw(self) -> Side {
match self {
Self::TopLeft => Side::Top,
Self::TopRight => Side::Right,
Self::BottomRight => Side::Bottom,
Self::BottomLeft => Side::Left,
}
}
/// The next side, counter-clockwise.
pub fn side_ccw(self) -> Side {
match self {
Self::TopLeft => Side::Left,
Self::TopRight => Side::Top,
Self::BottomRight => Side::Right,
Self::BottomLeft => Side::Bottom,
}
}
}
impl<T: Reflect> Reflect for Corners<Option<T>> {
fn input() -> CastInfo {
T::input() + Dict::input()

View File

@ -17,8 +17,8 @@ mod paint;
mod path;
mod point;
mod ratio;
mod rect;
mod rel;
mod rounded;
mod scalar;
mod shape;
mod sides;
@ -42,8 +42,8 @@ pub use self::paint::Paint;
pub use self::path::{Path, PathItem};
pub use self::point::Point;
pub use self::ratio::Ratio;
pub use self::rect::styled_rect;
pub use self::rel::Rel;
pub use self::rounded::rounded_rect;
pub use self::scalar::Scalar;
pub use self::shape::{Geometry, Shape};
pub use self::sides::{Side, Sides};

View File

@ -0,0 +1,541 @@
use super::*;
/// Helper to draw arcs with bezier curves.
trait PathExtension {
fn arc(&mut self, start: Point, center: Point, end: Point);
fn arc_move(&mut self, start: Point, center: Point, end: Point);
fn arc_line(&mut self, start: Point, center: Point, end: Point);
}
/// Get the control points for a bezier curve that approximates a circular arc for
/// a start point, an end point and a center of the circle whose arc connects
/// the two.
fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] {
// https://stackoverflow.com/a/44829356/1567835
let a = start - center;
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);
[control_1, control_2]
}
impl PathExtension for Path {
fn arc(&mut self, start: Point, center: Point, end: Point) {
let arc = bezier_arc_control(start, center, end);
self.cubic_to(arc[0], arc[1], end);
}
fn arc_move(&mut self, start: Point, center: Point, end: Point) {
self.move_to(start);
self.arc(start, center, end);
}
fn arc_line(&mut self, start: Point, center: Point, end: Point) {
self.line_to(start);
self.arc(start, center, end);
}
}
/// Create a styled rectangle with shapes.
/// - use rect primitive for simple rectangles
/// - stroke sides if possible
/// - use fill for sides for best looks
pub fn styled_rect(
size: Size,
radius: Corners<Rel<Abs>>,
fill: Option<Paint>,
stroke: Sides<Option<FixedStroke>>,
) -> Vec<Shape> {
if stroke.is_uniform() && radius.iter().cloned().all(Rel::is_zero) {
simple_rect(size, fill, stroke.top)
} else {
segmented_rect(size, radius, fill, stroke)
}
}
/// Use rect primitive for the rectangle
fn simple_rect(
size: Size,
fill: Option<Paint>,
stroke: Option<FixedStroke>,
) -> Vec<Shape> {
vec![Shape { geometry: Geometry::Rect(size), fill, stroke }]
}
/// Use stroke and fill for the rectangle
fn segmented_rect(
size: Size,
radius: Corners<Rel<Abs>>,
fill: Option<Paint>,
strokes: Sides<Option<FixedStroke>>,
) -> Vec<Shape> {
let mut res = vec![];
let stroke_widths = strokes
.clone()
.map(|s| s.map(|s| s.thickness / 2.0).unwrap_or(Abs::zero()));
let max_radius = (size.x.min(size.y)) / 2.0
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
let corners = Corners {
top_left: Corner::TopLeft,
top_right: Corner::TopRight,
bottom_right: Corner::BottomRight,
bottom_left: Corner::BottomLeft,
}
.map(|corner| ControlPoints {
radius: radius.get(corner),
stroke_before: stroke_widths.get(corner.side_ccw()),
stroke_after: stroke_widths.get(corner.side_cw()),
corner,
size,
same: match (
strokes.get_ref(corner.side_ccw()),
strokes.get_ref(corner.side_cw()),
) {
(Some(a), Some(b)) => a.paint == b.paint && a.dash_pattern == b.dash_pattern,
(None, None) => true,
_ => false,
},
});
// insert stroked sides below filled sides
let mut stroke_insert = 0;
// fill shape with inner curve
if let Some(fill) = fill {
let mut path = Path::new();
let c = corners.get_ref(Corner::TopLeft);
if c.arc() {
path.arc_move(c.start(), c.center(), c.end());
} else {
path.move_to(c.center());
};
for corner in [Corner::TopRight, Corner::BottomRight, Corner::BottomLeft] {
let c = corners.get_ref(corner);
if c.arc() {
path.arc_line(c.start(), c.center(), c.end());
} else {
path.line_to(c.center());
}
}
path.close_path();
res.push(Shape {
geometry: Geometry::Path(path),
fill: Some(fill),
stroke: None,
});
stroke_insert += 1;
}
let current = corners.iter().find(|c| !c.same).map(|c| c.corner);
if let Some(mut current) = current {
// multiple segments
// start at a corner with a change between sides and iterate clockwise all other corners
let mut last = current;
for _ in 0..4 {
current = current.next_cw();
if corners.get_ref(current).same {
continue;
}
// create segment
let start = last;
let end = current;
last = current;
let stroke = match strokes.get_ref(start.side_cw()) {
None => continue,
Some(stroke) => stroke.clone(),
};
let (shape, ontop) = segment(start, end, &corners, stroke);
if ontop {
res.push(shape);
} else {
res.insert(stroke_insert, shape);
stroke_insert += 1;
}
}
} else if let Some(stroke) = strokes.top {
// single segment
let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke);
res.push(shape);
}
res
}
/// Returns the shape for the segment and whether the shape should be drawn on top.
fn segment(
start: Corner,
end: Corner,
corners: &Corners<ControlPoints>,
stroke: FixedStroke,
) -> (Shape, bool) {
fn fill_corner(corner: &ControlPoints) -> bool {
corner.stroke_before != corner.stroke_after
|| corner.radius() < corner.stroke_before
}
fn fill_corners(
start: Corner,
end: Corner,
corners: &Corners<ControlPoints>,
) -> bool {
if fill_corner(corners.get_ref(start)) {
return true;
}
if fill_corner(corners.get_ref(end)) {
return true;
}
let mut current = start.next_cw();
while current != end {
if fill_corner(corners.get_ref(current)) {
return true;
}
current = current.next_cw();
}
false
}
let solid = stroke
.dash_pattern
.as_ref()
.map(|pattern| pattern.array.is_empty())
.unwrap_or(true);
let use_fill = solid && fill_corners(start, end, corners);
let shape = if use_fill {
fill_segment(start, end, corners, stroke)
} else {
stroke_segment(start, end, corners, stroke)
};
(shape, use_fill)
}
/// Stroke the sides from `start` to `end` clockwise.
fn stroke_segment(
start: Corner,
end: Corner,
corners: &Corners<ControlPoints>,
stroke: FixedStroke,
) -> Shape {
// create start corner
let c = corners.get_ref(start);
let mut path = Path::new();
if start == end || !c.arc() {
path.move_to(c.end());
} else {
path.arc_move(c.mid(), c.center(), c.end());
}
// create corners between start and end
let mut current = start.next_cw();
while current != end {
let c = corners.get_ref(current);
if c.arc() {
path.arc_line(c.start(), c.center(), c.end());
} else {
path.line_to(c.end());
}
current = current.next_cw();
}
// create end corner
let c = corners.get_ref(end);
if !c.arc() {
path.line_to(c.start());
} else if start == end {
path.arc_line(c.start(), c.center(), c.end());
} else {
path.arc_line(c.start(), c.center(), c.mid());
}
Shape {
geometry: Geometry::Path(path),
stroke: Some(stroke),
fill: None,
}
}
/// Fill the sides from `start` to `end` clockwise.
fn fill_segment(
start: Corner,
end: Corner,
corners: &Corners<ControlPoints>,
stroke: FixedStroke,
) -> Shape {
let mut path = Path::new();
// create the start corner
// begin on the inside and finish on the outside
// no corner if start and end are equal
// half corner if different
if start == end {
let c = corners.get_ref(start);
path.move_to(c.end_inner());
path.line_to(c.end_outer());
} else {
let c = corners.get_ref(start);
if c.arc_inner() {
path.arc_move(c.end_inner(), c.center_inner(), c.mid_inner());
} else {
path.move_to(c.end_inner());
}
if c.arc_outer() {
path.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
} else {
path.line_to(c.outer());
path.line_to(c.end_outer());
}
}
// create the clockwise outside path for the corners between start and end
let mut current = start.next_cw();
while current != end {
let c = corners.get_ref(current);
if c.arc_outer() {
path.arc_line(c.start_outer(), c.center_outer(), c.end_outer());
} else {
path.line_to(c.outer());
}
current = current.next_cw();
}
// create the end corner
// begin on the outside and finish on the inside
// full corner if start and end are equal
// half corner if different
if start == end {
let c = corners.get_ref(end);
if c.arc_outer() {
path.arc_line(c.start_outer(), c.center_outer(), c.end_outer());
} else {
path.line_to(c.outer());
path.line_to(c.end_outer());
}
if c.arc_inner() {
path.arc_line(c.end_inner(), c.center_inner(), c.start_inner());
} else {
path.line_to(c.center_inner());
}
} else {
let c = corners.get_ref(end);
if c.arc_outer() {
path.arc_line(c.start_outer(), c.center_outer(), c.mid_outer());
} else {
path.line_to(c.outer());
}
if c.arc_inner() {
path.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
} else {
path.line_to(c.center_inner());
}
}
// create the counterclockwise inside path for the corners between start and end
let mut current = end.next_ccw();
while current != start {
let c = corners.get_ref(current);
if c.arc_inner() {
path.arc_line(c.end_inner(), c.center_inner(), c.start_inner());
} else {
path.line_to(c.center_inner());
}
current = current.next_ccw();
}
path.close_path();
Shape {
geometry: Geometry::Path(path),
stroke: None,
fill: Some(stroke.paint),
}
}
/// Helper to calculate different control points for the corners.
/// Clockwise orientation from start to end.
/// ```text
/// O-------------------EO --- - Z: Zero/Origin ({x: 0, y: 0} for top left corner)
/// |\ ___----''' | | - O: Outer: intersection between the straight outer lines
/// | \ / | | - S_: start
/// | MO | | - M_: midpoint
/// | /Z\ __-----------E | - E_: end
/// |/ \M | ro - r_: radius
/// | /\ | | - middle of the stroke
/// | / \ | | - arc from S through M to E with center C and radius r
/// | | MI--EI------- | - outer curve
/// | | / \ | - arc from SO through MO to EO with center CO and radius ro
/// SO | | \ CO --- - inner curve
/// | | | \ - arc from SI through MI to EI with center CI and radius ri
/// |--S-SI-----CI C
/// |--ri--|
/// |-------r--------|
/// ```
struct ControlPoints {
radius: Abs,
stroke_after: Abs,
stroke_before: Abs,
corner: Corner,
size: Size,
same: bool,
}
impl ControlPoints {
/// Move and rotate the point from top-left to the required corner.
fn rotate(&self, point: Point) -> Point {
match self.corner {
Corner::TopLeft => point,
Corner::TopRight => Point { x: self.size.x - point.y, y: point.x },
Corner::BottomRight => {
Point { x: self.size.x - point.x, y: self.size.y - point.y }
}
Corner::BottomLeft => Point { x: point.y, y: self.size.y - point.x },
}
}
/// Outside intersection of the sides.
pub fn outer(&self) -> Point {
self.rotate(Point { x: -self.stroke_before, y: -self.stroke_after })
}
/// Center for the outer arc.
pub fn center_outer(&self) -> Point {
let r = self.radius_outer();
self.rotate(Point {
x: r - self.stroke_before,
y: r - self.stroke_after,
})
}
/// Center for the middle arc.
pub fn center(&self) -> Point {
let r = self.radius();
self.rotate(Point { x: r, y: r })
}
/// Center for the inner arc.
pub fn center_inner(&self) -> Point {
let r = self.radius_inner();
self.rotate(Point {
x: self.stroke_before + r,
y: self.stroke_after + r,
})
}
/// Radius of the outer arc.
pub fn radius_outer(&self) -> Abs {
self.radius
}
/// Radius of the middle arc.
pub fn radius(&self) -> Abs {
(self.radius - self.stroke_before.min(self.stroke_after)).max(Abs::zero())
}
/// Radius of the inner arc.
pub fn radius_inner(&self) -> Abs {
(self.radius - 2.0 * self.stroke_before.max(self.stroke_after)).max(Abs::zero())
}
/// Middle of the corner on the outside of the stroke.
pub fn mid_outer(&self) -> Point {
let c_i = self.center_inner();
let c_o = self.center_outer();
let o = self.outer();
let r = self.radius_outer();
// https://math.stackexchange.com/a/311956
// intersection between the line from inner center to outside and the outer arc
let a = (o.x - c_i.x).to_raw().powi(2) + (o.y - c_i.y).to_raw().powi(2);
let b = 2.0 * (o.x - c_i.x).to_raw() * (c_i.x - c_o.x).to_raw()
+ 2.0 * (o.y - c_i.y).to_raw() * (c_i.y - c_o.y).to_raw();
let c = (c_i.x - c_o.x).to_raw().powi(2) + (c_i.y - c_o.y).to_raw().powi(2)
- r.to_raw().powi(2);
let t = (-b + (b * b - 4.0 * a * c).sqrt()) / (2.0 * a);
c_i + t * (o - c_i)
}
/// Middle of the corner in the middle of the stroke.
pub fn mid(&self) -> Point {
let center = self.center_outer();
let outer = self.outer();
let diff = outer - center;
center + diff / diff.hypot().to_raw() * self.radius().to_raw()
}
/// Middle of the corner on the inside of the stroke.
pub fn mid_inner(&self) -> Point {
let center = self.center_inner();
let outer = self.outer();
let diff = outer - center;
center + diff / diff.hypot().to_raw() * self.radius_inner().to_raw()
}
/// If an outer arc is required.
pub fn arc_outer(&self) -> bool {
self.radius_outer() > Abs::zero()
}
pub fn arc(&self) -> bool {
self.radius() > Abs::zero()
}
/// If an inner arc is required.
pub fn arc_inner(&self) -> bool {
self.radius_inner() > Abs::zero()
}
/// Start of the corner on the outside of the stroke.
pub fn start_outer(&self) -> Point {
self.rotate(Point {
x: -self.stroke_before,
y: self.radius_outer() - self.stroke_after,
})
}
/// Start of the corner in the center of the stroke.
pub fn start(&self) -> Point {
self.rotate(Point::with_y(self.radius()))
}
/// Start of the corner on the inside of the stroke.
pub fn start_inner(&self) -> Point {
self.rotate(Point {
x: self.stroke_before,
y: self.stroke_after + self.radius_inner(),
})
}
/// End of the corner on the outside of the stroke.
pub fn end_outer(&self) -> Point {
self.rotate(Point {
x: self.radius_outer() - self.stroke_before,
y: -self.stroke_after,
})
}
/// End of the corner in the center of the stroke.
pub fn end(&self) -> Point {
self.rotate(Point::with_x(self.radius()))
}
/// End of the corner on the inside of the stroke.
pub fn end_inner(&self) -> Point {
self.rotate(Point {
x: self.stroke_before + self.radius_inner(),
y: self.stroke_after,
})
}
}

View File

@ -1,182 +0,0 @@
use super::*;
/// Produce shapes that together make up a rounded rectangle.
pub fn rounded_rect(
size: Size,
radius: Corners<Abs>,
fill: Option<Paint>,
stroke: Sides<Option<FixedStroke>>,
) -> Vec<Shape> {
let mut res = vec![];
if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) {
res.push(Shape {
geometry: fill_geometry(size, radius),
fill,
stroke: if stroke.is_uniform() { stroke.top.clone() } else { None },
});
}
if !stroke.is_uniform() {
for (path, stroke) in stroke_segments(size, radius, stroke) {
if stroke.is_some() {
res.push(Shape { geometry: Geometry::Path(path), fill: None, stroke });
}
}
}
res
}
/// Output the shape of the rectangle as a path or primitive rectangle,
/// depending on whether it is rounded.
fn fill_geometry(size: Size, radius: Corners<Abs>) -> Geometry {
if radius.iter().copied().all(Abs::is_zero) {
Geometry::Rect(size)
} else {
let mut paths = stroke_segments(size, radius, Sides::splat(None));
assert_eq!(paths.len(), 1);
Geometry::Path(paths.pop().unwrap().0)
}
}
/// Output the minimum number of paths along the rectangles border.
fn stroke_segments(
size: Size,
radius: Corners<Abs>,
stroke: Sides<Option<FixedStroke>>,
) -> Vec<(Path, Option<FixedStroke>)> {
let mut res = vec![];
let mut connection = Connection::default();
let mut path = Path::new();
let mut always_continuous = true;
let max_radius = size.x.min(size.y).max(Abs::zero()) / 2.0;
for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] {
let continuous = stroke.get_ref(side) == stroke.get_ref(side.next_cw());
connection = connection.advance(continuous && side != Side::Left);
always_continuous &= continuous;
draw_side(
&mut path,
side,
size,
radius.get(side.start_corner()).clamp(Abs::zero(), max_radius),
radius.get(side.end_corner()).clamp(Abs::zero(), max_radius),
connection,
);
if !continuous {
res.push((std::mem::take(&mut path), stroke.get_ref(side).clone()));
}
}
if always_continuous {
path.close_path();
}
if !path.0.is_empty() {
res.push((path, stroke.left));
}
res
}
/// Draws one side of the rounded rectangle. Will always draw the left arc. The
/// right arc will be drawn halfway if and only if there is no connection.
fn draw_side(
path: &mut Path,
side: Side,
size: Size,
start_radius: Abs,
end_radius: Abs,
connection: Connection,
) {
let angle_left = Angle::deg(if connection.prev { 90.0 } else { 45.0 });
let angle_right = Angle::deg(if connection.next { 90.0 } else { 45.0 });
let length = size.get(side.axis());
// The arcs for a border of the rectangle along the x-axis, starting at (0,0).
let p1 = Point::with_x(start_radius);
let mut arc1 = bezier_arc(
p1 + Point::new(
-angle_left.sin() * start_radius,
(1.0 - angle_left.cos()) * start_radius,
),
Point::new(start_radius, start_radius),
p1,
);
let p2 = Point::with_x(length - end_radius);
let mut arc2 = bezier_arc(
p2,
Point::new(length - end_radius, end_radius),
p2 + Point::new(
angle_right.sin() * end_radius,
(1.0 - angle_right.cos()) * end_radius,
),
);
let transform = match side {
Side::Left => Transform::rotate(Angle::deg(-90.0))
.post_concat(Transform::translate(Abs::zero(), size.y)),
Side::Bottom => Transform::rotate(Angle::deg(180.0))
.post_concat(Transform::translate(size.x, size.y)),
Side::Right => Transform::rotate(Angle::deg(90.0))
.post_concat(Transform::translate(size.x, Abs::zero())),
_ => Transform::identity(),
};
arc1 = arc1.map(|x| x.transform(transform));
arc2 = arc2.map(|x| x.transform(transform));
if !connection.prev {
path.move_to(if start_radius.is_zero() { arc1[3] } else { arc1[0] });
}
if !start_radius.is_zero() {
path.cubic_to(arc1[1], arc1[2], arc1[3]);
}
path.line_to(arc2[0]);
if !connection.next && !end_radius.is_zero() {
path.cubic_to(arc2[1], arc2[2], arc2[3]);
}
}
/// Get the control points for a bezier curve that describes a circular arc for
/// a start point, an end point and a center of the circle whose arc connects
/// the two.
fn bezier_arc(start: Point, center: Point, end: Point) -> [Point; 4] {
// https://stackoverflow.com/a/44829356/1567835
let a = start - center;
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);
[start, control_1, control_2, end]
}
/// Indicates which sides of the border strokes in a 2D polygon are connected to
/// their neighboring sides.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
struct Connection {
prev: bool,
next: bool,
}
impl Connection {
/// Advance to the next clockwise side of the polygon. The argument
/// indicates whether the border is connected on the right side of the next
/// edge.
pub fn advance(self, next: bool) -> Self {
Self { prev: self.next, next }
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 876 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -43,8 +43,51 @@
// Test stroke composition.
#set square(stroke: 4pt)
#set text(font: "Roboto")
#square(
stroke: (left: red, top: yellow, right: green, bottom: blue),
radius: 100%, align(center+horizon)[*G*],
inset: 8pt
#stack(
dir: ltr,
square(
stroke: (left: red, top: yellow, right: green, bottom: blue),
radius: 50%, align(center+horizon)[*G*],
inset: 8pt
),
h(0.5cm),
square(
stroke: (left: red, top: yellow + 8pt, right: green, bottom: blue + 2pt),
radius: 50%, align(center+horizon)[*G*],
inset: 8pt
),
h(0.5cm),
square(
stroke: (left: red, top: yellow, right: green, bottom: blue),
radius: 100%, align(center+horizon)[*G*],
inset: 8pt
),
)
// Join between different solid strokes
#set square(size: 20pt, stroke: 2pt)
#set square(stroke: (left: green + 4pt, top: black + 2pt, right: blue, bottom: black + 2pt))
#stack(
dir: ltr,
square(),
h(0.2cm),
square(radius: (top-left: 0pt, rest: 1pt)),
h(0.2cm),
square(radius: (top-left: 0pt, rest: 8pt)),
h(0.2cm),
square(radius: (top-left: 0pt, rest: 100pt)),
)
// Join between solid and dotted strokes
#set square(stroke: (left: green + 4pt, top: black + 2pt, right: (paint: blue, dash: "dotted"), bottom: (paint: black, dash: "dotted")))
#stack(
dir: ltr,
square(),
h(0.2cm),
square(radius: (top-left: 0pt, rest: 1pt)),
h(0.2cm),
square(radius: (top-left: 0pt, rest: 8pt)),
h(0.2cm),
square(radius: (top-left: 0pt, rest: 100pt)),
)

View File

@ -32,7 +32,7 @@
#stack(
dir: ltr,
spacing: 1fr,
rect(width: 2cm, radius: 60%),
rect(width: 2cm, radius: 30%),
rect(width: 1cm, radius: (left: 10pt, right: 5pt)),
rect(width: 1.25cm, radius: (
top-left: 2pt,

View File

@ -1,6 +1,53 @@
// Test rounded rectangles and squares.
---
// Ensure that radius is clamped.
#rect(radius: -20pt)
#square(radius: 30pt)
#set square(size: 20pt, stroke: 4pt)
// no radius for non-rounded corners
#stack(
dir: ltr,
square(),
h(10pt),
square(radius: 0pt),
h(10pt),
square(radius: -10pt),
)
#stack(
dir: ltr,
square(),
h(10pt),
square(radius: 0%),
h(10pt),
square(radius: -10%),
)
// small values for small radius
#stack(
dir: ltr,
square(radius: 1pt),
h(10pt),
square(radius: 5%),
h(10pt),
square(radius: 2pt),
)
// large values for large radius or circle
#stack(
dir: ltr,
square(radius: 8pt),
h(10pt),
square(radius: 10pt),
h(10pt),
square(radius: 12pt),
)
#stack(
dir: ltr,
square(radius: 45%),
h(10pt),
square(radius: 50%),
h(10pt),
square(radius: 55%),
)