4D window → animated GIF
Record a 4D form in motion (slides, transitions, bursts, spinners) to a GIF.
FORM SCREENSHOT / the 4d-form-screenshot skill only give a still frame and do
not run On Load; this skill runs the real app and films the window.
The engine is scripts/capture_demo.py. It:
- Launches
/Applications/4D.app with --startup-method=<method> --dataless --skip-onstartup — a real window. (Headless tool4d shows nothing, so it
cannot be used here.)
- Finds the form window via
CGWindowListCopyWindowInfo (owner 4D, layer 0).
When a --click is given, the form is read so the window whose size matches the
form wins — an incidental 4D IDE window can't be mistaken for the demo.
- Clicks (optional): a button is located by its label in the
.4DForm, its
centre mapped to screen coordinates (title-bar and border insets derived from
frame − form content, no magic numbers), 4D is brought to the front, and a HID
click is posted. Clicks are scheduled, so several can be staged.
- Records with
CGWindowListCreateImage in-process — this is essential:
the screencapture CLI fails from a subprocess (wrong TCC attribution), whereas
in-process capture runs under the host app's Screen Recording grant.
- Kills 4D and writes the GIF (frames down-scaled off the Retina buffer, with
their real inter-frame timings).
Requirements (once)
macOS with /Applications/4D.app.
Python deps — install into a venv:
python3 -m venv .venv && ./.venv/bin/pip install -r "$SKILL_DIR/scripts/requirements.txt"
Screen Recording permission for the app that runs the script (the terminal /
the Claude app / …). Without it every frame is black. Check and, if needed,
prompt for it:
import Quartz
print(Quartz.CGPreflightScreenCaptureAccess()) # True == granted
Quartz.CGRequestScreenCaptureAccess() # shows the system prompt
If it is off, the user grants it in System Settings ▸ Privacy & Security ▸ Screen
Recording and restarts that app (a fresh grant needs a relaunch). This is a
security setting — you cannot toggle it for them.
Usage
python "$SKILL_DIR/scripts/capture_demo.py" \
--project /path/to/<Name>/Project/<Name>.4DProject \
--method PLAY_<Name> \
--out /path/to/<Name>/Documentation/<name>.gif \
[--click "Label@1.2" ...] [--duration 4] [--fps 18] [--width 520] [--colors 128]
--method is the startup method that opens the form window (for these demos,
PLAY_<Name>, which the base's On Startup normally calls).
No click — it animates on load
python "$SKILL_DIR/scripts/capture_demo.py" --project …/MatrixRain.4DProject \
--method PLAY_MatrixRain --duration 4 --out MatrixRain/Documentation/matrixrain.gif
One click
python "$SKILL_DIR/scripts/capture_demo.py" --project …/Confetti.4DProject \
--method PLAY_Confetti --click Celebrate --duration 4.5 \
--out Confetti/Documentation/confetti.gif
Several clicks, staged (Label@seconds)
# three toasts stacking; then a segmented control's pill gliding across
--click "Success@0.5" --click "Info@1.2" --click "Error@1.9"
--click "Week@0.6" --click "Month@1.3" --click "Year@2.0" --click "Day@2.7"
A click target is a label looked up in the form (a button, or any object with
that text — e.g. a segment's text label sitting over a transparent click button),
or literal form-point coordinates x,y. Without an explicit @time, clicks are
spaced by --click-gap starting at --click-delay.
Key options
| Option |
Meaning |
--click (repeatable) |
Label | Label@1.2 | x,y | x,y@1.2 |
--click-delay / --click-gap |
timing of clicks that have no explicit @time |
--duration / --fps |
length and frame rate of the recording |
--width / --colors |
GIF width (px) and palette size |
--settle |
pause after the window appears before recording (default 1.5 s) |
--title |
substring the window title must contain (disambiguation) |
--form |
explicit .4DForm path (default: <project>/Sources/Forms/Demo/form.4DForm) |
--crop-titlebar |
drop the macOS title bar from the frames |
--fourd |
path to the 4D executable (default /Applications/4D.app/...) |
Tips
- File size. Scenes with constant motion (rain, spinners) can't dedup static
frames, so their GIFs are larger — trim with
--colors 48 --fps 14 --width 440.
Scenes that settle (a toast that stops, a list that lands) stay small on their own.
- Odd form path. If the demo form is not
Forms/Demo/form.4DForm, pass --form
(e.g. MatrixRain's is Forms/Rain/form.4DForm).
- Focus race. The script brings 4D to the front before clicking; if a GIF ever
comes out with no effect (the click was swallowed by window focus), just re-run.
- Verify. Read a few frames back to confirm motion before shipping:
Image.open(gif); g.seek(i); g.convert("RGB").
1---2name: 4d-capture-gif3description: Record a running 4D form window to an animated GIF (macOS). Use this skill when the user wants a GIF or video of a 4D demo/form in motion — a README animation, showing a button's effect, capturing an animation or transition. Launches /Applications/4D.app on a startup method, waits for the form window, optionally clicks buttons on a schedule, captures the window frame by frame with CGWindowListCreateImage, and writes a GIF. Not for a single still frame — use 4d-form-screenshot for that.4license: Apache 2.05---67# 4D window → animated GIF89Record a 4D form **in motion** (slides, transitions, bursts, spinners) to a GIF.10`FORM SCREENSHOT` / the `4d-form-screenshot` skill only give a still frame and do11not run `On Load`; this skill runs the real app and films the window.1213The engine is [`scripts/capture_demo.py`](scripts/capture_demo.py). It:14151. **Launches** `/Applications/4D.app` with `--startup-method=<method> --dataless16 --skip-onstartup` — a *real* window. (Headless `tool4d` shows nothing, so it17 cannot be used here.)182. **Finds** the form window via `CGWindowListCopyWindowInfo` (owner `4D`, layer 0).19 When a `--click` is given, the form is read so the window whose size matches the20 form wins — an incidental 4D IDE window can't be mistaken for the demo.213. **Clicks** (optional): a button is located *by its label in the `.4DForm`*, its22 centre mapped to screen coordinates (title-bar and border insets derived from23 frame − form content, no magic numbers), 4D is brought to the front, and a HID24 click is posted. Clicks are scheduled, so several can be staged.254. **Records** with `CGWindowListCreateImage` **in-process** — this is essential:26 the `screencapture` CLI fails from a subprocess (wrong TCC attribution), whereas27 in-process capture runs under the host app's Screen Recording grant.285. **Kills** 4D and writes the GIF (frames down-scaled off the Retina buffer, with29 their real inter-frame timings).3031## Requirements (once)3233- macOS with `/Applications/4D.app`.34- Python deps — install into a venv:3536 ```bash37 python3 -m venv .venv && ./.venv/bin/pip install -r "$SKILL_DIR/scripts/requirements.txt"38 ```3940- **Screen Recording permission** for the app that runs the script (the terminal /41 the Claude app / …). Without it every frame is black. Check and, if needed,42 prompt for it:4344 ```python45 import Quartz46 print(Quartz.CGPreflightScreenCaptureAccess()) # True == granted47 Quartz.CGRequestScreenCaptureAccess() # shows the system prompt48 ```4950 If it is off, the user grants it in System Settings ▸ Privacy & Security ▸ Screen51 Recording and **restarts that app** (a fresh grant needs a relaunch). This is a52 security setting — you cannot toggle it for them.5354## Usage5556```bash57python "$SKILL_DIR/scripts/capture_demo.py" \58 --project /path/to/<Name>/Project/<Name>.4DProject \59 --method PLAY_<Name> \60 --out /path/to/<Name>/Documentation/<name>.gif \61 [--click "Label@1.2" ...] [--duration 4] [--fps 18] [--width 520] [--colors 128]62```6364`--method` is the startup method that opens the form window (for these demos,65`PLAY_<Name>`, which the base's `On Startup` normally calls).6667### No click — it animates on load6869```bash70python "$SKILL_DIR/scripts/capture_demo.py" --project …/MatrixRain.4DProject \71 --method PLAY_MatrixRain --duration 4 --out MatrixRain/Documentation/matrixrain.gif72```7374### One click7576```bash77python "$SKILL_DIR/scripts/capture_demo.py" --project …/Confetti.4DProject \78 --method PLAY_Confetti --click Celebrate --duration 4.5 \79 --out Confetti/Documentation/confetti.gif80```8182### Several clicks, staged (`Label@seconds`)8384```bash85# three toasts stacking; then a segmented control's pill gliding across86--click "Success@0.5" --click "Info@1.2" --click "Error@1.9"87--click "Week@0.6" --click "Month@1.3" --click "Year@2.0" --click "Day@2.7"88```8990A click target is a **label** looked up in the form (a button, or any object with91that text — e.g. a segment's text label sitting over a transparent click button),92or literal form-point coordinates `x,y`. Without an explicit `@time`, clicks are93spaced by `--click-gap` starting at `--click-delay`.9495## Key options9697| Option | Meaning |98|---|---|99| `--click` (repeatable) | `Label` \| `Label@1.2` \| `x,y` \| `x,y@1.2` |100| `--click-delay` / `--click-gap` | timing of clicks that have no explicit `@time` |101| `--duration` / `--fps` | length and frame rate of the recording |102| `--width` / `--colors` | GIF width (px) and palette size |103| `--settle` | pause after the window appears before recording (default 1.5 s) |104| `--title` | substring the window title must contain (disambiguation) |105| `--form` | explicit `.4DForm` path (default: `<project>/Sources/Forms/Demo/form.4DForm`) |106| `--crop-titlebar` | drop the macOS title bar from the frames |107| `--fourd` | path to the 4D executable (default `/Applications/4D.app/...`) |108109## Tips110111- **File size.** Scenes with constant motion (rain, spinners) can't dedup static112 frames, so their GIFs are larger — trim with `--colors 48 --fps 14 --width 440`.113 Scenes that settle (a toast that stops, a list that lands) stay small on their own.114- **Odd form path.** If the demo form is not `Forms/Demo/form.4DForm`, pass `--form`115 (e.g. MatrixRain's is `Forms/Rain/form.4DForm`).116- **Focus race.** The script brings 4D to the front before clicking; if a GIF ever117 comes out with no effect (the click was swallowed by window focus), just re-run.118- **Verify.** Read a few frames back to confirm motion before shipping:119 `Image.open(gif); g.seek(i); g.convert("RGB")`.