repo: remove superseded pilot files, ASCII-clean the build and nginx configs
Drops the legacy course-1 template, the two python assemblers the node assembler replaced, and the unversioned scene manifest left over from the manifest-<type> split - none referenced by build.sh, the assembler or the README, and none produce build output. The v2 pages stay: the README lists them as parked. Also converts typographic dashes to ASCII in build.sh and the two nginx configs, and corrects an assembler comment that still pointed at the removed python build.
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Assemble dist/ — the CSP-clean static build served at learn.bztmon.com.
|
# Assemble dist/ - the CSP-clean static build served at learn.bztmon.com.
|
||||||
# The site's face is the delivery arc (index.html); scripts stay SEPARATE files so
|
# The site's face is the delivery arc (index.html); scripts stay SEPARATE files so
|
||||||
# `script-src 'self'` holds with no 'unsafe-inline' exception. Assembly logic lives
|
# `script-src 'self'` holds with no 'unsafe-inline' exception. Assembly logic lives
|
||||||
# in pilot/assemble-arc.mjs (node — the build image carries no python).
|
# in pilot/assemble-arc.mjs (node - the build image carries no python).
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
IFS=$'\n\t'
|
IFS=$'\n\t'
|
||||||
trap 'printf "build failed at line %s\n" "$LINENO" >&2' ERR
|
trap 'printf "build failed at line %s\n" "$LINENO" >&2' ERR
|
||||||
@@ -35,7 +35,7 @@ node pilot/assemble-arc.mjs "$dist"
|
|||||||
# No inline JS may survive into ANY served HTML, or the CSP silently kills the page.
|
# No inline JS may survive into ANY served HTML, or the CSP silently kills the page.
|
||||||
for page in "$dist"/*.html; do
|
for page in "$dist"/*.html; do
|
||||||
if grep -oiE '<script[^>]*>' -- "$page" | grep -qivE 'src='; then
|
if grep -oiE '<script[^>]*>' -- "$page" | grep -qivE 'src='; then
|
||||||
printf '%s contains an inline <script> — CSP script-src self would block it\n' "$page" >&2
|
printf '%s contains an inline <script> - CSP script-src self would block it\n' "$page" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@ server {
|
|||||||
|
|
||||||
server_tokens off;
|
server_tokens off;
|
||||||
|
|
||||||
# Behind the Cloudflare Tunnel on :443 while nginx listens on :8080 — without these,
|
# Behind the Cloudflare Tunnel on :443 while nginx listens on :8080 - without these,
|
||||||
# nginx emits ABSOLUTE redirects to http://host:8080/... and breaks every link.
|
# nginx emits ABSOLUTE redirects to http://host:8080/... and breaks every link.
|
||||||
absolute_redirect off;
|
absolute_redirect off;
|
||||||
port_in_redirect off;
|
port_in_redirect off;
|
||||||
@@ -51,7 +51,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ---- Compression ---------------------------------------------------------
|
# ---- Compression ---------------------------------------------------------
|
||||||
# WebP/PNG are already compressed — gzipping them burns CPU for nothing.
|
# WebP/PNG are already compressed - gzipping them burns CPU for nothing.
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_comp_level 6;
|
gzip_comp_level 6;
|
||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Shared security headers. `include`d in the server block AND in every location that
|
# Shared security headers. `include`d in the server block AND in every location that
|
||||||
# sets its own add_header — a location-level add_header REPLACES inherited headers
|
# sets its own add_header - a location-level add_header REPLACES inherited headers
|
||||||
# rather than merging them (the classic nginx footgun).
|
# rather than merging them (the classic nginx footgun).
|
||||||
#
|
#
|
||||||
# HSTS is host-scoped: NO includeSubDomains / preload, because the *.bztmon.com
|
# HSTS is host-scoped: NO includeSubDomains / preload, because the *.bztmon.com
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Reads pilot/arc.tpl.html, splits the app <script> out to a fingerprinted arc.js, injects
|
// Reads pilot/arc.tpl.html, splits the app <script> out to a fingerprinted arc.js, injects
|
||||||
// the scene manifests, fingerprints every /assets/ file, sets intrinsic image dimensions,
|
// the scene manifests, fingerprints every /assets/ file, sets intrinsic image dimensions,
|
||||||
// wraps a full HTML document, writes build.json, and runs preflight gates.
|
// wraps a full HTML document, writes build.json, and runs preflight gates.
|
||||||
// Runs on bare node (the build image has no python). Artifact build = assemble-arc.py.
|
// Runs on bare node (the build image has no python). Superseded the earlier python assembler.
|
||||||
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Assemble the delivery-arc page: tpl + inlined GSAP + scene manifests + base64 heroes.
|
|
||||||
|
|
||||||
Heroes are inlined ONCE (in the <img> src); the slice engine reuses img.src for the
|
|
||||||
slab backgrounds, so nothing is duplicated. Manifests come from tools/slice-arc.py.
|
|
||||||
"""
|
|
||||||
import base64, json, pathlib, re
|
|
||||||
|
|
||||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
||||||
|
|
||||||
def b64(webp):
|
|
||||||
return "data:image/webp;base64," + base64.b64encode((ROOT / "assets/dist" / webp).read_bytes()).decode()
|
|
||||||
|
|
||||||
def man(tag):
|
|
||||||
return json.dumps(json.loads((ROOT / "pilot" / f"manifest-{tag}.json").read_text()),
|
|
||||||
separators=(",", ":"))
|
|
||||||
|
|
||||||
out = (ROOT / "pilot/arc.tpl.html").read_text() \
|
|
||||||
.replace("__MAN_SURROUND__", man("surround")) \
|
|
||||||
.replace("__MAN_NODE__", man("node")) \
|
|
||||||
.replace("__MAN_POD__", man("pod")) \
|
|
||||||
.replace("__MAN_TRAFFIC__", man("traffic")) \
|
|
||||||
.replace("__MAN_ORBIT__", man("orbit")) \
|
|
||||||
.replace("__MAN_DOCKER__", man("docker")) \
|
|
||||||
.replace("__MAN_CLUSTER__", man("cluster")) \
|
|
||||||
.replace("__MAN_GITOPS__", man("gitops")) \
|
|
||||||
.replace("__MAN_SUPPLY__", man("supply")) \
|
|
||||||
.replace("__MAN_HELM__", man("helm")) \
|
|
||||||
.replace("__HERO_INTRO__", b64("intro-establishing.webp")) \
|
|
||||||
.replace("__HERO_SURROUND__", b64("course-00-surround.webp")) \
|
|
||||||
.replace("__HERO_NODE__", b64("course-III-node.webp")) \
|
|
||||||
.replace("__HERO_POD__", b64("course-III-pod.webp")) \
|
|
||||||
.replace("__HERO_TRAFFIC__", b64("course-IV-traffic.webp")) \
|
|
||||||
.replace("__HERO_ORBIT__", b64("course-V-orbit.webp")) \
|
|
||||||
.replace("__HERO_DOCKER__", b64("docker-layers.webp")) \
|
|
||||||
.replace("__HERO_CLUSTER__", b64("course-II-cluster.webp")) \
|
|
||||||
.replace("__HERO_GITOPS__", b64("course-VI-gitops.webp")) \
|
|
||||||
.replace("__HERO_SUPPLY__", b64("course-VII-supply-chain.webp")) \
|
|
||||||
.replace("__HERO_HELM__", b64("course-VIII-helm-press.webp")) \
|
|
||||||
.replace("__HERO_APPENDIX__", b64("appendix-dependency-ledger.webp")) \
|
|
||||||
.replace("__DIAG_FLOW__", "data:image/svg+xml;base64," + base64.b64encode((ROOT / "assets/dist/delivery-flow.svg").read_bytes()).decode())
|
|
||||||
|
|
||||||
assert "__MAN_" not in out and "__HERO_" not in out and "__DIAG_" not in out, "unfilled placeholder"
|
|
||||||
|
|
||||||
out = (f"<script>{(ROOT / 'gsap.min.js').read_text()}</script>\n"
|
|
||||||
f"<script>{(ROOT / 'st.min.js').read_text()}</script>\n" + out)
|
|
||||||
|
|
||||||
dest = ROOT / "pilot/arc.html"
|
|
||||||
dest.write_text(out)
|
|
||||||
heroes = len(re.findall(r"data:image/webp", out))
|
|
||||||
print(f"built pilot/arc.html {len(out)/1024:.0f}KB ({heroes} heroes inlined)")
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
#!/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"<script>{(ROOT / 'gsap.min.js').read_text()}</script>\n"
|
|
||||||
f"<script>{(ROOT / 'st.min.js').read_text()}</script>\n" + out)
|
|
||||||
|
|
||||||
dest = ROOT / "pilot/course-1.html"
|
|
||||||
dest.write_text(out)
|
|
||||||
|
|
||||||
blocks = re.findall(r"<script>([\s\S]*?)</script>", 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()
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
<header class="masthead">
|
|
||||||
<p class="eyebrow">The Exploded Cluster · Course I</p>
|
|
||||||
<h1>An image is not a box.<br><em>It is a stack of frozen diffs.</em></h1>
|
|
||||||
<p class="lede">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.</p>
|
|
||||||
<p class="scrollcue" aria-hidden="true">scroll<span></span></p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="course" id="course-1" aria-labelledby="c1h">
|
|
||||||
<h2 id="c1h" class="sr-only">Docker image layers, exploded</h2>
|
|
||||||
|
|
||||||
<div class="pin">
|
|
||||||
<div class="viewport">
|
|
||||||
<figure class="scene" id="scene">
|
|
||||||
<img class="flat" src="__HERO__"
|
|
||||||
alt="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.">
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<ol class="legend" id="legend"></ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="after">
|
|
||||||
<p class="fieldnote">
|
|
||||||
<b>Field note.</b> The order of your Dockerfile is a caching decision, not a style choice.
|
|
||||||
Put <code>COPY . .</code> above your dependency install and you have told the builder to
|
|
||||||
throw away every cached layer every time you change one line of code.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:root{
|
|
||||||
--void:#070b14; --ink:#c9d7ef; --dim:#7286a8; --line:#1b2740;
|
|
||||||
--cyan:#3fbaf5; --magenta:#e879f9;
|
|
||||||
--sans:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
||||||
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
|
||||||
}
|
|
||||||
*{box-sizing:border-box}
|
|
||||||
body{margin:0;background:var(--void);color:var(--ink);font-family:var(--sans);
|
|
||||||
-webkit-font-smoothing:antialiased;overflow-x:hidden}
|
|
||||||
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
|
|
||||||
clip:rect(0 0 0 0);white-space:nowrap;border:0}
|
|
||||||
|
|
||||||
/* ---- masthead ---- */
|
|
||||||
.masthead{max-width:64ch;margin:0 auto;padding:18vh 24px 10vh;text-align:center}
|
|
||||||
.eyebrow{font-family:var(--mono);font-size:.72rem;letter-spacing:.22em;text-transform:uppercase;
|
|
||||||
color:var(--cyan);margin:0 0 1.6rem}
|
|
||||||
.masthead h1{font-size:clamp(2rem,5.5vw,3.6rem);line-height:1.08;margin:0 0 1.4rem;
|
|
||||||
font-weight:600;letter-spacing:-.02em;text-wrap:balance}
|
|
||||||
.masthead h1 em{font-style:normal;color:var(--magenta)}
|
|
||||||
.lede{font-size:clamp(1rem,1.7vw,1.16rem);line-height:1.65;color:var(--dim);margin:0 auto;max-width:52ch}
|
|
||||||
.scrollcue{font-family:var(--mono);font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
|
|
||||||
color:var(--dim);margin-top:5rem;display:flex;flex-direction:column;align-items:center;gap:.7rem}
|
|
||||||
.scrollcue span{display:block;width:1px;height:46px;
|
|
||||||
background:linear-gradient(var(--cyan),transparent)}
|
|
||||||
|
|
||||||
/* ---- pinned scene ---- */
|
|
||||||
.course{position:relative}
|
|
||||||
.pin{min-height:100svh;display:grid;place-items:center;padding:3vh 20px}
|
|
||||||
.viewport{width:min(1180px,100%);display:grid;grid-template-columns:minmax(0,1fr) 300px;
|
|
||||||
gap:28px;align-items:center}
|
|
||||||
.scene{position:relative;margin:0;line-height:0}
|
|
||||||
.scene .flat{display:block;width:100%;height:auto}
|
|
||||||
.slab{position:absolute;inset:0;background-size:100% 100%;background-repeat:no-repeat;
|
|
||||||
will-change:transform}
|
|
||||||
|
|
||||||
/* ---- legend ---- */
|
|
||||||
.legend{position:relative;list-style:none;margin:0;padding:0;height:100%;min-height:min(58vh,540px)}
|
|
||||||
.legend li{position:absolute;left:0;right:0;transform:translateY(-50%);
|
|
||||||
padding-left:26px;border-left:1px solid var(--line);
|
|
||||||
opacity:.34;transition:opacity .35s ease,border-color .35s ease}
|
|
||||||
.legend li.lit{opacity:1;border-left-color:var(--cyan)}
|
|
||||||
.legend .n{font-family:var(--mono);font-size:.68rem;letter-spacing:.16em;color:var(--cyan);
|
|
||||||
display:block;margin-bottom:.3rem}
|
|
||||||
.legend .t{font-size:.98rem;font-weight:600;margin:0 0 .3rem;letter-spacing:-.01em}
|
|
||||||
.legend .b{font-size:.8rem;line-height:1.55;color:var(--dim);margin:0}
|
|
||||||
|
|
||||||
.after{max-width:60ch;margin:0 auto;padding:12vh 24px 22vh}
|
|
||||||
.fieldnote{border-left:2px solid var(--magenta);padding:0 0 0 20px;margin:0;
|
|
||||||
font-size:.94rem;line-height:1.7;color:var(--dim)}
|
|
||||||
.fieldnote b{color:var(--magenta);font-family:var(--mono);font-size:.72rem;
|
|
||||||
letter-spacing:.16em;text-transform:uppercase;display:block;margin-bottom:.5rem}
|
|
||||||
.fieldnote code{font-family:var(--mono);font-size:.86em;color:var(--ink);
|
|
||||||
background:#101a2c;padding:.12em .4em;border-radius:3px}
|
|
||||||
|
|
||||||
/* ---- small screens: no pin, legend stacks under the scene ---- */
|
|
||||||
@media (max-width:860px){
|
|
||||||
.viewport{grid-template-columns:1fr;gap:20px}
|
|
||||||
.legend{height:auto;min-height:0;display:flex;flex-direction:column;gap:14px}
|
|
||||||
.legend li{position:static;transform:none;opacity:1;border-left-color:var(--line)}
|
|
||||||
.legend li.lit{border-left-color:var(--cyan)}
|
|
||||||
.pin{min-height:0;padding:4vh 18px 8vh}
|
|
||||||
.masthead{padding:10vh 22px 6vh}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- pilot bench (HUD + hand scrub) - strip before P3 wiring ---- */
|
|
||||||
.bench{width:min(720px,100%);margin:18px auto 0;text-align:center}
|
|
||||||
.bench input{width:100%;accent-color:var(--cyan)}
|
|
||||||
.bench .hud{font-family:var(--mono);font-size:.68rem;letter-spacing:.06em;
|
|
||||||
color:var(--dim);margin:.5rem 0 0}
|
|
||||||
|
|
||||||
/* ---- no JS: the flat hero and the full legend are the page ---- */
|
|
||||||
body:not(.fx) .slab{display:none}
|
|
||||||
body:not(.fx) .legend li{position:static;transform:none;opacity:1}
|
|
||||||
body:not(.fx) .legend{height:auto;min-height:0;display:flex;flex-direction:column;gap:14px}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(() => {
|
|
||||||
"use strict";
|
|
||||||
const MAN = __MANIFEST__;
|
|
||||||
const HERO = "__HERO__";
|
|
||||||
const scene = document.getElementById("scene");
|
|
||||||
const legend = document.getElementById("legend");
|
|
||||||
const RM = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
||||||
|
|
||||||
// ---- legend (always built, JS or not the markup would be empty otherwise) ----
|
|
||||||
MAN.parts.forEach((p, i) => {
|
|
||||||
const li = document.createElement("li");
|
|
||||||
li.dataset.part = p.name;
|
|
||||||
li.style.top = p.anchor + "%";
|
|
||||||
li.innerHTML =
|
|
||||||
'<span class="n">' + String(i + 1).padStart(2, "0") + '</span>' +
|
|
||||||
'<p class="t">' + p.title + '</p>' +
|
|
||||||
'<p class="b">' + p.blurb + '</p>';
|
|
||||||
legend.appendChild(li);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!window.gsap || !window.ScrollTrigger) return; // progressive enhancement: leave the flat hero
|
|
||||||
document.body.classList.add("fx");
|
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
|
||||||
|
|
||||||
// ---- slices: one layer per part, all cut from the SAME hero ----
|
|
||||||
const slabs = MAN.parts.map(p => {
|
|
||||||
const d = document.createElement("div");
|
|
||||||
d.className = "slab";
|
|
||||||
d.dataset.part = p.name;
|
|
||||||
d.style.backgroundImage = "url(" + HERO + ")";
|
|
||||||
d.style.clipPath = "polygon(" + p.points.map(q => q[0] + "% " + q[1] + "%").join(",") + ")";
|
|
||||||
scene.appendChild(d);
|
|
||||||
return d;
|
|
||||||
});
|
|
||||||
scene.querySelector(".flat").style.visibility = "hidden"; // slices replace it; alt text survives
|
|
||||||
|
|
||||||
const lis = [...legend.children];
|
|
||||||
const setLit = t => lis.forEach((li, i) => {
|
|
||||||
// a layer "arrives" as its own travel completes; base is home from the start
|
|
||||||
const share = MAN.parts[i].dy / (MAN.parts[MAN.parts.length - 1].dy || 1);
|
|
||||||
li.classList.toggle("lit", t >= share * 0.82);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Travel is CLAMPED at the hero layout: collapsed -> hero, never past it and never
|
|
||||||
// sideways. Occlusion only increases on the way in, which is what keeps each slice's
|
|
||||||
// hidden notch buried. Overshoot or fan and the tears show.
|
|
||||||
const apply = t => {
|
|
||||||
slabs.forEach((el, i) => {
|
|
||||||
el.style.transform = "translate3d(0," + (MAN.parts[i].dy * (1 - t)).toFixed(3) + "%,0)";
|
|
||||||
});
|
|
||||||
scene.style.transform = "scale(" + (0.94 + 0.06 * t).toFixed(4) + ")";
|
|
||||||
setLit(t);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---- pilot bench: HUD + hand scrub - strip before P3 wiring ----
|
|
||||||
const bench = document.createElement("div");
|
|
||||||
bench.className = "bench";
|
|
||||||
bench.innerHTML =
|
|
||||||
'<input type="range" min="0" max="1000" value="0" aria-label="scrub the teardown">' +
|
|
||||||
'<p class="hud">boot</p>';
|
|
||||||
document.querySelector(".pin").appendChild(bench);
|
|
||||||
const hud = bench.querySelector(".hud");
|
|
||||||
const slider = bench.querySelector("input");
|
|
||||||
let mode = "boot";
|
|
||||||
const show = v => {
|
|
||||||
hud.textContent = mode + " \u00b7 " + innerWidth + "x" + innerHeight +
|
|
||||||
" \u00b7 scroll " + Math.round(scrollY) + " of " +
|
|
||||||
Math.max(0, document.documentElement.scrollHeight - innerHeight) +
|
|
||||||
" \u00b7 t=" + v.toFixed(2);
|
|
||||||
slider.value = String(Math.round(v * 1000));
|
|
||||||
};
|
|
||||||
const drive = v => { apply(v); show(v); };
|
|
||||||
slider.addEventListener("input", () => drive(+slider.value / 1000));
|
|
||||||
|
|
||||||
// The artifact viewer can size the iframe to the content and scroll the PARENT page:
|
|
||||||
// then this window never scrolls and a pinned scrub can never fire. Pick the engine
|
|
||||||
// by what the environment can actually do, not by viewport width alone.
|
|
||||||
const initPin = () => {
|
|
||||||
mode = "pin+scrub";
|
|
||||||
ScrollTrigger.create({
|
|
||||||
trigger: "#course-1",
|
|
||||||
start: "top top",
|
|
||||||
end: "+=2200",
|
|
||||||
pin: ".pin",
|
|
||||||
scrub: 0.6,
|
|
||||||
onUpdate: self => drive(self.progress),
|
|
||||||
onRefreshInit: () => apply(0)
|
|
||||||
});
|
|
||||||
ScrollTrigger.refresh();
|
|
||||||
drive(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initAssemble = () => {
|
|
||||||
mode = "assemble";
|
|
||||||
document.querySelector(".pin").style.minHeight = "0"; // svh is degenerate when the frame is content-sized
|
|
||||||
drive(0);
|
|
||||||
const io = new IntersectionObserver(es => {
|
|
||||||
if (!es.some(e => e.isIntersecting)) return;
|
|
||||||
io.disconnect();
|
|
||||||
gsap.to({ v: 0 }, {
|
|
||||||
v: 1, duration: 1.6, ease: "power3.out",
|
|
||||||
onUpdate: function () { drive(this.targets()[0].v); }
|
|
||||||
});
|
|
||||||
}, { threshold: 0.15 });
|
|
||||||
io.observe(scene);
|
|
||||||
};
|
|
||||||
|
|
||||||
const boot = () => {
|
|
||||||
if (RM) { mode = "reduced-motion"; drive(1); return; }
|
|
||||||
const scrollable = document.documentElement.scrollHeight - innerHeight > 120;
|
|
||||||
if (scrollable && matchMedia("(min-width: 861px)").matches) initPin();
|
|
||||||
else initAssemble();
|
|
||||||
};
|
|
||||||
const flat = scene.querySelector(".flat");
|
|
||||||
(flat.decode ? flat.decode().catch(() => {}) : Promise.resolve())
|
|
||||||
.then(() => requestAnimationFrame(() => requestAnimationFrame(boot)));
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user