diff --git a/art/stages/hidden-garden/README.md b/art/stages/hidden-garden/README.md
index f41e69e..9993cb0 100644
--- a/art/stages/hidden-garden/README.md
+++ b/art/stages/hidden-garden/README.md
@@ -1,9 +1,13 @@
# Hidden Garden – stage chunks
-The prologue, drawn from the approved [Hidden Garden concept](../../locations/hidden-garden-clean.md) as a flat side elevation with its figures removed. One chunk, `hg-0-0` (2160×720): the bench where Dexter sits at the start (x ≈ 190–320), the stone path and the timber jetty (path top y = 441), the end of the jetty where Dexter points (x ≈ 1290–1323), and one wrong patch in the pond's reflection below it – a rusty girder with a ladder that isn't there. Clean only.
+The prologue, drawn from the approved [Hidden Garden concept](../../locations/hidden-garden-clean.md) as a flat side elevation: **five chunks, 10 800 px**, about 70 s of walking plus Dexter's waits and two detours ([docs/04 – Hidden Garden, the prologue level](../../../docs/04-WORLD-AND-LEVELS.md#hidden-garden--the-prologue-level), #50). `hg-4-0` is the approved pond chunk (the jetty where Dexter points, the wrong reflection). The four before it were grown leftwards from it with [grow_stage.py](../../../scripts/grow_stage.py) – scenery first, one masked repaint per route feature, the seams between new chunks repainted (docs/09): `hg-0-0` the gate (Dexter's bench, a birch log), `hg-1-0` the hill (an oak on the crest, a hollow behind the grass), `hg-2-0` the terraces (a shed with a ladder to its roof), `hg-3-0` the boardwalk (a gap with stepping stones). Clean only.
-**Layers** (#142): the model repainted the chunk as a flat-colour depth key, and the masks come from it ([depth_layers.py](../../../scripts/depth_layers.py), [layers.json](layers.json)). Sky, far and mid scroll slower than the path (0.1, 0.25, 0.6, docs/04), and their plates are filled where the nearer layers hide them, so nothing tears open when the camera moves. The foreground scrolls at 1.2. [00 sky](layers/hg-0-0.00-sky.filled.png) · [01 far](layers/hg-0-0.01-far.filled.png) · [02 mid](layers/hg-0-0.02-mid.filled.png) · [03 rear](layers/hg-0-0.03-rear.png) · [04 water](layers/hg-0-0.04-water.png) · [05 play](layers/hg-0-0.05-play.png) · [07 foreground](layers/hg-0-0.07-foreground.png). The parallax at three camera positions: [parallax.png](images/hg-0-0-parallax.png) (`python scripts/compare.py parallax art/stages/hidden-garden`).
+**Layers** (#142): each chunk's depth key comes from the model, its masks from the key ([depth_layers.py](../../../scripts/depth_layers.py), [layers.json](layers.json)). Sky, far and mid scroll at 0.1, 0.25 and 0.6 (docs/04) from plates filled where nearer layers hide them; the foreground at 1.2. The pond keeps its exact rules for path, water and foreground; the grown chunks take every band from their key. **Collision**: walk lines traced from each key's play band (`depth_layers.py collision`), spawns, Dexter's waypoints and triggers placed by hand; overlays in [collision/](collision/).
+
+The whole walk: [prologue.png](images/prologue.png) · the parallax at three camera positions: [parallax.png](images/parallax.png) (`python scripts/compare.py parallax art/stages/hidden-garden`).
| | |
| --- | --- |
-| [](hg-0-0.md)
**[Hidden Garden · the bench and the pond](hg-0-0.md)** | |
+| [](hg-0-0.md)
**[Hidden Garden · the gate](hg-0-0.md)** | [](hg-1-0.md)
**[Hidden Garden · the hill](hg-1-0.md)** |
+| [](hg-2-0.md)
**[Hidden Garden · the terraces](hg-2-0.md)** | [](hg-3-0.md)
**[Hidden Garden · the boardwalk](hg-3-0.md)** |
+| [](hg-4-0.md)
**[Hidden Garden · the bench and the pond](hg-4-0.md)** | |
diff --git a/art/stages/hidden-garden/boards.json b/art/stages/hidden-garden/boards.json
index f434451..df45168 100644
--- a/art/stages/hidden-garden/boards.json
+++ b/art/stages/hidden-garden/boards.json
@@ -1,6 +1,6 @@
{
"name": "Hidden Garden – stage chunks",
- "readme": "The prologue, drawn from the approved [Hidden Garden concept](../../locations/hidden-garden-clean.md) as a flat side elevation with its figures removed. One chunk, `hg-0-0` (2160×720): the bench where Dexter sits at the start (x ≈ 190–320), the stone path and the timber jetty (path top y = 441), the end of the jetty where Dexter points (x ≈ 1290–1323), and one wrong patch in the pond's reflection below it – a rusty girder with a ladder that isn't there. Clean only.\n\n**Layers** (#142): the model repainted the chunk as a flat-colour depth key, and the masks come from it ([depth_layers.py](../../../scripts/depth_layers.py), [layers.json](layers.json)). Sky, far and mid scroll slower than the path (0.1, 0.25, 0.6, docs/04), and their plates are filled where the nearer layers hide them, so nothing tears open when the camera moves. The foreground scrolls at 1.2. [00 sky](layers/hg-0-0.00-sky.filled.png) · [01 far](layers/hg-0-0.01-far.filled.png) · [02 mid](layers/hg-0-0.02-mid.filled.png) · [03 rear](layers/hg-0-0.03-rear.png) · [04 water](layers/hg-0-0.04-water.png) · [05 play](layers/hg-0-0.05-play.png) · [07 foreground](layers/hg-0-0.07-foreground.png). The parallax at three camera positions: [parallax.png](images/hg-0-0-parallax.png) (`python scripts/compare.py parallax art/stages/hidden-garden`).",
+ "readme": "The prologue, drawn from the approved [Hidden Garden concept](../../locations/hidden-garden-clean.md) as a flat side elevation: **five chunks, 10 800 px**, about 70 s of walking plus Dexter's waits and two detours ([docs/04 – Hidden Garden, the prologue level](../../../docs/04-WORLD-AND-LEVELS.md#hidden-garden--the-prologue-level), #50). `hg-4-0` is the approved pond chunk (the jetty where Dexter points, the wrong reflection). The four before it were grown leftwards from it with [grow_stage.py](../../../scripts/grow_stage.py) – scenery first, one masked repaint per route feature, the seams between new chunks repainted (docs/09): `hg-0-0` the gate (Dexter's bench, a birch log), `hg-1-0` the hill (an oak on the crest, a hollow behind the grass), `hg-2-0` the terraces (a shed with a ladder to its roof), `hg-3-0` the boardwalk (a gap with stepping stones). Clean only.\n\n**Layers** (#142): each chunk's depth key comes from the model, its masks from the key ([depth_layers.py](../../../scripts/depth_layers.py), [layers.json](layers.json)). Sky, far and mid scroll at 0.1, 0.25 and 0.6 (docs/04) from plates filled where nearer layers hide them; the foreground at 1.2. The pond keeps its exact rules for path, water and foreground; the grown chunks take every band from their key. **Collision**: walk lines traced from each key's play band (`depth_layers.py collision`), spawns, Dexter's waypoints and triggers placed by hand; overlays in [collision/](collision/).\n\nThe whole walk: [prologue.png](images/prologue.png) · the parallax at three camera positions: [parallax.png](images/parallax.png) (`python scripts/compare.py parallax art/stages/hidden-garden`).",
"model": "gpt-image-2.5-sunburst",
"size": "2160x720",
"style": "FORMAT: a STAGE CONCEPT for a 2D side-scroller explored in all four directions - one wide 2:1 image in strict side view, drawn like an architect's ELEVATION: every path, wall, bed and building runs PARALLEL to the screen from left to right; depth comes only from layers stacked behind each other, never from things receding toward a vanishing point; no path, corridor or bed leads into the distance. The hero is small, about one ninth of the image height. Build real level structure into the scenery: a main route plus at least one higher branch and one lower branch or underground pocket, smooth hills and slopes (some steep enough to slide down), ledges with grabbable edges, something to climb (ladder, vines, scaffold, stairs), a dead end with a small reward glint. DEPTH in clear layers: hazy far background, mid-ground, the play layer, and a dark, out-of-focus foreground of leaves and grass partly covering the bottom edge and corners as if the camera sits behind foliage; reflections where there is water; a few falling leaves. LOOK: detailed hi-bit pixel art, fine 2-pixel grain, the warm palette and light of the style master. TONE: secret horror - calm and pretty first. No text, no logos, no UI.",
@@ -8,6 +8,290 @@
"boards": [
{
"id": "hg-0-0",
+ "title": "Hidden Garden · the gate",
+ "inputs": [
+ "repo:art/locations/images/hidden-garden-clean.png",
+ "repo:art/direction/images/style-master-preview.png"
+ ],
+ "prompt": "The way into the garden: at the left edge an old wrought-iron garden gate stands open in a tall trimmed hedge (x=20-220, up to y=200). A gravel path at y=441 runs across the whole chunk. A wooden park bench with iron legs faces the viewer on the path at x=700-860 (seat at y=405, back up to y=370), under an old lantern post at x=900. Low flowering shrubs and the first reeds behind the path. Near the right edge a fallen birch log lies across the path at x=1700-1830, its top at y=411 (30 px high). Late-afternoon autumn light from the left, golden reeds, orange and yellow trees, a hazy blue conifer line far behind, warm stone and weathered timber, as in the pond chunk next to it. The walkable surface is clearly readable: a lit top edge, darker below.",
+ "factor": 1,
+ "review": "To review (24 Oct, #50): grown leftwards from the pond with grow_stage.py; route feature repainted; seams joined.",
+ "route": {
+ "walk": [
+ [
+ [
+ 0,
+ 441
+ ],
+ [
+ 1700,
+ 441
+ ],
+ [
+ 1700,
+ 411
+ ],
+ [
+ 1830,
+ 411
+ ],
+ [
+ 1830,
+ 441
+ ],
+ [
+ 2160,
+ 441
+ ]
+ ]
+ ],
+ "structure": [
+ [
+ 20,
+ 200,
+ 220,
+ 441
+ ],
+ [
+ 700,
+ 370,
+ 860,
+ 441
+ ]
+ ]
+ },
+ "keyed": false,
+ "colours": 256
+ },
+ {
+ "id": "hg-1-0",
+ "title": "Hidden Garden · the hill",
+ "inputs": [
+ "repo:art/locations/images/hidden-garden-clean.png",
+ "repo:art/direction/images/style-master-preview.png"
+ ],
+ "prompt": "A gravel path at y=441 at both edges rolls over a smooth grassy hill: it starts rising at x=320, reaches the round crest at x=1080, y=331, and comes back down to y=441 at x=1840 - gentle slopes, no steps. An old oak with a wide autumn crown stands on the crest (trunk at x=1040-1120). Under the hill's left flank, behind a curtain of tall golden grass, a small dark hollow opens below the path: its floor at y=521 from x=440 to x=700, its roof the hill itself; the grass curtain hangs from the path's edge. Late-afternoon autumn light from the left, golden reeds, orange and yellow trees, a hazy blue conifer line far behind, warm stone and weathered timber, as in the pond chunk next to it. The walkable surface is clearly readable: a lit top edge, darker below.",
+ "factor": 1,
+ "review": "To review (24 Oct, #50): grown leftwards from the pond with grow_stage.py; route feature repainted; seams joined.",
+ "route": {
+ "walk": [
+ [
+ [
+ 0,
+ 441
+ ],
+ [
+ 320,
+ 441
+ ],
+ [
+ 700,
+ 370
+ ],
+ [
+ 1080,
+ 331
+ ],
+ [
+ 1460,
+ 370
+ ],
+ [
+ 1840,
+ 441
+ ],
+ [
+ 2160,
+ 441
+ ]
+ ],
+ [
+ [
+ 440,
+ 521
+ ],
+ [
+ 700,
+ 521
+ ]
+ ]
+ ],
+ "hollow": [
+ [
+ 440,
+ 450,
+ 700,
+ 521
+ ]
+ ]
+ },
+ "keyed": false,
+ "colours": 256
+ },
+ {
+ "id": "hg-2-0",
+ "title": "Hidden Garden · the terraces",
+ "inputs": [
+ "repo:art/locations/images/hidden-garden-clean.png",
+ "repo:art/direction/images/style-master-preview.png"
+ ],
+ "prompt": "Old stone garden terraces. A gravel path at y=441 from x=0 to x=480. Three stone terrace steps rise to the right, each 40 px high: a step face at x=480 up to y=401, at x=620 up to y=361, at x=760 up to y=321. A flat stone terrace at y=321 runs from x=760 to x=1320, with a small weathered wooden garden shed standing on it from x=930 to x=1140 (walls up from y=321, its flat roof at y=170) and a wooden ladder leaning on the shed's right wall at x=1150-1180 from the terrace up to the roof. On the roof a small glint. Steps go down again on the right, 40 px each: at x=1320 to y=361, at x=1460 to y=401, at x=1600 to y=441, then the gravel path at y=441 to the right edge. Late-afternoon autumn light from the left, golden reeds, orange and yellow trees, a hazy blue conifer line far behind, warm stone and weathered timber, as in the pond chunk next to it. The walkable surface is clearly readable: a lit top edge, darker below.",
+ "factor": 1,
+ "review": "To review (24 Oct, #50): grown leftwards from the pond with grow_stage.py; route feature repainted; seams joined.",
+ "route": {
+ "walk": [
+ [
+ [
+ 0,
+ 441
+ ],
+ [
+ 480,
+ 441
+ ],
+ [
+ 480,
+ 401
+ ],
+ [
+ 620,
+ 401
+ ],
+ [
+ 620,
+ 361
+ ],
+ [
+ 760,
+ 361
+ ],
+ [
+ 760,
+ 321
+ ],
+ [
+ 1320,
+ 321
+ ],
+ [
+ 1320,
+ 361
+ ],
+ [
+ 1460,
+ 361
+ ],
+ [
+ 1460,
+ 401
+ ],
+ [
+ 1600,
+ 401
+ ],
+ [
+ 1600,
+ 441
+ ],
+ [
+ 2160,
+ 441
+ ]
+ ]
+ ],
+ "structure": [
+ [
+ 930,
+ 170,
+ 1140,
+ 321
+ ]
+ ],
+ "ladder": [
+ [
+ 1150,
+ 170,
+ 1180,
+ 321
+ ]
+ ]
+ },
+ "keyed": false,
+ "colours": 256
+ },
+ {
+ "id": "hg-3-0",
+ "title": "Hidden Garden · the boardwalk",
+ "inputs": [
+ "repo:art/locations/images/hidden-garden-clean.png",
+ "repo:art/direction/images/style-master-preview.png"
+ ],
+ "prompt": "A timber boardwalk on low wooden posts runs through tall golden reeds. Its walking surface is at y=441 from x=0 to x=880. At x=880-1000 the boardwalk is broken off: a 120 px gap over dark ditch water (the water surface at y=500). From x=1000 the boardwalk continues at y=441 to x=1500, then a low stone path at y=441 continues to the right edge, where it meets the stone path and the low wall of the existing strip. Three flat stepping stones stand in the ditch below the gap, tops at y=480, at x=900, x=940 and x=975, as a lower way across. Late-afternoon autumn light from the left, golden reeds, orange and yellow trees, a hazy blue conifer line far behind, warm stone and weathered timber, as in the pond chunk next to it. The walkable surface is clearly readable: a lit top edge, darker below.",
+ "factor": 1,
+ "review": "To review (24 Oct, #50): grown leftwards from the pond with grow_stage.py; route feature repainted; seams joined.",
+ "route": {
+ "below": [
+ 478,
+ "water"
+ ],
+ "walk": [
+ [
+ [
+ 0,
+ 441
+ ],
+ [
+ 880,
+ 441
+ ]
+ ],
+ [
+ [
+ 1000,
+ 441
+ ],
+ [
+ 2160,
+ 441
+ ]
+ ],
+ [
+ [
+ 890,
+ 470
+ ],
+ [
+ 925,
+ 470
+ ]
+ ],
+ [
+ [
+ 935,
+ 470
+ ],
+ [
+ 965,
+ 470
+ ]
+ ],
+ [
+ [
+ 972,
+ 470
+ ],
+ [
+ 995,
+ 470
+ ]
+ ]
+ ]
+ },
+ "keyed": false,
+ "colours": 256
+ },
+ {
+ "id": "hg-4-0",
"title": "Hidden Garden · the bench and the pond",
"factor": 1,
"keyed": false,
diff --git a/art/stages/hidden-garden/chunks.json b/art/stages/hidden-garden/chunks.json
index 3439780..6d0d03a 100644
--- a/art/stages/hidden-garden/chunks.json
+++ b/art/stages/hidden-garden/chunks.json
@@ -1,6 +1,14 @@
{
"region": "hidden-garden",
"note": "Chunk ids are grid positions (column-row); art names the plate each state is drawn on.",
- "chunks": {"hg-0-0": "hg-0-0"},
- "art": {"clean": "images/{chunk}.png"}
+ "chunks": {
+ "hg-0-0": "hg-0-0",
+ "hg-1-0": "hg-1-0",
+ "hg-2-0": "hg-2-0",
+ "hg-3-0": "hg-3-0",
+ "hg-4-0": "hg-4-0"
+ },
+ "art": {
+ "clean": "images/{chunk}.png"
+ }
}
diff --git a/art/stages/hidden-garden/collision/hg-0-0.clean.png b/art/stages/hidden-garden/collision/hg-0-0.clean.png
index ca72c44..a437daf 100644
Binary files a/art/stages/hidden-garden/collision/hg-0-0.clean.png and b/art/stages/hidden-garden/collision/hg-0-0.clean.png differ
diff --git a/art/stages/hidden-garden/collision/hg-1-0.clean.png b/art/stages/hidden-garden/collision/hg-1-0.clean.png
new file mode 100644
index 0000000..c8b6c82
Binary files /dev/null and b/art/stages/hidden-garden/collision/hg-1-0.clean.png differ
diff --git a/art/stages/hidden-garden/collision/hg-2-0.clean.png b/art/stages/hidden-garden/collision/hg-2-0.clean.png
new file mode 100644
index 0000000..8461308
Binary files /dev/null and b/art/stages/hidden-garden/collision/hg-2-0.clean.png differ
diff --git a/art/stages/hidden-garden/collision/hg-3-0.clean.png b/art/stages/hidden-garden/collision/hg-3-0.clean.png
new file mode 100644
index 0000000..28a21f1
Binary files /dev/null and b/art/stages/hidden-garden/collision/hg-3-0.clean.png differ
diff --git a/art/stages/hidden-garden/collision/hg-4-0.clean.png b/art/stages/hidden-garden/collision/hg-4-0.clean.png
new file mode 100644
index 0000000..9a0405e
Binary files /dev/null and b/art/stages/hidden-garden/collision/hg-4-0.clean.png differ
diff --git a/art/stages/hidden-garden/export.json b/art/stages/hidden-garden/export.json
index a627632..6aaf1bb 100644
--- a/art/stages/hidden-garden/export.json
+++ b/art/stages/hidden-garden/export.json
@@ -1,15 +1,85 @@
{
- "note": "Layers from split_layers.py (layers.json) and depth_layers.py (#142). Sky, far and mid are drawn from their filled plates: the art's own pixels plus what the nearer layers hide, so they can scroll slower (docs/04) without holes. At rest the stack still rebuilds the art exactly (--verify).",
- "chunks": {"hg-0-0": {"x": 0, "y": 0}},
+ "note": "Layers from split_layers.py (layers.json) and depth_layers.py (#142). Sky, far and mid scroll slower than the path, so each is ONE region-wide plate quilted from the chunks' filled plates (depth_layers.py region): per chunk they would overlap by 2160 x (1 - scroll) and cut each other off. They come in chunk-wide tiles, tile j carried by chunk j (any loader that places a layer at its chunk's origin then shows the right part); --verify rebuilds every chunk at rest from its own plates ('rest', repo-relative; the filled ones are intermediates in tmp/). The mid layer carries the earlier chunk's trees into columns where the next has none ('carry'); the far line may end on open sky.",
+ "chunks": {
+ "hg-0-0": {
+ "x": 0,
+ "y": 0
+ },
+ "hg-1-0": {
+ "x": 1,
+ "y": 0
+ },
+ "hg-2-0": {
+ "x": 2,
+ "y": 0
+ },
+ "hg-3-0": {
+ "x": 3,
+ "y": 0
+ },
+ "hg-4-0": {
+ "x": 4,
+ "y": 0
+ }
+ },
"layers": {
- "00-sky": {"file": "layers/{name}.00-sky.filled.png", "z": 0, "scroll": 0.1},
- "01-far": {"file": "layers/{name}.01-far.filled.png", "z": 1, "scroll": 0.25},
- "02-mid": {"file": "layers/{name}.02-mid.filled.png", "z": 2, "scroll": 0.6},
- "03-rear": {"file": "layers/{name}.03-rear.png", "z": 3, "scroll": 1.0},
- "04-water": {"file": "layers/{name}.04-water.png", "z": 4, "scroll": 1.0},
- "05-play": {"file": "layers/{name}.05-play.png", "z": 5, "scroll": 1.0},
- "07-foreground": {"file": "layers/{name}.07-foreground.png", "z": 7, "scroll": 1.2}
+ "00-sky": {
+ "file": "layers/hidden-garden.00-sky.{tile}.png",
+ "z": 0,
+ "scroll": 0.1,
+ "region": true,
+ "rest": "tmp/boards/hidden-garden/filled/{name}.00-sky.filled.png"
+ },
+ "01-far": {
+ "file": "layers/hidden-garden.01-far.{tile}.png",
+ "z": 1,
+ "scroll": 0.25,
+ "region": true,
+ "rest": "tmp/boards/hidden-garden/filled/{name}.01-far.filled.png"
+ },
+ "02-mid": {
+ "file": "layers/hidden-garden.02-mid.{tile}.png",
+ "z": 2,
+ "scroll": 0.6,
+ "region": true,
+ "rest": "tmp/boards/hidden-garden/filled/{name}.02-mid.filled.png",
+ "carry": true
+ },
+ "03-rear": {
+ "file": "layers/{name}.03-rear.png",
+ "z": 3,
+ "scroll": 1.0
+ },
+ "04-water": {
+ "file": "layers/{name}.04-water.png",
+ "z": 4,
+ "scroll": 1.0
+ },
+ "05-play": {
+ "file": "layers/{name}.05-play.png",
+ "z": 5,
+ "scroll": 1.0
+ },
+ "07-foreground": {
+ "file": "layers/hidden-garden.07-foreground.{tile}.png",
+ "z": 7,
+ "scroll": 1.2,
+ "region": true,
+ "rest": "art/stages/hidden-garden/layers/{name}.07-foreground.png"
+ }
+ },
+ "states": {
+ "clean": [
+ "00-sky",
+ "01-far",
+ "02-mid",
+ "03-rear",
+ "04-water",
+ "05-play",
+ "07-foreground"
+ ]
},
- "states": {"clean": ["00-sky", "01-far", "02-mid", "03-rear", "04-water", "05-play", "07-foreground"]},
- "composed": {"clean": "images/{name}.png"}
+ "composed": {
+ "clean": "images/{name}.png"
+ }
}
diff --git a/art/stages/hidden-garden/hg-0-0.clean.collision.json b/art/stages/hidden-garden/hg-0-0.clean.collision.json
index 1d8c525..022aba8 100644
--- a/art/stages/hidden-garden/hg-0-0.clean.collision.json
+++ b/art/stages/hidden-garden/hg-0-0.clean.collision.json
@@ -9,11 +9,43 @@
[
[
0,
- 441
+ 466
],
[
- 1323,
- 441
+ 240,
+ 452
+ ],
+ [
+ 440,
+ 470
+ ],
+ [
+ 652,
+ 470
+ ],
+ [
+ 700,
+ 431
+ ],
+ [
+ 960,
+ 431
+ ],
+ [
+ 1008,
+ 470
+ ],
+ [
+ 1550,
+ 474
+ ],
+ [
+ 1736,
+ 386
+ ],
+ [
+ 2160,
+ 384
]
]
],
@@ -26,83 +58,50 @@
"y": 0,
"w": 8,
"h": 720
- },
- {
- "x": 1326,
- "y": 300,
- "w": 24,
- "h": 141
}
],
"noGo": [
{
"x": 0,
- "y": 478,
+ "y": 482,
"w": 2160,
- "h": 242
+ "h": 238
}
],
"hazard": [],
"interact": [
- {
- "id": "pond-edge",
- "kind": "anomaly",
- "x": 1250,
- "y": 365,
- "w": 70,
- "h": 76,
- "note": "Dexter points at the ground here, then shoves the hero into the pond (M3-S3 #50, M3-S4 #51)"
- },
- {
- "id": "to-rusty-creek",
- "kind": "chunk-exit",
- "x": 1250,
- "y": 365,
- "w": 70,
- "h": 76,
- "note": "the crossing: after the shove the hero lands in clean Rusty Creek (rc-0-0)"
- },
{
"id": "dexter-waypoint-1",
"kind": "dexter-waypoint",
- "x": 215,
- "y": 365,
+ "x": 760,
+ "y": 355,
"w": 80,
"h": 76,
- "note": "the bench: Dexter waits here; walking up makes him hop off"
+ "note": "the bench: walking up makes Dexter hop off and waddle ahead"
},
{
"id": "dexter-waypoint-2",
"kind": "dexter-waypoint",
- "x": 680,
- "y": 365,
- "w": 80,
- "h": 76,
- "note": "mid-path: Dexter hops ahead and beckons"
- },
- {
- "id": "dexter-waypoint-3",
- "kind": "dexter-waypoint",
- "x": 1150,
- "y": 365,
+ "x": 1500,
+ "y": 398,
"w": 80,
"h": 76,
- "note": "the jetty: Dexter stops and points at the water"
+ "note": "at the fallen birch: Dexter hops onto it and looks back"
}
],
"spawn": [
{
"id": "player",
"kind": "player",
- "x": 90,
- "y": 441
+ "x": 120,
+ "y": 460
},
{
"id": "dexter-bench",
"kind": "dexter",
- "x": 255,
- "y": 421,
- "note": "Dexter sits on the bench at the start"
+ "x": 800,
+ "y": 405,
+ "note": "Dexter sits on the bench at the start (#50)"
}
],
"ledge": [],
diff --git a/art/stages/hidden-garden/hg-0-0.md b/art/stages/hidden-garden/hg-0-0.md
index daf32fb..3825ee6 100644
--- a/art/stages/hidden-garden/hg-0-0.md
+++ b/art/stages/hidden-garden/hg-0-0.md
@@ -1,12 +1,12 @@
-# Hidden Garden · the bench and the pond
+# Hidden Garden · the gate
-
+
-**Review:** 🟡 Drawn 24 Oct, waiting for Alexander's Design review. The figures of the concept are removed; the bench and the wrong reflection were painted in afterwards with masked edits that change only their own rectangles.
+**Review:** To review (24 Oct, #50): grown leftwards from the pond with grow_stage.py; route feature repainted; seams joined.
## Brief
-Wide side-scrolling stage chunk at 3:1, drawn as an architect's elevation with everything parallel to the screen (no vanishing point, no receding paths): the Hidden Garden pond from the first image, redrawn as a flat side view. Keep its look exactly - late-afternoon autumn light, golden reeds, orange and yellow trees, the misty pine forest behind, a blue sky with small clouds, the big tree in the upper right. Ground: from the left edge a stone-slab path at ground level, its top at about two thirds of the height, runs right onto a timber jetty over the pond; the jetty ends a little right of the middle with two posts - the spot where someone would stand and look down into the water. The calm pond fills the bottom across the whole width and reflects the reeds and trees. ONE patch of the reflection just below the end of the jetty is WRONG: it shows a rusty blue-green riveted girder structure that does not exist above the water. On the right, reeds and a low mossy stone wall close the scene. Along the very bottom edge, a few dark out-of-focus reed silhouettes in the foreground. Remove the two figures of the first image: NO people, NO animals, NO plush toy, nothing standing on the jetty.
+The way into the garden: at the left edge an old wrought-iron garden gate stands open in a tall trimmed hedge (x=20-220, up to y=200). A gravel path at y=441 runs across the whole chunk. A wooden park bench with iron legs faces the viewer on the path at x=700-860 (seat at y=405, back up to y=370), under an old lantern post at x=900. Low flowering shrubs and the first reeds behind the path. Near the right edge a fallen birch log lies across the path at x=1700-1830, its top at y=411 (30 px high). Late-afternoon autumn light from the left, golden reeds, orange and yellow trees, a hazy blue conifer line far behind, warm stone and weathered timber, as in the pond chunk next to it. The walkable surface is clearly readable: a lit top edge, darker below.
## Inputs
diff --git a/art/stages/hidden-garden/hg-1-0.clean.collision.json b/art/stages/hidden-garden/hg-1-0.clean.collision.json
new file mode 100644
index 0000000..0016d5f
--- /dev/null
+++ b/art/stages/hidden-garden/hg-1-0.clean.collision.json
@@ -0,0 +1,204 @@
+{
+ "chunk": "hg-1-0",
+ "state": "clean",
+ "size": [
+ 2160,
+ 720
+ ],
+ "walk": [
+ [
+ [
+ 0,
+ 384
+ ],
+ [
+ 300,
+ 336
+ ],
+ [
+ 407,
+ 312
+ ],
+ [
+ 470,
+ 301
+ ]
+ ],
+ [
+ [
+ 640,
+ 268
+ ],
+ [
+ 735,
+ 248
+ ],
+ [
+ 829,
+ 233
+ ],
+ [
+ 946,
+ 222
+ ],
+ [
+ 980,
+ 213
+ ],
+ [
+ 1040,
+ 203
+ ],
+ [
+ 1059,
+ 218
+ ],
+ [
+ 1075,
+ 219
+ ],
+ [
+ 1090,
+ 208
+ ],
+ [
+ 1115,
+ 219
+ ],
+ [
+ 1137,
+ 209
+ ],
+ [
+ 1194,
+ 223
+ ],
+ [
+ 1234,
+ 223
+ ],
+ [
+ 1255,
+ 231
+ ],
+ [
+ 1327,
+ 247
+ ],
+ [
+ 1362,
+ 251
+ ],
+ [
+ 1481,
+ 277
+ ],
+ [
+ 1701,
+ 339
+ ],
+ [
+ 1807,
+ 364
+ ],
+ [
+ 1918,
+ 379
+ ],
+ [
+ 1942,
+ 385
+ ],
+ [
+ 2081,
+ 386
+ ],
+ [
+ 2104,
+ 389
+ ],
+ [
+ 2160,
+ 387
+ ]
+ ],
+ [
+ [
+ 480,
+ 449
+ ],
+ [
+ 628,
+ 449
+ ]
+ ]
+ ],
+ "slide": [],
+ "oneWay": [
+ [
+ [
+ 470,
+ 301
+ ],
+ [
+ 556,
+ 286
+ ],
+ [
+ 640,
+ 268
+ ]
+ ]
+ ],
+ "climb": [
+ {
+ "id": "hollow-grass",
+ "kind": "vines",
+ "x": 472,
+ "y": 297,
+ "w": 24,
+ "h": 152,
+ "note": "the grass curtain: climb back out of the hollow"
+ }
+ ],
+ "solid": [],
+ "noGo": [
+ {
+ "x": 0,
+ "y": 123,
+ "w": 2160,
+ "h": 597
+ }
+ ],
+ "hazard": [],
+ "interact": [
+ {
+ "id": "dexter-waypoint-3",
+ "kind": "dexter-waypoint",
+ "x": 1040,
+ "y": 139,
+ "w": 80,
+ "h": 76,
+ "note": "the oak on the crest: Dexter waits and looks back"
+ },
+ {
+ "id": "hollow",
+ "kind": "secret",
+ "x": 480,
+ "y": 373,
+ "w": 148,
+ "h": 76,
+ "note": "behind the grass: a muffin (#50 'secrets behind grass')"
+ }
+ ],
+ "spawn": [
+ {
+ "id": "muffin-hollow",
+ "kind": "item",
+ "x": 560,
+ "y": 449
+ }
+ ],
+ "ledge": [],
+ "glideLift": []
+}
diff --git a/art/stages/hidden-garden/hg-1-0.md b/art/stages/hidden-garden/hg-1-0.md
new file mode 100644
index 0000000..ced9668
--- /dev/null
+++ b/art/stages/hidden-garden/hg-1-0.md
@@ -0,0 +1,16 @@
+# Hidden Garden · the hill
+
+
+
+**Review:** To review (24 Oct, #50): grown leftwards from the pond with grow_stage.py; route feature repainted; seams joined.
+
+## Brief
+
+A gravel path at y=441 at both edges rolls over a smooth grassy hill: it starts rising at x=320, reaches the round crest at x=1080, y=331, and comes back down to y=441 at x=1840 - gentle slopes, no steps. An old oak with a wide autumn crown stands on the crest (trunk at x=1040-1120). Under the hill's left flank, behind a curtain of tall golden grass, a small dark hollow opens below the path: its floor at y=521 from x=440 to x=700, its roof the hill itself; the grass curtain hangs from the path's edge. Late-afternoon autumn light from the left, golden reeds, orange and yellow trees, a hazy blue conifer line far behind, warm stone and weathered timber, as in the pond chunk next to it. The walkable surface is clearly readable: a lit top edge, darker below.
+
+## Inputs
+
+- [hidden-garden-clean.png](../../locations/images/hidden-garden-clean.png)
+- [style-master-preview.png](../../direction/images/style-master-preview.png)
+
+Model `gpt-image-2.5-sunburst` · pixel factor 1 · source image `images/hg-1-0.png` · rebuild: `python scripts/make_boards.py draw hg-1-0 --redo --config art/stages/hidden-garden/boards.json`
diff --git a/art/stages/hidden-garden/hg-2-0.clean.collision.json b/art/stages/hidden-garden/hg-2-0.clean.collision.json
new file mode 100644
index 0000000..b2a8f04
--- /dev/null
+++ b/art/stages/hidden-garden/hg-2-0.clean.collision.json
@@ -0,0 +1,122 @@
+{
+ "chunk": "hg-2-0",
+ "state": "clean",
+ "size": [
+ 2160,
+ 720
+ ],
+ "walk": [
+ [
+ [
+ 0,
+ 387
+ ],
+ [
+ 530,
+ 387
+ ],
+ [
+ 562,
+ 363
+ ],
+ [
+ 638,
+ 363
+ ],
+ [
+ 678,
+ 333
+ ],
+ [
+ 745,
+ 333
+ ],
+ [
+ 781,
+ 304
+ ],
+ [
+ 1440,
+ 304
+ ],
+ [
+ 1476,
+ 333
+ ],
+ [
+ 1545,
+ 333
+ ],
+ [
+ 1581,
+ 363
+ ],
+ [
+ 1640,
+ 363
+ ],
+ [
+ 1672,
+ 387
+ ],
+ [
+ 2160,
+ 387
+ ]
+ ],
+ [
+ [
+ 920,
+ 146
+ ],
+ [
+ 1262,
+ 146
+ ]
+ ]
+ ],
+ "slide": [],
+ "oneWay": [],
+ "climb": [
+ {
+ "id": "shed-ladder",
+ "kind": "ladder",
+ "x": 1240,
+ "y": 133,
+ "w": 47,
+ "h": 171
+ }
+ ],
+ "solid": [],
+ "noGo": [
+ {
+ "x": 0,
+ "y": 482,
+ "w": 2160,
+ "h": 238
+ }
+ ],
+ "hazard": [],
+ "interact": [
+ {
+ "id": "dexter-waypoint-4",
+ "kind": "dexter-waypoint",
+ "x": 1500,
+ "y": 228,
+ "w": 80,
+ "h": 76,
+ "note": "the top of the terrace: Dexter points up at the roof, then hops on"
+ }
+ ],
+ "spawn": [
+ {
+ "id": "glint-roof",
+ "kind": "item",
+ "x": 1207,
+ "y": 146,
+ "note": "the glint on the shed roof"
+ }
+ ],
+ "ledge": [],
+ "glideLift": []
+}
diff --git a/art/stages/hidden-garden/hg-2-0.md b/art/stages/hidden-garden/hg-2-0.md
new file mode 100644
index 0000000..33086ea
--- /dev/null
+++ b/art/stages/hidden-garden/hg-2-0.md
@@ -0,0 +1,16 @@
+# Hidden Garden · the terraces
+
+
+
+**Review:** To review (24 Oct, #50): grown leftwards from the pond with grow_stage.py; route feature repainted; seams joined.
+
+## Brief
+
+Old stone garden terraces. A gravel path at y=441 from x=0 to x=480. Three stone terrace steps rise to the right, each 40 px high: a step face at x=480 up to y=401, at x=620 up to y=361, at x=760 up to y=321. A flat stone terrace at y=321 runs from x=760 to x=1320, with a small weathered wooden garden shed standing on it from x=930 to x=1140 (walls up from y=321, its flat roof at y=170) and a wooden ladder leaning on the shed's right wall at x=1150-1180 from the terrace up to the roof. On the roof a small glint. Steps go down again on the right, 40 px each: at x=1320 to y=361, at x=1460 to y=401, at x=1600 to y=441, then the gravel path at y=441 to the right edge. Late-afternoon autumn light from the left, golden reeds, orange and yellow trees, a hazy blue conifer line far behind, warm stone and weathered timber, as in the pond chunk next to it. The walkable surface is clearly readable: a lit top edge, darker below.
+
+## Inputs
+
+- [hidden-garden-clean.png](../../locations/images/hidden-garden-clean.png)
+- [style-master-preview.png](../../direction/images/style-master-preview.png)
+
+Model `gpt-image-2.5-sunburst` · pixel factor 1 · source image `images/hg-2-0.png` · rebuild: `python scripts/make_boards.py draw hg-2-0 --redo --config art/stages/hidden-garden/boards.json`
diff --git a/art/stages/hidden-garden/hg-3-0.clean.collision.json b/art/stages/hidden-garden/hg-3-0.clean.collision.json
new file mode 100644
index 0000000..a33f154
--- /dev/null
+++ b/art/stages/hidden-garden/hg-3-0.clean.collision.json
@@ -0,0 +1,137 @@
+{
+ "chunk": "hg-3-0",
+ "state": "clean",
+ "size": [
+ 2160,
+ 720
+ ],
+ "walk": [
+ [
+ [
+ 0,
+ 390
+ ],
+ [
+ 294,
+ 390
+ ]
+ ],
+ [
+ [
+ 362,
+ 390
+ ],
+ [
+ 577,
+ 393
+ ],
+ [
+ 627,
+ 390
+ ],
+ [
+ 832,
+ 389
+ ]
+ ],
+ [
+ [
+ 911,
+ 462
+ ],
+ [
+ 922,
+ 459
+ ],
+ [
+ 961,
+ 458
+ ],
+ [
+ 978,
+ 461
+ ]
+ ],
+ [
+ [
+ 1039,
+ 468
+ ],
+ [
+ 1044,
+ 464
+ ],
+ [
+ 1075,
+ 461
+ ]
+ ],
+ [
+ [
+ 1079,
+ 393
+ ],
+ [
+ 1224,
+ 392
+ ],
+ [
+ 1284,
+ 440
+ ],
+ [
+ 1332,
+ 437
+ ],
+ [
+ 1385,
+ 393
+ ],
+ [
+ 1818,
+ 391
+ ],
+ [
+ 1989,
+ 392
+ ]
+ ],
+ [
+ [
+ 2027,
+ 392
+ ],
+ [
+ 2160,
+ 391
+ ]
+ ]
+ ],
+ "slide": [],
+ "oneWay": [],
+ "climb": [],
+ "solid": [],
+ "noGo": [
+ {
+ "x": 0,
+ "y": 482,
+ "w": 2160,
+ "h": 238
+ }
+ ],
+ "hazard": [],
+ "interact": [
+ {
+ "id": "dexter-waypoint-5",
+ "kind": "dexter-waypoint",
+ "x": 1100,
+ "y": 320,
+ "w": 80,
+ "h": 76,
+ "note": "past the gap: Dexter waits on the far side, hopping on the spot"
+ }
+ ],
+ "spawn": [],
+ "ledge": [],
+ "glideLift": []
+}
diff --git a/art/stages/hidden-garden/hg-3-0.md b/art/stages/hidden-garden/hg-3-0.md
new file mode 100644
index 0000000..85cb5a4
--- /dev/null
+++ b/art/stages/hidden-garden/hg-3-0.md
@@ -0,0 +1,16 @@
+# Hidden Garden · the boardwalk
+
+
+
+**Review:** To review (24 Oct, #50): grown leftwards from the pond with grow_stage.py; route feature repainted; seams joined.
+
+## Brief
+
+A timber boardwalk on low wooden posts runs through tall golden reeds. Its walking surface is at y=441 from x=0 to x=880. At x=880-1000 the boardwalk is broken off: a 120 px gap over dark ditch water (the water surface at y=500). From x=1000 the boardwalk continues at y=441 to x=1500, then a low stone path at y=441 continues to the right edge, where it meets the stone path and the low wall of the existing strip. Three flat stepping stones stand in the ditch below the gap, tops at y=480, at x=900, x=940 and x=975, as a lower way across. Late-afternoon autumn light from the left, golden reeds, orange and yellow trees, a hazy blue conifer line far behind, warm stone and weathered timber, as in the pond chunk next to it. The walkable surface is clearly readable: a lit top edge, darker below.
+
+## Inputs
+
+- [hidden-garden-clean.png](../../locations/images/hidden-garden-clean.png)
+- [style-master-preview.png](../../direction/images/style-master-preview.png)
+
+Model `gpt-image-2.5-sunburst` · pixel factor 1 · source image `images/hg-3-0.png` · rebuild: `python scripts/make_boards.py draw hg-3-0 --redo --config art/stages/hidden-garden/boards.json`
diff --git a/art/stages/hidden-garden/hg-4-0.clean.collision.json b/art/stages/hidden-garden/hg-4-0.clean.collision.json
new file mode 100644
index 0000000..27383f8
--- /dev/null
+++ b/art/stages/hidden-garden/hg-4-0.clean.collision.json
@@ -0,0 +1,100 @@
+{
+ "chunk": "hg-4-0",
+ "state": "clean",
+ "size": [
+ 2160,
+ 720
+ ],
+ "walk": [
+ [
+ [
+ 0,
+ 392
+ ],
+ [
+ 160,
+ 392
+ ]
+ ],
+ [
+ [
+ 160,
+ 441
+ ],
+ [
+ 1323,
+ 441
+ ]
+ ]
+ ],
+ "slide": [],
+ "oneWay": [],
+ "climb": [],
+ "solid": [
+ {
+ "x": 1326,
+ "y": 300,
+ "w": 24,
+ "h": 141
+ }
+ ],
+ "noGo": [
+ {
+ "x": 0,
+ "y": 478,
+ "w": 2160,
+ "h": 242
+ }
+ ],
+ "hazard": [],
+ "interact": [
+ {
+ "id": "pond-edge",
+ "kind": "anomaly",
+ "x": 1250,
+ "y": 365,
+ "w": 70,
+ "h": 76,
+ "note": "Dexter points at the ground here, then shoves the hero into the pond (M3-S3 #50, M3-S4 #51)"
+ },
+ {
+ "id": "to-rusty-creek",
+ "kind": "chunk-exit",
+ "x": 1250,
+ "y": 365,
+ "w": 70,
+ "h": 76,
+ "note": "the crossing: after the shove the hero lands in clean Rusty Creek (rc-0-0)"
+ },
+ {
+ "id": "dexter-waypoint-pond-1",
+ "kind": "dexter-waypoint",
+ "x": 215,
+ "y": 365,
+ "w": 80,
+ "h": 76,
+ "note": "the old bench by the pond: Dexter waits here and looks back"
+ },
+ {
+ "id": "dexter-waypoint-pond-2",
+ "kind": "dexter-waypoint",
+ "x": 680,
+ "y": 365,
+ "w": 80,
+ "h": 76,
+ "note": "mid-path: Dexter hops ahead and beckons"
+ },
+ {
+ "id": "dexter-waypoint-pond-3",
+ "kind": "dexter-waypoint",
+ "x": 1150,
+ "y": 365,
+ "w": 80,
+ "h": 76,
+ "note": "the jetty: Dexter stops and points at the water"
+ }
+ ],
+ "spawn": [],
+ "ledge": [],
+ "glideLift": []
+}
diff --git a/art/stages/hidden-garden/hg-4-0.md b/art/stages/hidden-garden/hg-4-0.md
new file mode 100644
index 0000000..17a97b6
--- /dev/null
+++ b/art/stages/hidden-garden/hg-4-0.md
@@ -0,0 +1,16 @@
+# Hidden Garden · the bench and the pond
+
+
+
+**Review:** 🟡 Drawn 24 Oct, waiting for Alexander's Design review. The figures of the concept are removed; the bench and the wrong reflection were painted in afterwards with masked edits that change only their own rectangles.
+
+## Brief
+
+Wide side-scrolling stage chunk at 3:1, drawn as an architect's elevation with everything parallel to the screen (no vanishing point, no receding paths): the Hidden Garden pond from the first image, redrawn as a flat side view. Keep its look exactly - late-afternoon autumn light, golden reeds, orange and yellow trees, the misty pine forest behind, a blue sky with small clouds, the big tree in the upper right. Ground: from the left edge a stone-slab path at ground level, its top at about two thirds of the height, runs right onto a timber jetty over the pond; the jetty ends a little right of the middle with two posts - the spot where someone would stand and look down into the water. The calm pond fills the bottom across the whole width and reflects the reeds and trees. ONE patch of the reflection just below the end of the jetty is WRONG: it shows a rusty blue-green riveted girder structure that does not exist above the water. On the right, reeds and a low mossy stone wall close the scene. Along the very bottom edge, a few dark out-of-focus reed silhouettes in the foreground. Remove the two figures of the first image: NO people, NO animals, NO plush toy, nothing standing on the jetty.
+
+## Inputs
+
+- [hidden-garden-clean.png](../../locations/images/hidden-garden-clean.png)
+- [style-master-preview.png](../../direction/images/style-master-preview.png)
+
+Model `gpt-image-2.5-sunburst` · pixel factor 1 · source image `images/hg-4-0.png` · rebuild: `python scripts/make_boards.py draw hg-4-0 --redo --config art/stages/hidden-garden/boards.json`
diff --git a/art/stages/hidden-garden/images/hg-0-0-parallax.png b/art/stages/hidden-garden/images/hg-0-0-parallax.png
deleted file mode 100644
index 3bbc4c7..0000000
Binary files a/art/stages/hidden-garden/images/hg-0-0-parallax.png and /dev/null differ
diff --git a/art/stages/hidden-garden/images/hg-0-0-preview.png b/art/stages/hidden-garden/images/hg-0-0-preview.png
index 3ba2c01..3753068 100644
Binary files a/art/stages/hidden-garden/images/hg-0-0-preview.png and b/art/stages/hidden-garden/images/hg-0-0-preview.png differ
diff --git a/art/stages/hidden-garden/images/hg-0-0.png b/art/stages/hidden-garden/images/hg-0-0.png
index 585a48c..fa68127 100644
Binary files a/art/stages/hidden-garden/images/hg-0-0.png and b/art/stages/hidden-garden/images/hg-0-0.png differ
diff --git a/art/stages/hidden-garden/images/hg-1-0-preview.png b/art/stages/hidden-garden/images/hg-1-0-preview.png
new file mode 100644
index 0000000..d97dec8
Binary files /dev/null and b/art/stages/hidden-garden/images/hg-1-0-preview.png differ
diff --git a/art/stages/hidden-garden/images/hg-1-0.png b/art/stages/hidden-garden/images/hg-1-0.png
new file mode 100644
index 0000000..a42e5a6
Binary files /dev/null and b/art/stages/hidden-garden/images/hg-1-0.png differ
diff --git a/art/stages/hidden-garden/images/hg-2-0-preview.png b/art/stages/hidden-garden/images/hg-2-0-preview.png
new file mode 100644
index 0000000..45fcfd4
Binary files /dev/null and b/art/stages/hidden-garden/images/hg-2-0-preview.png differ
diff --git a/art/stages/hidden-garden/images/hg-2-0.png b/art/stages/hidden-garden/images/hg-2-0.png
new file mode 100644
index 0000000..f70a260
Binary files /dev/null and b/art/stages/hidden-garden/images/hg-2-0.png differ
diff --git a/art/stages/hidden-garden/images/hg-3-0-preview.png b/art/stages/hidden-garden/images/hg-3-0-preview.png
new file mode 100644
index 0000000..f95c75d
Binary files /dev/null and b/art/stages/hidden-garden/images/hg-3-0-preview.png differ
diff --git a/art/stages/hidden-garden/images/hg-3-0.png b/art/stages/hidden-garden/images/hg-3-0.png
new file mode 100644
index 0000000..cb798ec
Binary files /dev/null and b/art/stages/hidden-garden/images/hg-3-0.png differ
diff --git a/art/stages/hidden-garden/images/hg-4-0-preview.png b/art/stages/hidden-garden/images/hg-4-0-preview.png
new file mode 100644
index 0000000..3ba2c01
Binary files /dev/null and b/art/stages/hidden-garden/images/hg-4-0-preview.png differ
diff --git a/art/stages/hidden-garden/images/hg-4-0.png b/art/stages/hidden-garden/images/hg-4-0.png
new file mode 100644
index 0000000..585a48c
Binary files /dev/null and b/art/stages/hidden-garden/images/hg-4-0.png differ
diff --git a/art/stages/hidden-garden/images/parallax.png b/art/stages/hidden-garden/images/parallax.png
new file mode 100644
index 0000000..d8a7f57
Binary files /dev/null and b/art/stages/hidden-garden/images/parallax.png differ
diff --git a/art/stages/hidden-garden/images/prologue.png b/art/stages/hidden-garden/images/prologue.png
new file mode 100644
index 0000000..3b3c2e9
Binary files /dev/null and b/art/stages/hidden-garden/images/prologue.png differ
diff --git a/art/stages/hidden-garden/layers.json b/art/stages/hidden-garden/layers.json
index ce38135..af48773 100644
--- a/art/stages/hidden-garden/layers.json
+++ b/art/stages/hidden-garden/layers.json
@@ -1,5 +1,5 @@
{
- "note": "Rules run in order; the last rule that claims a pixel wins. Sky, far and mid come from masks of the model's depth key (scripts/depth_layers.py, #142); the exact rectangles and colours for water, path and foreground follow and win.",
+ "note": "Rules run in order; the last rule that claims a pixel wins. Sky, far and mid come from masks of the model's depth key (scripts/depth_layers.py, #142); the exact rectangles and colours for water, path and foreground follow and win. The grown chunks hg-0-0 to hg-3-0 take every band from their key's masks (masks --all); the pond keeps its exact rules for path, water and foreground.",
"chunks": {
"hg-0-0": {
"states": {
@@ -19,6 +19,134 @@
"layer": "02-mid",
"alpha": "masks/hg-0-0.02-mid.png"
},
+ {
+ "layer": "04-water",
+ "alpha": "masks/hg-0-0.04-water.png"
+ },
+ {
+ "layer": "05-play",
+ "alpha": "masks/hg-0-0.05-play.png"
+ },
+ {
+ "layer": "07-foreground",
+ "alpha": "masks/hg-0-0.07-foreground.png"
+ }
+ ]
+ },
+ "hg-1-0": {
+ "states": {
+ "clean": "images/hg-1-0.png"
+ },
+ "default": "03-rear",
+ "rules": [
+ {
+ "layer": "00-sky",
+ "alpha": "masks/hg-1-0.00-sky.png"
+ },
+ {
+ "layer": "01-far",
+ "alpha": "masks/hg-1-0.01-far.png"
+ },
+ {
+ "layer": "02-mid",
+ "alpha": "masks/hg-1-0.02-mid.png"
+ },
+ {
+ "layer": "04-water",
+ "alpha": "masks/hg-1-0.04-water.png"
+ },
+ {
+ "layer": "05-play",
+ "alpha": "masks/hg-1-0.05-play.png"
+ },
+ {
+ "layer": "07-foreground",
+ "alpha": "masks/hg-1-0.07-foreground.png"
+ }
+ ]
+ },
+ "hg-2-0": {
+ "states": {
+ "clean": "images/hg-2-0.png"
+ },
+ "default": "03-rear",
+ "rules": [
+ {
+ "layer": "00-sky",
+ "alpha": "masks/hg-2-0.00-sky.png"
+ },
+ {
+ "layer": "01-far",
+ "alpha": "masks/hg-2-0.01-far.png"
+ },
+ {
+ "layer": "02-mid",
+ "alpha": "masks/hg-2-0.02-mid.png"
+ },
+ {
+ "layer": "04-water",
+ "alpha": "masks/hg-2-0.04-water.png"
+ },
+ {
+ "layer": "05-play",
+ "alpha": "masks/hg-2-0.05-play.png"
+ },
+ {
+ "layer": "07-foreground",
+ "alpha": "masks/hg-2-0.07-foreground.png"
+ }
+ ]
+ },
+ "hg-3-0": {
+ "states": {
+ "clean": "images/hg-3-0.png"
+ },
+ "default": "03-rear",
+ "rules": [
+ {
+ "layer": "00-sky",
+ "alpha": "masks/hg-3-0.00-sky.png"
+ },
+ {
+ "layer": "01-far",
+ "alpha": "masks/hg-3-0.01-far.png"
+ },
+ {
+ "layer": "02-mid",
+ "alpha": "masks/hg-3-0.02-mid.png"
+ },
+ {
+ "layer": "04-water",
+ "alpha": "masks/hg-3-0.04-water.png"
+ },
+ {
+ "layer": "05-play",
+ "alpha": "masks/hg-3-0.05-play.png"
+ },
+ {
+ "layer": "07-foreground",
+ "alpha": "masks/hg-3-0.07-foreground.png"
+ }
+ ]
+ },
+ "hg-4-0": {
+ "states": {
+ "clean": "images/hg-4-0.png"
+ },
+ "default": "03-rear",
+ "rules": [
+ {
+ "layer": "00-sky",
+ "alpha": "masks/hg-4-0.00-sky.png"
+ },
+ {
+ "layer": "01-far",
+ "alpha": "masks/hg-4-0.01-far.png"
+ },
+ {
+ "layer": "02-mid",
+ "alpha": "masks/hg-4-0.02-mid.png"
+ },
{
"layer": "04-water",
"rect": [
diff --git a/art/stages/hidden-garden/layers/hg-0-0.00-sky.filled.png b/art/stages/hidden-garden/layers/hg-0-0.00-sky.filled.png
deleted file mode 100644
index eb139ba..0000000
Binary files a/art/stages/hidden-garden/layers/hg-0-0.00-sky.filled.png and /dev/null differ
diff --git a/art/stages/hidden-garden/layers/hg-0-0.00-sky.png b/art/stages/hidden-garden/layers/hg-0-0.00-sky.png
index 5e2b8e3..eecdce2 100644
Binary files a/art/stages/hidden-garden/layers/hg-0-0.00-sky.png and b/art/stages/hidden-garden/layers/hg-0-0.00-sky.png differ
diff --git a/art/stages/hidden-garden/layers/hg-0-0.01-far.filled.png b/art/stages/hidden-garden/layers/hg-0-0.01-far.filled.png
deleted file mode 100644
index 85e707d..0000000
Binary files a/art/stages/hidden-garden/layers/hg-0-0.01-far.filled.png and /dev/null differ
diff --git a/art/stages/hidden-garden/layers/hg-0-0.01-far.png b/art/stages/hidden-garden/layers/hg-0-0.01-far.png
index 36c4ff4..ca80fa2 100644
Binary files a/art/stages/hidden-garden/layers/hg-0-0.01-far.png and b/art/stages/hidden-garden/layers/hg-0-0.01-far.png differ
diff --git a/art/stages/hidden-garden/layers/hg-0-0.02-mid.filled.png b/art/stages/hidden-garden/layers/hg-0-0.02-mid.filled.png
deleted file mode 100644
index 25342c1..0000000
Binary files a/art/stages/hidden-garden/layers/hg-0-0.02-mid.filled.png and /dev/null differ
diff --git a/art/stages/hidden-garden/layers/hg-0-0.02-mid.png b/art/stages/hidden-garden/layers/hg-0-0.02-mid.png
index 91aba6f..20511dc 100644
Binary files a/art/stages/hidden-garden/layers/hg-0-0.02-mid.png and b/art/stages/hidden-garden/layers/hg-0-0.02-mid.png differ
diff --git a/art/stages/hidden-garden/layers/hg-0-0.03-rear.png b/art/stages/hidden-garden/layers/hg-0-0.03-rear.png
index e21f95b..5f37a0f 100644
Binary files a/art/stages/hidden-garden/layers/hg-0-0.03-rear.png and b/art/stages/hidden-garden/layers/hg-0-0.03-rear.png differ
diff --git a/art/stages/hidden-garden/layers/hg-0-0.04-water.png b/art/stages/hidden-garden/layers/hg-0-0.04-water.png
index d7b6095..903b4c4 100644
Binary files a/art/stages/hidden-garden/layers/hg-0-0.04-water.png and b/art/stages/hidden-garden/layers/hg-0-0.04-water.png differ
diff --git a/art/stages/hidden-garden/layers/hg-0-0.05-play.png b/art/stages/hidden-garden/layers/hg-0-0.05-play.png
index 13bab75..0687f55 100644
Binary files a/art/stages/hidden-garden/layers/hg-0-0.05-play.png and b/art/stages/hidden-garden/layers/hg-0-0.05-play.png differ
diff --git a/art/stages/hidden-garden/layers/hg-0-0.07-foreground.png b/art/stages/hidden-garden/layers/hg-0-0.07-foreground.png
index a5efb90..9ba5baa 100644
Binary files a/art/stages/hidden-garden/layers/hg-0-0.07-foreground.png and b/art/stages/hidden-garden/layers/hg-0-0.07-foreground.png differ
diff --git a/art/stages/hidden-garden/layers/hg-1-0.00-sky.png b/art/stages/hidden-garden/layers/hg-1-0.00-sky.png
new file mode 100644
index 0000000..74ed610
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-1-0.00-sky.png differ
diff --git a/art/stages/hidden-garden/layers/hg-1-0.01-far.png b/art/stages/hidden-garden/layers/hg-1-0.01-far.png
new file mode 100644
index 0000000..58eb63b
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-1-0.01-far.png differ
diff --git a/art/stages/hidden-garden/layers/hg-1-0.02-mid.png b/art/stages/hidden-garden/layers/hg-1-0.02-mid.png
new file mode 100644
index 0000000..1771634
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-1-0.02-mid.png differ
diff --git a/art/stages/hidden-garden/layers/hg-1-0.03-rear.png b/art/stages/hidden-garden/layers/hg-1-0.03-rear.png
new file mode 100644
index 0000000..b8c1b45
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-1-0.03-rear.png differ
diff --git a/art/stages/hidden-garden/layers/hg-1-0.04-water.png b/art/stages/hidden-garden/layers/hg-1-0.04-water.png
new file mode 100644
index 0000000..56a14ed
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-1-0.04-water.png differ
diff --git a/art/stages/hidden-garden/layers/hg-1-0.05-play.png b/art/stages/hidden-garden/layers/hg-1-0.05-play.png
new file mode 100644
index 0000000..2be6019
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-1-0.05-play.png differ
diff --git a/art/stages/hidden-garden/layers/hg-1-0.07-foreground.png b/art/stages/hidden-garden/layers/hg-1-0.07-foreground.png
new file mode 100644
index 0000000..1ba9ba3
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-1-0.07-foreground.png differ
diff --git a/art/stages/hidden-garden/layers/hg-2-0.00-sky.png b/art/stages/hidden-garden/layers/hg-2-0.00-sky.png
new file mode 100644
index 0000000..70a0376
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-2-0.00-sky.png differ
diff --git a/art/stages/hidden-garden/layers/hg-2-0.01-far.png b/art/stages/hidden-garden/layers/hg-2-0.01-far.png
new file mode 100644
index 0000000..bffc2d2
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-2-0.01-far.png differ
diff --git a/art/stages/hidden-garden/layers/hg-2-0.02-mid.png b/art/stages/hidden-garden/layers/hg-2-0.02-mid.png
new file mode 100644
index 0000000..6fc29e5
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-2-0.02-mid.png differ
diff --git a/art/stages/hidden-garden/layers/hg-2-0.03-rear.png b/art/stages/hidden-garden/layers/hg-2-0.03-rear.png
new file mode 100644
index 0000000..28168a5
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-2-0.03-rear.png differ
diff --git a/art/stages/hidden-garden/layers/hg-2-0.04-water.png b/art/stages/hidden-garden/layers/hg-2-0.04-water.png
new file mode 100644
index 0000000..f3b8e87
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-2-0.04-water.png differ
diff --git a/art/stages/hidden-garden/layers/hg-2-0.05-play.png b/art/stages/hidden-garden/layers/hg-2-0.05-play.png
new file mode 100644
index 0000000..6ae80f3
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-2-0.05-play.png differ
diff --git a/art/stages/hidden-garden/layers/hg-2-0.07-foreground.png b/art/stages/hidden-garden/layers/hg-2-0.07-foreground.png
new file mode 100644
index 0000000..c130f1b
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-2-0.07-foreground.png differ
diff --git a/art/stages/hidden-garden/layers/hg-3-0.00-sky.png b/art/stages/hidden-garden/layers/hg-3-0.00-sky.png
new file mode 100644
index 0000000..8930896
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-3-0.00-sky.png differ
diff --git a/art/stages/hidden-garden/layers/hg-3-0.01-far.png b/art/stages/hidden-garden/layers/hg-3-0.01-far.png
new file mode 100644
index 0000000..cc277f4
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-3-0.01-far.png differ
diff --git a/art/stages/hidden-garden/layers/hg-3-0.02-mid.png b/art/stages/hidden-garden/layers/hg-3-0.02-mid.png
new file mode 100644
index 0000000..8a945dd
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-3-0.02-mid.png differ
diff --git a/art/stages/hidden-garden/layers/hg-3-0.03-rear.png b/art/stages/hidden-garden/layers/hg-3-0.03-rear.png
new file mode 100644
index 0000000..aed5ade
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-3-0.03-rear.png differ
diff --git a/art/stages/hidden-garden/layers/hg-3-0.04-water.png b/art/stages/hidden-garden/layers/hg-3-0.04-water.png
new file mode 100644
index 0000000..05cf1a7
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-3-0.04-water.png differ
diff --git a/art/stages/hidden-garden/layers/hg-3-0.05-play.png b/art/stages/hidden-garden/layers/hg-3-0.05-play.png
new file mode 100644
index 0000000..69f4210
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-3-0.05-play.png differ
diff --git a/art/stages/hidden-garden/layers/hg-3-0.07-foreground.png b/art/stages/hidden-garden/layers/hg-3-0.07-foreground.png
new file mode 100644
index 0000000..5b4d3fe
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-3-0.07-foreground.png differ
diff --git a/art/stages/hidden-garden/layers/hg-4-0.00-sky.png b/art/stages/hidden-garden/layers/hg-4-0.00-sky.png
new file mode 100644
index 0000000..5e2b8e3
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-4-0.00-sky.png differ
diff --git a/art/stages/hidden-garden/layers/hg-4-0.01-far.png b/art/stages/hidden-garden/layers/hg-4-0.01-far.png
new file mode 100644
index 0000000..36c4ff4
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-4-0.01-far.png differ
diff --git a/art/stages/hidden-garden/layers/hg-4-0.02-mid.png b/art/stages/hidden-garden/layers/hg-4-0.02-mid.png
new file mode 100644
index 0000000..91aba6f
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-4-0.02-mid.png differ
diff --git a/art/stages/hidden-garden/layers/hg-4-0.03-rear.png b/art/stages/hidden-garden/layers/hg-4-0.03-rear.png
new file mode 100644
index 0000000..e21f95b
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-4-0.03-rear.png differ
diff --git a/art/stages/hidden-garden/layers/hg-4-0.04-water.png b/art/stages/hidden-garden/layers/hg-4-0.04-water.png
new file mode 100644
index 0000000..d7b6095
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-4-0.04-water.png differ
diff --git a/art/stages/hidden-garden/layers/hg-4-0.05-play.png b/art/stages/hidden-garden/layers/hg-4-0.05-play.png
new file mode 100644
index 0000000..13bab75
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-4-0.05-play.png differ
diff --git a/art/stages/hidden-garden/layers/hg-4-0.07-foreground.png b/art/stages/hidden-garden/layers/hg-4-0.07-foreground.png
new file mode 100644
index 0000000..a5efb90
Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-4-0.07-foreground.png differ
diff --git a/art/stages/hidden-garden/layers/hidden-garden.00-sky.0.png b/art/stages/hidden-garden/layers/hidden-garden.00-sky.0.png
new file mode 100644
index 0000000..2a72df3
Binary files /dev/null and b/art/stages/hidden-garden/layers/hidden-garden.00-sky.0.png differ
diff --git a/art/stages/hidden-garden/layers/hidden-garden.00-sky.1.png b/art/stages/hidden-garden/layers/hidden-garden.00-sky.1.png
new file mode 100644
index 0000000..4becb77
Binary files /dev/null and b/art/stages/hidden-garden/layers/hidden-garden.00-sky.1.png differ
diff --git a/art/stages/hidden-garden/layers/hidden-garden.01-far.0.png b/art/stages/hidden-garden/layers/hidden-garden.01-far.0.png
new file mode 100644
index 0000000..165dc69
Binary files /dev/null and b/art/stages/hidden-garden/layers/hidden-garden.01-far.0.png differ
diff --git a/art/stages/hidden-garden/layers/hidden-garden.01-far.1.png b/art/stages/hidden-garden/layers/hidden-garden.01-far.1.png
new file mode 100644
index 0000000..431d463
Binary files /dev/null and b/art/stages/hidden-garden/layers/hidden-garden.01-far.1.png differ
diff --git a/art/stages/hidden-garden/layers/hidden-garden.02-mid.0.png b/art/stages/hidden-garden/layers/hidden-garden.02-mid.0.png
new file mode 100644
index 0000000..eac322c
Binary files /dev/null and b/art/stages/hidden-garden/layers/hidden-garden.02-mid.0.png differ
diff --git a/art/stages/hidden-garden/layers/hidden-garden.02-mid.1.png b/art/stages/hidden-garden/layers/hidden-garden.02-mid.1.png
new file mode 100644
index 0000000..1560dd3
Binary files /dev/null and b/art/stages/hidden-garden/layers/hidden-garden.02-mid.1.png differ
diff --git a/art/stages/hidden-garden/layers/hidden-garden.02-mid.2.png b/art/stages/hidden-garden/layers/hidden-garden.02-mid.2.png
new file mode 100644
index 0000000..fb50c1f
Binary files /dev/null and b/art/stages/hidden-garden/layers/hidden-garden.02-mid.2.png differ
diff --git a/art/stages/hidden-garden/layers/hidden-garden.02-mid.3.png b/art/stages/hidden-garden/layers/hidden-garden.02-mid.3.png
new file mode 100644
index 0000000..8cbb998
Binary files /dev/null and b/art/stages/hidden-garden/layers/hidden-garden.02-mid.3.png differ
diff --git a/art/stages/hidden-garden/layers/hidden-garden.07-foreground.0.png b/art/stages/hidden-garden/layers/hidden-garden.07-foreground.0.png
new file mode 100644
index 0000000..31e6288
Binary files /dev/null and b/art/stages/hidden-garden/layers/hidden-garden.07-foreground.0.png differ
diff --git a/art/stages/hidden-garden/layers/hidden-garden.07-foreground.1.png b/art/stages/hidden-garden/layers/hidden-garden.07-foreground.1.png
new file mode 100644
index 0000000..63788db
Binary files /dev/null and b/art/stages/hidden-garden/layers/hidden-garden.07-foreground.1.png differ
diff --git a/art/stages/hidden-garden/layers/hidden-garden.07-foreground.2.png b/art/stages/hidden-garden/layers/hidden-garden.07-foreground.2.png
new file mode 100644
index 0000000..e414383
Binary files /dev/null and b/art/stages/hidden-garden/layers/hidden-garden.07-foreground.2.png differ
diff --git a/art/stages/hidden-garden/layers/hidden-garden.07-foreground.3.png b/art/stages/hidden-garden/layers/hidden-garden.07-foreground.3.png
new file mode 100644
index 0000000..d770390
Binary files /dev/null and b/art/stages/hidden-garden/layers/hidden-garden.07-foreground.3.png differ
diff --git a/art/stages/hidden-garden/layers/hidden-garden.07-foreground.4.png b/art/stages/hidden-garden/layers/hidden-garden.07-foreground.4.png
new file mode 100644
index 0000000..e71b9cc
Binary files /dev/null and b/art/stages/hidden-garden/layers/hidden-garden.07-foreground.4.png differ
diff --git a/art/stages/hidden-garden/masks/hg-0-0.00-sky.png b/art/stages/hidden-garden/masks/hg-0-0.00-sky.png
index 578adce..158a18e 100644
Binary files a/art/stages/hidden-garden/masks/hg-0-0.00-sky.png and b/art/stages/hidden-garden/masks/hg-0-0.00-sky.png differ
diff --git a/art/stages/hidden-garden/masks/hg-0-0.01-far.png b/art/stages/hidden-garden/masks/hg-0-0.01-far.png
index 6675a17..9784831 100644
Binary files a/art/stages/hidden-garden/masks/hg-0-0.01-far.png and b/art/stages/hidden-garden/masks/hg-0-0.01-far.png differ
diff --git a/art/stages/hidden-garden/masks/hg-0-0.02-mid.png b/art/stages/hidden-garden/masks/hg-0-0.02-mid.png
index 7a0e234..e3ecbc7 100644
Binary files a/art/stages/hidden-garden/masks/hg-0-0.02-mid.png and b/art/stages/hidden-garden/masks/hg-0-0.02-mid.png differ
diff --git a/art/stages/hidden-garden/masks/hg-0-0.04-water.png b/art/stages/hidden-garden/masks/hg-0-0.04-water.png
new file mode 100644
index 0000000..b174a59
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-0-0.04-water.png differ
diff --git a/art/stages/hidden-garden/masks/hg-0-0.05-play.png b/art/stages/hidden-garden/masks/hg-0-0.05-play.png
new file mode 100644
index 0000000..bcb329a
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-0-0.05-play.png differ
diff --git a/art/stages/hidden-garden/masks/hg-0-0.07-foreground.png b/art/stages/hidden-garden/masks/hg-0-0.07-foreground.png
new file mode 100644
index 0000000..714425c
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-0-0.07-foreground.png differ
diff --git a/art/stages/hidden-garden/masks/hg-1-0.00-sky.png b/art/stages/hidden-garden/masks/hg-1-0.00-sky.png
new file mode 100644
index 0000000..e9279cb
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-1-0.00-sky.png differ
diff --git a/art/stages/hidden-garden/masks/hg-1-0.01-far.png b/art/stages/hidden-garden/masks/hg-1-0.01-far.png
new file mode 100644
index 0000000..f88eb7b
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-1-0.01-far.png differ
diff --git a/art/stages/hidden-garden/masks/hg-1-0.02-mid.png b/art/stages/hidden-garden/masks/hg-1-0.02-mid.png
new file mode 100644
index 0000000..6920a20
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-1-0.02-mid.png differ
diff --git a/art/stages/hidden-garden/masks/hg-1-0.04-water.png b/art/stages/hidden-garden/masks/hg-1-0.04-water.png
new file mode 100644
index 0000000..2e582d2
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-1-0.04-water.png differ
diff --git a/art/stages/hidden-garden/masks/hg-1-0.05-play.png b/art/stages/hidden-garden/masks/hg-1-0.05-play.png
new file mode 100644
index 0000000..7121a2f
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-1-0.05-play.png differ
diff --git a/art/stages/hidden-garden/masks/hg-1-0.07-foreground.png b/art/stages/hidden-garden/masks/hg-1-0.07-foreground.png
new file mode 100644
index 0000000..0460734
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-1-0.07-foreground.png differ
diff --git a/art/stages/hidden-garden/masks/hg-2-0.00-sky.png b/art/stages/hidden-garden/masks/hg-2-0.00-sky.png
new file mode 100644
index 0000000..a069303
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-2-0.00-sky.png differ
diff --git a/art/stages/hidden-garden/masks/hg-2-0.01-far.png b/art/stages/hidden-garden/masks/hg-2-0.01-far.png
new file mode 100644
index 0000000..6596e69
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-2-0.01-far.png differ
diff --git a/art/stages/hidden-garden/masks/hg-2-0.02-mid.png b/art/stages/hidden-garden/masks/hg-2-0.02-mid.png
new file mode 100644
index 0000000..03313c0
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-2-0.02-mid.png differ
diff --git a/art/stages/hidden-garden/masks/hg-2-0.04-water.png b/art/stages/hidden-garden/masks/hg-2-0.04-water.png
new file mode 100644
index 0000000..be8e23d
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-2-0.04-water.png differ
diff --git a/art/stages/hidden-garden/masks/hg-2-0.05-play.png b/art/stages/hidden-garden/masks/hg-2-0.05-play.png
new file mode 100644
index 0000000..c3cdcf6
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-2-0.05-play.png differ
diff --git a/art/stages/hidden-garden/masks/hg-2-0.07-foreground.png b/art/stages/hidden-garden/masks/hg-2-0.07-foreground.png
new file mode 100644
index 0000000..a303d56
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-2-0.07-foreground.png differ
diff --git a/art/stages/hidden-garden/masks/hg-3-0.00-sky.png b/art/stages/hidden-garden/masks/hg-3-0.00-sky.png
new file mode 100644
index 0000000..8e2f95a
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-3-0.00-sky.png differ
diff --git a/art/stages/hidden-garden/masks/hg-3-0.01-far.png b/art/stages/hidden-garden/masks/hg-3-0.01-far.png
new file mode 100644
index 0000000..1b875c2
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-3-0.01-far.png differ
diff --git a/art/stages/hidden-garden/masks/hg-3-0.02-mid.png b/art/stages/hidden-garden/masks/hg-3-0.02-mid.png
new file mode 100644
index 0000000..c576166
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-3-0.02-mid.png differ
diff --git a/art/stages/hidden-garden/masks/hg-3-0.04-water.png b/art/stages/hidden-garden/masks/hg-3-0.04-water.png
new file mode 100644
index 0000000..64f5370
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-3-0.04-water.png differ
diff --git a/art/stages/hidden-garden/masks/hg-3-0.05-play.png b/art/stages/hidden-garden/masks/hg-3-0.05-play.png
new file mode 100644
index 0000000..9a0d71a
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-3-0.05-play.png differ
diff --git a/art/stages/hidden-garden/masks/hg-3-0.07-foreground.png b/art/stages/hidden-garden/masks/hg-3-0.07-foreground.png
new file mode 100644
index 0000000..8dc8689
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-3-0.07-foreground.png differ
diff --git a/art/stages/hidden-garden/masks/hg-4-0.00-sky.png b/art/stages/hidden-garden/masks/hg-4-0.00-sky.png
new file mode 100644
index 0000000..578adce
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-4-0.00-sky.png differ
diff --git a/art/stages/hidden-garden/masks/hg-4-0.01-far.png b/art/stages/hidden-garden/masks/hg-4-0.01-far.png
new file mode 100644
index 0000000..6675a17
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-4-0.01-far.png differ
diff --git a/art/stages/hidden-garden/masks/hg-4-0.02-mid.png b/art/stages/hidden-garden/masks/hg-4-0.02-mid.png
new file mode 100644
index 0000000..7a0e234
Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-4-0.02-mid.png differ
diff --git a/docs/04-WORLD-AND-LEVELS.md b/docs/04-WORLD-AND-LEVELS.md
index 249cbca..623b2f5 100644
--- a/docs/04-WORLD-AND-LEVELS.md
+++ b/docs/04-WORLD-AND-LEVELS.md
@@ -108,6 +108,30 @@ The route already exists as a graph ([route.json](../art/stages/rusty-creek/rout
Target play time for the slice: 8–12 minutes.
+## Hidden Garden – the prologue level
+
+Docs/03 beat 2 asks for about three minutes of calm garden before the pond: hills, the deck, reeds, a secret behind grass, a log to jump, a slope to climb ([#50](https://github.com/THD-AI/WinCS/issues/50)). At 3.5 m/s (153 px/s) one 2160 px chunk is 14 s of walking, so the prologue is **five chunks** in a row: ≈ 70 s on foot, plus Dexter's waits and two detours. The pond chunk that exists becomes the last one, `hg-4-0`; the four before it grow leftwards from it, each from the edge of the one to its right, so the path meets at the same height (top y = 441).
+
+| Chunk | Beat | Route | Teaches |
+| --- | --- | --- | --- |
+| `hg-0-0` the gate | Hero enters at the garden gate; **Dexter sits on a bench**, hops off when you come close | flat path, a fallen log at the end (≈ 30 px, a tap jump of 52) | walk, the first jump |
+| `hg-1-0` the hill | The path rolls over a grassy hill with an old oak on top | slopes ≤ 25°; below the hill a **hollow behind tall grass** with a muffin (lower branch, dead end) | slopes, secrets |
+| `hg-2-0` the terraces | Old stone garden terraces and a wooden shed | steps ≤ 48 px; a **ladder up the shed** to its roof, a glint up there (higher branch, dead end) | ladders, looking up |
+| `hg-3-0` the boardwalk | A timber boardwalk through the reeds | one gap (≈ 120 px, a running jump), stepping stones in a ditch | jumping gaps |
+| `hg-4-0` the pond | The jetty, the wrong reflection, the shove (today's `hg-0-0`) | – | – |
+
+```mermaid
+flowchart LR
+ P["hg-4-0 the pond
(approved chunk)"] -->|"grow left: its left 384 px
as context"| B["hg-3-0 boardwalk"]
+ B -->|grow left| T["hg-2-0 terraces"]
+ T -->|grow left| H["hg-1-0 hill"]
+ H -->|grow left| G["hg-0-0 gate"]
+ subgraph each["Each new chunk"]
+ direction TB
+ g1["grow_stage.py: canvas 2160 + 384,
the neighbour's edge band at the seam,
route brief with heights in px"] --> g2["review: seam, path height,
route readable"] --> g3["depth_layers.py key → masks → fill
(play band from the key too)"] --> g4["collision: walk lines traced
from the play band, then edited"]
+ end
+```
+
## Lex's next maps
For each next region: one hand-drawn route sketch on top of its collage. Mark the entry, branches, secrets, enemies and the boss room with arrows, using the colour legend above. Screenshots from reference games are welcome. These sketches are the input for generating that region's stage art.
diff --git a/docs/07-ASSET-MANIFEST.md b/docs/07-ASSET-MANIFEST.md
index bcfa102..ce471ac 100644
--- a/docs/07-ASSET-MANIFEST.md
+++ b/docs/07-ASSET-MANIFEST.md
@@ -58,7 +58,7 @@ Frame lists approved as they are (A14).
| --- | --- | --- |
| Rusty Creek | 3 chunks × 7 layers, clean + anomaly states, route graph | ✅ art · ⬜ collision · ⬜ foreground-blur + particle layers |
| Stage concepts (6 regions × clean/anomaly) | 2176×1088 concepts with up/down routes ([art/locations](../art/locations/README.md)) | 🟡 in review |
-| Hidden Garden (prologue) | 1 chunk `hg-0-0` ([art/stages/hidden-garden](../art/stages/hidden-garden/README.md)): bench, path, jetty, the wrong reflection; layers 03/04/05/07. A second chunk waits for seam repair | 🟡 in review · ⬜ collision |
+| Hidden Garden (prologue) | 5 chunks `hg-0-0`…`hg-4-0` ([art/stages/hidden-garden](../art/stages/hidden-garden/README.md)): the gate and Dexter's bench, the hill and its hollow, the terraces and the shed, the boardwalk gap, the pond with the wrong reflection; layers 00–07 with parallax (#142, #50) | 🟡 in review · 🟡 collision traced, waypoints by hand |
| Jenny's arena | right chunk of Rusty Creek (boss clearing) | ✅ art · ⬜ collision |
## Audio (synthesized in code)
diff --git a/docs/09-ART-PIPELINE.md b/docs/09-ART-PIPELINE.md
index dfe8827..666fbff 100644
--- a/docs/09-ART-PIPELINE.md
+++ b/docs/09-ART-PIPELINE.md
@@ -41,6 +41,34 @@ What Codex's runs proved on Rusty Creek, generalised:
6. **Transition chunks** join different spaces – surface to underground, outside to the Mensa inside. They are generated with *both* neighbours as context and blend the two layer stacks over their height.
7. **Clean and anomaly share everything they can.** The anomaly state replaces only the layers that change (on Rusty Creek, only the structure layers). Masters are rebuilt from the plates and verified pixel-exact (`creek_layers.py`).
+## Growing a region chunk by chunk – grow, feature, join, trace
+
+`scripts/grow_stage.py` makes a region longer from an approved chunk, one 2160×720 chunk at a time, so a level is one continuous place instead of separate pictures ([#50](https://github.com/THD-AI/WinCS/issues/50), first used on the Hidden Garden prologue).
+
+```mermaid
+flowchart TD
+ A["Approved chunk
(the pond, hg-4-0)"] --> G1["grow, piece 1
2160×720 canvas: 1440 px to paint
over the layout guide + 720 px of the neighbour"]
+ G1 --> G2["grow, piece 2
720 px more + 128 px over piece 1
→ min-error cut joins them"]
+ G2 --> M["match the seam's colours to the neighbour
(ramp over 160 px, shapes untouched)"]
+ M --> N{"next chunk?"}
+ N -->|yes| G1
+ N -->|no| J["join: repaint 320 px across each
new–new seam, min-error cuts on both sides"]
+ J --> F["feature: one masked repaint per route feature
(the gap, the hill, the shed and ladder, the gate, the log)
+ 64 px margins, min-error cuts"]
+ F --> K["depth_layers.py key → masks → fill
(parallax, as below)"]
+ K --> C["depth_layers.py collision
walk lines traced from the key's play band
(RDP, split at steps > 8 px), water = no-go"]
+ C --> CC["collision_check.py: reach, joins, overlay
→ hand-placed spawns, Dexter waypoints, triggers"]
+```
+
+**What made it work** (24 Oct, the boardwalk chunk took five tries):
+
+| Tried | Happened | Rule now |
+| --- | --- | --- |
+| One canvas wider than 3:1 | The API refuses: at most 3:1 | Every call is 2160×720 |
+| A wider canvas padded with sky to 3:1 | The model reframed it and moved the whole picture 60–90 px; the path came out 47 px too high | Never pad; grow in two pieces of 1440 + 720 px |
+| The neighbour's band pasted back over a single painted canvas | A hard seam (colour step 45 against 8 inside the art) | Seam colours matched over a ramp; pieces overlap 128 px and join along a min-error cut |
+| The route as numbers in the brief, or as a separate guide image | Ignored: a stone bridge instead of a gap | The guide is the image being edited, under the mask |
+| The route's features in the same call as the scenery | Smoothed away: the path stays flat and continuous | Grow the scenery first, then one `feature` repaint per feature, with a short concrete brief |
+
## Parallax layers from a depth key
Painted chunks are one picture. To give them depth without hand masks or redrawing each layer (which drifts), the model repaints the finished chunk as a **depth key**: the same shapes, one flat colour per depth band. The masks come from that key, and a second pass paints what each slow layer has behind the faster ones ([#142](https://github.com/THD-AI/WinCS/issues/142), first used on the Hidden Garden).
@@ -57,7 +85,7 @@ flowchart LR
- **The key only says which band, never which pixel.** It drifts a few pixels at fine edges, so near the sky the real pixel decides: open sky is ≈ (40–60, 120–140, 240) and hazy far treetops ≈ (80, 120, 180), so "sky" means blue above 200. Path, water and foreground keep their exact rectangles and colour rules, which run after the masks and win.
- **Fills never change the picture at rest.** A fill is kept only under the nearer layers, and the layer's own pixels are copied from the art, so `--verify` still rebuilds the chunk exactly. The camera only ever reveals painted scenery.
-- **Several chunks:** a plate at scroll *s* belongs at `chunk.x × 2160 × s`; lane B's loader places it at `chunk.x × 2160` today (commented on [#116](https://github.com/THD-AI/WinCS/pull/116)).
+- **Several chunks: one plate per slow layer for the whole region.** Per chunk, plates at scroll *s* < 1 overlap by 2160 × (1 − *s*) and cut each other off with hard vertical edges; at *s* > 1 (the foreground) they spread apart and leave gaps. `depth_layers.py region` quilts each layer marked `"region": true` in `export.json` from the chunks' own plates (`"rest"`): chunk *i* at *i* × 2160 × *s*, each next plate taking over along a min-error path in a 192 px band and only where it has pixels; a foreground gap is filled with the earlier plate's edge mirrored. The first chunk carries the plate, so it lands at x = 0 under any placement rule, and `--verify` still rebuilds every chunk at rest from its own plates. The Hidden Garden's mid plate is 6992 px wide (8192 is the common WebGL limit).
## Prompting lessons (23 Oct, all runs)
diff --git a/scripts/compare.py b/scripts/compare.py
index 6e4ddbc..7a4cb7e 100644
--- a/scripts/compare.py
+++ b/scripts/compare.py
@@ -9,8 +9,9 @@
--atlas-dir tmp/atlas reads a review export instead of the game's atlases
python scripts/compare.py parallax art/stages/hidden-garden --state clean --at 0 440 880 --out …
- the region's layers from export.json, drawn as the game draws them at each camera x (a layer at
- scroll s sits at chunk.x * 2160 * s and moves s px per camera px), one 1280x720 view per row
+ the region's layers from export.json, drawn as the game draws them at each camera x (a layer sits
+ at its chunk's origin and moves s px per camera px; a region plate's tile j rides on chunk j),
+ one 1280x720 view per row
Paths are relative to the repository. Images and crops are in their own pixels; `--width` sets the common width.
"""
@@ -120,11 +121,14 @@ def parallax(region: Path, state: str, cameras: list[int]) -> Image.Image:
for layer_id in config["states"][state]:
layer = config["layers"][layer_id]
for name, chunk in config["chunks"].items():
- path = region / layer["file"].format(name=chunk.get("name", name))
+ path = region / (layer["file"].format(tile=chunk["x"]) if layer.get("region")
+ else layer["file"].format(name=chunk.get("name", name)))
if not path.exists():
continue
s = layer["scroll"]
- x = round(chunk["x"] * w * s - cam * s)
+ x = round(chunk["x"] * w - cam * s) # the game: a layer at its chunk's origin, moving at its scroll
+ if x <= -w or x >= VIEW[0]: # wholly off the view
+ continue
plate = Image.open(path).convert("RGBA")
if x < 0: # alpha_composite needs a non-negative destination: crop the plate instead
plate, x = plate.crop((-x, 0, plate.width, plate.height)), 0
diff --git a/scripts/depth_layers.py b/scripts/depth_layers.py
index 6db4532..0ea73a0 100644
--- a/scripts/depth_layers.py
+++ b/scripts/depth_layers.py
@@ -2,6 +2,9 @@
python scripts/depth_layers.py key art/stages/ # API: repaint the chunk as flat depth colours
python scripts/depth_layers.py masks art/stages/ # key -> one mask per layer (committed)
+ [--all] # every band (grown chunks), not only parallax
+ python scripts/depth_layers.py collision art/stages/ # walk lines + water no-go from the key
+ python scripts/depth_layers.py region art/stages/ # quilt the chunks' slow layers into one plate each
python scripts/depth_layers.py fill art/stages/ # API: paint what nearer layers hide
[--only far mid] [--reuse] # --reuse: re-cut from tmp/ paintings
@@ -15,7 +18,8 @@
3. fill: a layer that scrolls slower than the ones in front of it shows what they hid. For each such
layer the model repaints the chunk with the nearer layers masked out, painting only that layer's
scenery behind them; the result is kept only under the nearer layers (and below the layer's own
- skyline), so the composed picture at rest is unchanged. Writes layers/..filled.png.
+ skyline), so the composed picture at rest is unchanged. Writes tmp/boards//filled/ (intermediates:
+ the region plates below are quilted from them and committed).
Needs OPENAI_API_KEY (environment or .env) for key and fill; the key goes in a header file (make_boards.edit).
"""
@@ -55,8 +59,11 @@
"- the open sky and clouds: pure cyan #00FFFF\n"
"- the hazy far tree line and distant forest on the horizon: pure blue #0000FF\n"
"- the mid-distance autumn trees behind the reeds: pure green #00FF00\n"
- "- the reeds, bushes, stone walls and the big tree close behind the path: pure yellow #FFFF00\n"
- "- the walkable stone path, the bench and the wooden jetty with its posts: pure red #FF0000\n"
+ "- the reeds, bushes, stone walls, posts, benches, sheds, gates and the big trees close behind the path: "
+ "pure yellow #FFFF00\n"
+ "- ONLY what a person walks on - the path, a boardwalk or jetty deck, steps and terrace tops, the hill's "
+ "ground, stepping stones, a roof you can stand on - as a solid band from its walking surface down to the "
+ "water or the bottom: pure red #FF0000\n"
"- the pond water with its reflections: navy #000080\n"
"- the dark out-of-focus leaves and grass along the bottom edge, in front of everything: magenta #FF00FF\n"
"Every pixel is exactly one of these seven colours.")
@@ -137,11 +144,14 @@ def grown(band: str):
return out
-def write_masks(region: Path, chunk: str, bands: list[str], size: tuple[int, int]) -> list[Path]:
+def write_masks(region: Path, chunk: str, bands: list[str], size: tuple[int, int],
+ which: tuple[str, ...] = PARALLAX) -> list[Path]:
+ """One mask per band. By default only the parallax bands: a hand-tuned chunk keeps exact rules for the rest;
+ a grown chunk takes every band from its key (--all)."""
folder = region / "masks"
folder.mkdir(exist_ok=True)
written = []
- for name in PARALLAX: # the nearer layers come from exact rules in layers.json
+ for name in which:
m = Image.new("L", size)
m.putdata([255 if b == name else 0 for b in bands])
if m.getbbox() is None:
@@ -245,20 +255,196 @@ def fill(region: Path, chunk: str, art: Image.Image, only: tuple[str, ...] = PAR
keep.putdata([255 if own[i] else 0 for i in range(w * h)])
plate.paste(painted.convert("RGBA"), mask=ImageChops.multiply(prompt_hole, in_front))
plate.paste(art.convert("RGBA"), mask=keep) # its own pixels stay exactly the art's
- out = region / "layers" / f"{chunk}.{DEPTH[name][0]}.filled.png"
+ out = raw_dir(region) / "filled" / f"{chunk}.{DEPTH[name][0]}.filled.png" # an intermediate: not committed
+ out.parent.mkdir(exist_ok=True)
plate.save(out, optimize=True)
print(f"{chunk}: {name} filled -> {out.relative_to(ROOT)}", flush=True)
+def rdp(points: list[tuple[int, int]], tolerance: float) -> list[tuple[int, int]]:
+ """Ramer–Douglas–Peucker: the fewest points that stay within `tolerance` px of the traced line."""
+ if len(points) < 3:
+ return points
+ (x0, y0), (x1, y1) = points[0], points[-1]
+ length = max(1e-9, ((x1 - x0) ** 2 + (y1 - y0) ** 2) ** 0.5)
+ far, at = 0.0, 0
+ for i, (x, y) in enumerate(points[1:-1], 1):
+ d = abs((y1 - y0) * x - (x1 - x0) * y + x1 * y0 - y1 * x0) / length
+ if d > far:
+ far, at = d, i
+ if far <= tolerance:
+ return [points[0], points[-1]]
+ return rdp(points[:at + 1], tolerance)[:-1] + rdp(points[at:], tolerance)
+
+
+def surfaces(bands: list[str], w: int, h: int, x: int, every: bool = False) -> list[int]:
+ """Where the play band starts in column x: its top only, or (every) each start below something else.
+
+ The top only by default: grass and bushes keyed in front of a hill punch holes into its play band,
+ and their lower edges are not floors. A real second floor (a hollow under the path) is measured by
+ hand and comes from the board's "walkExtra"."""
+ tops = [y for y in range(h) if bands[y * w + x] == "play" and (y == 0 or bands[(y - 1) * w + x] != "play")]
+ return tops if every else tops[:1]
+
+
+def ramp(line: list[tuple[int, int]], rise: int, slope: float = 0.7) -> list[tuple[int, int]]:
+ """Stairs are walk slopes, never steps (docs/13): each jump of up to `rise` px becomes a ramp of about 35°."""
+ pts = list(line)
+ for k in range(1, len(pts)):
+ dy = pts[k][1] - pts[k - 1][1]
+ if 1 < abs(dy) <= rise:
+ half = max(1, round(abs(dy) / slope / 2))
+ a, b = max(0, k - half), min(len(pts) - 1, k + half - 1)
+ ya, yb = pts[a][1], pts[b][1]
+ for m in range(a, b + 1):
+ pts[m] = (pts[m][0], round(ya + (yb - ya) * (m - a) / max(1, b - a)))
+ return pts
+
+
+def walk_lines(bands: list[str], w: int, h: int, step: int = 48, tolerance: float = 2.0, shortest: int = 24,
+ every: bool = False) -> list:
+ """The walkable surfaces of the play band, followed column by column.
+
+ A surface continues into the next column at the nearest height within `step` px (a terrace step,
+ turned into a ramp); anything further starts a new line. Several surfaces may share a column (the
+ path over a hollow)."""
+ active: list[list[tuple[int, int]]] = []
+ done: list[list[tuple[int, int]]] = []
+ for x in range(w):
+ here = surfaces(bands, w, h, x, every)
+ nxt = []
+ for line in active:
+ y0 = line[-1][1]
+ near = min(here, key=lambda y: abs(y - y0), default=None)
+ if near is not None and abs(near - y0) <= step:
+ line.append((x, near))
+ here.remove(near)
+ nxt.append(line)
+ else:
+ done.append(line)
+ nxt += [[(x, y)] for y in here]
+ active = nxt
+ done += active
+ out = []
+ for line in sorted((l for l in done if len(l) >= shortest), key=lambda l: (l[0][0], l[0][1])):
+ pts = rdp(ramp(line, step), tolerance)
+ pts[-1] = (pts[-1][0] + 1, pts[-1][1]) # the line covers its last column
+ out.append([[x, y] for x, y in pts])
+ return out
+
+
+def collision(region: Path, chunk: str, art: Image.Image) -> Path:
+ """Walk lines traced from the key's play band and the water as no-go; hand-placed fields are kept."""
+ w, h = art.size
+ raw = raw_dir(region) / f"{chunk}--depth.png"
+ bands = vote(snap(Image.open(raw).resize(art.size, Image.Resampling.NEAREST)), art.size)
+ path = region / f"{chunk}.clean.collision.json"
+ data = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {
+ "chunk": chunk, "state": "clean", "size": [w, h], "walk": [], "slide": [], "oneWay": [], "climb": [],
+ "solid": [], "noGo": [], "hazard": [], "interact": [], "spawn": [], "ledge": [], "glideLift": []}
+ board = next((b for b in json.loads((region / "boards.json").read_text(encoding="utf-8"))["boards"]
+ if b["id"] == chunk), {})
+ data["walk"] = walk_lines(bands, w, h) + board.get("walkExtra", [])
+ water = [y for y in range(h) for x in range(0, w, 8) if bands[y * w + x] == "water"]
+ data["noGo"] = [{"x": 0, "y": min(water), "w": w, "h": h - min(water)}] if water else []
+ path.write_text(json.dumps(data, separators=(", ", ": ")) + "\n", encoding="utf-8")
+ return path
+
+
+def seam_path(a: Image.Image, b: Image.Image) -> list[int]:
+ """Per row, the column of least difference between two RGBA strips; moves at most one column a row."""
+ w, h = a.size
+ pa, pb = a.load(), b.load()
+ diff = [[sum(abs(pa[x, y][c] - pb[x, y][c]) for c in range(4)) for x in range(w)] for y in range(h)]
+ cost = [diff[0][:]]
+ for y in range(1, h):
+ prev = cost[-1]
+ cost.append([diff[y][x] + min(prev[max(0, x - 1):x + 2]) for x in range(w)])
+ x = min(range(w), key=lambda i: cost[-1][i])
+ path = [x]
+ for y in range(h - 2, -1, -1):
+ x = min(range(max(0, x - 1), min(w, x + 2)), key=lambda i: cost[y][i])
+ path.append(x)
+ return path[::-1]
+
+
+def region_plate(plates: list[Image.Image], scroll: float, window: int = 192, carry: bool = False) -> Image.Image:
+ """One plate for a row of chunks at scroll != 1: chunk i's plate sits at i * W * scroll.
+
+ Slower than 1, neighbouring chunk plates overlap by W * (1 - scroll) and would cut each other off with
+ hard vertical edges; each next plate takes over along the least visible path through a `window` px band
+ just right of where it starts, and only where it has pixels (past its own tree line the earlier plate
+ shows on). Faster than 1 (a foreground), they spread apart by W * (scroll - 1): the gap is filled with
+ the earlier plate's last columns mirrored, which meets it without a seam, and the next plate takes over
+ from there the same way."""
+ w, h = plates[0].size
+ step = round(w * scroll)
+ strip = Image.new("RGBA", (step * (len(plates) - 1) + w, h))
+ strip.paste(plates[0], (0, 0))
+ for i, plate in enumerate(plates[1:], 1):
+ o = i * step
+ prev_end = (i - 1) * step + w
+ if o > prev_end: # a gap: mirror the strip's last columns into it, plus a window to cut through
+ g = o - prev_end + window
+ mirrored = strip.crop((prev_end - g, 0, prev_end, h)).transpose(Image.Transpose.FLIP_LEFT_RIGHT)
+ strip.paste(mirrored, (prev_end, 0))
+ k = min(window, max(1, w - step) if step < w else window)
+ cut = seam_path(strip.crop((o, 0, o + k, h)), plate.crop((0, 0, k, h)))
+ s, q = strip.load(), plate.load()
+ # With carry, a column where the plate has no pixels at all (past its own tree line) keeps the
+ # earlier plate (mid trees: no holes). Every other column is the plate's own, transparency included,
+ # so its chunk at rest looks exactly like its art. Without carry, open sky shows there instead.
+ empty = [carry and all(q[x, y][3] == 0 for y in range(h)) for x in range(w)]
+ for y in range(h):
+ for x in range(cut[y], w):
+ if not empty[x] or o > prev_end:
+ s[o + x, y] = q[x, y]
+ return strip
+
+
+def region_layers(region: Path) -> list[Path]:
+ """Quilt every layer marked "region" in export.json from its chunks' "rest" plates into one plate each."""
+ export = json.loads((region / "export.json").read_text(encoding="utf-8"))
+ chunks = sorted(export["chunks"].items(), key=lambda kv: kv[1]["x"])
+ written = []
+ for layer_id, layer in export["layers"].items():
+ if not layer.get("region"):
+ continue
+ plates = [Image.open(ROOT / layer["rest"].format(name=c)).convert("RGBA") for c, _ in chunks]
+ plate = region_plate(plates, layer["scroll"], carry=layer.get("carry", False))
+ # A preview-free game plate: 256 colours keep it small; --verify checks the chunks' own plates ("rest").
+ alpha = plate.getchannel("A")
+ plate = plate.convert("RGB").quantize(256, dither=Image.Dither.NONE).convert("RGBA")
+ plate.putalpha(alpha)
+ # Tile j (2160 px of the plate) is carried by chunk j: a layer at chunk.x * 2160 moving at its scroll
+ # factor then shows exactly the plate's column the camera is over, with any loader that places
+ # layers at their chunk's origin - and no file is wider than a chunk.
+ w, n = plates[0].width, len(plates)
+ for old in (region / "layers").glob(f"{region.name}.{layer_id}.*.png"):
+ old.unlink()
+ for j in range(min(n, -(-plate.width // w))):
+ # The last chunk's tile takes the rest: a foreground faster than 1 runs past the last chunk.
+ right = plate.width if j == n - 1 else min(plate.width, (j + 1) * w)
+ out = region / "layers" / f"{region.name}.{layer_id}.{j}.png"
+ plate.crop((j * w, 0, right, plate.height)).save(out, optimize=True)
+ written.append(out)
+ return written
+
+
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
- parser.add_argument("step", choices=["key", "masks", "fill"])
+ parser.add_argument("step", choices=["key", "masks", "fill", "collision", "region"])
parser.add_argument("region", type=Path)
- parser.add_argument("chunk")
+ parser.add_argument("chunk", nargs="?", help="not for region")
parser.add_argument("--only", nargs="+", choices=PARALLAX, default=list(PARALLAX), help="fill: these layers")
parser.add_argument("--reuse", action="store_true", help="fill: cut the plates from the paintings in tmp/ again")
+ parser.add_argument("--all", action="store_true", help="masks: every band, not only the parallax ones")
args = parser.parse_args()
region = args.region if args.region.is_absolute() else ROOT / args.region
+ if args.step == "region":
+ for path in region_layers(region):
+ print(f"{region.name}: region plate -> {path.relative_to(ROOT)}")
+ return
art = art_for(region, args.chunk)
raw = raw_dir(region) / f"{args.chunk}--depth.png"
if args.step == "key":
@@ -271,8 +457,11 @@ def main() -> None:
if not raw.exists():
sys.exit(f"{raw.relative_to(ROOT)} missing: run the key step first")
bands = refine(vote(snap(Image.open(raw)), art.size), art)
- for path in write_masks(region, args.chunk, bands, art.size):
+ which = tuple(n for n in BACK_TO_FRONT if n != "rear") if args.all else PARALLAX # rear = the default rule
+ for path in write_masks(region, args.chunk, bands, art.size, which):
print(f"{args.chunk}: mask -> {path.relative_to(ROOT)}")
+ elif args.step == "collision":
+ print(f"{args.chunk}: walk lines and no-go -> {collision(region, args.chunk, art).relative_to(ROOT)}")
else:
fill(region, args.chunk, art, tuple(args.only), args.reuse)
diff --git a/scripts/export_stage.py b/scripts/export_stage.py
index 7856e2a..259c08a 100644
--- a/scripts/export_stage.py
+++ b/scripts/export_stage.py
@@ -14,6 +14,9 @@
"layers": {"01-distance": {"file": "images/01-distance-{name}.png", "z": 1, "scroll": 0.2}},
"states": {"anomaly": ["01-distance", ...], "clean": [...]},
"composed": {"anomaly": "images/after-{name}.png"}, # what --verify compares with
+ # a layer with "region": true is one plate for the whole region (per chunk, plates at scroll != 1
+ # overlap or leave gaps), cut into chunk-wide tiles: "file" takes {tile}, chunk j carries tile j;
+ # "rest": the chunk's own plate it was quilted from, which --verify rebuilds the chunk with
"aliases": "media-sources.json"} # optional: shared plates
Usage:
@@ -75,6 +78,12 @@ def publish(path: Path) -> str:
if layer_id not in config["layers"]:
raise ExportError(f"{region.name}: state {state} draws unknown layer {layer_id!r}")
layer = config["layers"][layer_id]
+ if layer.get("region"): # one plate for the region in chunk-wide tiles; chunk j carries tile j
+ tile = region / layer["file"].format(tile=chunk["x"])
+ if chunk["y"] != 0 or not tile.exists():
+ continue
+ layers.append({"id": layer_id, "z": layer["z"], "scroll": layer["scroll"], "file": publish(tile)})
+ continue
plate = source(region, layer["file"], chunk.get("name", chunk_id), aliases)
layers.append({"id": layer_id, "z": layer["z"], "scroll": layer["scroll"], "file": publish(plate)})
collision = region / f"{chunk_id}.{state}.collision.json"
@@ -99,7 +108,10 @@ def export(region: Path, out: Path) -> dict:
def verify(region: Path, out: Path) -> list[str]:
- """Rebuild every chunk state from the exported plates and compare with the region's art."""
+ """Rebuild every chunk state from the exported plates and compare with the region's art.
+
+ A region-wide parallax plate ("region": true) is not the chunk's own picture: for it, the chunk's
+ "rest" plate (its own filled layer, what the region plate was quilted from) stands in."""
from PIL import Image, ImageChops # only --verify needs Pillow
config = json.loads((region / "export.json").read_text(encoding="utf-8"))
@@ -111,9 +123,16 @@ def verify(region: Path, out: Path) -> list[str]:
pattern = config.get("composed", {}).get(state)
if not pattern:
continue
+ exported = {layer["id"]: out / region.name / layer["file"] for layer in content["layers"]}
+ rests = {lid: ROOT / L["rest"].format(name=name) for lid, L in config["layers"].items() if L.get("region")}
+ if any(not path.exists() for path in rests.values()):
+ report.append(f"{chunk['id']}.{state} (not checked: its region plates' sources are not in tmp/)")
+ continue
canvas = Image.new("RGBA", tuple(stage["chunkSize"]), (0, 0, 0, 0))
- for layer in content["layers"]:
- canvas = Image.alpha_composite(canvas, Image.open(out / region.name / layer["file"]).convert("RGBA"))
+ for layer_id in config["states"][state]:
+ layer = config["layers"][layer_id]
+ path = rests[layer_id] if layer.get("region") else exported[layer_id]
+ canvas = Image.alpha_composite(canvas, Image.open(path).convert("RGBA"))
art = Image.open(region / pattern.format(name=name)).convert("RGB")
if ImageChops.difference(canvas.convert("RGB"), art).getbbox():
raise ExportError(f"{region.name}/{chunk['id']}.{state}: the exported layers don't rebuild "
diff --git a/scripts/grow_stage.py b/scripts/grow_stage.py
new file mode 100644
index 0000000..8628620
--- /dev/null
+++ b/scripts/grow_stage.py
@@ -0,0 +1,338 @@
+"""Grow a stage region one chunk at a time, from the edge of the chunk next to it (docs/04, docs/09).
+
+ python scripts/grow_stage.py art/stages/
+ python scripts/grow_stage.py feature art/stages/ --box x0,y0,x1,y1 --brief "…"
+ python scripts/grow_stage.py join art/stages/ # both grown, both may change
+
+Every call to the model is one 2160x720 canvas (3:1, the API's limit): known art on one side, the area to
+paint on the other. A chunk grows in two pieces – first 1440 px next to 720 px of its neighbour, then the
+last 720 px next to that first piece – so the model always continues a wide strip of finished art and the
+new chunk lies right next to its neighbour with nothing repeated. The second piece also paints 128 px over
+the first; a minimum-error path through that overlap joins them (image quilting). The neighbour is never
+repainted. (A wider canvas padded with sky to reach
+3:1 failed: the model reframed it and moved the whole picture 60–90 px.)
+
+The image the model edits is the LAYOUT GUIDE: the board's "route" (walk lines, water, structures, ladders,
+hollows, in chunk pixels) drawn flat where the new art goes, the known art beside it. The mask says where to
+paint. Painting over the guide keeps the walkable surfaces where the route needs them.
+
+The model keeps a grown chunk's path continuous but tends to smooth route features away, so features are a
+second step: `feature` repaints one window (plus 64 px margins joined back by min-error cuts) over the guide,
+with a short concrete brief – a gap with stepping stones, a fallen log, a hill.
+
+After both pieces, the colours along the seam are matched to the neighbour (shapes untouched). Raw output
+stays in tmp/boards//: .png, its guides and the model's pieces. After review, copy it to
+the region's images/ and add it to chunks.json, layers.json and export.json.
+Needs OPENAI_API_KEY (environment or .env); the key goes in a header file (make_boards.edit).
+"""
+
+from __future__ import annotations
+
+import argparse
+import io
+import json
+import sys
+import tempfile
+from pathlib import Path
+
+from PIL import Image, ImageChops, ImageDraw
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+import make_boards as mb # noqa: E402
+
+ROOT = Path(__file__).resolve().parents[1]
+W, H = 2160, 720
+PIECES = (1440, 720) # painted per call; the rest of the canvas is known art
+OVERLAP = 128 # a later piece also paints this far over the earlier one; a min-error cut joins them
+
+PROMPT = (
+ "This image is part of a side-scrolling stage. The finished art along its {edge} side must stay exactly as it "
+ "is. Paint the rest - where a flat LAYOUT GUIDE is drawn - as the part of the stage {where} that art, one "
+ "continuous place: the ground line, the light, the palette, the fine 2-pixel grain and the scale continue "
+ "without a break, and the walkable path meets the finished art at exactly the same height. Follow the guide "
+ "exactly: red lines are the tops of the walkable surfaces - put the path, deck or step edge exactly there; "
+ "brown is solid ground under them; blue is water (by the pond it reaches up under the path); grey boxes are "
+ "built things (a gate, a bench, a shed); yellow is a ladder; purple is a hollow you can walk into; the pale "
+ "blue above is open for sky and trees. Never draw the guide's flat colours or lines themselves. Strict side "
+ "view, an architect's elevation: everything runs parallel to the screen, nothing recedes. The same depth "
+ "layers as the finished art: sky, a hazy far tree line, mid trees, the play layer, water if any, a dark "
+ "out-of-focus foreground along the bottom edge. No characters, no animals, no text.\n\n"
+ "WHAT IS HERE: {brief}")
+
+GUIDE = {"sky": (200, 220, 240), "ground": (120, 80, 50), "walk": (230, 30, 30), "water": (40, 90, 220),
+ "structure": (140, 140, 140), "ladder": (240, 220, 40), "hollow": (150, 60, 170)}
+
+
+def guide_for(route: dict, known: Image.Image, ox: int) -> Image.Image:
+ """The canvas the model edits: the route drawn flat (chunk x + ox = canvas x), the known art pasted over it."""
+ guide = Image.new("RGB", (W, H), GUIDE["sky"])
+ draw = ImageDraw.Draw(guide)
+ for line in route.get("walk", []): # ground fills from each walk line down
+ pts = [(ox + x, y) for x, y in line]
+ draw.polygon(pts + [(pts[-1][0], H), (pts[0][0], H)], fill=GUIDE["ground"])
+ if "below" in route: # what lies under the walkable layer from this height on (a pond: water)
+ y, kind = route["below"]
+ draw.rectangle((0, y, W, H), fill=GUIDE[kind])
+ for kind in ("water", "structure", "hollow", "ladder"):
+ for x1, y1, x2, y2 in route.get(kind, []):
+ draw.rectangle((ox + x1, y1, ox + x2, y2), fill=GUIDE[kind])
+ for line in route.get("walk", []):
+ draw.line([(ox + x, y) for x, y in line], fill=GUIDE["walk"], width=6)
+ guide.paste(known.convert("RGB"), mask=known.getchannel("A"))
+ return guide
+
+
+def known_canvas(strip: Image.Image, left: bool, piece: int, overlap: int = 0) -> Image.Image:
+ """W x H: the nearest W - piece px of `strip` beside `piece` px to paint; transparent = paint.
+
+ With an overlap the transparent area reaches that far over the strip too (the strip's pixels stay
+ underneath, so the model sees them, but it may repaint them)."""
+ canvas = Image.new("RGBA", (W, H))
+ keep = W - piece
+ if left: # paint on the left, the strip's left end on the right
+ canvas.paste(strip.crop((0, 0, keep, H)), (piece, 0))
+ if overlap:
+ alpha = canvas.getchannel("A")
+ alpha.paste(0, (piece, 0, piece + overlap, H))
+ canvas.putalpha(alpha)
+ else:
+ canvas.paste(strip.crop((strip.width - keep, 0, strip.width, H)), (0, 0))
+ if overlap:
+ alpha = canvas.getchannel("A")
+ alpha.paste(0, (keep - overlap, 0, keep, H))
+ canvas.putalpha(alpha)
+ return canvas
+
+
+def min_cut(a: Image.Image, b: Image.Image) -> list[int]:
+ """Per row, the column where the overlap switches from a to b: the vertical path of least colour difference.
+
+ a and b are the same overlap strip painted twice; the path moves at most one column per row (image quilting)."""
+ w, h = a.size
+ diff = ImageChops.difference(a.convert("RGB"), b.convert("RGB")).convert("L").load()
+ cost = [[diff[x, 0] for x in range(w)]]
+ for y in range(1, h):
+ prev = cost[-1]
+ cost.append([diff[x, y] + min(prev[max(0, x - 1):x + 2]) for x in range(w)])
+ x = min(range(w), key=lambda i: cost[-1][i])
+ path = [x]
+ for y in range(h - 2, -1, -1):
+ x = min(range(max(0, x - 1), min(w, x + 2)), key=lambda i: cost[y][i])
+ path.append(x)
+ return path[::-1]
+
+
+def offset(left: bool, done: int, piece: int) -> int:
+ """Canvas x minus chunk x for this piece. Growing left, the new chunk's right part is painted first."""
+ return -(W - done - piece) if left else W - piece - done
+
+
+def match_seam(new: Image.Image, neighbour: Image.Image, left: bool, ramp: int = 160, blur: int = 9) -> Image.Image:
+ """Shift the new chunk's colours near the seam so its edge column meets the neighbour's, fading out over `ramp` px.
+
+ Per row: the colour step across the seam, smoothed over ±blur rows; added in full at the seam and linearly
+ less further in. Shapes are untouched – only the light and tint the model got slightly wrong."""
+ edge_x, other_x = (W - 1, 0) if left else (0, W - 1)
+ a, b = new.convert("RGB").load(), neighbour.convert("RGB").load()
+ step = [[b[other_x, y][c] - a[edge_x, y][c] for c in range(3)] for y in range(H)]
+ smooth = [[sum(step[j][c] for j in range(max(0, y - blur), min(H, y + blur + 1))) /
+ (min(H, y + blur + 1) - max(0, y - blur)) for c in range(3)] for y in range(H)]
+ out = new.convert("RGB").copy()
+ px = out.load()
+ for i in range(ramp):
+ x = W - 1 - i if left else i
+ k = 1 - i / ramp
+ for y in range(H):
+ r, g, bl = px[x, y]
+ dr, dg, db = smooth[y]
+ px[x, y] = (max(0, min(255, round(r + dr * k))), max(0, min(255, round(g + dg * k))),
+ max(0, min(255, round(bl + db * k))))
+ return out
+
+
+def column_step(a: Image.Image, ax: int, b: Image.Image, bx: int) -> float:
+ """Mean colour jump between column ax of a and column bx of b."""
+ diff = ImageChops.difference(a.crop((ax, 0, ax + 1, H)).convert("RGB"), b.crop((bx, 0, bx + 1, H)).convert("RGB"))
+ return sum(sum(px) for px in diff.getdata()) / (3 * H)
+
+
+def seam_step(new: Image.Image, neighbour: Image.Image, left: bool) -> float:
+ """The jump across the seam between the new chunk and its neighbour."""
+ return column_step(new, W - 1, neighbour, 0) if left else column_step(neighbour, W - 1, new, 0)
+
+
+def own_step(neighbour: Image.Image, left: bool) -> float:
+ """The jump between the neighbour's own two columns at the seam: what a seamless join looks like."""
+ return column_step(neighbour, 0, neighbour, 1) if left else column_step(neighbour, W - 2, neighbour, W - 1)
+
+
+FEATURE = (
+ "This is one chunk of a side-scrolling stage. Change ONLY the transparent window: {brief} Everything outside "
+ "the window stays exactly as it is, and the window's edges join it seamlessly - the same light, palette, fine "
+ "2-pixel grain and scale. Under the window lies a flat LAYOUT GUIDE: red lines are the tops of walkable "
+ "surfaces, blue is water, brown is solid ground, grey is a built thing, yellow a ladder, purple a hollow. "
+ "Follow it exactly, but never draw its flat colours or lines. Strict side view, no characters, no text.")
+MARGIN = 64 # the feature window also repaints this much on each side; min-error cuts join it back
+
+
+def repaint(art: Image.Image, box: tuple[int, int, int, int], route: dict, prompt: str, refs_in: list[str],
+ model: str, name: str, guide_out: Path) -> Image.Image:
+ """Repaint one window of a W x H image over the layout guide, joined back on both sides by min-error cuts.
+
+ The window is the box plus MARGIN px each side; inside the box the painting wins, in the margins the
+ least visible path decides where the painting ends and the old art begins."""
+ x0, y0, x1, y1 = box
+ wx0, wx1 = max(0, x0 - MARGIN), min(W, x1 + MARGIN)
+ known = art.copy()
+ alpha = known.getchannel("A")
+ alpha.paste(0, (wx0, y0, wx1, y1))
+ known.putalpha(alpha)
+ guide = guide_for(route, known, 0)
+ guide.save(guide_out)
+ with tempfile.TemporaryDirectory() as folder:
+ base, mask = Path(folder) / "guide.png", Path(folder) / "mask.png"
+ guide.save(base)
+ known.save(mask)
+ refs = [base, *(mb.load_ref(ref, Path(folder)) for ref in refs_in)]
+ png = mb.edit(name, {"model": model, "prompt": prompt, "size": f"{W}x{H}"}, refs, mb.api_key(), mask=mask)
+ if png is None:
+ sys.exit(1)
+ painted = Image.open(io.BytesIO(png)).convert("RGBA").resize((W, H))
+ out = art.copy()
+ o, s = out.load(), painted.load()
+ left_cut = min_cut(painted.crop((wx0, y0, x0, y1)), art.crop((wx0, y0, x0, y1))) if x0 > wx0 else []
+ right_cut = min_cut(art.crop((x1, y0, wx1, y1)), painted.crop((x1, y0, wx1, y1))) if wx1 > x1 else []
+ for y in range(y0, y1):
+ start = wx0 + (left_cut[y - y0] if left_cut else 0)
+ end = x1 + (right_cut[y - y0] if right_cut else 0)
+ for x in range(start, end):
+ o[x, y] = s[x, y]
+ return out
+
+
+def feature(region: Path, chunk: str, box: tuple[int, int, int, int], brief: str) -> None:
+ """Repaint one window of a grown chunk to put a route feature there (a gap, a log, a hill), following the route."""
+ config = json.loads((region / "boards.json").read_text(encoding="utf-8"))
+ board = next(b for b in config["boards"] if b["id"] == chunk)
+ raw = ROOT / "tmp" / "boards" / region.name
+ art = Image.open(raw / f"{chunk}.png").convert("RGBA")
+ prompt = "\n\n".join([FEATURE.format(brief=brief), config["style"], config["people"]])
+ out = repaint(art, box, board.get("route", {}), prompt, board["inputs"], config["model"], f"{chunk} feature",
+ raw / f"{chunk}--feature-guide.png")
+ out.convert("RGB").save(raw / f"{chunk}.png")
+ print(f"{chunk}: feature painted in {box} -> {(raw / (chunk + '.png')).relative_to(ROOT)}")
+
+
+JOIN = ("This image shows two neighbouring chunks of a side-scrolling stage meeting in the middle. Repaint ONLY the "
+ "transparent window across the join so the two sides become one continuous place: the sky, the trees, the "
+ "path, the water and the foreground run through without a break, a cut or a dark column, in the same light, "
+ "palette, fine 2-pixel grain and scale. Keep the walkable path at the height the guide's red line shows. "
+ "Strict side view, no characters, no text.")
+
+
+def join(region: Path, left_chunk: str, right_chunk: str, half: int = 160) -> None:
+ """Repaint a window across the seam of two grown chunks (both raw, both may change) and split them again."""
+ config = json.loads((region / "boards.json").read_text(encoding="utf-8"))
+ board = next(b for b in config["boards"] if b["id"] == left_chunk)
+ raw = ROOT / "tmp" / "boards" / region.name
+ a, b = (Image.open(raw / f"{c}.png").convert("RGBA") for c in (left_chunk, right_chunk))
+ strip = Image.new("RGBA", (W, H))
+ strip.paste(a.crop((W // 2, 0, W, H)), (0, 0))
+ strip.paste(b.crop((0, 0, W // 2, H)), (W // 2, 0))
+ route = {"walk": [[[0, 441], [W, 441]]]} if "walk" in board.get("route", {}) else {}
+ prompt = "\n\n".join([JOIN, config["style"], config["people"]])
+ out = repaint(strip, (W // 2 - half, 0, W // 2 + half, H), route, prompt, board["inputs"], config["model"],
+ f"{left_chunk}|{right_chunk} join", raw / f"{left_chunk}--join-guide.png")
+ a.paste(out.crop((0, 0, W // 2, H)), (W // 2, 0))
+ b.paste(out.crop((W // 2, 0, W, H)), (0, 0))
+ a.convert("RGB").save(raw / f"{left_chunk}.png")
+ b.convert("RGB").save(raw / f"{right_chunk}.png")
+ print(f"{left_chunk}|{right_chunk}: joined (seam step now {column_step(a, W - 1, b, 0):.1f})")
+
+
+def main() -> None:
+ if len(sys.argv) > 1 and sys.argv[1] == "join":
+ parser = argparse.ArgumentParser(description="Repaint the seam between two grown chunks.")
+ parser.add_argument("step")
+ parser.add_argument("region", type=Path)
+ parser.add_argument("left")
+ parser.add_argument("right")
+ args = parser.parse_args()
+ region = args.region if args.region.is_absolute() else ROOT / args.region
+ return join(region, args.left, args.right)
+ if len(sys.argv) > 1 and sys.argv[1] == "feature":
+ parser = argparse.ArgumentParser(description="Repaint one window of a grown chunk with a route feature.")
+ parser.add_argument("step")
+ parser.add_argument("region", type=Path)
+ parser.add_argument("chunk")
+ parser.add_argument("--box", required=True, help="x0,y0,x1,y1 in chunk pixels")
+ parser.add_argument("--brief", required=True)
+ args = parser.parse_args()
+ region = args.region if args.region.is_absolute() else ROOT / args.region
+ return feature(region, args.chunk, tuple(int(v) for v in args.box.split(",")), args.brief)
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ parser.add_argument("region", type=Path)
+ parser.add_argument("source")
+ parser.add_argument("direction", choices=["left", "right"])
+ parser.add_argument("new")
+ args = parser.parse_args()
+ region = args.region if args.region.is_absolute() else ROOT / args.region
+ config = json.loads((region / "boards.json").read_text(encoding="utf-8"))
+ board = next((b for b in config["boards"] if b["id"] == args.new), None)
+ if board is None:
+ sys.exit(f"{args.new}: add its board (id, brief, route, inputs) to {region.name}/boards.json first")
+ raw = ROOT / "tmp" / "boards" / region.name
+ raw.mkdir(parents=True, exist_ok=True)
+ source = raw / f"{args.source}.png"
+ if not source.exists():
+ source = region / "images" / f"{args.source}.png"
+ neighbour = Image.open(source).convert("RGBA")
+ left = args.direction == "left"
+ prompt = "\n\n".join([PROMPT.format(edge="right" if left else "left",
+ where="to the left of" if left else "to the right of", brief=board["prompt"]),
+ config["style"], config["people"]])
+ strip, done = neighbour, 0 # the known art, growing piece by piece; px of the new chunk painted so far
+ for n, piece in enumerate(PIECES, 1):
+ overlap = OVERLAP if n > 1 else 0 # the first piece meets the neighbour, which stays exact
+ known = known_canvas(strip, left, piece, overlap) # the mask: the overlap is open for painting
+ guide = guide_for(board.get("route", {}), known_canvas(strip, left, piece), offset(left, done, piece))
+ guide.save(raw / f"{args.new}--guide-{n}.png")
+ with tempfile.TemporaryDirectory() as folder:
+ base, mask = Path(folder) / "guide.png", Path(folder) / "mask.png"
+ guide.save(base)
+ known.save(mask) # transparent = paint here
+ refs = [base, *(mb.load_ref(ref, Path(folder)) for ref in board["inputs"])]
+ png = mb.edit(f"{args.new} piece {n}", {"model": config["model"], "prompt": prompt, "size": f"{W}x{H}"},
+ refs, mb.api_key(), mask=mask)
+ if png is None:
+ sys.exit(1)
+ painted = Image.open(io.BytesIO(png)).convert("RGBA").resize((W, H))
+ painted.save(raw / f"{args.new}--piece-{n}.png")
+ part = painted.crop((0, 0, piece, H)) if left else painted.crop((W - piece, 0, W, H))
+ grown = Image.new("RGBA", (strip.width + piece, H))
+ grown.paste(part, (0, 0) if left else (strip.width, 0))
+ grown.paste(strip, (piece, 0) if left else (0, 0))
+ if overlap: # join the new piece to the earlier one along the least visible path through the overlap
+ ox = piece if left else strip.width - overlap # the overlap in `grown`
+ px0 = piece if left else W - piece - overlap # the same columns in `painted`
+ new_side = painted.crop((px0, 0, px0 + overlap, H))
+ old_side = grown.crop((ox, 0, ox + overlap, H))
+ cut = min_cut(new_side, old_side)
+ g, s = grown.load(), new_side.load()
+ for y, c in enumerate(cut):
+ for x in (range(c) if left else range(c, overlap)):
+ g[ox + x, y] = s[x, y]
+ strip, done = grown, done + piece
+ new = (strip.crop((0, 0, W, H)) if left else strip.crop((strip.width - W, 0, strip.width, H))).convert("RGB")
+ before = seam_step(new, neighbour, left)
+ new = match_seam(new, neighbour, left)
+ new.save(raw / f"{args.new}.png")
+ strip.paste(new, (0, 0) if left else (strip.width - W, 0))
+ strip.convert("RGB").save(raw / f"{args.new}--with-neighbour.png")
+ print(f"{args.new}: grown {args.direction} of {args.source} -> {(raw / (args.new + '.png')).relative_to(ROOT)} "
+ f"(seam step {before:.1f} -> {seam_step(new, neighbour, left):.1f}; "
+ f"the neighbour's own column step is {own_step(neighbour, left):.1f})")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/split_layers.py b/scripts/split_layers.py
index b3f6c6f..20de8b5 100644
--- a/scripts/split_layers.py
+++ b/scripts/split_layers.py
@@ -35,6 +35,11 @@
LAYERS = ("00-sky", "01-far", "02-mid", "03-rear", "04-water", "05-play", "06-near", "07-foreground")
+def filled(path: Path) -> bool:
+ """depth_layers.py's parallax plates (..filled.png) live next to the split; never ours to remove."""
+ return path.name.endswith(".filled.png")
+
+
class SplitError(Exception):
"""The config or the files on disk do not add up; the message says where."""
@@ -143,7 +148,7 @@ def run(region: Path, check: bool) -> list[str]:
raise SplitError(f"{chunk_id}: its states have different sizes {sizes}")
size = sizes.pop()
if check:
- on_disk = sorted(p.name for p in (region / "layers").glob(f"{chunk_id}.*.png"))
+ on_disk = sorted(p.name for p in (region / "layers").glob(f"{chunk_id}.*.png") if not filled(p))
for state, source in sources.items():
if not same(stack(load(region, chunk_id, state, on_disk), size), source):
raise SplitError(f"{chunk_id}.{state}: the committed layers no longer rebuild "
@@ -160,7 +165,7 @@ def run(region: Path, check: bool) -> list[str]:
out = region / "layers"
out.mkdir(exist_ok=True)
for old in out.glob(f"{chunk_id}.*.png"):
- if old.name not in files:
+ if old.name not in files and not filled(old):
old.unlink()
for name, image in files.items():
image.save(out / name, optimize=True)
diff --git a/scripts/test_depth_layers.py b/scripts/test_depth_layers.py
index 2bb883d..16d57e4 100644
--- a/scripts/test_depth_layers.py
+++ b/scripts/test_depth_layers.py
@@ -56,6 +56,49 @@ def test_skyline_spans_every_column(self):
self.assertEqual(dl.skyline(own, 8, 5, smooth=0), [5, 5, 1, 1, 3, 3, 5, 5]) # no plate past the ends
self.assertEqual(dl.skyline(own, 8, 5, smooth=1)[4], 1)
+ def test_walk_lines_turn_a_small_step_into_a_ramp_and_split_at_a_cliff(self):
+ # 90 wide, 80 tall: floor y=60 for x<30, a 10 px step up to y=50 until x=60, a 30 px cliff up to y=20.
+ w, h = 90, 80
+ bands = ["sky"] * (w * h)
+ for x in range(w):
+ top = 60 if x < 30 else 50 if x < 60 else 20
+ for y in range(top, h):
+ bands[y * w + x] = "play"
+ lines = dl.walk_lines(bands, w, h, step=12, shortest=10)
+ self.assertEqual(len(lines), 2)
+ first = lines[0]
+ self.assertEqual((first[0], first[-1]), ([0, 60], [60, 50]))
+ self.assertEqual(first, [[0, 60], [23, 60], [36, 50], [60, 50]]) # the step became a ramp, ~38°
+ self.assertEqual(lines[1], [[60, 20], [90, 20]])
+
+ def test_walk_lines_find_a_hollow_under_the_path(self):
+ # The path at y=10 across, and a hollow floor at y=30 under x=20..40 (sky between).
+ w, h = 60, 40
+ bands = ["play"] * (w * h)
+ for x in range(w):
+ for y in range(10):
+ bands[y * w + x] = "sky"
+ for x in range(20, 40):
+ for y in range(20, 30):
+ bands[y * w + x] = "rear"
+ self.assertEqual(dl.walk_lines(bands, w, h, step=5, shortest=10), [[[0, 10], [60, 10]]]) # top only
+ lines = dl.walk_lines(bands, w, h, step=5, shortest=10, every=True)
+ self.assertIn([[0, 10], [60, 10]], lines)
+ self.assertIn([[20, 30], [40, 30]], lines)
+
+ def test_rdp_keeps_corners_only(self):
+ pts = [(x, 10) for x in range(10)] + [(10 + x, 10 - x) for x in range(10)]
+ self.assertEqual(dl.rdp(pts, 1.0), [(0, 10), (10, 10), (19, 1)])
+
+ def test_region_plate_places_each_chunk_at_its_scroll_offset(self):
+ # Two 100x4 plates at scroll 0.5: the strip is 150 wide, the second plate starts at 50.
+ a = Image.new("RGBA", (100, 4), (255, 0, 0, 255))
+ b = Image.new("RGBA", (100, 4), (0, 0, 255, 255))
+ strip = dl.region_plate([a, b], 0.5, window=20)
+ self.assertEqual(strip.size, (150, 4))
+ self.assertEqual(strip.getpixel((10, 1)), (255, 0, 0, 255)) # chunk 0 at rest
+ self.assertEqual(strip.getpixel((140, 1)), (0, 0, 255, 255)) # chunk 1 past the window
+
def test_masks_are_written_for_the_parallax_bands_only(self):
with tempfile.TemporaryDirectory() as tmp:
region = Path(tmp)