diff --git a/.gitignore b/.gitignore index 3f0d5d2..52116f9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ *.log assets/work/ dist/ +pilot/course-1.html diff --git a/pilot/assemble.py b/pilot/assemble.py new file mode 100644 index 0000000..b494078 --- /dev/null +++ b/pilot/assemble.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Derive the Course I slice manifest from measured geometry, then assemble the pilot. + +The cut lines below are NOT eyeballed. They were fitted from the hero's own pixels: +each slab's bottom edge tracked column-by-column through the void gaps, least-squares +lines through the left and right arms, tip at their intersection. Symmetry of the two +arms came out within 0-10% on every slab, which is the check that the fit is real. +Re-derive with tools/fit-geometry.py if the hero is ever regenerated. +""" +import base64, json, pathlib, re, sys + +ROOT = pathlib.Path(__file__).resolve().parent.parent +HERO = ROOT / "assets/dist/docker-layers.webp" + +# measured bottom silhouettes: left corner -> bottom tip -> right corner (percent of frame) +CUTS = { + "writable": dict(l=(27.75, 27.24), tip=(50.77, 40.73), r=(72.00, 26.87)), + "code": dict(l=(28.38, 41.34), tip=(49.76, 56.15), r=(71.50, 41.02)), + "deps": dict(l=(27.88, 56.36), tip=(49.48, 76.21), r=(71.88, 56.30)), + "base": dict(l=(28.12, 74.41), tip=(49.95, 94.20), r=(71.75, 74.40)), +} +EPS = 0.7 # regions grow UPWARD only โ€” the slab in front always covers the overlap. + # Growing downward would drag a copy of the slab behind into the slice. +NEST = 0.75 # fraction of each gap closed when collapsed + +ORDER = ["base", "deps", "code", "writable"] # paint order: back -> front +LABEL = { + "base": ("Base image", "The OS layer everything else is stacked on. Pull it once, " + "share it across every image on the host."), + "deps": ("Dependencies", "Your packages and runtime. Change these and every layer " + "above has to be rebuilt."), + "code": ("Application code", "Your actual program โ€” usually the smallest layer, and " + "the one that changes every single build."), + "writable": ("Writable layer", "Created fresh per container, thrown away when it dies. " + "Anything written here is not in the image."), +} + + +def V(c): return [list(c["l"]), list(c["tip"]), list(c["r"])] +def up(v, e): return [[p[0], round(p[1] - e, 2)] for p in v] + + +def band(top, bot): + """Region between two silhouettes; sides run to the frame edge, which is pure void.""" + p = [] + p += ([[0, top[0][1]]] + top + [[100, top[2][1]]]) if top else [[0, 0], [100, 0]] + p += ([[100, bot[2][1]]] + bot[::-1] + [[0, bot[0][1]]]) if bot else [[100, 100], [0, 100]] + return p + + +def manifest(): + tips = {k: CUTS[k]["tip"][1] for k in CUTS} + dy, acc = {}, 0.0 + for i, k in enumerate(ORDER): + if i: + acc += (tips[ORDER[i - 1]] - tips[k]) * NEST + dy[k] = round(acc, 2) + + regions = { + "base": band(up(V(CUTS["deps"]), EPS), None), + "deps": band(up(V(CUTS["code"]), EPS), V(CUTS["deps"])), + "code": band(up(V(CUTS["writable"]), EPS), V(CUTS["code"])), + "writable": band(None, V(CUTS["writable"])), + } + return { + "scene": "course-I-docker-layers", "eps": EPS, "nest": NEST, + "source": "assets/dist/docker-layers.webp", + "parts": [{"name": k, "title": LABEL[k][0], "blurb": LABEL[k][1], + "anchor": round(CUTS[k]["tip"][1] - 6, 2), + "points": regions[k], "dy": dy[k]} for k in ORDER], + } + + +def main(): + if not HERO.exists(): + sys.exit(f"missing hero: {HERO}") + man = manifest() + (ROOT / "pilot/manifest.json").write_text(json.dumps(man, indent=1)) + + hero = "data:image/webp;base64," + base64.b64encode(HERO.read_bytes()).decode() + out = (ROOT / "pilot/course-1.tpl.html").read_text() \ + .replace("__MANIFEST__", json.dumps(man, separators=(",", ":"))) \ + .replace("__HERO__", hero) + out = (f"\n" + f"\n" + out) + + dest = ROOT / "pilot/course-1.html" + dest.write_text(out) + + blocks = re.findall(r"", out) + print(f"built {dest.relative_to(ROOT)} {len(out)/1024:.0f}KB " + f"({len(blocks)} script blocks, hero {len(hero)/1024:.0f}KB inlined)") + for name, part in zip(ORDER, man["parts"]): + print(f" {name:<9} {len(part['points']):2d} pts collapse travel {part['dy']:5.2f}%") + + +if __name__ == "__main__": + main() diff --git a/pilot/course-1.tpl.html b/pilot/course-1.tpl.html new file mode 100644 index 0000000..c24a97d --- /dev/null +++ b/pilot/course-1.tpl.html @@ -0,0 +1,193 @@ +
+

The Exploded Cluster ยท Course I

+

An image is not a box.
It is a stack of frozen diffs.

+

Scroll, and the thing you keep calling "a container image" comes apart in your hands. + Four layers. Each one only stores what changed from the layer under it.

+ +
+ +
+

Docker image layers, exploded

+ +
+
+
+ An exploded isometric view of a container image: four stacked slabs floating apart โ€”
+                  a heavy metal base, a circuit-etched dependency layer, a magenta-traced code layer,
+                  and a thin frosted-glass writable layer on top. +
+ +
    +
    +
    +
    + +
    +

    + Field note. The order of your Dockerfile is a caching decision, not a style choice. + Put COPY . . above your dependency install and you have told the builder to + throw away every cached layer every time you change one line of code. +

    +
    + + + + diff --git a/pilot/manifest.json b/pilot/manifest.json new file mode 100644 index 0000000..8ad2bdf --- /dev/null +++ b/pilot/manifest.json @@ -0,0 +1,180 @@ +{ + "scene": "course-I-docker-layers", + "eps": 0.7, + "nest": 0.75, + "source": "assets/dist/docker-layers.webp", + "parts": [ + { + "name": "base", + "title": "Base image", + "blurb": "The OS layer everything else is stacked on. Pull it once, share it across every image on the host.", + "anchor": 88.2, + "points": [ + [ + 0, + 55.66 + ], + [ + 27.88, + 55.66 + ], + [ + 49.48, + 75.51 + ], + [ + 71.88, + 55.6 + ], + [ + 100, + 55.6 + ], + [ + 100, + 100 + ], + [ + 0, + 100 + ] + ], + "dy": 0.0 + }, + { + "name": "deps", + "title": "Dependencies", + "blurb": "Your packages and runtime. Change these and every layer above has to be rebuilt.", + "anchor": 70.21, + "points": [ + [ + 0, + 40.64 + ], + [ + 28.38, + 40.64 + ], + [ + 49.76, + 55.45 + ], + [ + 71.5, + 40.32 + ], + [ + 100, + 40.32 + ], + [ + 100, + 56.3 + ], + [ + 71.88, + 56.3 + ], + [ + 49.48, + 76.21 + ], + [ + 27.88, + 56.36 + ], + [ + 0, + 56.36 + ] + ], + "dy": 13.49 + }, + { + "name": "code", + "title": "Application code", + "blurb": "Your actual program \u2014 usually the smallest layer, and the one that changes every single build.", + "anchor": 50.15, + "points": [ + [ + 0, + 26.54 + ], + [ + 27.75, + 26.54 + ], + [ + 50.77, + 40.03 + ], + [ + 72.0, + 26.17 + ], + [ + 100, + 26.17 + ], + [ + 100, + 41.02 + ], + [ + 71.5, + 41.02 + ], + [ + 49.76, + 56.15 + ], + [ + 28.38, + 41.34 + ], + [ + 0, + 41.34 + ] + ], + "dy": 28.54 + }, + { + "name": "writable", + "title": "Writable layer", + "blurb": "Created fresh per container, thrown away when it dies. Anything written here is not in the image.", + "anchor": 34.73, + "points": [ + [ + 0, + 0 + ], + [ + 100, + 0 + ], + [ + 100, + 26.87 + ], + [ + 72.0, + 26.87 + ], + [ + 50.77, + 40.73 + ], + [ + 27.75, + 27.24 + ], + [ + 0, + 27.24 + ] + ], + "dy": 40.1 + } + ] +} \ No newline at end of file