diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-25 23:37:11 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-25 23:37:11 +0300 |
| commit | a3a14a667c1b2123793d3769efe46117a7482e12 (patch) | |
| tree | 80ad6e38f10c73e26d80ebf37907fce50950cf6d | |
| parent | 131ef226bbffac1007672dfbd477917e92f1fe78 (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>
| -rwxr-xr-x | bin/shuriken | 121 | ||||
| -rw-r--r-- | src/lib/album-thumbnail-html.source.sh | 78 | ||||
| -rw-r--r-- | src/lib/album-tile-layout.source.sh | 43 |
3 files changed, 164 insertions, 78 deletions
diff --git a/bin/shuriken b/bin/shuriken index 935c3d6..342ad9e 100755 --- a/bin/shuriken +++ b/bin/shuriken @@ -2924,6 +2924,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 @@ -3055,6 +3098,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=() @@ -3062,48 +3113,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-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 |
