Commits: 2
Implement the divide and conquer solution
Stacks and gutters work together!
index 699bf9f..595359b 100644
--- a/playground.typ
+++ b/playground.typ
@@ -526,7 +526,7 @@ The height of two images in the stack combined would be
=
=It checks! Both cells have approximately the same height.
=
-Now the question is how to scale it arbitrary number of cells. For each cell there would be an additional unknown $w_i$, but also an additional equation $h = w_i r_i + g_i$
+Now the question is how to scale it arbitrary number of cells. For each cell there would be an additional unknown $w_i$, but also an additional equation $h = w_i / r_i + g_i$
=
=== Divide and conquer approximation
=
@@ -536,9 +536,205 @@ In very broad terms:
=
=1. Identify the minimum possible height $h_min$ (that would be the height without any gutters). The current implementation of `image-row` function can calculate this.
=2. Identify the maximum possible height $h_max$ (that would be $h_min + max(g_i)$, so all the gutters without shrinking the content, which would be a case in a single cell containing a stack)
-3. Solve the $h = r_i w_i + g_i$ equations using $h = (h_min + h_max) / 2$ (i.e. midpoint)
+3. Solve the $h = w_i / r_i + g_i$ equations using $h = (h_min + h_max) / 2$ (i.e. midpoint). This can be re-written as $w_i = r_i (h - g_i)$
=4. If $sum_(i=o)^n w_i approx w$ then that's it.
=5. If $sum_(i=o)^n w_i < w$, set $h_min := h$ and solve again.
=6. If $sum_(i=o)^n w_i > w$, set $h_max := h$ and solve again.
=
=Do it at most 100 times or so. Consider some small length, like 1pt to be sufficiently precise.
+
+To make it work:
+
+TODO: Uniform representation of single and stack cell.
+
+Single:
+
+```typst
+(
+ content_ratio: 0.5,
+ gutters_height: 0pt,
+ images: (image-1) # Always an array
+)
+```
+
+Stack:
+
+```typst
+(
+ content_ratio: 0.5,
+ gutters_height: 24pt,
+ images: (image-1, image-2)
+)
+```
+
+#let image_ratio(image) = {
+ let size = measure(image)
+ size.width / size.height
+}
+
+#let cell(gutter: 0pt, ..args) = {
+ let paths = args.pos()
+ let images = paths.map(path => image(path))
+ let gutters_height = (images.len() - 1) * gutter
+ let aspect_ratio = 1.0 / images.map(image => 1.0 / image_ratio(image)).sum()
+
+ (
+ images: images,
+ gutters_height: gutters_height,
+ aspect_ratio: aspect_ratio,
+ )
+}
+
+A cell with a single image:
+
+#context cell(
+ gutter: 12pt,
+ "500x480.jpg",
+)
+
+A cell with multiple images:
+
+#context cell(
+ gutter: 6pt,
+ "300x600.jpg",
+ "500x480.jpg",
+ "360x480.jpg",
+ "780x300.jpg",
+)
+
+Now, with the uniform data structure to represent each cell, we can make a new `image_row_dc` function (for divide and conquer).
+
+#let solve(step: 0, max_steps: 64, tolerance: 1pt, min_height, max_height, available_width, cells) = {
+ let height = (min_height + max_height) / 2
+ let widths = cells.map(cell => cell.aspect_ratio * (height - cell.gutters_height))
+
+ let solved_width = widths.fold(0pt, (sum, width) => sum + width)
+ let deviation = calc.abs(available_width - solved_width)
+
+ // TODO: Add to state for debugging purposes, like seeing how many steps it took
+ // [+ #solved_width × #height #widths]
+
+ if deviation < tolerance or step > max_steps {
+ return widths
+ }
+
+ if solved_width > available_width {
+ // Too high
+ solve(step: step + 1, max_steps: max_steps, tolerance: tolerance, min_height, height, available_width, cells)
+ } else {
+ // Too low
+ solve(step: step + 1, max_steps: max_steps, tolerance: tolerance, height, max_height, available_width, cells)
+ }
+
+
+}
+
+#let image_row_dc(gutter: 0pt, ..args) = {
+ context {
+ let cells = args.pos().map(paths => {
+ if type(paths) == array {
+ cell(gutter: gutter, ..paths)
+ } else {
+ cell(gutter: gutter, paths)
+ }
+ })
+
+ layout(parent => {
+ let gutters_width = (cells.len() - 1) * gutter
+ let available_width = parent.width - gutters_width
+
+ // Theoretical minimum height, as if horizontal gutters where not there
+ let min_ratio = cells.fold(0, (sum, cell) => sum + cell.aspect_ratio)
+ let min_height = available_width / min_ratio
+
+ // Theoretical maximum height would be the minimum height and all the gutters
+ let max_gutters = cells.fold(0pt, (current_max, cell) => calc.max(current_max, cell.gutters_height))
+ let max_height = min_height + max_gutters
+
+ // TODO: Add to state for debugging purposes
+ // [#(
+ // cells: cells,
+ // available_width: available_width,
+ // min_height: min_height,
+ // max_height: max_height,
+ // )]
+
+ let widths = solve(min_height, max_height, available_width, cells)
+
+ // TODO: Add to state for debugging
+ // [widths #widths]
+
+ grid(
+ gutter: gutter,
+ columns: cells.len(),
+ ..cells.enumerate().map(((index, cell)) => {
+ grid(
+ gutter: gutter,
+ columns: widths.at(index),
+ ..cell.images
+ )
+ })
+ )
+
+ })
+ }
+}
+
+Here's a row with gutter and a stack in the middle cell:
+
+#image_row_dc(
+ gutter: 12pt,
+ "300x600.jpg",
+ ("780x300.jpg", "500x480.jpg", "780x300.jpg"),
+ "360x480.jpg",
+)
+
+Same, without the gutter:
+
+#image_row_dc(
+ gutter: 0pt,
+ "300x600.jpg",
+ ("780x300.jpg", "500x480.jpg", "780x300.jpg"),
+ "360x480.jpg",
+)
+
+With a large gutter:
+
+#image_row_dc(
+ gutter: 20pt,
+ "300x600.jpg",
+ ("780x300.jpg", "500x480.jpg", "780x300.jpg"),
+ "360x480.jpg",
+)
+
+Only one stack in a box (kind of usesless, could just be images)
+
+#box(
+ width: 200pt,
+ stroke: 1pt + blue,
+
+ image_row_dc(
+ gutter: 20pt,
+ ("780x300.jpg", "500x480.jpg", "780x300.jpg"),
+ )
+)
+
+In a box, only singular cells:
+
+#box(
+ width: 300pt,
+ stroke: 1pt + blue,
+
+ image_row_dc(
+ gutter: 2pt,
+ "780x300.jpg", "500x480.jpg", "780x300.jpg",
+ )
+)
+
+Two stacks:
+
+#image_row_dc(
+ gutter: 3pt,
+ ("780x300.jpg", "500x480.jpg", "780x300.jpg"),
+ ( "300x600.jpg", "360x480.jpg"),
+)Create a preview document and a module to import
The module is called Erna, after my dear friend who inspired me to figure this out.
new file mode 100644
index 0000000..2517109
Binary files /dev/null and b/500x280.jpg differnew file mode 100644
index 0000000..503ed6f
Binary files /dev/null and b/960x250.jpg differnew file mode 100644
index 0000000..633687d
--- /dev/null
+++ b/erna.typ
@@ -0,0 +1,93 @@
+#let image_ratio(image) = {
+ let size = measure(image)
+ size.width / size.height
+}
+
+#let cell(gutter: 0pt, ..args) = {
+ let paths = args.pos()
+ let images = paths.map(path => image(path))
+ let gutters_height = (images.len() - 1) * gutter
+ let aspect_ratio = 1.0 / images.map(image => 1.0 / image_ratio(image)).sum()
+
+ (
+ images: images,
+ gutters_height: gutters_height,
+ aspect_ratio: aspect_ratio,
+ )
+}
+
+#let solve(step: 0, max_steps: 64, tolerance: 1pt, min_height, max_height, available_width, cells) = {
+ let height = (min_height + max_height) / 2
+ let widths = cells.map(cell => cell.aspect_ratio * (height - cell.gutters_height))
+
+ let solved_width = widths.fold(0pt, (sum, width) => sum + width)
+ let deviation = calc.abs(available_width - solved_width)
+
+ // TODO: Add to state for debugging purposes, like seeing how many steps it took
+ // [+ #solved_width × #height #widths]
+
+ if deviation < tolerance or step > max_steps {
+ return widths
+ }
+
+ if solved_width > available_width {
+ // Too high
+ solve(step: step + 1, max_steps: max_steps, tolerance: tolerance, min_height, height, available_width, cells)
+ } else {
+ // Too low
+ solve(step: step + 1, max_steps: max_steps, tolerance: tolerance, height, max_height, available_width, cells)
+ }
+
+
+}
+
+#let image_row(gutter: 0pt, ..args) = {
+ context {
+ let cells = args.pos().map(paths => {
+ if type(paths) == array {
+ cell(gutter: gutter, ..paths)
+ } else {
+ cell(gutter: gutter, paths)
+ }
+ })
+
+ layout(parent => {
+ let gutters_width = (cells.len() - 1) * gutter
+ let available_width = parent.width - gutters_width
+
+ // Theoretical minimum height, as if horizontal gutters where not there
+ let min_ratio = cells.fold(0, (sum, cell) => sum + cell.aspect_ratio)
+ let min_height = available_width / min_ratio
+
+ // Theoretical maximum height would be the minimum height and all the gutters
+ let max_gutters = cells.fold(0pt, (current_max, cell) => calc.max(current_max, cell.gutters_height))
+ let max_height = min_height + max_gutters
+
+ // TODO: Add to state for debugging purposes
+ // [#(
+ // cells: cells,
+ // available_width: available_width,
+ // min_height: min_height,
+ // max_height: max_height,
+ // )]
+
+ let widths = solve(min_height, max_height, available_width, cells)
+
+ // TODO: Add to state for debugging
+ // [widths #widths]
+
+ grid(
+ gutter: gutter,
+ columns: cells.len(),
+ ..cells.enumerate().map(((index, cell)) => {
+ grid(
+ gutter: gutter,
+ columns: widths.at(index),
+ ..cell.images
+ )
+ })
+ )
+
+ })
+ }
+}new file mode 100644
index 0000000..08e02a8
--- /dev/null
+++ b/preview.typ
@@ -0,0 +1,83 @@
+#import "erna.typ": image_row
+
+#show heading.where(level: 1): content => {
+ pagebreak(weak: true)
+ content
+ v(1em, weak: true)
+}
+
+#title[Erna]
+
+Erna places her images in neat rows.
+
+
+#image_row(
+ gutter: 12pt,
+ ("780x300.jpg", "500x480.jpg", "960x250.jpg"),
+ ("300x600.jpg", "360x480.jpg"),
+)
+
+Erna is a Typst library for aligning images in neat rows. It's easy to use. I made it for a friend who struggles with aligning images in LibreOffice. It's named after her. With Erna (the library), you can:
+
+- Cram as many images as you see fit!
+- Bravely mix sizes, resolutions and aspect ratios. Let the computer figure it out!
+- Got some wiiide images? Stack 'em up!
+
+Erna's promises:
+
+#set list(marker: sym.checkmark.heavy)
+
+- To fill all the available width. \
+- Make the images align vertically. \
+- Preserve aspect ratios.
+
+#pagebreak()
+
+#show raw.where(lang: "typ"): content => {
+
+ content
+
+ eval(content.text, mode: "markup", scope: (image_row: image_row))
+}
+
+= Basic use
+
+At the heart of it is the `image_row` function. It takes a list of image paths, and scales them to equal height and to fit the available width, while preserving aspect ratio of each image. No cropping!
+
+```typ
+#image_row(
+ "300x600.jpg", "360x480.jpg", "500x480.jpg",
+)
+```
+
+
+= With gutters
+
+You can put some space between the images with the `gutter` option.
+
+```typ
+#image_row(
+ gutter: 4pt,
+ "300x600.jpg", "360x480.jpg", "500x480.jpg",
+)
+```
+
+
+= With stacks
+
+It's possible to place several images in a vertical stack. This is useful when mixing portraits and landscapes. Just group the paths in a parentheses to create a stack.
+
+```typ
+#image_row(
+ gutter: 6pt,
+ "360x480.jpg",
+ ("500x280.jpg", "780x300.jpg"),
+ ("960x250.jpg", "500x480.jpg"),
+)
+```
+
+= TODO: Multiple rows
+
+= TODO: Diagram
+
+Brought to you by #link("https://tad-lispy.com/")[Tad Lispy].