423 lines
13 KiB
Python
Executable File
423 lines
13 KiB
Python
Executable File
#!/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]()
|