summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-22 09:52:57 +0300
committerPaul Buetow <paul@buetow.org>2026-06-22 09:52:57 +0300
commit6945a0ae9afb18313636c09f34959ee5b225f78d (patch)
treee74aa7b861543f5407de863f9667b4ccc994bbbd /src/lib
parent79d78a296d2919155fe4d4bb2fac73d2fa7adeec (diff)
Add dynamic subdivided thumbnail tiles
Make the album preview grid livelier: with a configurable probability (THUMB_SUBDIVIDE_PERCENT, default 30%) a square thumbnail tile is subdivided into several smaller thumbnails packed into the same square footprint, chosen at random from: - quad: 2x2 squares (4 photos) - two-wide: two stacked full-width strips (2 photos) - squares+wide: two squares plus one full-width strip, strip on the top or the bottom (3 photos) Each sub-thumbnail stays its own clickable photo with its own view page; subdivision only groups consecutive photos visually, so preview numbering and the view/details/redirect pages are unchanged. No new images are generated (CSS object-fit crops the existing aspect-correct thumbs into squares or wide strips). Sub-thumbnails get the same random entry animation and the same dramatic hover (flip/scale/rotate/filter) as full thumbs. The layout choice reuses the seeded random_index, so builds stay reproducible under RANDOM_SEED. THUMB_SUBDIVIDE_PERCENT=0 reproduces the previous output byte-for-byte. The new option is wired through the config defaults, validation (0..100), --print-config, the --subdivide CLI flag, the usage text, and the shuriken.json / --dry-run metadata, with docs and tests updated. Generated HTML and CSS pass the W3C Nu HTML checker and CSS validator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/album-metadata.source.sh5
-rw-r--r--src/lib/album-render.source.sh187
-rw-r--r--src/lib/bootstrap.source.sh1
-rw-r--r--src/lib/config.print.source.sh1
-rw-r--r--src/lib/config.source.sh3
-rw-r--r--src/lib/config.validate.source.sh14
6 files changed, 178 insertions, 33 deletions
diff --git a/src/lib/album-metadata.source.sh b/src/lib/album-metadata.source.sh
index 01bf826..3d38cbf 100644
--- a/src/lib/album-metadata.source.sh
+++ b/src/lib/album-metadata.source.sh
@@ -209,6 +209,7 @@ _collect_generation_metadata() {
_GENERATION_METADATA["settings_height"]="$HEIGHT"
_GENERATION_METADATA["settings_thumbheight"]="$THUMBHEIGHT"
_GENERATION_METADATA["settings_maxpreviews"]="$MAXPREVIEWS"
+ _GENERATION_METADATA["settings_subdivide_percent"]="$THUMB_SUBDIVIDE_PERCENT"
_GENERATION_METADATA["settings_image_jobs"]="$IMAGE_JOBS"
_GENERATION_METADATA["settings_random_seed"]="$RANDOM_SEED"
_GENERATION_METADATA["settings_shuffle"]="$SHUFFLE"
@@ -264,6 +265,8 @@ _generation_metadata_json() {
"$(_json_string "${_GENERATION_METADATA["settings_thumbheight"]}")"
printf ' "maxpreviews": %s,\n' \
"$(_json_string "${_GENERATION_METADATA["settings_maxpreviews"]}")"
+ printf ' "subdivide_percent": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["settings_subdivide_percent"]}")"
printf ' "image_jobs": %s,\n' \
"$(_json_string "${_GENERATION_METADATA["settings_image_jobs"]}")"
printf ' "random_seed": %s,\n' \
@@ -345,6 +348,7 @@ collect_dry_run_plan() {
plan_ref["height"]="$HEIGHT"
plan_ref["thumbheight"]="$THUMBHEIGHT"
plan_ref["maxpreviews"]="$MAXPREVIEWS"
+ plan_ref["subdivide_percent"]="$THUMB_SUBDIVIDE_PERCENT"
plan_ref["image_jobs"]="$IMAGE_JOBS"
plan_ref["random_seed"]="$RANDOM_SEED"
plan_ref["shuffle"]="$SHUFFLE"
@@ -375,6 +379,7 @@ print_dry_run_plan() {
printf 'Height: %s\n' "${plan_ref["height"]}"
printf 'Thumb height: %s\n' "${plan_ref["thumbheight"]}"
printf 'Max previews per page: %s\n' "${plan_ref["maxpreviews"]}"
+ printf 'Subdivide percent: %s\n' "${plan_ref["subdivide_percent"]}"
printf 'Image jobs: %s\n' "${plan_ref["image_jobs"]}"
printf 'Random seed: %s\n' "${plan_ref["random_seed"]}"
printf 'Shuffle: %s\n' "${plan_ref["shuffle"]}"
diff --git a/src/lib/album-render.source.sh b/src/lib/album-render.source.sh
index b077dd5..63c0c57 100644
--- a/src/lib/album-render.source.sh
+++ b/src/lib/album-render.source.sh
@@ -100,8 +100,6 @@ render_full_preview_page() {
local -r header_bar="$1"; shift
local -r prev_page="$1"; shift
local -r next_page="$1"; shift
- local -i preview_num=0
- local photo
start_preview_page \
"$photos_dir" "$html_dir" "$blurs_dir" "$backhref" "$page_name" \
@@ -113,13 +111,12 @@ render_full_preview_page() {
# Batch all of this page's thumbnails into ONE template call. Building the
# markup in bash and emitting it via the raw "preview_thumbs" field collapses
# what used to be N "template preview" renders (one env -i bash per
- # thumbnail) into a single previewpage render per page.
+ # thumbnail) into a single previewpage render per page. append_preview_grid
+ # also groups the page's photos into tiles (some subdivided into smaller
+ # thumbnails), so the per-page ordering and preview numbering stay here.
local preview_thumbs=''
- for photo in "$@"; do
- (( ++preview_num ))
- append_preview_thumbnail preview_thumbs \
- "$thumbs_dir" "$backhref" "$page_num" "$preview_num" "$photo"
- done
+ append_preview_grid preview_thumbs \
+ "$thumbs_dir" "$backhref" "$page_num" "$@"
template previewpage "$page_name.html" \
html_dir "$html_dir" \
preview_thumbs "$preview_thumbs"
@@ -186,38 +183,151 @@ queue_preview_page_render_job() {
render_job_labels_ref["$!"]="template render job for preview $page_name"
}
-# Append one thumbnail's markup to a page's accumulating thumbnail-grid buffer.
-# Produces the per-thumbnail preview.tmpl block (the
-# <a id=... href=...><img class='thumb <anim>' alt=... src=...></a> block), so
-# batching all thumbnails into one previewpage render stays consistent. Every
-# interpolated value is HTML-escaped like the template's context_html fields; the
-# seeded "slow" animation class is preserved exactly. Blocks are separated by a
-# newline; the previewpage template adds the single trailing newline, matching
-# the old N sequential renders.
-append_preview_thumbnail() {
+# Build a page's whole thumbnail-grid buffer by walking its photos and grouping
+# them into tiles. Most tiles are a single square thumbnail, but (controlled by
+# THUMB_SUBDIVIDE_PERCENT) some are subdivided into several smaller thumbnails
+# packed into the same square footprint. Each photo keeps its 1-based page
+# position as its preview number (subdivision only groups CONSECUTIVE photos
+# visually, never reorders them), so the view-page links stay correct. Tile
+# blocks are separated by a single newline; the previewpage template adds the
+# trailing newline, matching the old per-thumbnail rendering.
+append_preview_grid() {
local -n buffer_ref="$1"; shift
local -r thumbs_dir="$1"; shift
local -r backhref="$1"; shift
local -r page_num="$1"; shift
- local -r preview_num="$1"; shift
- local -r photo_file="$1"; shift
- local animation_class
+ local -a photos=("$@")
+ local -i i=0
+ local -i count
+ local layout
local block
- animation_class=$(random_animation_css_class slow "$photo_file")
- block=$(build_preview_thumbnail \
- "$thumbs_dir" "$backhref" "$page_num" "$preview_num" "$photo_file" \
- "$animation_class")
- if [ -z "$buffer_ref" ]; then
- buffer_ref="$block"
- else
- buffer_ref+=$'\n'"$block"
+ 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.
+ read -r layout count < <(
+ tile_layout_for "$(( ${#photos[@]} - i ))" "${photos[i]}"
+ )
+ block=$(build_tile_block \
+ "$thumbs_dir" "$backhref" "$page_num" "$layout" "$(( i + 1 ))" \
+ "${photos[@]:i:count}")
+ if [ -z "$buffer_ref" ]; then
+ buffer_ref="$block"
+ else
+ buffer_ref+=$'\n'"$block"
+ fi
+ (( i += count ))
+ done
+}
+
+# Decide the layout for the next tile, printing "<layout> <photo-count>". A tile
+# is subdivided with THUMB_SUBDIVIDE_PERCENT probability, but only into a layout
+# that fits the photos still remaining on the page; otherwise it is a single
+# square thumbnail. Randomness reuses the seeded random_index, so builds are
+# reproducible when RANDOM_SEED is set (and varied otherwise).
+tile_layout_for() {
+ local -ri remaining="$1"; shift
+ local -r context="$1"; shift
+ local -a names=(two_wide)
+ local -a counts=(2)
+ local -i roll choice
+
+ # A single tile when subdivision is disabled, too few photos remain to fill
+ # even the smallest subdivided layout (two_wide needs 2), or the roll misses.
+ if (( remaining < 2 || THUMB_SUBDIVIDE_PERCENT == 0 )); then
+ printf 'single 1\n'
+ return
fi
+ roll=$(random_index "subdivide:$context" 100)
+ if (( roll >= THUMB_SUBDIVIDE_PERCENT )); then
+ printf 'single 1\n'
+ return
+ fi
+
+ # Offer only the subdivision layouts that fit the remaining photo count.
+ if (( remaining >= 3 )); then
+ names+=(squares_wide_top squares_wide_bottom)
+ counts+=(3 3)
+ fi
+ if (( remaining >= 4 )); then
+ names+=(quad)
+ counts+=(4)
+ fi
+
+ choice=$(random_index "sublayout:$context" "${#names[@]}")
+ printf '%s %s\n' "${names[choice]}" "${counts[choice]}"
+}
+
+# Render one tile's markup (no trailing newline). A "single" tile is the plain
+# square thumbnail, byte-identical to the previous per-thumbnail output; any
+# other layout is a subdivided tile. The tile's photos are the trailing args and
+# their preview numbers run from start_preview upward.
+build_tile_block() {
+ local -r thumbs_dir="$1"; shift
+ local -r backhref="$1"; shift
+ local -r page_num="$1"; shift
+ local -r layout="$1"; shift
+ local -ri start_preview="$1"; shift
+ local -a photos=("$@")
+ local animation_class
+
+ if [ "$layout" = single ]; then
+ animation_class=$(random_animation_css_class slow "${photos[0]}")
+ build_preview_thumbnail \
+ "$thumbs_dir" "$backhref" "$page_num" "$start_preview" \
+ "${photos[0]}" "$animation_class"
+ return
+ fi
+ build_subdivided_tile \
+ "$thumbs_dir" "$backhref" "$page_num" "$layout" "$start_preview" \
+ "${photos[@]}"
+}
+
+# Emit a subdivided tile: a <div class='tile'> wrapping the smaller
+# sub-thumbnails (class 'subthumb'). Each sub-thumbnail is still its own
+# clickable photo/view page; only its size and (for the full-width strip) an
+# extra 'wide' anchor class differ from a normal square thumbnail. The anchor
+# ORDER encodes the layout for the CSS grid's auto-placement:
+# two_wide -> two full-width strips (both 'wide'), stacked
+# squares_wide_top -> strip first (top row), then two squares (bottom row)
+# squares_wide_bottom -> two squares first (top row), then strip (bottom row)
+# quad -> four squares (2x2)
+build_subdivided_tile() {
+ local -r thumbs_dir="$1"; shift
+ local -r backhref="$1"; shift
+ local -r page_num="$1"; shift
+ local -r layout="$1"; shift
+ local -ri start_preview="$1"; shift
+ local -a photos=("$@")
+ local -a anchor_classes
+ local -i k
+ local animation_class
+
+ case "$layout" in
+ two_wide) anchor_classes=(wide wide) ;;
+ squares_wide_top) anchor_classes=(wide '' '') ;;
+ squares_wide_bottom) anchor_classes=('' '' wide) ;;
+ quad) anchor_classes=('' '' '' '') ;;
+ esac
+
+ printf "<div class='tile'>\n"
+ for (( k = 0; k < ${#photos[@]}; k++ )); do
+ animation_class=$(random_animation_css_class slow "${photos[k]}")
+ build_preview_thumbnail \
+ "$thumbs_dir" "$backhref" "$page_num" "$(( start_preview + k ))" \
+ "${photos[k]}" "$animation_class" subthumb "${anchor_classes[k]:-}"
+ printf '\n'
+ done
+ printf '</div>'
}
# Render the HTML for a single preview thumbnail (HTML-escaping every value the
# way preview.tmpl's context_html fields did). Returned without a trailing
-# newline so callers control separators.
+# newline so callers control separators. img_class defaults to 'thumb' (the full
+# square); subdivided tiles pass 'subthumb'. anchor_class is an optional extra
+# class on the <a> ('wide' marks the full-width strip inside a subdivided tile);
+# when empty the <a> has no class attribute, keeping the single-tile output
+# byte-identical to before.
build_preview_thumbnail() {
local -r thumbs_dir="$1"; shift
local -r backhref="$1"; shift
@@ -225,19 +335,30 @@ build_preview_thumbnail() {
local -r preview_num="$1"; shift
local -r photo_file="$1"; shift
local -r animation_class="$1"; shift
+ local -r img_class="${1:-thumb}"
+ local -r anchor_class="${2:-}"
local photo_html
local anim_html
local backhref_html
local thumbs_dir_html
+ local anchor_class_html
photo_html=$(_html_escape "$photo_file")
anim_html=$(_html_escape "$animation_class")
backhref_html=$(_html_escape "$backhref")
thumbs_dir_html=$(_html_escape "$thumbs_dir")
- printf '<a id=%s href=%s>\n' \
- "'$photo_html'" "'$page_num-$preview_num.html'"
- printf " <img class='thumb %s' alt='%s' src='%s/%s/%s'>\n</a>" \
- "$anim_html" "$photo_html" "$backhref_html" "$thumbs_dir_html" "$photo_html"
+ if [ -n "$anchor_class" ]; then
+ anchor_class_html=$(_html_escape "$anchor_class")
+ printf '<a id=%s class=%s href=%s>\n' \
+ "'$photo_html'" "'$anchor_class_html'" \
+ "'$page_num-$preview_num.html'"
+ else
+ printf '<a id=%s href=%s>\n' \
+ "'$photo_html'" "'$page_num-$preview_num.html'"
+ fi
+ printf " <img class='%s %s' alt='%s' src='%s/%s/%s'>\n</a>" \
+ "$img_class" "$anim_html" "$photo_html" "$backhref_html" \
+ "$thumbs_dir_html" "$photo_html"
}
render_view_page() {
diff --git a/src/lib/bootstrap.source.sh b/src/lib/bootstrap.source.sh
index 43cef59..7fa5ea3 100644
--- a/src/lib/bootstrap.source.sh
+++ b/src/lib/bootstrap.source.sh
@@ -28,6 +28,7 @@ usage() {
--height VALUE
--thumbheight VALUE
--maxpreviews N
+ --subdivide PERCENT
--image-jobs N
--random-seed VALUE
--splash
diff --git a/src/lib/config.print.source.sh b/src/lib/config.print.source.sh
index b886f8b..401d84c 100644
--- a/src/lib/config.print.source.sh
+++ b/src/lib/config.print.source.sh
@@ -33,6 +33,7 @@ print_config() {
print_shell_assignment HEIGHT "$HEIGHT"
print_shell_assignment THUMBHEIGHT "$THUMBHEIGHT"
print_shell_assignment MAXPREVIEWS "$MAXPREVIEWS"
+ print_shell_assignment THUMB_SUBDIVIDE_PERCENT "$THUMB_SUBDIVIDE_PERCENT"
print_shell_assignment IMAGE_JOBS "$IMAGE_JOBS"
print_shell_assignment IMAGEMAGICK_TIMEOUT "$IMAGEMAGICK_TIMEOUT"
print_shell_assignment RANDOM_SEED "$RANDOM_SEED"
diff --git a/src/lib/config.source.sh b/src/lib/config.source.sh
index 58895d7..538724f 100644
--- a/src/lib/config.source.sh
+++ b/src/lib/config.source.sh
@@ -46,6 +46,9 @@ apply_config_defaults() {
SOURCE_URL="${SOURCE_URL:-https://codeberg.org/snonux/shuriken.sh}"
SPLASH_PAGE="${SPLASH_PAGE:-yes}"
STATS_PAGE="${STATS_PAGE:-no}"
+ # Optional with a default (unlike the required THUMBHEIGHT): the percent
+ # chance a preview tile is subdivided into smaller thumbnails. 0 disables it.
+ THUMB_SUBDIVIDE_PERCENT="${THUMB_SUBDIVIDE_PERCENT:-30}"
SYNC_DELETE="${SYNC_DELETE:-yes}"
TARBALL_INCLUDE="${TARBALL_INCLUDE:-no}"
TARBALL_SUFFIX="${TARBALL_SUFFIX:-.tar}"
diff --git a/src/lib/config.validate.source.sh b/src/lib/config.validate.source.sh
index 28aeace..e3c55e7 100644
--- a/src/lib/config.validate.source.sh
+++ b/src/lib/config.validate.source.sh
@@ -32,6 +32,19 @@ validate_optional_positive_integer_config_var() {
fi
}
+# A percentage config var: an integer in the inclusive range 0..100. Unlike
+# validate_positive_integer_config_var, 0 is allowed (it is the natural "off"
+# value, e.g. THUMB_SUBDIVIDE_PERCENT=0 disables tile subdivision entirely).
+validate_percentage_config_var() {
+ local -r name="$1"; shift
+ local -r value="${!name}"
+
+ if [[ ! "$value" =~ ^[0-9]+$ ]] || (( value > 100 )); then
+ config_error "$name must be an integer between 0 and 100"
+ return 1
+ fi
+}
+
validate_yes_no_config_var() {
local -r name="$1"; shift
local -r value="${!name}"
@@ -273,6 +286,7 @@ validate_common_config() {
validate_optional_positive_integer_config_var HEIGHT || return
validate_positive_integer_config_var THUMBHEIGHT || return
validate_positive_integer_config_var MAXPREVIEWS || return
+ validate_percentage_config_var THUMB_SUBDIVIDE_PERCENT || return
validate_positive_integer_config_var IMAGE_JOBS || return
validate_positive_integer_config_var IMAGEMAGICK_TIMEOUT || return
validate_positive_integer_config_var TAR_TIMEOUT || return