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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
You start with one painting of a character. You end with that character rigged — split
into movable pieces on a skeleton — breathing, blinking and waving in your browser.

<!-- demo gif -->
![The guide's painting, its See-through layers fanning apart, the rig's actual bones and mesh triangles, then the character breathing, blinking and waving in the browser.](docs/demo/walkthrough.webp)

Under-eight-second looping WebP, from this walkthrough's outputs.
[Rebuild it](docs/demo/README.md). Example artwork: CC BY-NC 4.0; credit below.

## The whole thing in one glance

Expand Down
89 changes: 89 additions & 0 deletions docs/demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Rebuild the README demo

`walkthrough.webp` is a 720 × 600, 7.75-second looping WebP. WebP works as a
Markdown image and stays below 2 MB without requiring a video player. Its stages
show the guide's painting, five of its original See-through full-run layers fanned
apart, the built rig's bones and mesh triangles, and the real `@rig-c/play` page
playing the idle's blink, then `hello` over `idle`. The last stage includes the
page's breeze. It selects these through the guide's animation menu: 16 captures
of idle from 2.083 s, then 41 of hello from 1.583 s, sampled at 12 Hz. The hello's
own eye-narrowing keys take precedence over idle, so the separate idle view keeps
the real blink visible without changing either animation. The introduction lasts
0.75 s for the painting, 1.25 s for the layers and 1 s for the rig.

Complete **Try it first** in the root README through step 10, using the published
packages and the pinned example archive. Keep that `my-rig` directory, including
its `node_modules`. No drawing, repaired pixels, replacement joints or extra motion
keys are inputs to this exporter.

Use a dedicated scratch directory outside the checkout and consumer. The commands
below install export tools there, without changing the guide's dependencies.
`uv` supplies Python and Pillow; the two JavaScript scripts run with Bun.

```sh
export DEMO_SCRATCH="$HOME/.cache/rig-demo-export"
export TMPDIR="$DEMO_SCRATCH/tmp"
export XDG_CACHE_HOME="$DEMO_SCRATCH/cache"
export BUN_INSTALL_CACHE_DIR="$DEMO_SCRATCH/bun-cache"
export UV_CACHE_DIR="$DEMO_SCRATCH/uv-cache"
export UV_PYTHON_INSTALL_DIR="$DEMO_SCRATCH/python"
export PLAYWRIGHT_BROWSERS_PATH="$DEMO_SCRATCH/browsers"
export PLAYWRIGHT_SKIP_BROWSER_GC=1
mkdir -p "$TMPDIR" "$XDG_CACHE_HOME" "$BUN_INSTALL_CACHE_DIR" "$UV_CACHE_DIR" "$UV_PYTHON_INSTALL_DIR" "$PLAYWRIGHT_BROWSERS_PATH" "$DEMO_SCRATCH/browser-tools"
cd "$DEMO_SCRATCH/browser-tools"
bun add --exact playwright@1.58.2
bun node_modules/playwright/cli.js install chromium
cd /path/to/getting-started
bun docs/demo/regenerate.ts --consumer /path/to/my-rig --scratch "$DEMO_SCRATCH" --check
bun docs/demo/regenerate.ts --consumer /path/to/my-rig --scratch "$DEMO_SCRATCH"
```

On Linux, Chromium's required system libraries must be installed before running
the exporter. If Playwright reports missing libraries, install the dependencies
required by your distribution before continuing.
Keep `PLAYWRIGHT_SKIP_BROWSER_GC=1` so installs do not remove other cached browser
revisions, especially when sharing a browser cache.

The preflight reads the actual layer manifest, animation data and every model page
path. The full run serves the unchanged `scripts/play.html`, loads its model via
the real browser loader and requires WebGL, changing poses/pixels and no browser
errors. Only the clock is controlled: the guide's RAF callback advances at 24 Hz,
and screenshots sample it at 12 Hz. Bone lines and mesh edges are derived from
`player.bone()` and `player.posedFrame()`, not approximated by hand. Labels, image
layout and layer transforms provide presentation. Pillow prepares deterministic
thumbnails: LANCZOS downscaling, and each layer's opaque-alpha bounding box plus
eight pixels of padding for its presentation crop. Original files and all rig
inputs remain unchanged. This export does not repair the guide's separately documented
elbow warning ([issue 18](https://github.com/rig-c/getting-started/issues/18)).

The script captures and encodes twice in fresh pages, records both raw screenshot
digests and requires matching **final WebP bytes**, then writes the media and
`provenance.json` beside it. Raw PNG hashes can differ due to browser rasterization;
the lossy encoding can map those differences to identical WebP bytes. This is
reported explicitly and never used to waive the final-byte check. Pillow
11.3.0 reads the encoded file back to check its dimensions, duration, infinite loop
and byte limit. The receipt identifies the browser, encoder, package versions,
README digest, source assets and compiled-model hashes. Identical bytes are checked
within that environment; different browser, font or libwebp versions may change
pixels or encoding. Keep the recorded environment when comparing rebuilds.
Optionally pass `--guide-commit <full 40-character SHA>` to record the source guide
commit beside its digest. Verify that commit against the checkout before exporting;
the exporter validates the SHA's format and records the supplied identity.

Raw PNGs and the two trial WebPs are scratch files. Remove that dedicated scratch
directory after reviewing the output and preserving its digests. Commit only the
final WebP, the scripts, this note and the provenance receipt.

Changing the root README changes its **whole-file** guide SHA. Per
[the CI contract](../CI.md), it requires a fresh walkthrough and an automated
REFERENCE record proposal even when no guide command changed. Do not copy that
proposal over the historical measurements by hand. The walkthrough's literal
clone uses public helpers; separately running this checkout's exporter validates
unpublished exporter changes, while a later published-guide run validates what a
reader can fetch.

The scripts are MIT. The demo's character imagery is derived from `rig-c/exam`
inputs and remains **CC BY-NC 4.0**, not MIT. The painting's required credit is:
*Painted with Nova Anime XL IL v8.0 by Crody (https://civitai.com/models/376130).* See
[the guide credits](../../README.md#credits-and-licences) and the example archive's
`LICENSE` and `NOTICE.md`.
50 changes: 50 additions & 0 deletions docs/demo/encode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Encode and read back actual browser captures; never paint or retouch a frame."""
import sys
import json
from pathlib import Path
from PIL import Image, features

if sys.argv[1] == '--prepare':
source, out = Path(sys.argv[2]), Path(sys.argv[3])
out.mkdir(parents=True, exist_ok=True)
painting = Image.open(source / 'inputs/painting.png').convert('RGBA')
painting.thumbnail((380, 490), Image.Resampling.LANCZOS)
painting.save(out / 'painting.png')
layers = json.loads((source / 'inputs/layers/full/layers.json').read_text())
names = ['back hair', 'bottomwear', 'topwear', 'face', 'front hair']
for i, name in enumerate(names):
assert any(layer['name'] == name for layer in layers['layers'])
image = Image.open(source / f'inputs/layers/full/parts/{name}.png').convert('RGBA')
# Presentation crops around opaque art, with padding; original pixels
# and source files are untouched. This is never a part/rig repair.
box = image.getchannel('A').point(lambda a: 255 if a >= 250 else 0).getbbox()
assert box, f'No opaque art for {name}'
box = (max(0, box[0]-8), max(0, box[1]-8), min(image.width, box[2]+8), min(image.height, box[3]+8))
image = image.crop(box)
image.thumbnail((122, 294), Image.Resampling.LANCZOS)
canvas = Image.new('RGBA', (122, 294))
canvas.alpha_composite(image, ((122-image.width)//2, (294-image.height)//2))
canvas.save(out / f'layer-{i}.png')
print(f'PRESENTATION_PREPARE Pillow=11.3.0 libwebp={features.version("webp")} original_files_unchanged=true')
sys.exit(0)

frames = sorted(Path(sys.argv[1]).glob('[0-9][0-9][0-9].png'))
assert len(frames) == 93, f'Expected 93 captures, found {len(frames)}'
images = [Image.open(f).convert('RGB') for f in frames]
assert all(im.size == (720, 600) for im in images)
durations = [round((i + 1) * 1000 / 12) - round(i * 1000 / 12) for i in range(len(images))]
images[0].save(sys.argv[2], format='WEBP', save_all=True, append_images=images[1:],
duration=durations, loop=0, quality=72, method=6, minimize_size=False)
with Image.open(sys.argv[2]) as result:
assert result.size == (720, 600) and result.info['loop'] == 0
duration = 0
for i in range(result.n_frames):
result.seek(i)
result.load()
duration += result.info['duration']
assert duration == 7750, duration
size = Path(sys.argv[2]).stat().st_size
assert size < 2_000_000, size
print(f'WEBP_READER bytes={size} duration_ms={duration} width=720 loop=0 libwebp={features.version("webp")}')
Path(sys.argv[2] + '.json').write_text(json.dumps({'Pillow': '11.3.0', 'libwebp': features.version('webp'),
'quality': 72, 'method': 6, 'loop': 0, 'bytes': size, 'duration_ms': duration}) + '\n')
117 changes: 117 additions & 0 deletions docs/demo/provenance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
{
"spec": "walkthrough-demo/1",
"width": 720,
"height": 600,
"duration_ms": 7750,
"fps": 12,
"source_frames": 93,
"bytes": 466228,
"walkthrough_input": "rig-c/exam@01436ee3f02c189fe1a1e9656fe62068889837f5/tasks/01-build/inputs",
"guide_sha256": "5c9fd1ede594e4180a5fb550f29aba64df8808a17df2d93d62e735bb645c09e4",
"guide_commit": "9c422e08be005139928e4fb6514eb19d44e248df",
"versions": {
"@rig-c/parts": "1.82.0",
"@rig-c/core": "2.82.0",
"@rig-c/pose": "1.6.0",
"@rig-c/play": "1.21.2"
},
"browser": "145.0.7632.6",
"graphics": [
"--no-sandbox",
"--use-gl=angle",
"--use-angle=swiftshader",
"--enable-unsafe-swiftshader",
"--disable-gpu-rasterization",
"--disable-accelerated-2d-canvas",
"--force-color-profile=srgb"
],
"encoder": "Pillow 11.3.0 / libwebp (version printed by encode.py); lossy quality 72, method 6, loop 0",
"input_sha256": {
"config.json": "c6220d3cc531828c27b0e7174f4f52a859e93d2a2369036665abfa945c334c8f",
"inputs/layers/full/layers.json": "22af0a8945889c657bfe153e7577e0740647402c0fa2ca87e5a29714d71d848c",
"inputs/layers/full/parts/back hair.png": "7aeced05dc951e50a2805bb498158204dcc61605a97ab69f8af7a69ad16abd27",
"inputs/layers/full/parts/bottomwear.png": "8c40aa16102602d08592721b0ab95fad6647acc966eb030d0118e79d235938f8",
"inputs/layers/full/parts/face.png": "216a510a382f437fd8ab357b952c2110bc40790fb558a2646d028eada2e66331",
"inputs/layers/full/parts/front hair.png": "3203bde03e693c1a5d1311efb1aededc1bf1895ab4f0fced6fee1e5f82ec6c3d",
"inputs/layers/full/parts/topwear.png": "4e87cfa4b06b4592f0871eb4c44f22db9dbd55c8ce4c2f1cfb8e7f5b52bd67a7",
"inputs/painting.png": "1064fc12130381dcfea6d617d4812b5b7809586f5e165925d2f29815e1cb90e5",
"inputs/seethrough.json": "b280325c6733b0c6670d7bd0003dfb45449fe106d9beeb785709c0e38730163a",
"keypoints.json": "76e912e77e743b0b6c89639bad4aac38c830ddb27878cd00d8153a1d1470d589",
"out/check/build/skeleton.json": "569858e360cb9bb51e59a0e07589b039bea4c2b72a62bff11189c61e0f21b195",
"out/check/build/skeleton.model.json": "8a6be9194202c8bc2194d786f218a8180689013ced44cf21a4ec43241a4d28a8",
"out/check/build/skeleton.png": "e74553da76db90b6e9dbeffc941affe2027ee4703d94753478b88ccf12bb6367"
},
"player_sha256": "4ce8843428205e6a0c093cfa3d75112e7574be98510921aa7e30918d81dc427d",
"script_sha256": "a35071579de2cfb7c160500961f21e9515794e987c45dee7e5b93f6a85c5e1d2",
"encoder_script_sha256": "11e2e88f5fc9dd96315d79e990e3e79b51dc21566035bb9304068365d9c4f30b",
"encoder_runtime": {
"Pillow": "11.3.0",
"libwebp": "1.5.0",
"quality": 72,
"method": 6,
"loop": 0,
"bytes": 466228,
"duration_ms": 7750
},
"raw_screenshots_identical": true,
"passes": [
{
"frames_sha256": "247593fd03b988dfe6345664eea19ef68ac03dc5e070df4f4dc6f3ffaa4168b7",
"webp_sha256": "1de8a7dd45cc3461c408f0e4d0d03df64c4d877b1ed2e2180281e31a0be1168f",
"geometry": {
"meshes": 6,
"triangles": 434,
"bones": 48,
"backend": "webgl"
},
"ranges": {
"eye_r_scaleY": {
"min": 0.12000019950726247,
"max": 1.000231093453364
},
"eye_l_scaleY": {
"min": 0.12000019950726247,
"max": 1.000231093453364
},
"chest_scaleX": {
"min": 1,
"max": 1.0079994125443006
},
"forearm_rotation": {
"min": -175.49207819215727,
"max": 163.94211154807687
}
}
},
{
"frames_sha256": "247593fd03b988dfe6345664eea19ef68ac03dc5e070df4f4dc6f3ffaa4168b7",
"webp_sha256": "1de8a7dd45cc3461c408f0e4d0d03df64c4d877b1ed2e2180281e31a0be1168f",
"geometry": {
"meshes": 6,
"triangles": 434,
"bones": 48,
"backend": "webgl"
},
"ranges": {
"eye_r_scaleY": {
"min": 0.12000019950726247,
"max": 1.000231093453364
},
"eye_l_scaleY": {
"min": 0.12000019950726247,
"max": 1.000231093453364
},
"chest_scaleX": {
"min": 1,
"max": 1.0079994125443006
},
"forearm_rotation": {
"min": -175.49207819215727,
"max": 163.94211154807687
}
}
}
],
"licence": "Character imagery: CC BY-NC 4.0, rig-c/exam. Painted with Nova Anime XL IL v8.0 by Crody (https://civitai.com/models/376130). Scripts: MIT.",
"provenance": "Source files are unretouched. Pillow crops layer thumbnail bounds at opaque alpha>=250 plus 8px padding and scales thumbnails with LANCZOS for presentation only. Bone/mesh lines come from actual runtime geometry. Playing frames capture the unchanged guide page driven at fixed 24 Hz, with two compositor frames before each screenshot."
}
Loading
Loading