summaryrefslogtreecommitdiff
path: root/src/library/graphics
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-03-11 11:58:56 +0100
committerLaurenz <laurmaedje@gmail.com>2022-03-11 11:58:56 +0100
commite6b532391deb1e30dc356c4d20dd48199f748f29 (patch)
tree7b631414931164b9a47c9d154172195fc0e1316c /src/library/graphics
parentb71113d37a29bab5c7dc4b501c33ee9afbdb8213 (diff)
More restructuring
Diffstat (limited to 'src/library/graphics')
-rw-r--r--src/library/graphics/hide.rs30
-rw-r--r--src/library/graphics/image.rs116
-rw-r--r--src/library/graphics/mod.rs11
-rw-r--r--src/library/graphics/shape.rs166
-rw-r--r--src/library/graphics/transform.rs86
5 files changed, 409 insertions, 0 deletions
diff --git a/src/library/graphics/hide.rs b/src/library/graphics/hide.rs
new file mode 100644
index 00000000..861a1208
--- /dev/null
+++ b/src/library/graphics/hide.rs
@@ -0,0 +1,30 @@
+use crate::library::prelude::*;
+
+/// Hide a node without affecting layout.
+#[derive(Debug, Hash)]
+pub struct HideNode(pub LayoutNode);
+
+#[class]
+impl HideNode {
+ fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> {
+ Ok(Template::inline(Self(args.expect("body")?)))
+ }
+}
+
+impl Layout for HideNode {
+ fn layout(
+ &self,
+ ctx: &mut Context,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> TypResult<Vec<Arc<Frame>>> {
+ let mut frames = self.0.layout(ctx, regions, styles)?;
+
+ // Clear the frames.
+ for frame in &mut frames {
+ *frame = Arc::new(Frame { elements: vec![], ..**frame });
+ }
+
+ Ok(frames)
+ }
+}
diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs
new file mode 100644
index 00000000..66fb8f4b
--- /dev/null
+++ b/src/library/graphics/image.rs
@@ -0,0 +1,116 @@
+use crate::diag::Error;
+use crate::image::ImageId;
+use crate::library::prelude::*;
+use crate::library::text::TextNode;
+
+/// Show a raster or vector graphic.
+#[derive(Debug, Hash)]
+pub struct ImageNode(pub ImageId);
+
+#[class]
+impl ImageNode {
+ /// How the image should adjust itself to a given area.
+ pub const FIT: ImageFit = ImageFit::Cover;
+
+ fn construct(ctx: &mut Context, args: &mut Args) -> TypResult<Template> {
+ let path = args.expect::<Spanned<EcoString>>("path to image file")?;
+ let full = ctx.resolve(&path.v);
+ let id = ctx.images.load(&full).map_err(|err| {
+ Error::boxed(path.span, match err.kind() {
+ std::io::ErrorKind::NotFound => "file not found".into(),
+ _ => format!("failed to load image ({})", err),
+ })
+ })?;
+
+ let width = args.named("width")?;
+ let height = args.named("height")?;
+
+ Ok(Template::inline(
+ ImageNode(id).pack().sized(Spec::new(width, height)),
+ ))
+ }
+}
+
+impl Layout for ImageNode {
+ fn layout(
+ &self,
+ ctx: &mut Context,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> TypResult<Vec<Arc<Frame>>> {
+ let img = ctx.images.get(self.0);
+ let pxw = img.width() as f64;
+ let pxh = img.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(Length::pt(pxw), Length::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, 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_ref(TextNode::LINK) {
+ frame.link(url);
+ }
+
+ Ok(vec![Arc::new(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/src/library/graphics/mod.rs b/src/library/graphics/mod.rs
new file mode 100644
index 00000000..353f09ca
--- /dev/null
+++ b/src/library/graphics/mod.rs
@@ -0,0 +1,11 @@
+//! Graphical elements and effects.
+
+mod hide;
+mod image;
+mod shape;
+mod transform;
+
+pub use self::image::*;
+pub use hide::*;
+pub use shape::*;
+pub use transform::*;
diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs
new file mode 100644
index 00000000..8b967412
--- /dev/null
+++ b/src/library/graphics/shape.rs
@@ -0,0 +1,166 @@
+use std::f64::consts::SQRT_2;
+
+use crate::library::prelude::*;
+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>);
+
+/// Place a node into a square.
+pub type SquareNode = ShapeNode<SQUARE>;
+
+/// Place a node into a rectangle.
+pub type RectNode = ShapeNode<RECT>;
+
+/// Place a node into a circle.
+pub type CircleNode = ShapeNode<CIRCLE>;
+
+/// Place a node into an ellipse.
+pub type EllipseNode = ShapeNode<ELLIPSE>;
+
+#[class]
+impl<const S: ShapeKind> ShapeNode<S> {
+ /// How to fill the shape.
+ pub const FILL: Option<Paint> = None;
+ /// How the stroke the shape.
+ pub const STROKE: Smart<Option<Paint>> = Smart::Auto;
+ /// The stroke's thickness.
+ pub const THICKNESS: Length = Length::pt(1.0);
+ /// How much to pad the shape's content.
+ pub const PADDING: Linear = Linear::zero();
+
+ fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> {
+ let size = match S {
+ SQUARE => args.named::<Length>("size")?.map(Linear::from),
+ CIRCLE => args.named::<Length>("radius")?.map(|r| 2.0 * Linear::from(r)),
+ _ => None,
+ };
+
+ let width = match size {
+ None => args.named("width")?,
+ size => size,
+ };
+
+ let height = match size {
+ None => args.named("height")?,
+ size => size,
+ };
+
+ Ok(Template::inline(
+ Self(args.find()?).pack().sized(Spec::new(width, height)),
+ ))
+ }
+}
+
+impl<const S: ShapeKind> Layout for ShapeNode<S> {
+ fn layout(
+ &self,
+ ctx: &mut Context,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> 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 += Relative::new(0.5 - SQRT_2 / 4.0);
+ }
+
+ // Pad the child.
+ let child = child.clone().padded(Sides::splat(padding));
+
+ 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)
+ };
+
+ 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);
+ } else {
+ size = regions.expand.select(regions.first, size);
+ }
+
+ frames = vec![Arc::new(Frame::new(size))];
+ }
+
+ let frame = Arc::make_mut(&mut frames[0]);
+
+ // Add fill and/or stroke.
+ let fill = styles.get(Self::FILL);
+ let thickness = styles.get(Self::THICKNESS);
+ let stroke = styles
+ .get(Self::STROKE)
+ .unwrap_or(fill.is_none().then(|| Color::BLACK.into()))
+ .map(|paint| Stroke { paint, thickness });
+
+ if fill.is_some() || stroke.is_some() {
+ let geometry = if is_round(S) {
+ Geometry::Ellipse(frame.size)
+ } else {
+ Geometry::Rect(frame.size)
+ };
+
+ let shape = Shape { geometry, fill, stroke };
+ frame.prepend(Point::zero(), Element::Shape(shape));
+ }
+
+ // Apply link if it exists.
+ if let Some(url) = styles.get_ref(TextNode::LINK) {
+ frame.link(url);
+ }
+
+ 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)
+}
diff --git a/src/library/graphics/transform.rs b/src/library/graphics/transform.rs
new file mode 100644
index 00000000..fafb37a4
--- /dev/null
+++ b/src/library/graphics/transform.rs
@@ -0,0 +1,86 @@
+use crate::geom::Transform;
+use crate::library::prelude::*;
+
+/// Transform a node without affecting layout.
+#[derive(Debug, Hash)]
+pub struct TransformNode<const T: TransformKind> {
+ /// Transformation to apply to the contents.
+ pub transform: Transform,
+ /// The node whose contents should be transformed.
+ pub child: LayoutNode,
+}
+
+/// Transform a node by translating it without affecting layout.
+pub type MoveNode = TransformNode<MOVE>;
+
+/// Transform a node by rotating it without affecting layout.
+pub type RotateNode = TransformNode<ROTATE>;
+
+/// Transform a node by scaling it without affecting layout.
+pub type ScaleNode = TransformNode<SCALE>;
+
+#[class]
+impl<const T: TransformKind> TransformNode<T> {
+ /// The origin of the transformation.
+ pub const ORIGIN: Spec<Option<Align>> = Spec::default();
+
+ fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> {
+ let transform = match T {
+ MOVE => {
+ let tx = args.named("x")?.unwrap_or_default();
+ let ty = args.named("y")?.unwrap_or_default();
+ Transform::translation(tx, ty)
+ }
+ ROTATE => {
+ let angle = args.named_or_find("angle")?.unwrap_or_default();
+ Transform::rotation(angle)
+ }
+ SCALE | _ => {
+ let all = args.find()?;
+ let sx = args.named("x")?.or(all).unwrap_or(Relative::one());
+ let sy = args.named("y")?.or(all).unwrap_or(Relative::one());
+ Transform::scale(sx, sy)
+ }
+ };
+
+ Ok(Template::inline(Self {
+ transform,
+ child: args.expect("body")?,
+ }))
+ }
+}
+
+impl<const T: TransformKind> Layout for TransformNode<T> {
+ fn layout(
+ &self,
+ ctx: &mut Context,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> TypResult<Vec<Arc<Frame>>> {
+ let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON);
+ let mut frames = self.child.layout(ctx, regions, styles)?;
+
+ for frame in &mut frames {
+ let Spec { x, y } = origin.zip(frame.size).map(|(o, s)| o.resolve(s));
+ let transform = Transform::translation(x, y)
+ .pre_concat(self.transform)
+ .pre_concat(Transform::translation(-x, -y));
+
+ Arc::make_mut(frame).transform(transform);
+ }
+
+ Ok(frames)
+ }
+}
+
+/// Kinds of transformations.
+pub type TransformKind = usize;
+
+/// A translation on the X and Y axes.
+const MOVE: TransformKind = 0;
+
+/// A rotational transformation.
+const ROTATE: TransformKind = 1;
+
+/// A scale transformation.
+const SCALE: TransformKind = 2;