mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Fix clipping when a box/block has a radius
(#2338)
This commit is contained in:
parent
a8af6b449a
commit
9bca0bce73
@ -146,15 +146,18 @@ impl Layout for BoxElem {
|
|||||||
frame.set_baseline(frame.baseline() - shift);
|
frame.set_baseline(frame.baseline() - shift);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clip the contents
|
|
||||||
if self.clip(styles) {
|
|
||||||
frame.clip();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare fill and stroke.
|
// Prepare fill and stroke.
|
||||||
let fill = self.fill(styles);
|
let fill = self.fill(styles);
|
||||||
let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
|
let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
|
||||||
|
|
||||||
|
// Clip the contents
|
||||||
|
if self.clip(styles) {
|
||||||
|
let outset = self.outset(styles).relative_to(frame.size());
|
||||||
|
let size = frame.size() + outset.sum_by_axis();
|
||||||
|
let radius = self.radius(styles);
|
||||||
|
frame.clip(path_rect(size, radius, &stroke));
|
||||||
|
}
|
||||||
|
|
||||||
// Add fill and/or stroke.
|
// Add fill and/or stroke.
|
||||||
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
||||||
let outset = self.outset(styles);
|
let outset = self.outset(styles);
|
||||||
@ -408,17 +411,20 @@ impl Layout for BlockElem {
|
|||||||
frames
|
frames
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clip the contents
|
|
||||||
if self.clip(styles) {
|
|
||||||
for frame in frames.iter_mut() {
|
|
||||||
frame.clip();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare fill and stroke.
|
// Prepare fill and stroke.
|
||||||
let fill = self.fill(styles);
|
let fill = self.fill(styles);
|
||||||
let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
|
let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
|
||||||
|
|
||||||
|
// Clip the contents
|
||||||
|
if self.clip(styles) {
|
||||||
|
for frame in frames.iter_mut() {
|
||||||
|
let outset = self.outset(styles).relative_to(frame.size());
|
||||||
|
let size = frame.size() + outset.sum_by_axis();
|
||||||
|
let radius = self.radius(styles);
|
||||||
|
frame.clip(path_rect(size, radius, &stroke));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add fill and/or stroke.
|
// Add fill and/or stroke.
|
||||||
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
||||||
let mut skip = false;
|
let mut skip = false;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use typst::geom::Smart;
|
use typst::geom::{self, Smart};
|
||||||
use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
|
use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
|
||||||
use typst::util::option_eq;
|
use typst::util::option_eq;
|
||||||
|
|
||||||
@ -212,7 +212,7 @@ impl Layout for ImageElem {
|
|||||||
|
|
||||||
// Create a clipping group if only part of the image should be visible.
|
// Create a clipping group if only part of the image should be visible.
|
||||||
if fit == ImageFit::Cover && !target.fits(fitted) {
|
if fit == ImageFit::Cover && !target.fits(fitted) {
|
||||||
frame.clip();
|
frame.clip(geom::Path::rect(frame.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply metadata.
|
// Apply metadata.
|
||||||
|
@ -13,7 +13,7 @@ use crate::export::PdfPageLabel;
|
|||||||
use crate::font::Font;
|
use crate::font::Font;
|
||||||
use crate::geom::{
|
use crate::geom::{
|
||||||
self, styled_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,
|
Geometry, Length, Numeric, Paint, Path, Point, Rel, Shape, Sides, Size, Transform,
|
||||||
};
|
};
|
||||||
use crate::image::Image;
|
use crate::image::Image;
|
||||||
use crate::model::{Content, Location, MetaElem, StyleChain};
|
use crate::model::{Content, Location, MetaElem, StyleChain};
|
||||||
@ -351,10 +351,14 @@ impl Frame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clip the contents of a frame to its size.
|
/// Clip the contents of a frame to a clip path.
|
||||||
pub fn clip(&mut self) {
|
///
|
||||||
|
/// The clip path can be the size of the frame in the case of a
|
||||||
|
/// rectangular frame. In the case of a frame with rounded corner,
|
||||||
|
/// this should be a path that matches the frame's outline.
|
||||||
|
pub fn clip(&mut self, clip_path: Path) {
|
||||||
if !self.is_empty() {
|
if !self.is_empty() {
|
||||||
self.group(|g| g.clips = true);
|
self.group(|g| g.clip_path = Some(clip_path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -505,7 +509,7 @@ pub struct GroupItem {
|
|||||||
/// A transformation to apply to the group.
|
/// A transformation to apply to the group.
|
||||||
pub transform: Transform,
|
pub transform: Transform,
|
||||||
/// Whether the frame should be a clipping boundary.
|
/// Whether the frame should be a clipping boundary.
|
||||||
pub clips: bool,
|
pub clip_path: Option<Path>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GroupItem {
|
impl GroupItem {
|
||||||
@ -514,7 +518,7 @@ impl GroupItem {
|
|||||||
Self {
|
Self {
|
||||||
frame,
|
frame,
|
||||||
transform: Transform::identity(),
|
transform: Transform::identity(),
|
||||||
clips: false,
|
clip_path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -462,14 +462,8 @@ fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) {
|
|||||||
ctx.size(group.frame.size());
|
ctx.size(group.frame.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
if group.clips {
|
if let Some(clip_path) = &group.clip_path {
|
||||||
let size = group.frame.size();
|
write_path(ctx, 0.0, 0.0, clip_path);
|
||||||
let w = size.x.to_f32();
|
|
||||||
let h = size.y.to_f32();
|
|
||||||
ctx.content.move_to(0.0, 0.0);
|
|
||||||
ctx.content.line_to(w, 0.0);
|
|
||||||
ctx.content.line_to(w, h);
|
|
||||||
ctx.content.line_to(0.0, h);
|
|
||||||
ctx.content.clip_nonzero();
|
ctx.content.clip_nonzero();
|
||||||
ctx.content.end_path();
|
ctx.content.end_path();
|
||||||
}
|
}
|
||||||
|
@ -183,13 +183,9 @@ fn render_group(canvas: &mut sk::Pixmap, state: State, group: &GroupItem) {
|
|||||||
|
|
||||||
let mut mask = state.mask;
|
let mut mask = state.mask;
|
||||||
let storage;
|
let storage;
|
||||||
if group.clips {
|
if let Some(clip_path) = group.clip_path.as_ref() {
|
||||||
let size: geom::Axes<Abs> = group.frame.size();
|
if let Some(path) =
|
||||||
let w = size.x.to_f32();
|
convert_path(clip_path).and_then(|path| path.transform(state.transform))
|
||||||
let h = size.y.to_f32();
|
|
||||||
if let Some(path) = sk::Rect::from_xywh(0.0, 0.0, w, h)
|
|
||||||
.map(sk::PathBuilder::from_rect)
|
|
||||||
.and_then(|path| path.transform(state.transform))
|
|
||||||
{
|
{
|
||||||
if let Some(mask) = mask {
|
if let Some(mask) = mask {
|
||||||
let mut mask = mask.clone();
|
let mut mask = mask.clone();
|
||||||
|
@ -12,8 +12,9 @@ use crate::doc::{Frame, FrameItem, FrameKind, GroupItem, TextItem};
|
|||||||
use crate::eval::Repr;
|
use crate::eval::Repr;
|
||||||
use crate::font::Font;
|
use crate::font::Font;
|
||||||
use crate::geom::{
|
use crate::geom::{
|
||||||
Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint,
|
self, Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin,
|
||||||
PathItem, Point, Quadrant, Ratio, RatioOrAngle, Relative, Shape, Size, Transform,
|
Paint, PathItem, Point, Quadrant, Ratio, RatioOrAngle, Relative, Shape, Size,
|
||||||
|
Transform,
|
||||||
};
|
};
|
||||||
use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat};
|
use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat};
|
||||||
use crate::util::hash128;
|
use crate::util::hash128;
|
||||||
@ -269,16 +270,9 @@ impl SVGRenderer {
|
|||||||
self.xml.start_element("g");
|
self.xml.start_element("g");
|
||||||
self.xml.write_attribute("class", "typst-group");
|
self.xml.write_attribute("class", "typst-group");
|
||||||
|
|
||||||
if group.clips {
|
if let Some(clip_path) = &group.clip_path {
|
||||||
let hash = hash128(&group);
|
let hash = hash128(&group);
|
||||||
let size = group.frame.size();
|
let id = self.clip_paths.insert_with(hash, || convert_path(clip_path));
|
||||||
let x = size.x.to_pt();
|
|
||||||
let y = size.y.to_pt();
|
|
||||||
let id = self.clip_paths.insert_with(hash, || {
|
|
||||||
let mut builder = SvgPathBuilder(EcoString::new());
|
|
||||||
builder.rect(x as f32, y as f32);
|
|
||||||
builder.0
|
|
||||||
});
|
|
||||||
self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})"));
|
self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1014,8 +1008,14 @@ fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
|
|||||||
let y = rect.y.to_pt() as f32;
|
let y = rect.y.to_pt() as f32;
|
||||||
builder.rect(x, y);
|
builder.rect(x, y);
|
||||||
}
|
}
|
||||||
Geometry::Path(p) => {
|
Geometry::Path(p) => return convert_path(p),
|
||||||
for item in &p.0 {
|
};
|
||||||
|
builder.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_path(path: &geom::Path) -> EcoString {
|
||||||
|
let mut builder = SvgPathBuilder::default();
|
||||||
|
for item in &path.0 {
|
||||||
match item {
|
match item {
|
||||||
PathItem::MoveTo(m) => {
|
PathItem::MoveTo(m) => {
|
||||||
builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32)
|
builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32)
|
||||||
@ -1034,8 +1034,6 @@ fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
|
|||||||
PathItem::ClosePath => builder.close(),
|
PathItem::ClosePath => builder.close(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
builder.0
|
builder.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ pub use self::paint::Paint;
|
|||||||
pub use self::path::{Path, PathItem};
|
pub use self::path::{Path, PathItem};
|
||||||
pub use self::point::Point;
|
pub use self::point::Point;
|
||||||
pub use self::ratio::Ratio;
|
pub use self::ratio::Ratio;
|
||||||
pub use self::rect::styled_rect;
|
pub use self::rect::{path_rect, styled_rect};
|
||||||
pub use self::rel::Rel;
|
pub use self::rel::Rel;
|
||||||
pub use self::scalar::Scalar;
|
pub use self::scalar::Scalar;
|
||||||
pub use self::shape::{Geometry, Shape};
|
pub use self::shape::{Geometry, Shape};
|
||||||
|
@ -42,6 +42,19 @@ impl PathExtension for Path {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new rectangle as a path.
|
||||||
|
pub fn path_rect(
|
||||||
|
size: Size,
|
||||||
|
radius: Corners<Rel<Abs>>,
|
||||||
|
stroke: &Sides<Option<FixedStroke>>,
|
||||||
|
) -> Path {
|
||||||
|
if stroke.is_uniform() && radius.iter().cloned().all(Rel::is_zero) {
|
||||||
|
Path::rect(size)
|
||||||
|
} else {
|
||||||
|
segmented_path_rect(size, radius, stroke)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a styled rectangle with shapes.
|
/// Create a styled rectangle with shapes.
|
||||||
/// - use rect primitive for simple rectangles
|
/// - use rect primitive for simple rectangles
|
||||||
/// - stroke sides if possible
|
/// - stroke sides if possible
|
||||||
@ -68,24 +81,13 @@ fn simple_rect(
|
|||||||
vec![Shape { geometry: Geometry::Rect(size), fill, stroke }]
|
vec![Shape { geometry: Geometry::Rect(size), fill, stroke }]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use stroke and fill for the rectangle
|
fn corners_control_points(
|
||||||
fn segmented_rect(
|
|
||||||
size: Size,
|
size: Size,
|
||||||
radius: Corners<Rel<Abs>>,
|
radius: Corners<Abs>,
|
||||||
fill: Option<Paint>,
|
strokes: &Sides<Option<FixedStroke>>,
|
||||||
strokes: Sides<Option<FixedStroke>>,
|
stroke_widths: Sides<Abs>,
|
||||||
) -> Vec<Shape> {
|
) -> Corners<ControlPoints> {
|
||||||
let mut res = vec![];
|
Corners {
|
||||||
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_left: Corner::TopLeft,
|
||||||
top_right: Corner::TopRight,
|
top_right: Corner::TopRight,
|
||||||
bottom_right: Corner::BottomRight,
|
bottom_right: Corner::BottomRight,
|
||||||
@ -105,7 +107,67 @@ fn segmented_rect(
|
|||||||
(None, None) => true,
|
(None, None) => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn segmented_path_rect(
|
||||||
|
size: Size,
|
||||||
|
radius: Corners<Rel<Abs>>,
|
||||||
|
strokes: &Sides<Option<FixedStroke>>,
|
||||||
|
) -> Path {
|
||||||
|
let stroke_widths = strokes
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0));
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
// insert stroked sides below filled sides
|
||||||
|
let mut path = Path::new();
|
||||||
|
let corners = corners_control_points(size, radius, strokes, stroke_widths);
|
||||||
|
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;
|
||||||
|
path_segment(start, end, &corners, &mut path);
|
||||||
|
}
|
||||||
|
} else if strokes.top.is_some() {
|
||||||
|
// single segment
|
||||||
|
path_segment(Corner::TopLeft, Corner::TopLeft, &corners, &mut path);
|
||||||
|
}
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0));
|
||||||
|
|
||||||
|
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_control_points(size, radius, &strokes, stroke_widths);
|
||||||
|
|
||||||
// insert stroked sides below filled sides
|
// insert stroked sides below filled sides
|
||||||
let mut stroke_insert = 0;
|
let mut stroke_insert = 0;
|
||||||
@ -171,6 +233,43 @@ fn segmented_rect(
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn path_segment(
|
||||||
|
start: Corner,
|
||||||
|
end: Corner,
|
||||||
|
corners: &Corners<ControlPoints>,
|
||||||
|
path: &mut Path,
|
||||||
|
) {
|
||||||
|
// create start corner
|
||||||
|
let c = corners.get_ref(start);
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the shape for the segment and whether the shape should be drawn on top.
|
/// Returns the shape for the segment and whether the shape should be drawn on top.
|
||||||
fn segment(
|
fn segment(
|
||||||
start: Corner,
|
start: Corner,
|
||||||
@ -228,35 +327,8 @@ fn stroke_segment(
|
|||||||
stroke: FixedStroke,
|
stroke: FixedStroke,
|
||||||
) -> Shape {
|
) -> Shape {
|
||||||
// create start corner
|
// create start corner
|
||||||
let c = corners.get_ref(start);
|
|
||||||
let mut path = Path::new();
|
let mut path = Path::new();
|
||||||
if start == end || !c.arc() {
|
path_segment(start, end, corners, &mut path);
|
||||||
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 {
|
Shape {
|
||||||
geometry: Geometry::Path(path),
|
geometry: Geometry::Path(path),
|
||||||
|
@ -46,6 +46,16 @@ impl<T> Sides<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert from `&Sides<T>` to `Sides<&T>`.
|
||||||
|
pub fn as_ref(&self) -> Sides<&T> {
|
||||||
|
Sides {
|
||||||
|
left: &self.left,
|
||||||
|
top: &self.top,
|
||||||
|
right: &self.right,
|
||||||
|
bottom: &self.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Zip two instances into one.
|
/// Zip two instances into one.
|
||||||
pub fn zip<U>(self, other: Sides<U>) -> Sides<(T, U)> {
|
pub fn zip<U>(self, other: Sides<U>) -> Sides<(T, U)> {
|
||||||
Sides {
|
Sides {
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 29 KiB |
@ -24,7 +24,7 @@ world 2
|
|||||||
]
|
]
|
||||||
|
|
||||||
---
|
---
|
||||||
// Test cliping svg glyphs
|
// Test clipping svg glyphs
|
||||||
Emoji: #box(height: 0.5em, stroke: 1pt + black)[🐪, 🌋, 🏞]
|
Emoji: #box(height: 0.5em, stroke: 1pt + black)[🐪, 🌋, 🏞]
|
||||||
|
|
||||||
Emoji: #box(height: 0.5em, clip: true, stroke: 1pt + black)[🐪, 🌋, 🏞]
|
Emoji: #box(height: 0.5em, clip: true, stroke: 1pt + black)[🐪, 🌋, 🏞]
|
||||||
@ -40,3 +40,17 @@ First!
|
|||||||
But, soft! what light through yonder window breaks? It is the east, and Juliet
|
But, soft! what light through yonder window breaks? It is the east, and Juliet
|
||||||
is the sun.
|
is the sun.
|
||||||
]
|
]
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test clipping with `radius`.
|
||||||
|
|
||||||
|
#set page(height: 60pt)
|
||||||
|
|
||||||
|
#box(
|
||||||
|
radius: 5pt,
|
||||||
|
stroke: 2pt + black,
|
||||||
|
width: 20pt,
|
||||||
|
height: 20pt,
|
||||||
|
clip: true,
|
||||||
|
image("/files/rhino.png", width: 30pt)
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user