Compare commits
2 Commits
c7b5f862d5
...
dcbf896222
| Author | SHA1 | Date | |
|---|---|---|---|
| dcbf896222 | |||
| ef937e8b07 |
@@ -2,3 +2,4 @@ node_modules/
|
|||||||
*.log
|
*.log
|
||||||
assets/work/
|
assets/work/
|
||||||
dist/
|
dist/
|
||||||
|
pilot/course-1.html
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.0 MiB After Width: | Height: | Size: 5.0 MiB |
@@ -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"<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()
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<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}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mm = gsap.matchMedia();
|
||||||
|
|
||||||
|
mm.add("(min-width: 861px) and (prefers-reduced-motion: no-preference)", () => {
|
||||||
|
const st = ScrollTrigger.create({
|
||||||
|
trigger: "#course-1",
|
||||||
|
start: "top top",
|
||||||
|
end: "+=2200",
|
||||||
|
pin: ".pin",
|
||||||
|
scrub: 0.6,
|
||||||
|
onUpdate: self => apply(self.progress),
|
||||||
|
onRefreshInit: () => apply(0)
|
||||||
|
});
|
||||||
|
apply(0);
|
||||||
|
return () => { st.kill(); apply(1); };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Small screens and reduced-motion: no pin, no scrub. The scene assembles once on
|
||||||
|
// entry so the teardown still *reads*, then stays put.
|
||||||
|
mm.add("(max-width: 860px), (prefers-reduced-motion: reduce)", () => {
|
||||||
|
if (RM) { apply(1); return () => {}; }
|
||||||
|
apply(0);
|
||||||
|
const st = ScrollTrigger.create({
|
||||||
|
trigger: "#course-1",
|
||||||
|
start: "top 70%",
|
||||||
|
once: true,
|
||||||
|
onEnter: () => gsap.to({ v: 0 }, {
|
||||||
|
v: 1, duration: 1.5, ease: "power3.out",
|
||||||
|
onUpdate: function () { apply(this.targets()[0].v); }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return () => { st.kill(); apply(1); };
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user