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 differ
new file mode 100644
index 0000000..503ed6f
Binary files /dev/null and b/960x250.jpg differ
new 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].