|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Render the animated wordmark and assemble it for the site. |
| 3 | +# |
| 4 | +# ./make-anim.sh render 80 frames at 1800x700, encode the clip |
| 5 | +# ./make-anim.sh --draft quarter size, no antialiasing, ~30s, for timing |
| 6 | +# ./make-anim.sh --encode re-encode the frames already in frames/, no render |
| 7 | +# ./make-anim.sh --check verify the tail is still and matches the still PNG |
| 8 | +# |
| 9 | +# Output: ../assets/images/plinth-wordmark.webm, a VP9 clip that plays once and |
| 10 | +# stops on a frame identical to the committed plinth-wordmark.png. See |
| 11 | +# Note [The last frame is the still] in plinth-anim.pov for why that matters, |
| 12 | +# and index.md for how the two are served together. |
| 13 | +set -euo pipefail |
| 14 | + |
| 15 | +cd "$(dirname "$0")" |
| 16 | + |
| 17 | +FRAMES=frames |
| 18 | +STILL=../assets/images/plinth-wordmark.png |
| 19 | +OUT=../assets/images/plinth-wordmark.webm |
| 20 | +MP4=../assets/images/plinth-wordmark.mp4 |
| 21 | +POSTER=../assets/images/plinth-wordmark-poster.webp |
| 22 | +WIDE=1200 # delivered width, same as the still |
| 23 | +FPS=30 |
| 24 | +LAST=80 # keep in step with Final_Frame in anim.ini |
| 25 | + |
| 26 | +# Note [Downsampling does double duty] |
| 27 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 28 | +# Rendering at 1800 and resampling to 1200 sharpens the bevel edges, as it does |
| 29 | +# for the still -- but in an animation it also halves the frame-to-frame noise |
| 30 | +# from the jittered area lights and the radiosity sampling, which is what would |
| 31 | +# otherwise show up as a faint shimmer on the floor. Measured on two frames that |
| 32 | +# should be identical: 0.13% mean difference before resampling, well under half |
| 33 | +# of that after. Not worth touching the light rig for (and Note [Hitting the |
| 34 | +# brand colour] in plinth-common.inc says not to). |
| 35 | +mode=${1:-} |
| 36 | + |
| 37 | +case "$mode" in |
| 38 | + --check) |
| 39 | + # 1. Is the tail actually static? Consecutive frames near the end must |
| 40 | + # differ only by render noise, not by anything still in motion. |
| 41 | + # 2. Does the last frame match the committed still? Same tolerance: both |
| 42 | + # numbers are the noise floor, so a real mismatch stands out. |
| 43 | + [[ -f $FRAMES/f$LAST.png ]] || { echo "no frames: run ./make-anim.sh first" >&2; exit 1; } |
| 44 | + # The tolerance is the render's own noise floor. Two frames that are meant |
| 45 | + # to be identical still differ by about 0.4%, because the area lights are |
| 46 | + # jittered and the radiosity is sampled afresh each frame; anything actually |
| 47 | + # in motion is far above that. Note the "|| true" on every compare: it exits |
| 48 | + # 1 whenever the images differ at all, which under set -e would abort the |
| 49 | + # check rather than report it. |
| 50 | + tol=0.01 |
| 51 | + for pair in "$((LAST - 4)) $((LAST - 2))" "$((LAST - 2)) $LAST"; do |
| 52 | + read -r a b <<<"$pair" |
| 53 | + d=$( { magick compare -metric RMSE "$FRAMES/f$a.png" "$FRAMES/f$b.png" null: 2>&1 || true; } | |
| 54 | + sed 's/.*(\(.*\))/\1/') |
| 55 | + echo "frame $a vs $b: $d" |
| 56 | + awk -v d="$d" -v t="$tol" 'BEGIN { exit !(d > t) }' && |
| 57 | + { echo " FAIL: still moving at frame $b" >&2; exit 1; } |
| 58 | + done |
| 59 | + # -resize before both filenames applies to both as they are read, so this |
| 60 | + # compares the 1800-wide frame against the 1200-wide still on equal terms. |
| 61 | + d=$( { magick compare -metric RMSE -resize ${WIDE}x "$FRAMES/f$LAST.png" "$STILL" null: 2>&1 || true; } | |
| 62 | + sed 's/.*(\(.*\))/\1/') |
| 63 | + echo "frame $LAST vs $(basename $STILL): $d" |
| 64 | + awk -v d="$d" -v t="$tol" 'BEGIN { exit !(d > t) }' && |
| 65 | + { echo " FAIL: last frame is not the committed still" >&2; exit 1; } |
| 66 | + echo "ok" |
| 67 | + exit 0 |
| 68 | + ;; |
| 69 | + --draft) |
| 70 | + render=(povray anim.ini +W450 +H175 -A +Q5) |
| 71 | + OUT=draft.webm |
| 72 | + WIDE=450 |
| 73 | + ;; |
| 74 | + --encode) |
| 75 | + # Retuning the encoder is the thing you do repeatedly; re-rendering 80 |
| 76 | + # frames to do it is five minutes for nothing. |
| 77 | + [[ -f $FRAMES/f$LAST.png ]] || { echo "no frames: run ./make-anim.sh first" >&2; exit 1; } |
| 78 | + render=(true) |
| 79 | + ;; |
| 80 | + "") |
| 81 | + render=(povray anim.ini) |
| 82 | + ;; |
| 83 | + *) |
| 84 | + sed -n '2,9p' "$0" >&2 |
| 85 | + exit 1 |
| 86 | + ;; |
| 87 | +esac |
| 88 | + |
| 89 | +if [[ $mode != --encode ]]; then |
| 90 | + mkdir -p "$FRAMES" |
| 91 | + rm -f "$FRAMES"/f*.png |
| 92 | +fi |
| 93 | +"${render[@]}" |
| 94 | + |
| 95 | +# Note [VP9, not animated WebP] |
| 96 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 97 | +# This clip is 90% smooth near-white backdrop, which is the worst case for |
| 98 | +# WebP: it lays visible horizontal streaks across the gradient, and the quality |
| 99 | +# knob does not buy them off. Measured on the backdrop alone, against the |
| 100 | +# unencoded frame -- q88 0.0080, q99 0.0076, for 848K and 2.2M respectively. |
| 101 | +# VP9 reaches the same number at crf 20 in 200K and is visibly cleaner by |
| 102 | +# crf 16, because it has a deblocking filter and rate control built for exactly |
| 103 | +# this. Lossless WebP would do it in 12M. |
| 104 | +# |
| 105 | +# Odd heights are fine for VP9, so the frame is 1200x467 -- the same size as |
| 106 | +# the still, which is the poster. |
| 107 | +# |
| 108 | +# No -loop: a <video> stops on its last frame, which is the whole design (see |
| 109 | +# Note [The last frame is the still] in plinth-anim.pov). That frame is then on |
| 110 | +# screen indefinitely, so -force_key_frames codes it as a keyframe rather than |
| 111 | +# the tail of a 79-frame delta chain. Costs about 20K. |
| 112 | +ffmpeg -nostdin -loglevel error -y \ |
| 113 | + -framerate "$FPS" -start_number 1 -i "$FRAMES/f%02d.png" \ |
| 114 | + -vf "scale=$WIDE:-1:flags=lanczos" \ |
| 115 | + -c:v libvpx-vp9 -crf 16 -b:v 0 -pix_fmt yuv420p \ |
| 116 | + -force_key_frames "expr:eq(n,$((LAST - 1)))" \ |
| 117 | + -deadline good -cpu-used 1 -row-mt 1 -an \ |
| 118 | + "$OUT" |
| 119 | + |
| 120 | +printf '%s %s %s frames @ %sfps\n' \ |
| 121 | + "$OUT" "$(du -h "$OUT" | cut -f1)" "$LAST" "$FPS" |
| 122 | + |
| 123 | +if [[ $mode == --draft ]]; then |
| 124 | + exit 0 |
| 125 | +fi |
| 126 | + |
| 127 | +# Note [The poster is the FIRST frame] |
| 128 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 129 | +# Not the last one, which is the obvious choice and is wrong: the poster is what |
| 130 | +# the browser shows until playback starts, so posting the finished wordmark |
| 131 | +# means arriving on the page, seeing the completed mark, and then watching it |
| 132 | +# vanish and rebuild itself. Frame 1 makes the handover invisible. |
| 133 | +# |
| 134 | +# The cost is that a browser that cannot play either source is left on an empty |
| 135 | +# studio, which is why the H.264 fallback below exists -- with it, "no playable |
| 136 | +# source" stops being a case that happens. Cheap at 4K: frame 1 is nearly flat, |
| 137 | +# and the streaking that ruled WebP out for the animation (see Note [VP9, not |
| 138 | +# animated WebP]) does not arise on one still image at this bitrate. |
| 139 | +ffmpeg -nostdin -loglevel error -y -i "$FRAMES/f01.png" \ |
| 140 | + -vf "scale=$WIDE:-1:flags=lanczos" \ |
| 141 | + -c:v libwebp -lossless 0 -quality 90 -compression_level 6 \ |
| 142 | + "$POSTER" |
| 143 | + |
| 144 | +# H.264 for anything too old for VP9 -- pre-2021 Safari, mainly. Modern browsers |
| 145 | +# take the WebM listed first in index.md and never fetch this. 466 not 467: |
| 146 | +# H.264 requires even dimensions, and one pixel of letterbox is not visible. |
| 147 | +ffmpeg -nostdin -loglevel error -y \ |
| 148 | + -framerate "$FPS" -start_number 1 -i "$FRAMES/f%02d.png" \ |
| 149 | + -vf "scale=$WIDE:466:flags=lanczos" \ |
| 150 | + -c:v libx264 -crf 19 -preset slow -pix_fmt yuv420p \ |
| 151 | + -movflags +faststart -an \ |
| 152 | + "$MP4" |
| 153 | + |
| 154 | +printf '%s %s\n%s %s\n' \ |
| 155 | + "$POSTER" "$(du -h "$POSTER" | cut -f1)" \ |
| 156 | + "$MP4" "$(du -h "$MP4" | cut -f1)" |
0 commit comments