summaryrefslogtreecommitdiff
path: root/library/src/graphics
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-11-03 11:44:53 +0100
committerLaurenz <laurmaedje@gmail.com>2022-11-03 13:35:39 +0100
commit37a7afddfaffd44cb9bc013c9506599267e08983 (patch)
tree20e7d62d3c5418baff01a21d0406b91bf3096214 /library/src/graphics
parent56342bd972a13ffe21beaf2b87ab7eb1597704b4 (diff)
Split crates
Diffstat (limited to 'library/src/graphics')
-rw-r--r--library/src/graphics/hide.rs27
-rw-r--r--library/src/graphics/image.rs121
-rw-r--r--library/src/graphics/line.rs68
-rw-r--r--library/src/graphics/mod.rs11
-rw-r--r--library/src/graphics/shape.rs201
5 files changed, 428 insertions, 0 deletions
diff --git a/library/src/graphics/hide.rs b/library/src/graphics/hide.rs
new file mode 100644
index 00000000..3affd809
--- /dev/null
+++ b/library/src/graphics/hide.rs
@@ -0,0 +1,27 @@
+use crate::prelude::*;
+
+/// Hide content without affecting layout.
+#[derive(Debug, Hash)]
+pub struct HideNode(pub Content);
+
+#[node(LayoutInline)]
+impl HideNode {
+ fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ Ok(Self(args.expect("body")?).pack())
+ }
+}
+
+impl LayoutInline for HideNode {
+ fn layout_inline(
+ &self,
+ world: Tracked<dyn World>,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> SourceResult<Vec<Frame>> {
+ let mut frames = self.0.layout_inline(world, regions, styles)?;
+ for frame in &mut frames {
+ frame.clear();
+ }
+ Ok(frames)
+ }
+}
diff --git a/library/src/graphics/image.rs b/library/src/graphics/image.rs
new file mode 100644
index 00000000..6bf02265
--- /dev/null
+++ b/library/src/graphics/image.rs
@@ -0,0 +1,121 @@
+use std::ffi::OsStr;
+
+use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
+
+use crate::prelude::*;
+use crate::text::TextNode;
+
+/// Show a raster or vector graphic.
+#[derive(Debug, Hash)]
+pub struct ImageNode(pub Image);
+
+#[node(LayoutInline)]
+impl ImageNode {
+ /// How the image should adjust itself to a given area.
+ pub const FIT: ImageFit = ImageFit::Cover;
+
+ fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ let Spanned { v: path, span } =
+ args.expect::<Spanned<EcoString>>("path to image file")?;
+
+ let full = vm.locate(&path).at(span)?;
+ let buffer = vm.world.file(&full).at(span)?;
+ let ext = full.extension().and_then(OsStr::to_str).unwrap_or_default();
+ let format = match ext.to_lowercase().as_str() {
+ "png" => ImageFormat::Raster(RasterFormat::Png),
+ "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
+ "gif" => ImageFormat::Raster(RasterFormat::Gif),
+ "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
+ _ => bail!(span, "unknown image format"),
+ };
+
+ let image = Image::new(buffer, format).at(span)?;
+ let width = args.named("width")?;
+ let height = args.named("height")?;
+
+ Ok(ImageNode(image).pack().boxed(Axes::new(width, height)))
+ }
+}
+
+impl LayoutInline for ImageNode {
+ fn layout_inline(
+ &self,
+ _: Tracked<dyn World>,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> SourceResult<Vec<Frame>> {
+ let pxw = self.0.width() as f64;
+ let pxh = self.0.height() as f64;
+ let px_ratio = pxw / pxh;
+
+ // Find out whether the image is wider or taller than the target size.
+ let &Regions { first, expand, .. } = regions;
+ let region_ratio = first.x / first.y;
+ let wide = px_ratio > region_ratio;
+
+ // The space into which the image will be placed according to its fit.
+ let target = if expand.x && expand.y {
+ first
+ } else if expand.x || (!expand.y && wide && first.x.is_finite()) {
+ Size::new(first.x, first.y.min(first.x.safe_div(px_ratio)))
+ } else if first.y.is_finite() {
+ Size::new(first.x.min(first.y * px_ratio), first.y)
+ } else {
+ Size::new(Abs::pt(pxw), Abs::pt(pxh))
+ };
+
+ // Compute the actual size of the fitted image.
+ let fit = styles.get(Self::FIT);
+ let fitted = match fit {
+ ImageFit::Cover | ImageFit::Contain => {
+ if wide == (fit == ImageFit::Contain) {
+ Size::new(target.x, target.x / px_ratio)
+ } else {
+ Size::new(target.y * px_ratio, target.y)
+ }
+ }
+ ImageFit::Stretch => target,
+ };
+
+ // First, place the image in a frame of exactly its size and then resize
+ // the frame to the target size, center aligning the image in the
+ // process.
+ let mut frame = Frame::new(fitted);
+ frame.push(Point::zero(), Element::Image(self.0.clone(), fitted));
+ frame.resize(target, Align::CENTER_HORIZON);
+
+ // Create a clipping group if only part of the image should be visible.
+ if fit == ImageFit::Cover && !target.fits(fitted) {
+ frame.clip();
+ }
+
+ // Apply link if it exists.
+ if let Some(url) = styles.get(TextNode::LINK) {
+ frame.link(url.clone());
+ }
+
+ Ok(vec![frame])
+ }
+}
+
+/// How an image should adjust itself to a given area.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum ImageFit {
+ /// The image should completely cover the area.
+ Cover,
+ /// The image should be fully contained in the area.
+ Contain,
+ /// The image should be stretched so that it exactly fills the area.
+ Stretch,
+}
+
+castable! {
+ ImageFit,
+ Expected: "string",
+ Value::Str(string) => match string.as_str() {
+ "cover" => Self::Cover,
+ "contain" => Self::Contain,
+ "stretch" => Self::Stretch,
+ _ => Err(r#"expected "cover", "contain" or "stretch""#)?,
+ },
+}
diff --git a/library/src/graphics/line.rs b/library/src/graphics/line.rs
new file mode 100644
index 00000000..df427171
--- /dev/null
+++ b/library/src/graphics/line.rs
@@ -0,0 +1,68 @@
+use crate::prelude::*;
+
+/// Display a line without affecting the layout.
+#[derive(Debug, Hash)]
+pub struct LineNode {
+ /// Where the line starts.
+ origin: Axes<Rel<Length>>,
+ /// The offset from the `origin` where the line ends.
+ delta: Axes<Rel<Length>>,
+}
+
+#[node(LayoutInline)]
+impl LineNode {
+ /// How to stroke the line.
+ #[property(resolve, fold)]
+ pub const STROKE: PartialStroke = PartialStroke::default();
+
+ fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ let origin = args.named("origin")?.unwrap_or_default();
+
+ let delta = match args.named::<Axes<Rel<Length>>>("to")? {
+ Some(to) => to.zip(origin).map(|(to, from)| to - from),
+ None => {
+ let length =
+ args.named::<Rel<Length>>("length")?.unwrap_or(Abs::cm(1.0).into());
+
+ let angle = args.named::<Angle>("angle")?.unwrap_or_default();
+ let x = angle.cos() * length;
+ let y = angle.sin() * length;
+
+ Axes::new(x, y)
+ }
+ };
+
+ Ok(Self { origin, delta }.pack())
+ }
+}
+
+impl LayoutInline for LineNode {
+ fn layout_inline(
+ &self,
+ _: Tracked<dyn World>,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> SourceResult<Vec<Frame>> {
+ let stroke = styles.get(Self::STROKE).unwrap_or_default();
+
+ let origin = self
+ .origin
+ .resolve(styles)
+ .zip(regions.base)
+ .map(|(l, b)| l.relative_to(b));
+
+ let delta = self
+ .delta
+ .resolve(styles)
+ .zip(regions.base)
+ .map(|(l, b)| l.relative_to(b));
+
+ let target = regions.expand.select(regions.first, Size::zero());
+ let mut frame = Frame::new(target);
+
+ let shape = Geometry::Line(delta.to_point()).stroked(stroke);
+ frame.push(origin.to_point(), Element::Shape(shape));
+
+ Ok(vec![frame])
+ }
+}
diff --git a/library/src/graphics/mod.rs b/library/src/graphics/mod.rs
new file mode 100644
index 00000000..34182121
--- /dev/null
+++ b/library/src/graphics/mod.rs
@@ -0,0 +1,11 @@
+//! Graphical elements and effects.
+
+mod hide;
+mod image;
+mod line;
+mod shape;
+
+pub use self::image::*;
+pub use hide::*;
+pub use line::*;
+pub use shape::*;
diff --git a/library/src/graphics/shape.rs b/library/src/graphics/shape.rs
new file mode 100644
index 00000000..d980b64a
--- /dev/null
+++ b/library/src/graphics/shape.rs
@@ -0,0 +1,201 @@
+use std::f64::consts::SQRT_2;
+
+use crate::prelude::*;
+use crate::text::TextNode;
+
+/// A sizable and fillable shape with optional content.
+#[derive(Debug, Hash)]
+pub struct ShapeNode<const S: ShapeKind>(pub Option<Content>);
+
+/// A square with optional content.
+pub type SquareNode = ShapeNode<SQUARE>;
+
+/// A rectangle with optional content.
+pub type RectNode = ShapeNode<RECT>;
+
+/// A circle with optional content.
+pub type CircleNode = ShapeNode<CIRCLE>;
+
+/// A ellipse with optional content.
+pub type EllipseNode = ShapeNode<ELLIPSE>;
+
+#[node(LayoutInline)]
+impl<const S: ShapeKind> ShapeNode<S> {
+ /// How to fill the shape.
+ pub const FILL: Option<Paint> = None;
+ /// How to stroke the shape.
+ #[property(skip, resolve, fold)]
+ pub const STROKE: Smart<Sides<Option<PartialStroke>>> = Smart::Auto;
+
+ /// How much to pad the shape's content.
+ #[property(resolve, fold)]
+ pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
+ /// How much to extend the shape's dimensions beyond the allocated space.
+ #[property(resolve, fold)]
+ pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
+
+ /// How much to round the shape's corners.
+ #[property(skip, resolve, fold)]
+ pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero());
+
+ fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ let size = match S {
+ SQUARE => args.named::<Length>("size")?.map(Rel::from),
+ CIRCLE => args.named::<Length>("radius")?.map(|r| 2.0 * Rel::from(r)),
+ _ => None,
+ };
+
+ let width = match size {
+ None => args.named("width")?,
+ size => size,
+ };
+
+ let height = match size {
+ None => args.named("height")?,
+ size => size,
+ };
+
+ Ok(Self(args.eat()?).pack().boxed(Axes::new(width, height)))
+ }
+
+ fn set(...) {
+ if is_round(S) {
+ styles.set_opt(
+ Self::STROKE,
+ args.named::<Smart<Option<PartialStroke>>>("stroke")?
+ .map(|some| some.map(Sides::splat)),
+ );
+ } else {
+ styles.set_opt(Self::STROKE, args.named("stroke")?);
+ styles.set_opt(Self::RADIUS, args.named("radius")?);
+ }
+ }
+}
+
+impl<const S: ShapeKind> LayoutInline for ShapeNode<S> {
+ fn layout_inline(
+ &self,
+ world: Tracked<dyn World>,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> SourceResult<Vec<Frame>> {
+ let mut frames;
+ if let Some(child) = &self.0 {
+ let mut inset = styles.get(Self::INSET);
+ if is_round(S) {
+ inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
+ }
+
+ // Pad the child.
+ let child = child.clone().padded(inset.map(|side| side.map(Length::from)));
+
+ let mut pod = Regions::one(regions.first, regions.base, regions.expand);
+ frames = child.layout_inline(world, &pod, styles)?;
+
+ for frame in frames.iter_mut() {
+ frame.apply_role(Role::GenericBlock);
+ }
+
+ // 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)
+ };
+
+ pod.first = Size::splat(length);
+ pod.expand = Axes::splat(true);
+ frames = child.layout_inline(world, &pod, styles)?;
+ }
+ } else {
+ // The default size that a shape takes on if it has no child and
+ // enough space.
+ let mut size = Size::new(Abs::pt(45.0), Abs::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);
+ } else {
+ size = regions.expand.select(regions.first, size);
+ }
+
+ frames = vec![Frame::new(size)];
+ }
+
+ let frame = &mut frames[0];
+
+ // Add fill and/or stroke.
+ let fill = styles.get(Self::FILL);
+ let stroke = match styles.get(Self::STROKE) {
+ Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())),
+ Smart::Auto => Sides::splat(None),
+ Smart::Custom(strokes) => {
+ strokes.map(|s| s.map(PartialStroke::unwrap_or_default))
+ }
+ };
+
+ let outset = styles.get(Self::OUTSET).relative_to(frame.size());
+ let size = frame.size() + outset.sum_by_axis();
+
+ let radius = styles
+ .get(Self::RADIUS)
+ .map(|side| side.relative_to(size.x.min(size.y) / 2.0));
+
+ let pos = Point::new(-outset.left, -outset.top);
+
+ if fill.is_some() || stroke.iter().any(Option::is_some) {
+ if is_round(S) {
+ let shape = ellipse(size, fill, stroke.left);
+ frame.prepend(pos, Element::Shape(shape));
+ } else {
+ frame.prepend_multiple(
+ rounded_rect(size, radius, fill, stroke)
+ .into_iter()
+ .map(|x| (pos, Element::Shape(x))),
+ )
+ }
+ }
+
+ // Apply link if it exists.
+ if let Some(url) = styles.get(TextNode::LINK) {
+ frame.link(url.clone());
+ }
+
+ Ok(frames)
+ }
+}
+
+/// A category of shape.
+pub type ShapeKind = usize;
+
+/// A rectangle with equal side lengths.
+const SQUARE: ShapeKind = 0;
+
+/// A quadrilateral with four right angles.
+const RECT: ShapeKind = 1;
+
+/// An ellipse with coinciding foci.
+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)
+}