summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-25 12:16:17 +0300
committerPaul Buetow <paul@buetow.org>2026-06-25 12:16:17 +0300
commit964901d672d8e12baf4ac0eb29821d670ae15eb8 (patch)
treedea3e16972b0d6caa9bfbf2aa063d8c3fdb88cab /src
parent3377635c88a6a2f6631a4c416c32f0d57ec05ccd (diff)
Flush preview-grid rows: fixed breakpoint columns + multiple-of-12 pages
The overview grid used auto-fill columns (an unpredictable count at view time) while tiles per page were fixed at generation, so the last row was ragged -- an empty, cut-off bottom-right corner, made worse by a 2x2 feature tile. CSS (header.tmpl): replace auto-fill with a FIXED column count per width breakpoint -- 2 (phone) / 3 / 4 / 6 -- all divisors of 12. THUMBHEIGHT no longer drives the grid (it only sizes the thumbnail files), so its obsolete render-var is dropped (template.source.sh). Generator (album-tile-layout, album-thumbnail-html): append_preview_grid now decides a page's tiles, then snaps the grid-cell total onto a multiple of 12 before emitting, via two photo-preserving levers -- _grid_split_subdivides_to_add (round up: split subdivided tiles into singles; preferred, abundant) and _grid_merge_singles_to_remove (round down: merge adjacent singles). Because 2/3/4/6 all divide 12, a multiple-of-12 page tiles into a COMPLETE rectangle at every breakpoint: a flush last row at any window width, with no image distortion (object-fit: cover). Per-photo preview numbers and all navigation redirects are unchanged. Tiny pages (a short final page or small stats mini-album) are left as-is. Decrements use assignment, not bare "(( --k ))": under set -euo pipefail an arithmetic command evaluating to 0 returns status 1 and would abort generate. Helper namerefs are uniquely named and arrays are forwarded by name to avoid bash circular-nameref errors. Tests: add test_album_grid_cells_align_to_multiple_of_12 (both levers); update the two tests that pinned the old auto-fill CSS / render-var list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/lib/album-thumbnail-html.source.sh42
-rw-r--r--src/lib/album-tile-layout.source.sh159
-rw-r--r--src/lib/template.source.sh1
3 files changed, 195 insertions, 7 deletions
diff --git a/src/lib/album-thumbnail-html.source.sh b/src/lib/album-thumbnail-html.source.sh
index 8f34695..add5f36 100644
--- a/src/lib/album-thumbnail-html.source.sh
+++ b/src/lib/album-thumbnail-html.source.sh
@@ -22,6 +22,13 @@
# the single shared grid builder for both the main preview pages and the stats
# mini-album galleries. Tile blocks are separated by a single newline; the
# template adds the trailing newline.
+#
+# Three passes: (1) decide the whole page's tiles, recording each tile's layout,
+# first-photo index and photo count and accumulating the grid-cell footprint;
+# (2) snap that cell total onto a multiple of 12 (_align_page_tiles_to_grid) so
+# the fixed 2/3/4/6-column grid is a complete rectangle -- a flush last row -- at
+# every width; (3) emit the (possibly merged) tiles. Splitting decide-from-emit
+# is what lets pass 2 adjust the layout before any HTML is built.
append_preview_grid() {
local -n buffer_ref="$1"; shift
local -r thumbs_dir="$1"; shift
@@ -30,6 +37,7 @@ append_preview_grid() {
local -a photos=("$@")
local -i i=0
local -i count
+ local -i total_cells=0
local layout
local block
# Cap the big 2x2 feature tiles at this many per page; once reached, later
@@ -37,27 +45,49 @@ append_preview_grid() {
# subdivided/single), so a page never gets crowded with hero tiles.
local -ri max_features=2
local -i features_used=0
+ # 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=()
+ local -a tile_starts=()
+ 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
- # Decide this tile's layout from the photos still available; the first
- # photo's name is the seeded-random context so the choice is stable.
- # Features are only offered until the per-page cap is reached.
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 3: emit the tile blocks in order.
+ for (( t = 0; t < ${#tile_layouts[@]}; t++ )); do
block=$(build_tile_block \
- "$thumbs_dir" "$backhref" "$href_prefix" "$layout" "$(( i + 1 ))" \
- "${photos[@]:i:count}")
+ "$thumbs_dir" "$backhref" "$href_prefix" "${tile_layouts[t]}" \
+ "$(( tile_starts[t] + 1 ))" \
+ "${photos[@]:${tile_starts[t]}:${tile_counts[t]}}")
if [ -z "$buffer_ref" ]; then
buffer_ref="$block"
else
buffer_ref+=$'\n'"$block"
fi
- (( i += count ))
done
}
diff --git a/src/lib/album-tile-layout.source.sh b/src/lib/album-tile-layout.source.sh
index b30981b..cbddb6d 100644
--- a/src/lib/album-tile-layout.source.sh
+++ b/src/lib/album-tile-layout.source.sh
@@ -66,6 +66,165 @@ tile_layout_for() {
printf '%s %s\n' "${names[choice]}" "${counts[choice]}"
}
+# The valid subdivided-tile layout name for a given photo count (2..4); used when
+# a split leaves a smaller-but-still-subdivided remainder. A count of 1 is a
+# plain single and never asks here.
+_grid_subdivide_layout_for() {
+ case "$1" in
+ 2) printf 'two_wide\n' ;;
+ 3) printf 'squares_wide_top\n' ;;
+ *) printf 'quad\n' ;;
+ esac
+}
+
+# Snap a page's tiles onto a grid-cell total that is a multiple of 12 so the
+# fixed 2/3/4/6-column overview grid (all divisors of 12) forms a COMPLETE
+# rectangle at every breakpoint -- no ragged, cut-off last row at any window
+# width. The cell footprint is 4 for a 2x2 "feature" tile and 1 for every other
+# tile, so a page's natural total is rarely a multiple of 12.
+#
+# Two levers reach the nearest reachable multiple of 12, both keeping the SAME
+# photos in the SAME order (so per-photo preview numbers / view-page links never
+# change), only their visual grouping shifts:
+# - PREFERRED, round up: split subdivided tiles into singles (+1 cell per photo
+# peeled). Abundant on a normal album (many subdivided tiles) and needs no
+# adjacency, so this is the reliable lever.
+# - FALLBACK, round down: merge adjacent single tiles into two-up tiles (-1 cell
+# per merge). Used when there are too few subdivided cells to round up (e.g. a
+# single-heavy page, or THUMB_SUBDIVIDE_PERCENT=0).
+# Left untouched when the page has fewer than 12 cells (a tiny stats mini-album or
+# the album's short final page) or neither lever can reach a multiple of 12.
+# Operates in place on the parallel layout/start/count arrays passed by name.
+# The three array arguments are the NAMES of the caller's parallel arrays (passed
+# as strings and forwarded by name to the chosen lever helper). We bind only
+# read-only local namerefs here (uniquely named so they never alias a caller's
+# nameref of the same name -- bash would treat that as a circular reference).
+_align_page_tiles_to_grid() {
+ local -r layouts_name="$1"; shift
+ local -r starts_name="$1"; shift
+ local -r counts_name="$1"; shift
+ local -ri total_cells="$1"; shift
+
+ local -ri remainder=$(( total_cells % 12 ))
+ if (( total_cells < 12 || remainder == 0 )); then
+ return
+ fi
+
+ # How many extra cells splitting every subdivided tile fully could yield.
+ # shellcheck disable=SC2178
+ local -n align_layouts="$layouts_name"
+ # shellcheck disable=SC2178
+ local -n align_counts="$counts_name"
+ local -i capacity_up=0 i
+ for (( i = 0; i < ${#align_layouts[@]}; i++ )); do
+ if [ "${align_layouts[i]}" != single ] \
+ && [ "${align_layouts[i]}" != feature ] \
+ && (( align_counts[i] >= 2 )); then
+ capacity_up=$(( capacity_up + align_counts[i] - 1 ))
+ fi
+ done
+
+ if (( capacity_up >= 12 - remainder )); then
+ _grid_split_subdivides_to_add \
+ "$layouts_name" "$starts_name" "$counts_name" "$(( 12 - remainder ))"
+ else
+ _grid_merge_singles_to_remove \
+ "$layouts_name" "$starts_name" "$counts_name" "$remainder"
+ fi
+}
+
+# Round a page UP to the next multiple of 12 by peeling `to_add` photos off
+# subdivided tiles into trailing singles (each peel: +1 cell, photos preserved in
+# order). Peeling the tail of a subdivide keeps both pieces contiguous, so the
+# kept remainder (a smaller subdivide, or a single when only one photo is left)
+# and the peeled singles stay in photo order. Rebuilds the arrays in place.
+_grid_split_subdivides_to_add() {
+ # shellcheck disable=SC2178
+ local -n split_layouts="$1"; shift
+ # shellcheck disable=SC2178
+ local -n split_starts="$1"; shift
+ # shellcheck disable=SC2178
+ local -n split_counts="$1"; shift
+ local -i to_add="$1"; shift
+
+ local -a new_layouts=() new_starts=() new_counts=()
+ local -i i p peel keep start cnt
+ for (( i = 0; i < ${#split_layouts[@]}; i++ )); do
+ start=${split_starts[i]}
+ cnt=${split_counts[i]}
+ if (( to_add > 0 )) && [ "${split_layouts[i]}" != single ] \
+ && [ "${split_layouts[i]}" != feature ] && (( cnt >= 2 )); then
+ peel=$(( to_add < cnt - 1 ? to_add : cnt - 1 ))
+ keep=$(( cnt - peel ))
+ if (( keep == 1 )); then
+ new_layouts+=(single)
+ else
+ new_layouts+=("$(_grid_subdivide_layout_for "$keep")")
+ fi
+ new_starts+=("$start")
+ new_counts+=("$keep")
+ for (( p = 0; p < peel; p++ )); do
+ new_layouts+=(single)
+ new_starts+=("$(( start + keep + p ))")
+ new_counts+=(1)
+ done
+ to_add=$(( to_add - peel ))
+ else
+ new_layouts+=("${split_layouts[i]}")
+ new_starts+=("$start")
+ new_counts+=("$cnt")
+ fi
+ done
+
+ split_layouts=("${new_layouts[@]}")
+ split_starts=("${new_starts[@]}")
+ split_counts=("${new_counts[@]}")
+}
+
+# Round a page DOWN to the previous multiple of 12 by merging `to_remove` pairs of
+# adjacent single tiles into two-up "two_wide" tiles (each merge: -1 cell). Walks
+# right-to-left building a reversed result, then un-reverses it. Decrements use
+# assignment ("k=$(( k - 1 ))"), never a bare "(( --k ))": under set -euo pipefail
+# an arithmetic command whose result is 0 (e.g. --k reaching 0) returns status 1
+# and would abort the whole generate; an assignment always returns 0.
+_grid_merge_singles_to_remove() {
+ # shellcheck disable=SC2178
+ local -n merge_layouts="$1"; shift
+ # shellcheck disable=SC2178
+ local -n merge_starts="$1"; shift
+ # shellcheck disable=SC2178
+ local -n merge_counts="$1"; shift
+ local -i merges_left="$1"; shift
+
+ local -a new_layouts=() new_starts=() new_counts=()
+ local -i k=${#merge_layouts[@]} j
+ while (( k > 0 )); do
+ k=$(( k - 1 ))
+ if (( merges_left > 0 && k > 0 )) \
+ && [ "${merge_layouts[k]}" = single ] \
+ && [ "${merge_layouts[k - 1]}" = single ]; then
+ new_layouts+=(two_wide)
+ new_starts+=("${merge_starts[k - 1]}")
+ new_counts+=(2)
+ merges_left=$(( merges_left - 1 ))
+ k=$(( k - 1 ))
+ else
+ new_layouts+=("${merge_layouts[k]}")
+ new_starts+=("${merge_starts[k]}")
+ new_counts+=("${merge_counts[k]}")
+ fi
+ done
+
+ merge_layouts=() merge_starts=() merge_counts=()
+ j=${#new_layouts[@]}
+ while (( j > 0 )); do
+ j=$(( j - 1 ))
+ merge_layouts+=("${new_layouts[j]}")
+ merge_starts+=("${new_starts[j]}")
+ merge_counts+=("${new_counts[j]}")
+ done
+}
+
# 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
diff --git a/src/lib/template.source.sh b/src/lib/template.source.sh
index 016d2a2..6201647 100644
--- a/src/lib/template.source.sh
+++ b/src/lib/template.source.sh
@@ -262,7 +262,6 @@ declare -ra TEMPLATE_RENDER_FIELD_SPECS=(
'render_stats_page_html|config_html|STATS_PAGE||header'
'render_tarball_include|tarball_include|||footer'
'render_tarball_name_html|context_html|tarball_name|tarball_name|footer'
- 'render_thumbheight_html|config_html|THUMBHEIGHT||header'
'render_thumbs_dir_html|context_html|thumbs_dir|thumbs_dir|preview'
'render_title_html|config_html|TITLE||camera header splash stats'
'render_view_next_html|preview_num_next_html|preview_num||details view'