Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions .claude/skills/film/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
name: film
description: >-
Making the videos: the website's teaser (Tools/film/teaser.rb) and the App
Store app previews for iPhone, iPad, Mac and Vision Pro
(Tools/film/previews.rb) — recording UI tests on the simulators and on this
Mac, cutting them to Apple's rules, checking the result, and handing the
films over for music and upload. Load this before reshooting either, before
changing what a film shows, and whenever a film run fails or a film looks
wrong. Still pictures are the `screenshots` skill.
---

# Films

Two kinds, one set of tools in `Tools/film/`. The scripts are UI tests. The
Ruby records them, cuts out the waiting, and edits. **`Tools/film/README.md` is
the why** — every rule, number and trap is explained there, so read it before
changing anything. This file is the how.

```bash
ruby Tools/film/teaser.rb # the website's teaser, ~15 min
ruby Tools/film/previews.rb # iphone ipad mac vision, ~5 min each
ruby Tools/film/previews.rb ipad mac # only the ones named
ruby Tools/film/previews.rb --compose # re-cut the last recordings, no shooting
```

Every film comes out English, silent and uncommitted, in `$TMPDIR/tortoise-teaser/`
or `$TMPDIR/tortoise-previews/`. The maintainer adds the music by hand. The
previews are uploaded by hand as well: fastlane's deliver does not take them.

## Before a run

- **One rig at a time.** The films share DerivedData with the screenshot rigs;
check `pgrep -f 'shots.rb|film/'` first.
- **Tell the maintainer before the Mac preview, and wait for them.** It drives
this Mac's real pointer for three or four minutes, the screen has to stay
unlocked, and macOS asks for the password before a UI test may take the
pointer (`automationmodetool` says "requires user authentication"). The
maintainer chose to type it each time rather than switch the check off, so
say when the dialog is coming, and say again when the Mac is free. The
simulator films need nothing from them.
- A run changes the Simulator's `ConnectHardwareKeyboard` and the simulators'
language, and puts both back when it ends. After an interrupted run, check
`defaults read com.apple.iphonesimulator ConnectHardwareKeyboard` is back to
what it was (0 here).

## Changing what a film shows

- **The story is the test.** `TeaserTests`, `AppPreviewTests` (iPhone, iPad)
and `MacPreviewTests`. Pauses only give the app time to settle. The pace is
set by the cut, and `linger(_:)` is how a script asks for a moment to be held.
- **A preview starts from a document that is already nearly a program**,
written in `PREVIEWS` in `previews.rb`. Keep a preview to 15–30s: a short
cut holds its last frame, and a long one stops the run. Vision Pro has no
script, only launch arguments (sample, sheet, speed) in the same table.
- **What a preview may show is Apple's call, not ours**: the screen as
captured, with no zoom, backdrop, cards or captions. The README's "The
previews" section lists the rules. The teaser has none of those limits.

## After a run

Nothing counts as done until someone has watched it, and that someone is the
maintainer. Movement cannot be judged from stills: a sheet of frames has
already passed legs as walking that barely moved, and the maintainer caught it
on playback.

1. **Check the file against the spec.** Run `ffprobe -show_entries
stream=codec_name,width,height,r_frame_rate,level:format=duration`. It should
show H.264, the device's exact size, 30fps, 15–30s and an AAC track. The
sizes are iPhone 886×1920, iPad 1600×1200, Mac 1920×1080 and Vision Pro
3840×2160 (Level 5.1; the others 4.0).
2. **Look at a contact sheet** for the story and for anything wrong in a frame,
such as a home screen, a keyboard, or a sheet that never opened:
`ffmpeg -i f.mp4 -vf fps=1,scale=320:-1,tile=6x4 -frames:v 1 sheet.png`.
3. **Spot-check the touches** at full size over a second or two around a
press. The ring should land on the control as it lights, not after it.
4. **Hand the films over.** Copy them out of `$TMPDIR` to somewhere the
maintainer can open, give the links, and say what cannot be judged from
stills. For previews, remind them of two things at upload: the poster frame
defaults to 5s, which is mid-story, so pick the finished drawing; and
Vision Pro's Level 5.1 file has to be checked against what App Store Connect
accepts.

## When a run fails

- **A simulator script stopped.** The reason is in the result bundle:
`xcrun xcresulttool get test-results tests --path <raw>/test.xcresult`, then
the "Failure Message" nodes. `failure.txt` beside it is the screen the test
last saw, with frames. Read it before guessing.
- **The Mac script stopped.** Its runner is sandboxed, so there is no
`failure.txt`. Export the attachments (`xcresulttool export attachments`):
XCTest's own UI hierarchy dumps and screen recording are among them.
"Timed out while enabling automation mode" means the password dialog went
unanswered.
- **Vision Pro gives `TBNotReady` every time.** Something is recording the
simulator while the app launches. The recorder must start after `TBReady`.
- **A film cuts to the home screen, or its touches drift.** The conversion has
lost `-fflags +igndts`.
- **A cut is far longer than expected.** Something keeps drawing frames:
usually a blinking caret outside a `typing` span, or on the Mac a recording
judged by frames rather than by content.
5 changes: 4 additions & 1 deletion .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ size of an Apple TV one, and that collision is a real trap further down.
pass every reshoot has to end with, and the traps that produce a picture of the
wrong thing. Nothing reaches App Store Connect without going through it: every
source this project shoots from writes an alpha channel, which Apple refuses.
The text is `appstore/metadata/<locale>/`, one file per field — **except
**App previews are the one piece of the listing that fastlane does not
carry.** deliver uploads screenshots but not previews, so they are made by the
`film` skill and put up by hand in App Store Connect, and nothing in
`appstore/` holds them. The text is `appstore/metadata/<locale>/`, one file per field — **except
visionOS**, which is pushed from `appstore/metadata-visionos/` instead (#53).
That split is not tidiness: the App Store shows a Vision Pro shopper the
visionOS description and nothing else, and the app is a different product
Expand Down
23 changes: 8 additions & 15 deletions .claude/skills/screenshots/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ description: >-
(Tools/ipad-shots.rb, Tools/macos-shots.rb, Tools/visionos-shots.rb), the
flatten-and-optimise pass
(Tools/screenshots.rb), what a sendable capture has to be, and the traps that
make a screenshot tool fail silently. Also the website's teaser video
(Tools/teaser/). Load this before reshooting, before adding a shot or a
platform, and whenever a capture looks wrong.
make a screenshot tool fail silently. Load this before reshooting, before
adding a shot or a platform, and whenever a capture looks wrong. Videos —
the website's teaser and the App Store previews — are the `film` skill.
---

# Screenshots
Expand Down Expand Up @@ -259,19 +259,12 @@ Seven ways the Mac differs from the iPad, all handled but all worth knowing:
what makes 1280×800pt — 2560×1600px — reproducible. Keep that `defaultSize`:
a capture at any other size would need cropping or resampling.

## The teaser video
## The films

The website's two-minute video is made the same way: `ruby Tools/teaser/teaser.rb`
records `TortoiseBlocksUITests/TeaserTests.swift` on the 11-inch iPad simulator
and cuts it into a silent 1080p film; music goes on by hand afterwards. The
test is the script, and it logs every press, caption and camera move for the
editor to work from. `Tools/teaser/README.md` has why each piece is as it is.
It shares DerivedData with the rigs above, so it counts as one of them: one at
a time.

Judge it the way the stills are judged, but on the film: a contact sheet
shows what is in it (`ffmpeg -i teaser.mp4 -vf fps=1/3,scale=320:-1,tile=6x9`),
and only playing it shows whether it moves well.
The website's teaser and the App Store previews are made the same way as the
iPad and Mac captures, with UI tests pressing and a script recording, and they
share DerivedData with these rigs. So they count as rigs: one at a time. How
to make them is the `film` skill.

## Judging the result

Expand Down
6 changes: 5 additions & 1 deletion App/Views/Viewer/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ tortoise also turns, and a turn changes the silhouette of every leg from frame
to frame; a sheet of consecutive frames looked like legs moving when they
barely were, and it was the maintainer who saw that they were not. Holding
two opposite moments of the cycle and comparing them is the honest still; the
honest check is `-TBPlay YES -TBSpeed 1` (×0.2) and `simctl io recordVideo`.
honest check is `-TBPlay YES -TBSpeed 1` (×0.2) and `simctl io recordVideo` —
started once the app is up, never before the launch: a recording running
through it keeps the immersive space from opening at all (`TBNotReady`, every
time). To catch a drawing from its first line, `-TBPlay <seconds>` holds it
until the recorder has started, and `TBPlaying` says when it went.
**The visionOS simulator shows all of this** — paper, drawing and tortoise —
and this note said the opposite for a while, which is worth keeping as a
correction rather than an edit. The symptom was real: a blank sheet, a nil
Expand Down
23 changes: 17 additions & 6 deletions App/Views/Viewer/ViewerWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@
// put the drawing down
// -TBSample <name> square | star | spiral | tree
// -TBDraw <0…1> run the drawing that far and stop
// -TBPlay YES then keep playing from there, for a
// recording of the tortoise walking
// -TBPlay YES|<s> then keep playing from there, for a
// recording of the tortoise walking —
// after <s> seconds, if a number
// -TBSpeed <level> the transport's speed, 1…10 (5 is ×1)
// -TBSheet s,r,d the sheet's side, how far ahead of the
// eyes it lands, and how far below them
Expand All @@ -116,7 +117,12 @@
// which a still cannot show: the check is a recording
// (`simctl io recordVideo`) of a drawing that keeps playing,
// usually at level 1 (×0.2), where a step lasts long enough to
// see the legs change feet.
// see the legs change feet. The delay is for a recording that
// has to catch the first line: `simctl io recordVideo` running
// while the app launches stops the immersive space opening at
// all (`TBNotReady`, every time), so a recording can only start
// once the sheet is up — and by then an undelayed drawing is
// already under way. `TBPlaying` marks the moment it starts.
//
// `-TBSheet` is the framing one, and it exists because
// nothing in the simulator can reach out and pinch the sheet
Expand Down Expand Up @@ -178,9 +184,10 @@
openWindow(id: ViewerModel.programWindowID)
openWindow(id: ViewerModel.codeWindowID)

let play = Self.value(of: "-TBPlay", in: arguments)
await settle(
drawingTo: Self.value(of: "-TBDraw", in: arguments).flatMap(Double.init),
playing: arguments.contains("-TBPlay"))
playing: arguments.contains("-TBPlay") ? play.flatMap(Double.init) ?? 0 : nil)
}
}

Expand Down Expand Up @@ -235,7 +242,7 @@
/// all, and from the outside that is indistinguishable from one that is
/// merely slow. Told which it is, the script can relaunch instead of
/// filing a picture of an empty room.
private func settle(drawingTo fraction: Double?, playing: Bool) async {
private func settle(drawingTo fraction: Double?, playing delay: Double?) async {
let clock = ContinuousClock()
let deadline = clock.now + .seconds(20)
while model.runner.player.currentTortoiseState == nil, clock.now < deadline {
Expand All @@ -251,8 +258,12 @@
let clamped = min(max(fraction, 0), 1)
model.runner.seek(to: Int((Double(commands - 1) * clamped).rounded()))
}
if playing { model.runner.player.isPaused = false }
Self.shoot.notice("TBReady")
if let delay {
try? await Task.sleep(for: .seconds(delay))
model.runner.player.isPaused = false
Self.shoot.notice("TBPlaying")
}
}

/// Only ever written to under `-TBPlace`, and read only by the
Expand Down
14 changes: 10 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,21 @@ pkill -x TortoiseBlocks; open ~/Library/Developer/Xcode/DerivedData/TortoiseBloc
# `-TBSheet side,reach,drop` frames it (metres). The sheet is aimed at the
# camera, so no recentring — but a run occasionally comes up without it, so
# look at the capture. The walk is motion, so it is checked on a recording:
# `-TBPlay YES` keeps the drawing playing and `-TBSpeed 1` slows it to ×0.2.
# `-TBPlay YES` keeps the drawing playing and `-TBSpeed 1` slows it to ×0.2;
# start recordVideo only once the app is up — running it through the launch
# stops the immersive space opening (`-TBPlay <seconds>` delays the drawing).
xcrun simctl install <device> ~/Library/Developer/Xcode/DerivedData/TortoiseBlocks-*/Build/Products/Debug-xrsimulator/TortoiseBlocks.app
xcrun simctl launch <device> space.hiraku.tortoiseblocks \
-TBPlace YES -TBSample star -TBDraw 1 -TBSheet 0.5,0.95,0.42
xcrun simctl io <device> screenshot shot.png # 3840x2160, with an alpha channel
xcrun simctl io <device> recordVideo walk.mov # Ctrl-C to stop

# The website's teaser video: records TeaserTests on the iPad simulator, then
# cuts it to a silent 1080p film (Tools/teaser/README.md). ~15 minutes.
ruby Tools/teaser/teaser.rb # --compose re-cuts the last recording
# The films (Tools/film/README.md): the website's teaser, and the App Store
# previews for iphone / ipad / mac / vision. Silent, for music added by hand;
# `--compose` re-cuts the last recording. The Mac one drives this Mac's real
# pointer and asks for the password first.
ruby Tools/film/teaser.rb # ~15 minutes
ruby Tools/film/previews.rb [ipad …] # ~5 minutes each

# The App Store listing (appstore/). The check needs no key and no bundle;
# the other two need ASC_ISSUER_ID / ASC_KEY_ID / ASC_PRIVATE_KEY_PATH.
Expand Down Expand Up @@ -455,6 +460,7 @@ the four files it describes.

**Releasing, the store listing and the website are in the `release` skill.** Tags, Xcode Cloud, TestFlight, `appstore/`, fastlane, and `site/`.
**Making the pictures is the `screenshots` skill** — the capture rigs for iPad, Mac and Vision Pro, the pass every reshoot ends with (`ruby Tools/screenshots.rb`), and the traps that hand back a perfectly well-made capture of the wrong thing.
**Making the videos is the `film` skill** — the website's teaser and the App Store previews, from UI tests, cut to Apple's rules.

**Localization**: `en` is the source language; Japanese (kid-friendly
hiragana) lives in `App/Localizable.xcstrings`. Palette titles are
Expand Down
Loading
Loading