summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-25 23:37:11 +0300
committerPaul Buetow <paul@buetow.org>2026-06-25 23:37:11 +0300
commita3a14a667c1b2123793d3769efe46117a7482e12 (patch)
tree80ad6e38f10c73e26d80ebf37907fce50950cf6d /src
parent131ef226bbffac1007672dfbd477917e92f1fe78 (diff)
Keep 2x2 features off the page bottom; flush short final pages
Two layout-breaking cases remained after the fixed-column grid landed: 1. A 2x2 feature spans two rows, so one placed near the bottom of a page left an L-shaped gap grid-auto-flow: dense could not backfill (nothing follows it) -- a cut-off corner. append_preview_grid now only offers the feature layout while at least feature_tail_margin (16) photos remain, so a hero always sits in the upper rows with enough trailing 1-cell tiles to complete its rows at every breakpoint (and short pages get no hero). 2. A short final page (a leftover handful of photos) could be subdivided down below 12 cells, where it can't be aligned to a multiple of 12 and is ragged. Such pages now go through _build_final_page_tiles: plain singles, then merge down to a multiple of 12 (count >= 12 -> flush grid) or a full-row "fill" filmstrip (count < 12 -> one clean banner, or stacked banners). Longer/full pages keep the normal roll-and-align path. Verified hole-free at 2/3/4/6 columns with a grid-auto-flow:dense packing simulator across ~480 generated pages (regular + short final), 0 failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/lib/album-thumbnail-html.source.sh78
-rw-r--r--src/lib/album-tile-layout.source.sh43
2 files changed, 82 insertions, 39 deletions
diff --git a/src/lib/album-thumbnail-html.source.sh b/src/lib/album-thumbnail-html.source.sh
index d9f17db..c5b14c0 100644
--- a/src/lib/album-thumbnail-html.source.sh
+++ b/src/lib/album-thumbnail-html.source.sh
@@ -50,6 +50,14 @@ append_preview_grid() {
# subdivided/single), so a page never gets crowded with hero tiles.
local -ri max_features=2
local -i features_used=0
+ # A 2x2 feature spans TWO grid rows, so one placed near the bottom of a page
+ # leaves an L-shaped gap that grid-auto-flow: dense cannot backfill (nothing
+ # follows it) -- a broken, cut-off corner at some breakpoints. Keep features
+ # out of the last feature_tail_margin photos so a feature always sits in the
+ # upper rows with enough following 1-cell tiles to complete its rows at every
+ # column count. This also disables features on pages too short to host one
+ # safely (a feature needs >= feature_tail_margin photos after it).
+ local -ri feature_tail_margin=16
# Parallel records of the page's tiles: layout, the first photo's 0-based
# index, and how many photos the tile spans.
local -a tile_layouts=()
@@ -57,48 +65,40 @@ append_preview_grid() {
local -a tile_counts=()
local -i t
- # Pass 1: decide every tile (deterministic, seeded off each tile's first photo
- # name). A 2x2 feature occupies 4 grid cells; every other tile occupies 1.
- while (( i < ${#photos[@]} )); do
- read -r layout count < <(
- tile_layout_for "$(( ${#photos[@]} - i ))" "${photos[i]}" \
- "$(( features_used < max_features ? 1 : 0 ))"
- )
- if [ "$layout" = feature ]; then
- (( ++features_used ))
- (( total_cells += 4 ))
- else
- (( ++total_cells ))
- fi
- tile_layouts+=("$layout")
- tile_starts+=("$i")
- tile_counts+=("$count")
- (( i += count ))
- done
-
- # Pass 2: force the cell total onto a multiple of 12 so the grid is flush at
- # every column breakpoint (no-op for tiny pages / too-few-singles -- see the
- # helper). Per-photo preview numbers are preserved.
- _align_page_tiles_to_grid \
- tile_layouts tile_starts tile_counts "$total_cells"
-
- # Pass 2b: if this is the album's last page and it still could not be aligned
- # to a multiple of 12 (a short final page), widen its final single tile to a
- # full-row "fill" tile so the bottom edge is flush at every breakpoint instead
- # of an orphaned corner. Recompute the post-alignment cell total first.
- if [ "$fill_last" = yes ] && (( ${#tile_layouts[@]} > 0 )); then
- local -i aligned_cells=0
- for (( t = 0; t < ${#tile_layouts[@]}; t++ )); do
- if [ "${tile_layouts[t]}" = feature ]; then
- (( aligned_cells += 4 ))
+ # A SHORT final page (the album's last page with too few photos to tile into
+ # a clean rectangle) is built by a dedicated all-singles helper that is
+ # guaranteed flush at every breakpoint; subdividing/featuring it could drop
+ # the cell count below 12 where it can't be aligned. Longer pages (including a
+ # full final page) use the normal roll-and-align path, which is flush.
+ local -ri short_final_page_max=24
+ if [ "$fill_last" = yes ] && (( ${#photos[@]} < short_final_page_max )); then
+ _build_final_page_tiles \
+ tile_layouts tile_starts tile_counts "${#photos[@]}"
+ else
+ # Pass 1: decide every tile (deterministic, seeded off each tile's first
+ # photo name). A 2x2 feature occupies 4 grid cells; every other tile 1.
+ while (( i < ${#photos[@]} )); do
+ read -r layout count < <(
+ tile_layout_for "$(( ${#photos[@]} - i ))" "${photos[i]}" \
+ "$(( features_used < max_features \
+ && i + feature_tail_margin < ${#photos[@]} ? 1 : 0 ))"
+ )
+ if [ "$layout" = feature ]; then
+ (( ++features_used ))
+ (( total_cells += 4 ))
else
- (( ++aligned_cells ))
+ (( ++total_cells ))
fi
+ tile_layouts+=("$layout")
+ tile_starts+=("$i")
+ tile_counts+=("$count")
+ (( i += count ))
done
- local -i last=$(( ${#tile_layouts[@]} - 1 ))
- if (( aligned_cells % 12 != 0 )) && [ "${tile_layouts[last]}" = single ]; then
- tile_layouts[last]=fill
- fi
+
+ # Pass 2: force the cell total onto a multiple of 12 so the grid is flush
+ # at every column breakpoint. Per-photo preview numbers are preserved.
+ _align_page_tiles_to_grid \
+ tile_layouts tile_starts tile_counts "$total_cells"
fi
# Pass 3: emit the tile blocks in order.
diff --git a/src/lib/album-tile-layout.source.sh b/src/lib/album-tile-layout.source.sh
index 3d0fa23..29e4928 100644
--- a/src/lib/album-tile-layout.source.sh
+++ b/src/lib/album-tile-layout.source.sh
@@ -225,6 +225,49 @@ _grid_merge_singles_to_remove() {
done
}
+# Build the tile layout for the album's SHORT final page (a leftover handful of
+# photos that can't tile into a clean rectangle). Subdividing/featuring such a
+# page can drop its cell count below 12, where it can't be aligned to a multiple
+# of 12 and so is ragged at some breakpoints. Instead lay it out as plain singles
+# (one cell each) and then:
+# - count >= 12: merge down to the nearest multiple of 12 -> a flush grid;
+# - count < 12 : make every tile a full-row "fill" banner -> a flush filmstrip
+# (one photo -> one clean full-width closer; a few photos -> stacked banners).
+# Either way the page is a complete set of full rows at 2/3/4/6 columns. Writes
+# the parallel layout/start/count arrays named by $1/$2/$3 in place.
+_build_final_page_tiles() {
+ local -r layouts_name="$1"; shift
+ local -r starts_name="$1"; shift
+ local -r counts_name="$1"; shift
+ local -ri photo_count="$1"; shift
+
+ # shellcheck disable=SC2178
+ local -n final_layouts="$layouts_name"
+ # shellcheck disable=SC2178
+ local -n final_starts="$starts_name"
+ # shellcheck disable=SC2178
+ local -n final_counts="$counts_name"
+ local -i p
+
+ final_layouts=()
+ final_starts=()
+ final_counts=()
+ for (( p = 0; p < photo_count; p++ )); do
+ final_layouts+=(single)
+ final_starts+=("$p")
+ final_counts+=(1)
+ done
+
+ if (( photo_count >= 12 )); then
+ _grid_merge_singles_to_remove \
+ "$layouts_name" "$starts_name" "$counts_name" "$(( photo_count % 12 ))"
+ else
+ for (( p = 0; p < photo_count; p++ )); do
+ final_layouts[p]=fill
+ done
+ fi
+}
+
# Render one tile's markup (no trailing newline). A "single" tile is the plain
# square thumbnail, byte-identical to the previous per-thumbnail output. A
# "feature" tile is the same single thumbnail but with the 'feature' anchor class