diff --git a/docs/manual/README.md b/docs/manual/README.md index 1336ec6..e1117e3 100644 --- a/docs/manual/README.md +++ b/docs/manual/README.md @@ -359,6 +359,8 @@ It opens in develop all the same, on a reduced copy that is drawn from the full resolution wherever you zoom in, and it exports at full size. The same goes for a panorama Lightroom stitched and saved as a DNG. +![A 22 927 × 8966 panorama of a glacier at dusk: zoomed from the whole frame into the peaks, panned along the ridge and back out, then 1:1 on the peaks with a double-click](media/giant-pano.gif) + ![Twelve frames aligned, the first left out and brought back, the projections tried, and the border filled](media/panorama.gif) ![The alignment on a cylinder, each frame outlined where it landed](media/panorama-aligned.png) diff --git a/docs/manual/index.html b/docs/manual/index.html index 066050c..444af7f 100644 --- a/docs/manual/index.html +++ b/docs/manual/index.html @@ -400,6 +400,7 @@ preview and in the composite.

It opens in develop all the same, on a reduced copy that is drawn from the full resolution wherever you zoom in, and it exports at full size. The same goes for a panorama Lightroom stitched and saved as a DNG.

+
A 22 927 × 8966 panorama of a glacier at dusk: zoomed from the whole frame into the peaks, panned along the ridge and back out, then 1:1 on the peaks with a double-click
A 22 927 × 8966 panorama of a glacier at dusk: zoomed from the whole frame into the peaks, panned along the ridge and back out, then 1:1 on the peaks with a double-click
Twelve frames aligned, the first left out and brought back, the projections tried, and the border filled
Twelve frames aligned, the first left out and brought back, the projections tried, and the border filled
The alignment on a cylinder, each frame outlined where it landed
The alignment on a cylinder, each frame outlined where it landed
The same, with the ragged border filled by the model rather than cropped away
The same, with the ragged border filled by the model rather than cropped away
diff --git a/docs/manual/media/giant-pano.gif b/docs/manual/media/giant-pano.gif new file mode 100644 index 0000000..887240c --- /dev/null +++ b/docs/manual/media/giant-pano.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67b186fffde402eea87a0ef9ce09320ddde9e1b5a77e2d718c4fbfb156cc82e2 +size 8704058 diff --git a/tools/manual/README.md b/tools/manual/README.md index 8267b61..b5ab30f 100644 --- a/tools/manual/README.md +++ b/tools/manual/README.md @@ -47,6 +47,12 @@ manual does not show; it reads two files and runs in CI. `--changed` re-records a scene when one of its sources, or its own code, has changed since the commit that last touched its pictures. +GIFs are made at 960 px and ten frames a second. A scene whose film does +not compress at that size says otherwise in `GIF_SIZE`, which `record.sh` +reads through `scenes.py gif NAME`. `giant_pano` borrows a 500 MB panorama +from outside the library (`DR_GIANT_DNG`, by default +`/var/tmp/dr-dng/IMG_4181-Pano.dng`) and removes it again, so it runs last. + Two things the scripts know that are not obvious: a Slint `TouchArea` wants a held press, not xdotool's `click`; and the app is driven on Xvfb rather than the desktop because under XWayland at scale 2 a pointer warp lands at diff --git a/tools/manual/record.sh b/tools/manual/record.sh index 60041d4..939cf19 100755 --- a/tools/manual/record.sh +++ b/tools/manual/record.sh @@ -150,7 +150,8 @@ python3 "$scenes" record "$media" "${@:-all}" # GIFs for the page; the MP4s are working files and are not kept. for mp4 in "$media"/*.mp4; do [ -e "$mp4" ] || continue - "$here/gif.sh" "$mp4" "${mp4%.mp4}.gif" 960 10 + read -r width fps < <(python3 "$scenes" gif "$(basename "${mp4%.mp4}")") + "$here/gif.sh" "$mp4" "${mp4%.mp4}.gif" "$width" "$fps" rm "$mp4" done find "$media" -name '*.png' -newer "$stamp" -exec optipng -quiet -o2 {} + || true diff --git a/tools/manual/scenes.py b/tools/manual/scenes.py index 44381cc..2b258e8 100755 --- a/tools/manual/scenes.py +++ b/tools/manual/scenes.py @@ -9,6 +9,8 @@ scenes.py changed the scenes whose sources changed since their pictures were last committed scenes.py list every scene, its pictures and sources + scenes.py gif NAME the width and frame rate NAME.gif is + made at (960 10 unless a scene says) Each scene drives the running app through `drive.py` and leaves the pictures it names in OUT_DIR: PNGs, and MP4s that `record.sh` turns into @@ -1527,6 +1529,99 @@ def duplicates(): pause(3) + +# --- a photograph larger than one texture ------------------------------------- +# Last on purpose: it adds a file to the library for its own length only, and +# no scene after it should find that file there. +GIANT_DNG = os.environ.get('DR_GIANT_DNG', '/var/tmp/dr-dng/IMG_4181-Pano.dng') +GIANT = 'IMG_4181-Pano' # a 22927 × 8966 Lightroom panorama of a glacier at dusk + +# A GIF that is fine rock and ice in every frame does not compress; at the +# usual 960 px and ten frames a second this one was 22 MB, and 11 at 720. +GIF_SIZE = {'giant-pano': (640, 8)} + + +def restart_on_library(): + dr.stop() + dr.launch([]) + dr.wait_ready() + pause(5) + at_library() # it reopens on the photograph it closed on + pause(3) + + +def remove_giant(folder, made_folder): + for f in os.listdir(folder) if os.path.isdir(folder) else []: + if f.startswith(f'{GIANT}.'): + os.remove(os.path.join(folder, f)) + if made_folder and os.path.isdir(folder) and not os.listdir(folder): + os.rmdir(folder) + + +@scene(media=['giant-pano.gif'], + sources=DEVELOP_SRC + ['ui/dr-ui/src/develop/session.rs', 'ui/dr-ui/src/develop/render.rs', + 'core/dr-gpu/src/demosaic.rs', 'core/dr-pipeline/src/tiles.rs']) +def giant_pano(): + """A linear DNG too large for one GPU texture, opened and navigated: in + from fit on the peaks at its left as far as the wheel goes (sixteen + times fit, short of 1:1 on a frame this wide), a slow pan along the + ridge, back out, then 1:1 on the peaks with a double-click and fit with + another. The file is not in the demo library (it is 500 MB): it is + copied into a `panorama` folder for this scene, the app restarted so the + scan catalogues it, and the copy and the sidecar the app writes beside + it deleted, and the app restarted again, afterwards. DR_GIANT_DNG names it.""" + folder = os.path.join(DEMO_LIBRARY, 'panorama') + copy = os.path.join(folder, f'{GIANT}.dng') + made_folder = not os.path.isdir(folder) + try: + os.makedirs(folder, exist_ok=True) + shutil.copy2(GIANT_DNG, copy) + restart_on_library() + at_library() + grid_top() + for _ in range(60): # until the scan has catalogued it + if dr.present(f'{GIANT}@ListItem'): + break + pause(2) + dr.click(*cell(GIANT)) + dr.wait_for(f'{GIANT}.dng', 120) + wait_canvas(300) + pause(8) # the reduced copy, drawn + a = dr.photo(0.14, 0.22) # the snow peaks at the left + rec('giant-pano') + pause(1.5) + for _ in range(10): # the wheel's limit, 16 times fit + wheel(-1, *a) + pause(0.7) + pause(1.5) + cx, cy = mid(dr.rect('id:canvas-image')) + for _ in range(3): # along the ridge, rightwards + dr.drag(cx + 350, cy, cx - 350, cy, 90) + pause(1.0) + for _ in range(10): + wheel(1, cx, cy) + pause(0.5) + pause(1.5) + # 1:1 is past the wheel's limit on a frame this wide: a double-click + # reaches it, on the peaks the film started from, and another fits. + b = dr.photo(0.2, 0.2) + dr.move(*b) + pause(0.3) + dr.x('click', '--repeat', 2, '--delay', 80, 1) + pause(3.0) + dr.x('click', '--repeat', 2, '--delay', 80, 1) + pause(2.0) + cut() + finally: + # Back to the grid first, or the next start reopens the photograph + # it closed on and writes its sidecar again. + at_library() + dr.stop() + remove_giant(folder, made_folder) + restart_on_library() + remove_giant(folder, made_folder) + + # --- running them ----------------------------------------------------------- def order(names): """`names` with their prerequisites, each once, prerequisites first.""" @@ -1677,6 +1772,8 @@ def main(): elif cmd == 'changed': for name, why in changed(): print(f'{name}\t{why}') + elif cmd == 'gif': + print(*GIF_SIZE.get(rest[0], (960, 10))) elif cmd == 'list': for s in SCENES.values(): after = f' (after {", ".join(s.after)})' if s.after else ''