diff options
| author | Gabriel Schneider <[email protected]> | 2026-10-01 14:04:47 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 14:04:47 -0300 |
| commit | 54948bf22fb891bd31fcd5dc33e0bce7552fe1d4 (patch) | |
| tree | 180f523481936626607d7da8c44bdcd231c896e0 /test/record_demos.py | |
| parent | bf3ab868aa9092d90bdc318e6bac8d92044e5c2c (diff) | |
| download | pardes-54948bf22fb891bd31fcd5dc33e0bce7552fe1d4.tar.gz pardes-54948bf22fb891bd31fcd5dc33e0bce7552fe1d4.zip | |
A chords clip for the guide: B1 sweeps and stays down, B2 cuts, B3 pastes, B2 then B3 copies, every held button drawn
record_demos.py chords: a small notes file; each chord glides, stops,
presses B1 and keeps it down while B2 or B3 clicks. The pointer wears a
ring per button down and a mouse whose held buttons are lit, named
"left held + middle-click" and the like; B1 is drawn blue. clips.toml
lists it with landing = false, for the guide to embed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'test/record_demos.py')
| -rw-r--r-- | test/record_demos.py | 143 |
1 files changed, 134 insertions, 9 deletions
diff --git a/test/record_demos.py b/test/record_demos.py index 3cc07f15..7ab22d95 100644 --- a/test/record_demos.py +++ b/test/record_demos.py @@ -110,6 +110,7 @@ def make_project(): (PROJECT / 'src/main.c').write_text(MAIN_C) (PROJECT / 'Makefile').write_text(MAKEFILE) (PROJECT / 'README').write_text(README) + (PROJECT / 'notes.txt').write_text(NOTES) class Session: @@ -157,6 +158,7 @@ class Session: self.pointer = None # the drawn pointer's (x, y), None for none self.button = None # the SDL button held, drawn as a ring and its name self.badge = None # a key or two, drawn in the corner + self.held = [] # the buttons a chord holds, each drawn as a ring and on a mouse self.settle() def _drain(self): @@ -309,16 +311,43 @@ class Session: path.write_bytes(data) self.shots.append(dict(frame=path, secs=seconds, start=self.pointer, end=self.pointer, button=self.button, badge=self.badge, poster=poster, ring=ring, - ring_button=ring_button or self.button)) + ring_button=ring_button or self.button, held=tuple(self.held))) def glide(self, x, y, seconds=.6): """Glide the drawn pointer to (x, y) over the frame on screen, then move the window's pointer there (dragging any button held).""" if self.shots and self.pointer is not None: last = self.shots[-1] - self.shots.append(dict(last, secs=seconds, start=self.pointer, end=(x, y), poster=False, ring=None)) + self.shots.append(dict(last, secs=seconds, start=self.pointer, end=(x, y), poster=False, ring=None, + held=tuple(self.held))) self.pointer = (x, y) - self.mouse('motion', self.button or 0, x, y) + self.mouse('motion', self.button or (self.held[0] if self.held else 0), x, y) + + def chord_press(self, x, y, rest=.4, hold=.5): + """Glide to (x, y), stop, and press B1 there, holding it.""" + if self.pointer != (x, y): + self.glide(x, y) + self.shot(rest) + self.mouse('down', 1, x, y) + self.held = [1] + self.shot(hold) + + def chord_click(self, button, done, what, hold=.45, after=1.0): + """With B1 held and the pointer still, click `button`: both rings + and the mouse's two buttons lit on the press, then a pulse.""" + self.mouse('down', button, *self.pointer) + self.held.append(button) + self.shot(hold, ring='press', ring_button=button) + self.mouse('up', button, *self.pointer) + self.held.remove(button) + self.wait(done, what) + self.shot(after, ring='pulse', ring_button=button) + + def chord_release(self, after=.6): + """Let B1 go, the pointer still.""" + self.mouse('up', 1, *self.pointer) + self.held = [] + self.shot(after) def click(self, button, x, y, done=None, what='the click', rest=.4, hold=.35, after=1.0): """Glide to (x, y) and stop; a beat later press `button` (SDL: 1 @@ -368,8 +397,9 @@ def pointer_sprite(scale): return im -# acme's colours for the buttons: B2 red, B3 green. -BUTTONS = {1: ('click', (70, 70, 70)), 2: ('middle-click', (170, 0, 0)), 3: ('right-click', (0, 110, 40))} +# acme's colours for the buttons: B2 red, B3 green; B1, which acme leaves +# black, is blue here so a held left button shows on the mouse. +BUTTONS = {1: ('click', (30, 80, 190)), 2: ('middle-click', (170, 0, 0)), 3: ('right-click', (0, 110, 40))} def draw_badge(img, text, xy, font, fill=(40, 40, 40), anchor='lt'): @@ -386,7 +416,50 @@ class Painter: self.font = ImageFont.truetype(str(BADGE_FONT), 24) self.size = size - def paint(self, img, pointer, badge, ring=None, button=None, since=0.0): + def chord(self, img, x, y, held, ring, button, since): + """A chord: a ring for every button down (left inside, middle, + right outside), a mouse whose held buttons are lit, and its name.""" + w, h = self.size + d = ImageDraw.Draw(img, 'RGBA') + radius = {1: 15, 2: 23, 3: 31} + for b in held: + r = radius[b] + d.ellipse((x - r, y - r, x + r, y + r), outline=BUTTONS[b][1] + (255,), width=4) + clicked = [b for b in held if b != 1] + if ring == 'pulse' and button and button not in held: + t = min(since / .45, 1) + r = radius[button] + 18 * t + if t < 1: + d.ellipse((x - r, y - r, x + r, y + r), outline=BUTTONS[button][1] + (int(255 * (1 - t)),), width=4) + if since < .7: + clicked = [button] + lit = set(held) | set(clicked) + names = {2: 'middle', 3: 'right'} + label = 'left held' + ''.join(f' + {names[b]}-click' for b in clicked) + width = self.font.getlength(label) + 14 + # The mouse, below and right of the pointer (else left of it): three + # buttons over a body, the held ones lit, the name beside it. + mx, my, mw, mh = x + 40, y + 30, 52, 74 + if my + mh + 8 > h: + my = y - 34 - mh + if mx + mw + 12 + width + 16 > w: + mx = x - 40 - mw + d.rounded_rectangle((mx, my, mx + mw, my + mh), radius=18, fill=(250, 250, 250, 245), outline=(30, 30, 30, 255), + width=3) + third = mw / 3 + for b in (1, 2, 3): + box = (mx + third * (b - 1) + 3, my + 3, mx + third * b - 3, my + mh * .42) + if b in lit: + d.rectangle(box, fill=BUTTONS[b][1] + (255,)) + d.line((mx + 3, my + mh * .42 + 2, mx + mw - 3, my + mh * .42 + 2), fill=(30, 30, 30, 255), width=2) + for b in (1, 2): + lx = mx + third * b + d.line((lx, my + 3, lx, my + mh * .42), fill=(30, 30, 30, 255), width=2) + colour = BUTTONS[clicked[-1]][1] if clicked else BUTTONS[1][1] + lx = mx + mw + 12 if mx > x else mx - 12 - width + draw_badge(img, label, (lx, my + mh / 2), self.font, fill=colour, anchor='lm') + + def paint(self, img, pointer, badge, ring=None, button=None, since=0.0, held=()): """`img` with the badge, the pointer, and a click: `ring` 'press' (a ring and the button's name) or 'pulse' (`since` seconds after the release: the ring widening and fading, the name kept a moment).""" @@ -396,7 +469,9 @@ class Painter: draw_badge(img, badge, (w - 16, h - 16), self.font, anchor='rb') if pointer is not None: x, y = pointer - if ring and button: + if held: + self.chord(img, x, y, held, ring, button, since) + elif ring and button: label, colour = BUTTONS[button] d = ImageDraw.Draw(img, 'RGBA') if ring == 'press': @@ -446,10 +521,13 @@ def compose(shots, out): for i in range(n): t = ease((i + 1) / n) frame = painter.paint(img, lerp(shot['start'], shot['end'], t), shot['badge'], shot['ring'], - shot['ring_button'], i / FPS) + shot['ring_button'], i / FPS, shot.get('held', ())) ff.stdin.write(frame.tobytes()) if shot['poster'] and poster is None: - poster = painter.paint(img, shot['end'], None) + # A chord's still keeps its rings and mouse; a click's has none. + chord = shot.get('held') or () + poster = painter.paint(img, shot['end'], None, shot['ring'] if chord else None, shot['ring_button'], 0.0, + chord) emitted += n last = shots[-1] n = round(.7 * FPS) @@ -837,6 +915,50 @@ def record_themes(binary, work): (THEMES / 'themes.toml').write_text('\n'.join(toml)) +NOTES = '''To buy: apples plums pears + + + +In the bowl: bananas +''' + + +def chords_setup(s): + s.mouse('motion', 0, *s.cell_px(COLS - 2, ROWS - 1)) + + +def body_text(s): + return s.read('/pane/1/body') + + +def chords(s): + """acme's chords: B1 sweeps and stays down; B2 cuts, B3 pastes, B2 then + B3 copies.""" + s.pointer = s.cell_px(40, 12) + s.shot(.8) + # Cut: sweep " plums", B2 with B1 still down. + col, row = s.find(' plums', 0) + s.chord_press(*s.cell_px(col, row), rest=.25, hold=.3) + s.glide(*s.cell_px(col + 7, row), seconds=.45) + s.shot(.3) + s.chord_click(2, lambda: 'plums' not in body_text(s), 'the cut', after=.75) + s.shots[-2]['poster'] = True + s.chord_release(after=.25) + # Paste after "bananas": B1 down there, B3. + col, row = s.find('bananas', 0) + s.chord_press(*s.cell_px(col + 7, row), rest=.25, hold=.3) + s.chord_click(3, lambda: 'bananas plums' in body_text(s), 'the paste', after=.8) + s.chord_release(after=.25) + # Copy: sweep "apples", B2 then B3 with B1 down: it is back, and kept. + col, row = s.find('apples', 0) + s.chord_press(*s.cell_px(col, row), rest=.25, hold=.3) + s.glide(*s.cell_px(col + 6, row), seconds=.45) + s.shot(.3) + s.chord_click(2, lambda: 'apples' not in body_text(s), 'the copy\'s cut', after=.45) + s.chord_click(3, lambda: 'apples' in body_text(s), 'the copy\'s paste', after=.8) + s.chord_release(after=1.0) + + CLIPS = { 'pdf-navigation': dict( setup=pdf_setup, run=pdf_navigation, args=pdf_args, @@ -859,6 +981,9 @@ CLIPS = { 'diff-review': dict( setup=diff_setup, run=diff_review, args=[PROJECT / 'README'], caption='Right-click a diff line to open that line of the file.'), + 'chords': dict( + setup=chords_setup, run=chords, args=[PROJECT / 'notes.txt'], + caption='Hold left, click middle to cut; click right to paste.'), } |
