Files
DarkRoom/tools/manual/scenes.py
T

423 lines
13 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""The manual's scenes: `scenes.py OUT_DIR scene [scene...]`, or `all`.
Each scene drives the running app through `drive.py` and leaves a PNG or an
MP4 in OUT_DIR, named after itself. `record.sh` runs them in order and turns
the MP4s into GIFs.
Coordinates are window pixels for a 1600×1100 window over the manual's own
library (see record.sh): which cell holds which photograph is part of the
scene, so a different library needs the numbers looked at again. Panel
coordinates hold for any library.
"""
import os
import re
import subprocess
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import drive as dr # noqa: E402
OUT = sys.argv[1] if len(sys.argv) > 1 else '.'
DEMO_LIBRARY = os.environ.get('DR_LIBRARY', '') # the folder the merge writes its DNG into
def shot(name):
dr.shot(f'{OUT}/{name}.png')
def rec(name):
dr.rec_start(f'{OUT}/{name}.mp4')
def cut():
dr.rec_stop()
def pause(s):
time.sleep(s)
def hold(px, py, seconds):
dr.move(px, py)
pause(0.15)
dr.x('mousedown', 1)
pause(seconds)
dr.x('mouseup', 1)
def wheel(n, px, py):
dr.move(px, py)
dr.x('click', '--repeat', abs(n), '--delay', 40, 5 if n > 0 else 4)
def ctrl_wheel(n, px, py):
dr.move(px, py)
dr.x('keydown', 'ctrl')
for _ in range(abs(n)):
dr.x('click', 5 if n > 0 else 4)
pause(0.35)
dr.x('keyup', 'ctrl')
# --- places on the screen ---------------------------------------------------
LIBRARY = (55, 22) # ‹ Library, in the develop header
SELECT = (942, 22) # Select / Done, in the library header
SETTINGS_LIB = (1543, 22)
SETTINGS_DEV = (1343, 22)
EXPORT = (720, 22)
BACK = (60, 22) # ‹ Back, on the settings page
RAIL = {'photo': (30, 75), 'compose': (30, 130), 'local': (30, 185), 'repair': (30, 240)}
GROUPS = {'all': 1269, 'optics': 1313, 'light': 1359, 'colour': 1406, 'effects': 1457, 'detail': 1506}
BEFORE = (210, 1045)
RESET_ADJUST = (1565, 741) # "reset" in the Adjust heading, All group, scrolled to top
CELL_TEAPOT = (424, 165)
CELL_PANO_FIRST = (1264, 165)
CELL_CHINATOWN = (963, 525)
CELL_NY_LAST = (1502, 525)
COLLECTION_ROW = (60, 90)
def group(name):
dr.click(GROUPS[name], 67)
pause(0.9)
def to_library():
dr.click(*LIBRARY)
pause(2)
def open_chinatown():
dr.click(*CELL_CHINATOWN)
pause(6)
def reset_edit():
group('all')
wheel(-60, 1420, 700)
dr.click(*RESET_ADJUST)
pause(1)
# --- library ----------------------------------------------------------------
def library():
shot('library')
def library_rating():
rec('library-rating')
dr.move(424, 900); pause(1.0)
dr.click(444, 939); pause(1.2)
dr.move(604, 900); pause(0.8)
dr.click(644, 939); pause(1.2)
dr.move(800, 700); pause(0.6)
dr.click(665, 61); pause(1.5) # 3+
dr.click(384, 61); pause(1.2) # All
cut()
def library_timeline():
rec('library-timeline')
dr.drag(300, 200, 300, 900, 40); pause(1.0)
dr.drag(300, 900, 300, 150, 40); pause(1.0)
cut()
def library_thumbsize():
rec('library-thumbsize')
ctrl_wheel(-4, 900, 500); pause(0.8)
ctrl_wheel(4, 900, 500); pause(0.8)
cut()
def library_selection():
# The grid remembers its scroll between launches; the cells below are
# the grid at its top.
wheel(-40, 900, 500); pause(0.6)
dr.click(*SELECT); pause(0.5)
dr.click(*CELL_PANO_FIRST); pause(0.3)
dr.x('keydown', 'shift'); dr.click(*CELL_NY_LAST); dr.x('keyup', 'shift'); pause(0.8)
for x in (963, 1143, 1323, 1502):
dr.x('keydown', 'ctrl'); dr.click(x, 525); dr.x('keyup', 'ctrl'); pause(0.3)
pause(0.5)
shot('library-selection')
def library_keywords():
# Continues from library_selection: twelve frames selected.
rec('library-keywords')
dr.click(1117, 1079); pause(1.2)
for word in ('alps', 'panorama', 'summer'):
dr.x('type', '--delay', 90, word); dr.x('key', 'Return'); pause(0.7)
pause(0.8)
dr.click(800, 673); pause(1.0) # Done
cut()
dr.click(*SELECT); pause(0.5) # leave selecting
def new_collection(name):
# `+` makes a collection inside whichever row is selected, so select the
# top first for a top-level one.
dr.click(60, 48); pause(0.5)
dr.click(212, 22); pause(0.4)
dr.x('type', '--delay', 90, name); dr.x('key', 'Return'); pause(1.2)
# The menu a right-click on a collection opens, at 1600×1100.
MENU = {'rename': (800, 496), 'inside': (800, 536), 'top': (800, 576),
'offline': (800, 616), 'delete': (800, 669), 'cancel': (800, 709)}
def library_collections():
new_collection('Alps')
rec('library-collections')
dr.drag(*CELL_PANO_FIRST, *COLLECTION_ROW, 40); pause(1.5)
dr.click(*SELECT); pause(0.5)
dr.click(424, 350); pause(0.3)
dr.x('keydown', 'shift'); dr.click(1502, 350); dr.x('keyup', 'shift'); pause(0.8)
dr.drag(963, 350, *COLLECTION_ROW, 40); pause(1.5)
dr.click(*SELECT); pause(0.5)
dr.click(*COLLECTION_ROW); pause(1.5)
cut()
dr.click(60, 48); pause(1.2) # All photographs
def library_nesting():
# Continues from library_collections: "Alps" exists with photographs in
# it. Make "Trips", drag Alps into it, make "New York" inside Trips from
# the menu, file frames there, then open Trips to see it count both.
new_collection('Trips')
rec('library-nesting')
pause(0.5)
dr.drag(60, 90, 60, 128, 30); pause(1.5) # Alps onto Trips
dr.click(60, 90, 3); pause(1.2) # the menu, on Trips
dr.click(*MENU['inside']); pause(0.6)
dr.x('type', '--delay', 90, 'New York'); dr.x('key', 'Return'); pause(1.2)
dr.click(*SELECT); pause(0.5)
dr.click(963, 525); pause(0.3)
dr.x('keydown', 'shift'); dr.click(1502, 525); dr.x('keyup', 'shift'); pause(0.8)
dr.drag_path([(1143, 525), (200, 525), (90, 146)], 25); pause(1.5) # four frames into New York
dr.click(*SELECT); pause(0.5)
dr.click(60, 90); pause(1.5) # Trips: both children
cut()
shot('library-nesting')
dr.click(90, 146, 3); pause(1.2) # the menu, on New York
shot('library-collection-menu')
dr.click(*MENU['cancel']); pause(0.8)
dr.click(60, 48); pause(1.2)
# --- develop ----------------------------------------------------------------
def develop():
open_chinatown()
group('all')
shot('develop')
def develop_groups():
rec('develop-groups')
for g in ['optics', 'light', 'colour', 'effects', 'detail', 'all']:
group(g); pause(0.6)
cut()
def develop_light():
group('light')
rec('develop-light')
pause(0.5)
dr.drag(1398, 792, 1422, 792, 25); pause(0.8) # exposure up
dr.drag(1398, 918, 1320, 918, 25); pause(0.8) # highlights down
dr.drag(1398, 964, 1450, 964, 25); pause(0.8) # shadows up
hold(*BEFORE, 1.6); pause(1.0)
cut()
reset_edit()
def develop_zoom():
rec('develop-zoom')
dr.move(650, 500); pause(0.3)
dr.x('click', '--repeat', 2, '--delay', 80, 1); pause(1.5)
dr.drag(650, 500, 900, 700, 30); pause(0.8)
dr.drag(900, 700, 500, 450, 30); pause(0.8)
dr.x('click', '--repeat', 2, '--delay', 80, 1); pause(1.2)
cut()
def develop_wb():
group('colour')
rec('develop-wb')
pause(0.4)
dr.drag(1398, 826, 1330, 826, 25); pause(1.2) # temperature cold
dr.click(1487, 776); pause(0.8) # pick
dr.click(995, 635); pause(1.5) # the white air conditioner
hold(*BEFORE, 1.4); pause(0.8)
cut()
reset_edit()
def compose():
dr.click(*RAIL['compose']); pause(1.2)
rec('compose')
pause(0.4)
dr.drag(66, 182, 260, 330, 30); pause(0.8)
dr.drag(1236, 964, 1100, 900, 25); pause(0.8)
dr.drag(1420, 593, 1448, 593, 20); pause(1.0) # straighten
dr.click(1466, 647); pause(1.2) # 1:1
dr.click(1378, 647); pause(1.0) # Original
dr.click(132, 1045); pause(1.2) # Done composing
cut()
shot('compose-done')
dr.click(1567, 510); pause(1.0) # reset compose
dr.click(115, 1045); pause(1.0) # refit
def local_segment():
dr.click(*RAIL['local']); pause(1.2)
dr.click(1420, 435); pause(14) # Find subjects
shot('local-categories')
dr.click(1266, 646); pause(2.5) # Sky
shot('local-segment')
def local_paint():
# Continues from local_segment: the sky mask selected and shown.
rec('local-paint')
wheel(12, 1420, 800); pause(0.8)
shot('local-mask-row')
cut()
def local_done():
dr.click(62, 1045); pause(1.0) # Done masking
reset_edit()
def repair():
dr.click(*RAIL['repair']); pause(1.2)
rec('repair')
pause(0.4)
dr.click(700, 620); pause(3)
dr.drag(1264, 460, 1330, 460, 20); pause(1.5) # size
hold(*BEFORE, 1.4); pause(0.8)
cut()
dr.click(125, 1045); pause(1.0) # Done repairing
reset_edit()
def film():
group('all')
rec('film')
dr.click(1420, 779); pause(1.5) # Film ▸
dr.click(1420, 953); pause(3) # Velvia 100
hold(*BEFORE, 1.4); pause(0.8)
cut()
reset_edit()
def presets():
dr.click(1420, 692); pause(1.5)
shot('presets')
dr.click(800, 783); pause(1.0) # Done
def develop_export():
dr.click(*EXPORT); pause(8)
shot('export-done')
# --- panorama ---------------------------------------------------------------
def wait_for_new(folder, suffix, since, timeout):
"""Until a file ending in `suffix` newer than `since` appears in
`folder`'s tree — the merge's DNG — or `timeout` seconds pass."""
t0 = time.time()
while time.time() - t0 < timeout:
for root, _, files in os.walk(folder):
for f in files:
if f.lower().endswith(suffix) and os.path.getmtime(os.path.join(root, f)) > since:
return True
time.sleep(2)
return False
def wait_until_lit(px, py, timeout):
"""Until the window pixel at (px, py) is no longer black — the fill
preview has reached the corner of the border — or `timeout` passes."""
t0 = time.time()
while time.time() - t0 < timeout:
out = subprocess.run(
['import', '-window', dr.win(), '-crop', f'1x1+{px}+{py}', '-depth', '8', 'txt:-'],
capture_output=True, text=True).stdout
m = re.search(r'\((\d+),(\d+),(\d+)', out)
if m and max(int(v) for v in m.groups()) > 24:
return True
time.sleep(3)
return False
def join(name, parts):
"""Concatenate recorded clips into `name`.mp4 and drop the parts."""
lst = f'{OUT}/{name}.txt'
with open(lst, 'w') as f:
for p in parts:
f.write(f"file '{OUT}/{p}.mp4'\n")
subprocess.run(['ffmpeg', '-hide_banner', '-loglevel', 'error', '-y', '-f', 'concat', '-safe', '0',
'-i', lst, '-c', 'copy', f'{OUT}/{name}.mp4'], check=True)
os.remove(lst)
for p in parts:
os.remove(f'{OUT}/{p}.mp4')
def panorama():
to_library()
library_selection()
rec('panorama-a')
dr.click(1325, 1079); pause(40) # Merge to panorama
shot('panorama-aligned')
dr.click(184, 637); pause(6) # Fill the border
cut()
# The preview fills at the working scale — minutes on the CPU rung —
# so the film pauses until the border's corner has been painted.
wait_until_lit(60, 110, 400)
pause(3)
shot('panorama-filled')
rec('panorama-b')
pause(2)
started = time.time()
dr.click(1543, 22); pause(8) # Merge
cut()
join('panorama', ['panorama-a', 'panorama-b'])
# The merge itself takes minutes too; the last picture is the
# composite in the grid.
wait_for_new(DEMO_LIBRARY, '.dng', started, 600) if DEMO_LIBRARY else pause(60)
pause(5)
shot('panorama-done')
dr.click(*BACK); pause(2)
# --- settings ---------------------------------------------------------------
def settings():
dr.click(*SETTINGS_LIB); pause(2)
shot('settings')
wheel(30, 700, 600); pause(1)
shot('settings-export')
dr.click(*BACK); pause(1.5)
ALL = [
'library', 'library_rating', 'library_timeline', 'library_thumbsize',
'library_selection', 'library_keywords', 'library_collections', 'library_nesting',
'develop', 'develop_groups', 'develop_light', 'develop_zoom', 'develop_wb',
'compose', 'local_segment', 'local_paint', 'local_done', 'repair', 'film',
'presets', 'panorama', 'settings',
]
if __name__ == '__main__':
names = sys.argv[2:]
if names == ['all']:
names = ALL
for name in names:
print(f'-- {name}', flush=True)
globals()[name]()