New route prefab (routes.rhai): cars, buses, motorbikes, cyclists, people, deer or elephants move along waypoints, round a loop or there and back. Each way keeps to the left of the line (or right), corners are rounded into pieces of at most 45 degrees; there-and-back routes become loops with U-turns, so the two directions never share a strip. Engine: Prim::Travel / sprite.begin_travel / script travel(a, b, dur, windows) ... end(): the mover is drawn at a piece's start and slides to its end while it is on that piece, hidden otherwise. The renderer writes keyframes per piece (named by a hash of their content) into one <style> per view. Each piece is its own sprite with the lane strip as its sort extent (ext_reset), mitred at the corners, so movers pass in front of and behind trees, lamps and houses. Movers that look alike and never meet on a piece share one drawing. iso-travel pauses (not hides) in still, reduced-motion and ghosted views. Studio: meta shape "path" draws point by point (snapping like runs) and saves one object with pts; ending on the first point closes the loop. MCP: pts in the object schema, route guidance. Demos: streets gets traffic round the block, a bus, motorbikes, cyclists and strolling people (replacing the one-axis cars); country gets cars on the lane. Tests: Rust 79 (each mover on exactly one piece at every moment, in every rotation), Studio 125, web 99.
This commit is contained in:
1 parent
ccfd619a99
commit
5ec164a633
20 files changed
+698
-23
No files matched your search
@@ -201,6 +201,17 @@ pub enum Prim {
|
||||
},
|
||||
/// Items drawn back to front by the depth of their plan anchor.
|
||||
Seq { items: Vec<(P2, Vec<Prim>)> },
|
||||
/// A mover on one straight piece of a route: the items, drawn at `from`, slide to
|
||||
/// `to` (world points) during each window and are hidden the rest of the cycle of
|
||||
/// `dur` seconds. A window is [start, end] in fractions of the cycle; it may run
|
||||
/// past 1 and wraps round.
|
||||
Travel {
|
||||
from: P3,
|
||||
to: P3,
|
||||
dur: f64,
|
||||
wins: Vec<[f64; 2]>,
|
||||
items: Vec<Prim>,
|
||||
},
|
||||
}
|
||||
|
||||
/// One fish of a [`Prim::Swim`]: it goes round an ellipse of radii (rx, ry)
|
||||
|
||||
@@ -87,6 +87,8 @@ struct Ctx<'a> {
|
||||
p: Proj,
|
||||
uid: &'a str,
|
||||
out: String,
|
||||
/// Keyframes of the route movers by name, written once in one `<style>` at the end.
|
||||
keyframes: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl Ctx<'_> {
|
||||
@@ -572,6 +574,31 @@ impl Ctx<'_> {
|
||||
}
|
||||
self.out.push_str("</g>");
|
||||
}
|
||||
Prim::Travel {
|
||||
from,
|
||||
to,
|
||||
dur,
|
||||
wins,
|
||||
items,
|
||||
} => {
|
||||
let (a, b) = (
|
||||
self.p.s(from[0], from[1], from[2]),
|
||||
self.p.s(to[0], to[1], to[2]),
|
||||
);
|
||||
let kf = travel_keyframes(b[0] - a[0], b[1] - a[1], wins);
|
||||
// named by its content: views on one page never clash, equal ones share
|
||||
let name = format!("iso-tv{:08x}", fnv(&kf));
|
||||
let _ = write!(
|
||||
self.out,
|
||||
"<g class=\"iso-travel\" style=\"animation:{name} {}s linear infinite\">",
|
||||
r3s(*dur)
|
||||
);
|
||||
self.keyframes.entry(name).or_insert(kf);
|
||||
for q in items {
|
||||
self.prim(q);
|
||||
}
|
||||
self.out.push_str("</g>");
|
||||
}
|
||||
Prim::Seq { items } => {
|
||||
let p = self.p;
|
||||
let mut idx: Vec<usize> = (0..items.len()).collect();
|
||||
@@ -589,6 +616,61 @@ impl Ctx<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// FNV-1a hash of a text (keyframe names).
|
||||
fn fnv(s: &str) -> u32 {
|
||||
s.bytes().fold(0x811c_9dc5u32, |h, b| {
|
||||
(h ^ b as u32).wrapping_mul(0x0100_0193)
|
||||
})
|
||||
}
|
||||
|
||||
/// Keyframes of a [`Prim::Travel`]: hidden outside the windows, visible inside, sliding
|
||||
/// by (dx, dy) screen units from each window's start to its end.
|
||||
fn travel_keyframes(dx: f64, dy: f64, wins: &[[f64; 2]]) -> String {
|
||||
const EPS: f64 = 2e-5;
|
||||
// (time, visible, how far along) at the ends of every window, wrapped into 0..1
|
||||
let mut pts: Vec<(f64, bool, f64)> = vec![];
|
||||
for w in wins {
|
||||
let (s, e) = (w[0].rem_euclid(1.0), w[0].rem_euclid(1.0) + (w[1] - w[0]));
|
||||
if e - s <= 2.0 * EPS {
|
||||
continue;
|
||||
}
|
||||
let at = |t: f64| ((t - s) / (e - s)).clamp(0.0, 1.0);
|
||||
pts.push((s, false, 0.0));
|
||||
pts.push((s + EPS, true, at(s + EPS)));
|
||||
if e <= 1.0 {
|
||||
pts.push((e, true, 1.0));
|
||||
pts.push(((e + EPS).min(1.0), false, 1.0));
|
||||
} else {
|
||||
pts.push((1.0, true, at(1.0)));
|
||||
pts.push((0.0, true, at(1.0)));
|
||||
pts.push((e - 1.0, true, 1.0));
|
||||
pts.push((e - 1.0 + EPS, false, 1.0));
|
||||
}
|
||||
}
|
||||
pts.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let mut out = String::new();
|
||||
let put = |t: f64, vis: bool, f: f64, out: &mut String| {
|
||||
let _ = write!(
|
||||
out,
|
||||
"{}%{{visibility:{};transform:translate({}px,{}px)}}",
|
||||
r3s(t * 100.0),
|
||||
if vis { "visible" } else { "hidden" },
|
||||
r3s(dx * f),
|
||||
r3s(dy * f)
|
||||
);
|
||||
};
|
||||
if pts.first().is_none_or(|p| p.0 > 0.0) {
|
||||
put(0.0, false, pts.first().map_or(0.0, |p| p.2), &mut out);
|
||||
}
|
||||
for &(t, vis, f) in &pts {
|
||||
put(t, vis, f, &mut out);
|
||||
}
|
||||
if pts.last().is_none_or(|p| p.0 < 1.0) {
|
||||
put(1.0, false, pts.last().map_or(0.0, |p| p.2), &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The sprite's convex plan outline in view space when it is not its box.
|
||||
pub fn hull(s: &Sprite, p: Proj, bb: &Bb) -> Option<sort::Hull> {
|
||||
if s.ext.len() < 3 {
|
||||
@@ -627,6 +709,7 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
|
||||
p,
|
||||
uid: &uid,
|
||||
out: String::with_capacity(64 * 1024),
|
||||
keyframes: BTreeMap::new(),
|
||||
};
|
||||
let mut frame = [1e9f64, 1e9, -1e9, -1e9];
|
||||
let mut sprites = 0;
|
||||
@@ -743,6 +826,13 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
|
||||
);
|
||||
}
|
||||
cx.out.push_str("</g>");
|
||||
if !cx.keyframes.is_empty() {
|
||||
cx.out.push_str("<style>");
|
||||
for (name, kf) in &cx.keyframes {
|
||||
let _ = write!(cx.out, "@keyframes {name}{{{kf}}}");
|
||||
}
|
||||
cx.out.push_str("</style>");
|
||||
}
|
||||
|
||||
// frame: the scene bounds (plan rectangle down to the ground depth) or the drawing
|
||||
if let Some((b, zb)) = scene.bounds {
|
||||
|
||||
@@ -123,6 +123,13 @@ enum Frame {
|
||||
items: Vec<(P2, Vec<Prim>)>,
|
||||
cur: Option<(P2, Vec<Prim>)>,
|
||||
},
|
||||
Travel {
|
||||
from: P3,
|
||||
to: P3,
|
||||
dur: f64,
|
||||
wins: Vec<[f64; 2]>,
|
||||
items: Vec<Prim>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
@@ -172,7 +179,7 @@ impl Sprite {
|
||||
/// the sprite.
|
||||
pub fn push(&mut self, p: Prim) -> &mut Self {
|
||||
match self.frames.last_mut() {
|
||||
Some(Frame::Group { items, .. }) => items.push(p),
|
||||
Some(Frame::Group { items, .. }) | Some(Frame::Travel { items, .. }) => items.push(p),
|
||||
Some(Frame::Seq { cur, .. }) => match cur {
|
||||
Some((_, items)) => items.push(p),
|
||||
// a primitive before the first seq_item starts an item at the origin
|
||||
@@ -622,12 +629,46 @@ impl Sprite {
|
||||
}
|
||||
|
||||
pub fn end_group(&mut self) -> &mut Self {
|
||||
if let Some(Frame::Group { cls, style, items }) = self.frames.pop() {
|
||||
self.push(Prim::Group { cls, style, items });
|
||||
match self.frames.pop() {
|
||||
Some(Frame::Group { cls, style, items }) => {
|
||||
self.push(Prim::Group { cls, style, items });
|
||||
}
|
||||
Some(Frame::Travel {
|
||||
from,
|
||||
to,
|
||||
dur,
|
||||
wins,
|
||||
items,
|
||||
}) => {
|
||||
self.push(Prim::Travel {
|
||||
from,
|
||||
to,
|
||||
dur,
|
||||
wins,
|
||||
items,
|
||||
});
|
||||
}
|
||||
Some(f) => self.frames.push(f),
|
||||
None => {}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Open a mover on a straight piece from `a` to `b` (points of this frame): what
|
||||
/// is drawn until [`Sprite::end_group`] slides from a to b during each window of
|
||||
/// the `dur`-second cycle and is hidden otherwise (see [`Prim::Travel`]).
|
||||
pub fn begin_travel(&mut self, a: P3, b: P3, dur: f64, wins: Vec<[f64; 2]>) -> &mut Self {
|
||||
let (from, to) = (self.xf.p3(a), self.xf.p3(b));
|
||||
self.frames.push(Frame::Travel {
|
||||
from,
|
||||
to,
|
||||
dur,
|
||||
wins,
|
||||
items: vec![],
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Start a back-to-front sequence; each [`Sprite::seq_item`] starts an
|
||||
/// item anchored at a plan point; [`Sprite::end_seq`] closes it.
|
||||
pub fn begin_seq(&mut self) -> &mut Self {
|
||||
@@ -674,7 +715,7 @@ impl Sprite {
|
||||
let n = self.frames.len();
|
||||
while let Some(f) = self.frames.last() {
|
||||
match f {
|
||||
Frame::Group { .. } => {
|
||||
Frame::Group { .. } | Frame::Travel { .. } => {
|
||||
self.end_group();
|
||||
}
|
||||
Frame::Seq { .. } => {
|
||||
|
||||
Reference in new issue
Block a user