diff --git a/pilot/arc.tpl.html b/pilot/arc.tpl.html
index 23a78f9..130a807 100644
--- a/pilot/arc.tpl.html
+++ b/pilot/arc.tpl.html
@@ -943,13 +943,30 @@ body:not(.fx) .slab{display:none}
u.flat.style.visibility = "hidden";
const lis = u.legend ? [...u.legend.children] : [];
const n = lis.length;
+ // per-scene motion signature (occlusion-safe scenes only; default is straight translate).
+ // "away" is 0 at the hero layout, 1 at collapsed/flown - every signature returns to identity
+ // at the hero so the assembled slices always match the source image exactly.
+ const M = u.man.motion, RAD = Math.PI / 180;
u.apply = t => {
const sg = u.man.stagger || 0, k = u.man.parts.length;
u.man.parts.forEach((p, i) => {
const pt = sg ? Math.min(1, Math.max(0, t * (1 + sg * (k - 1)) - sg * i)) : t;
const e = u.man.reverse ? (1 - pt) : pt;
+ const away = 1 - e;
+ let ox = p.dx * away, oy = p.dy * away, extra = "";
+ if (M === "tumble") { // aperitif: fragments spin as they fly out
+ const spin = (p.spin != null ? p.spin : (i % 2 ? 20 : -20)) * away;
+ extra = " rotate(" + spin.toFixed(2) + "deg)";
+ } else if (M === "arc") { // traffic: slices travel a bowed (curved) path
+ const len = Math.hypot(p.dx, p.dy) || 1;
+ const bow = (u.man.bow || 9) * 4 * away * (1 - away);
+ ox += (-p.dy / len) * bow; oy += (p.dx / len) * bow;
+ } else if (M === "orbit") { // OpenShift: satellites spiral in around the core
+ const a = (u.man.orbit || 55) * away * RAD, c = Math.cos(a), sn = Math.sin(a);
+ ox = (p.dx * c - p.dy * sn) * away; oy = (p.dx * sn + p.dy * c) * away;
+ }
u.slabs[i].style.transform =
- "translate3d(" + (p.dx * (1 - e)).toFixed(3) + "%," + (p.dy * (1 - e)).toFixed(3) + "%,0)";
+ "translate3d(" + ox.toFixed(3) + "%," + oy.toFixed(3) + "%,0)" + extra;
});
u.scene.style.transform = "scale(" + (0.95 + 0.05 * t).toFixed(4) + ")";
if (n) lis.forEach((li, i) => li.classList.toggle("lit", t >= 0.9 * ((i + 0.5) / n)));
diff --git a/pilot/manifest-orbit.json b/pilot/manifest-orbit.json
index e4087b9..c770e26 100644
--- a/pilot/manifest-orbit.json
+++ b/pilot/manifest-orbit.json
@@ -828,5 +828,7 @@
"dx": 13.85,
"dy": 15.36
}
- ]
+ ],
+ "motion": "orbit",
+ "orbit": 60
}
\ No newline at end of file
diff --git a/pilot/manifest-surround.json b/pilot/manifest-surround.json
index a0ea6f5..da0fcfd 100644
--- a/pilot/manifest-surround.json
+++ b/pilot/manifest-surround.json
@@ -675,5 +675,6 @@
"dy": 31.74
}
],
- "reverse": true
+ "reverse": true,
+ "motion": "tumble"
}
\ No newline at end of file
diff --git a/pilot/manifest-traffic.json b/pilot/manifest-traffic.json
index 1a81cc7..635360d 100644
--- a/pilot/manifest-traffic.json
+++ b/pilot/manifest-traffic.json
@@ -667,5 +667,7 @@
"dy": -11.61
}
],
- "stagger": 0.22
+ "stagger": 0.22,
+ "motion": "arc",
+ "bow": 8
}
\ No newline at end of file
diff --git a/tools/slice-arc.py b/tools/slice-arc.py
index 3041955..50254ba 100644
--- a/tools/slice-arc.py
+++ b/tools/slice-arc.py
@@ -96,6 +96,8 @@ def emit(name, tag, spec):
man["stagger"] = spec["stagger"]
if spec.get("reverse"):
man["reverse"] = True
+ for kk in ("motion","bow","orbit"):
+ if kk in spec: man[kk]=spec[kk]
out = ROOT / "pilot" / f"manifest-{tag}.json"
out.write_text(json.dumps(man, indent=1))
print(f"manifest-{tag}.json: {len(parts)} parts ->",