Wildlife: hamster and hamster wheel prefabs

Ported from the thaiai.lol easter egg (thaiai iso/hamster/lib/hamster.rhai);
the data-centre joke parts stay there.

- hamster (Animals): golden, grey or cream with a white belly, pink nose,
  ears and paws, buck teeth and whiskers; standing, running on the spot or
  asleep (eyes shut, z Z); n and spread like the herds; life size (about
  17 cm) at scale 1, up to 40 times.
- hamster_wheel (Animals): diameter (0.3 m life size, up to 12 m), running,
  A-frame stand or a cage-wall axle stub, fur colour. Running legs and the
  rungs are flipbooks of 4 poses, 0.1 s each; the spokes turn by a CSS
  rotation in a matrix that maps the wheel's plane onto the screen.
- draw_parts learns loft2 (two-tone lofts), eye and flip parts;
  place_parts scales and moves any part list; loft and loft2 share
  loft_rings (the other animals draw byte for byte as before).
- Things under 2 m are drawn ten times bigger in a group that scales them
  back (fine_begin / fine_end), so a life-size hamster keeps its shape on
  the drawing's 0.1-unit grid; sort boxes stay real size.
- The legs push back on the ground and swing forward lifted (the original
  lifted them on the way back).
- Theme: hamster, hamsterg, hamsterc, hamsterw, hamsterp, hwheel, hwhub,
  whisker and hamz in both palettes, materials, rules for eyes, whiskers,
  rims, spokes, hub, rungs and the z Z. Viewer CSS: iso-flip4,
  iso-wheelspin, iso-runbob, iso-zzz; beast edges take --bw (default .3).
- Test hamsters_run_in_wheels_from_every_side: no sort cycles in any
  rotation, flipbook counts, scaled groups, real-size sort boxes, tooltips.
- README, ROADMAP, SCRIPTING and the MCP instructions list them.
This commit is contained in:
grabowski committed 2026-10-27 16:39:43 +07:00
1 parent c8becc2e59
commit 9619d0f267
8 files changed
+609 -29

No files matched your search

+11 -9
View File
@@ -83,15 +83,17 @@ swimming round under lily pads, ripples and reeds; trees, hedges and rows of
trees are in Nature. trees are in Nature.
**Animals and nature**: Deer (a herd of a stag, does and fawns, or one **Animals and nature**: Deer (a herd of a stag, does and fawns, or one
kind), Wild boar, Fox, Elephant (bull, cow, calf), Rabbits, a flock of Birds kind), Wild boar, Fox, Elephant (bull, cow, calf), Rabbits, Hamsters (golden,
circling and Butterflies; Beehives on stands with bees flying round, Bushes grey or cream; life size, or scaled up for a giant) and a Hamster wheel with
(round, flowering, berries), Flowers (a bed in rows or a wildflower meadow), one running in it, a flock of Birds circling and Butterflies; Beehives on
Rocks and a Log or stump. Animals are low-poly solids that read from every stands with bees flying round, Bushes (round, flowering, berries), Flowers (a
side; a herd scatters about the object's spot, each animal its own sprite. bed in rows or a wildflower meadow), Rocks and a Log or stump. Animals are
Grazing animals lower and raise their heads in turn, elephants curl their low-poly solids that read from every side; a herd scatters about the
trunks, rabbits hop, birds flap. `examples/country` (Country demo in the web object's spot, each animal its own sprite. Grazing animals lower and raise
Studio) puts them in a countryside corner with a lane, fence and hedgerow at their heads in turn, elephants curl their trunks, rabbits hop, hamsters run
an angle. (legs and the wheel's rungs step through four poses, the spokes turn), birds
flap. `examples/country` (Country demo in the web Studio) puts them in a
countryside corner with a lane, fence and hedgerow at an angle.
**Routes**: a Route moves cars, buses, motorbikes, cyclists, people, deer or **Routes**: a Route moves cars, buses, motorbikes, cyclists, people, deer or
elephants along waypoints. Pick Route and click point after point on the map elephants along waypoints. Pick Route and click point after point on the map
+2 -1
View File
@@ -27,8 +27,9 @@ Fences (props.style: mesh, wood, bars, half stone, cobble, brick) and site walls
meet, so for a plot boundary put each piece's ends exactly on the next piece's end or line. Line pieces (roads, walls, \ meet, so for a plot boundary put each piece's ends exactly on the next piece's end or line. Line pieces (roads, walls, \
fences, hedges, house walls) can run at any angle: a piece from (x0, y0) to (x1, y1) has x, y = the midpoint, \ fences, hedges, house walls) can run at any angle: a piece from (x0, y0) to (x1, y1) has x, y = the midpoint, \
w = the distance and rot = atan2(y1 - y0, x1 - x0) in degrees; where diagonal pieces meet, corners are mitred. \ w = the distance and rot = atan2(y1 - y0, x1 - x0) in degrees; where diagonal pieces meet, corners are mitred. \
Animals (category Animals: deer, boar, fox, elephant, rabbits, birds, butterflies) are herds: props.n animals \ Animals (category Animals: deer, boar, fox, elephant, rabbits, hamster, birds, butterflies) are herds: props.n animals \
scattered within props.r metres of x, y, facing about the rotation; keep them out of roads, ponds and buildings. \ scattered within props.r metres of x, y, facing about the rotation; keep them out of roads, ponds and buildings. \
Hamsters are life size (props.scale makes giants); a hamster_wheel (props.diameter, 0.3 m) has one running in it. \
A route (type route) moves props.n movers (props.kind: car, bus, motorbike, cyclist, person, deer, elephant) along \ A route (type route) moves props.n movers (props.kind: car, bus, motorbike, cyclist, person, deer, elephant) along \
waypoints pts (relative to x, y): round them when props.closed, else there and back; each way keeps to the left \ waypoints pts (relative to x, y): round them when props.closed, else there and back; each way keeps to the left \
of the line (props.keep). For traffic, put the points on the road centre lines; keep routes clear of buildings. \ of the line (props.keep). For traffic, put the points on the road centre lines; keep routes clear of buildings. \
+91
View File
@@ -1111,6 +1111,97 @@ fn wildlife_and_garden_prefabs_draw_from_every_side() {
assert_eq!(row("hives", "Apiary"), "4 hives, 2 supers each"); assert_eq!(row("hives", "Apiary"), "4 hives, 2 supers each");
} }
fn hamster_doc() -> Doc {
let mut turned = obj("tw", "hamster_wheel", 6.0, -4.0, json!({"diameter": 1.2}));
turned.rot = 30.0;
Doc {
objects: vec![
obj("h1", "hamster", 0.0, 0.0, json!({})),
obj(
"h3",
"hamster",
3.0,
0.0,
json!({"n": 3, "colour": "mixed", "pose": "running"}),
),
obj(
"hz",
"hamster",
0.0,
3.0,
json!({"pose": "asleep", "scale": 10}),
),
obj("w", "hamster_wheel", 3.0, 3.0, json!({})),
obj(
"big",
"hamster_wheel",
-8.0,
-4.0,
json!({"diameter": 8, "running": false, "stand": false, "colour": "grey"}),
),
turned,
],
..Default::default()
}
}
#[test]
fn hamsters_run_in_wheels_from_every_side() {
let r = Runtime::new(&Bundle::default()).unwrap();
let doc = hamster_doc();
for view in 0..4u8 {
let b = r.build("site", view, &cfg(), &doc).unwrap();
assert!(b.scene.warnings.is_empty(), "{:?}", b.scene.warnings);
let out = render(&b.scene, view, &RenderOpts::default());
assert!(!out.html.contains("NaN"));
assert_eq!(out.cycles, 0, "view {view}: depth-sort cycles");
// each hamster its own sprite, each wheel one
assert_eq!(sprites_of(&b, "h3"), 3);
assert_eq!(sprites_of(&b, "w"), 1);
let n = |s: &str| out.html.matches(s).count();
// flipbooks of 4 poses, the first shown unanimated: the legs of the three runners
// and of the hamsters in the two running wheels, and each running wheel's rungs
// behind and in front of its hamster
let books = 3 * 4 + 2 * 4 + 2 * 2;
assert_eq!(n("iso-a iso-flip4 iso-pose0"), books, "view {view}");
assert_eq!(n("iso-a iso-flip4\""), 3 * books, "view {view}");
assert_eq!(n("iso-a iso-wheelspin"), 2);
assert_eq!(n("iso-a iso-runbob"), 3 + 2);
assert_eq!(n("iso-a iso-zzz"), 1);
// two-tone fur in three colours, edged in its own shade; eyes open and shut
for c in ["hamster", "hamsterg", "hamsterc", "hamsterw", "hamsterp"] {
assert!(out.html.contains(&format!("iso-{c} iso-beast")), "{c}");
}
assert!(out.html.contains("iso-hamstereye"));
assert!(out.html.contains("iso-hamstereyec"));
// everything under 2 m is drawn ten times bigger and scaled back (the 0.1-unit grid
// of the drawing would make it blocky): all but the 8 m wheel
assert_eq!(n(" scale(0.1) "), 7, "view {view}");
}
let b = r.build("site", 0, &cfg(), &doc).unwrap();
// life size: a hamster about 17 cm long and 10 cm high, a 30 cm wheel on its stand;
// the sort boxes are real size, not the ten times bigger drawing
let h = b.boxes["h1"];
assert!(h[3] - h[0] > 0.15 && h[3] - h[0] < 0.2, "{h:?}");
assert!(h[5] > 0.09 && h[5] < 0.12, "{h:?}");
let w = b.boxes["w"];
assert!(w[5] > 0.3 && w[5] < 0.34, "{w:?}");
assert!(b.boxes["big"][5] > 8.0 && b.boxes["big"][5] < 8.3);
let out = render(&b.scene, 0, &RenderOpts::default());
let row = |id: &str, k: &str| {
out.info[id]
.rows
.iter()
.find(|r| r.0 == k)
.map(|r| r.1.clone())
.unwrap_or_default()
};
assert_eq!(row("h3", "Animals"), "3 hamsters, running");
assert_eq!(row("hz", "Animals"), "1 hamster, asleep");
assert_eq!(row("w", "Wheel"), "30 cm across, a turn every 9.6 s");
assert_eq!(row("big", "Wheel"), "8 m across, standing still");
}
#[test] #[test]
fn diagonal_walls_fences_and_hedges_mitre_and_sort() { fn diagonal_walls_fences_and_hedges_mitre_and_sort() {
let r = Runtime::new(&Bundle::default()).unwrap(); let r = Runtime::new(&Bundle::default()).unwrap();
+435 -18
View File
@@ -1,12 +1,14 @@
// isokit/wildlife: animals (deer, wild boar, fox, elephant, rabbits, a flock // isokit/wildlife: animals (deer, wild boar, fox, elephant, rabbits,
// of birds, butterflies) and beehives, bushes, flowers, rocks and logs. // hamsters and a hamster wheel, a flock of birds, butterflies) and beehives,
// bushes, flowers, rocks and logs.
// //
// Animals are low-poly solids drawn face by face (`face`: the engine culls // Animals are low-poly solids drawn face by face (`face`: the engine culls
// the faces turned away and shades the others), their parts back to front by // the faces turned away and shades the others), their parts back to front by
// depth, so they read right from every side and at any rotation. Herds stand // depth, so they read right from every side and at any rotation. Herds stand
// at scattered spots with their own headings, each animal its own sprite. // at scattered spots with their own headings, each animal its own sprite.
// Grazing animals take turns between two head poses (a CSS flipbook, paused // Grazing animals take turns between two head poses (a CSS flipbook, paused
// like every animation); birds, butterflies and bees fly round on orbits. // like every animation); running hamsters move their legs in a flipbook of
// four poses; birds, butterflies and bees fly round on orbits.
fn animal_kinds() { ["deer", "boar", "fox", "elephant", "rabbits"] } fn animal_kinds() { ["deer", "boar", "fox", "elephant", "rabbits"] }
@@ -30,6 +32,26 @@ fn vunit(a) {
/// limbs, necks, heads, trunks, logs. The rings keep their turn along a bent /// limbs, necks, heads, trunks, logs. The rings keep their turn along a bent
/// spine (parallel transport), so trunks and tails do not twist. /// spine (parallel transport), so trunks and tails do not twist.
fn loft(spine, rs, n, mats) { fn loft(spine, rs, n, mats) {
let rings = loft_rings(spine, rs, n);
let m = spine.len();
for i in 1..m {
let p = rings[i - 1];
let q = rings[i];
for k in 0..n {
let j = (k + 1) % n;
this.face([p[k], p[j], q[j], q[k]], mats[0]);
}
}
let c0 = rings[0];
c0.reverse();
this.face(c0, mats[1]);
this.face(rings[m - 1], mats[2]);
this
}
/// The rings of a loft: n points round each spine point, the first one to
/// the spine's left (towards +x on an upright spine), then up and round.
fn loft_rings(spine, rs, n) {
let m = spine.len(); let m = spine.len();
let rings = []; let rings = [];
let v1 = (); let v1 = ();
@@ -53,27 +75,18 @@ fn loft(spine, rs, n, mats) {
} }
rings.push(ring); rings.push(ring);
} }
for i in 1..m { rings
let p = rings[i - 1];
let q = rings[i];
for k in 0..n {
let j = (k + 1) % n;
this.face([p[k], p[j], q[j], q[k]], mats[0]);
}
}
let c0 = rings[0];
c0.reverse();
this.face(c0, mats[1]);
this.face(rings[m - 1], mats[2]);
this
} }
/// A tapered tube from a to b, radii ra and rb, n sides. /// A tapered tube from a to b, radii ra and rb, n sides.
fn tube(a, b, ra, rb, n, mat) { this.loft([a, b], [ra, rb], n, [mat, mat, mat]) } fn tube(a, b, ra, rb, n, mat) { this.loft([a, b], [ra, rb], n, [mat, mat, mat]) }
// Parts of a creature, drawn back to front: #{ at, t, ... } with t "loft" // Parts of a creature, drawn back to front: #{ at, t, ... } with t "loft"
// (spine, rs, n, mats), "face" (pts, mat), "line" (pts, cls) or "alt" (two // (spine, rs, n, mats), "face" (pts, mat), "line" (pts, cls), "alt" (two
// part lists shown in turn: a, b, css). // part lists shown in turn: a, b, css), "loft2" (a loft in two colours,
// see `loft2`), "eye" (a bead of radius r at `at`, or shut when `closed`)
// or "flip" (a flipbook: vars, a part list per pose, shown 0.1 s each,
// `delay` s in; 4 poses).
fn p_loft(spine, rs, n, mats) { #{ at: spine[spine.len() / 2], t: "loft", spine: spine, rs: rs, n: n, mats: mats } } fn p_loft(spine, rs, n, mats) { #{ at: spine[spine.len() / 2], t: "loft", spine: spine, rs: rs, n: n, mats: mats } }
fn p_tube(a, b, ra, rb, n, mat) { #{ at: vmul(vadd(a, b), 0.5), t: "loft", spine: [a, b], rs: [ra, rb], n: n, mats: [mat, mat, mat] } } fn p_tube(a, b, ra, rb, n, mat) { #{ at: vmul(vadd(a, b), 0.5), t: "loft", spine: [a, b], rs: [ra, rb], n: n, mats: [mat, mat, mat] } }
fn p_face(pts, mat) { #{ at: pts[0], t: "face", pts: pts, mat: mat } } fn p_face(pts, mat) { #{ at: pts[0], t: "face", pts: pts, mat: mat } }
@@ -102,11 +115,38 @@ fn draw_parts(parts) {
this.begin("a pose2", p.css); this.begin("a pose2", p.css);
this.draw_parts(p.b); this.draw_parts(p.b);
this.end(); this.end();
} else if p.t == "loft2" {
this.loft2(p);
} else if p.t == "eye" {
let c = p.at;
if p.closed {
let d = p.r * 0.9;
this.polyline([[c[0] - d, c[1], c[2] + d * 0.15], [c[0], c[1], c[2] - d * 0.3], [c[0] + d, c[1], c[2] + d * 0.15]], 0.0, "hamstereyec");
} else {
let r = p.r * px();
this.circle(c[0], c[1], c[2], r, "hamstereye");
this.circle(c[0], c[1], c[2], r * 0.36, "hamsterglint", -r * 0.3, -r * 0.34);
}
} else if p.t == "flip" {
let np = p.vars.len();
for k in 0..np {
this.begin(flip_cls(k, np), flip_css(k, np, p.delay));
this.draw_parts(p.vars[k]);
this.end();
}
} }
} }
this this
} }
/// Class and style of pose k in a flipbook of np poses shown 0.1 s each,
/// the whole book `delay` s in; pose 0 shows when unanimated. The viewer
/// styles books of 4 poses (`iso-flip4`).
fn flip_cls(k, np) { if k == 0 { `a flip${np} pose0` } else { `a flip${np}` } }
fn flip_css(k, np, delay) { `animation-delay:-${round4(((np - k) % np) * 0.1 + delay)}s` }
fn round4(x) { (x * 10000.0).round() / 10000.0 }
/// A point of a head: `a` along its way f, `b` to its left, `c` up from it, /// A point of a head: `a` along its way f, `b` to its left, `c` up from it,
/// from its base (f points from the neck to the snout, in the x-z plane). /// from its base (f points from the neck to the snout, in the x-z plane).
fn hp(base, f, a, b, c) { fn hp(base, f, a, b, c) {
@@ -132,6 +172,54 @@ fn scale_parts(parts, s) {
}) })
} }
/// Parts scaled by s about the origin, then moved by the 3D offset o (a
/// hamster of any size anywhere); every kind of part.
fn place_parts(parts, s, o) {
parts.map(|p| {
let q = p;
q.at = vadd(o, vmul(p.at, s));
if p.t == "loft" || p.t == "loft2" {
q.spine = p.spine.map(|v| vadd(o, vmul(v, s)));
q.rs = p.rs.map(|r| if type_of(r) == "array" { [r[0] * s, r[1] * s] } else { r * s });
} else if p.t == "face" || p.t == "line" {
q.pts = p.pts.map(|v| vadd(o, vmul(v, s)));
} else if p.t == "eye" {
q.r = p.r * s;
} else if p.t == "alt" {
q.a = place_parts(p.a, s, o);
q.b = place_parts(p.b, s, o);
} else if p.t == "flip" {
q.vars = p.vars.map(|v| place_parts(v, s, o));
}
q
})
}
/// A loft (see `loft`) in two colours, from a part map: the side faces of
/// segment i whose middle points down by more than wt[i] (the sine of the
/// angle below level; -1 none, 1 all) get `low` (belly, chin, muzzle), the
/// others `top`; the end caps cap0 and cap1. Faces carry `beast`.
fn loft2(p) {
let rings = loft_rings(p.spine, p.rs, p.n);
let n = p.n;
let m = rings.len();
for i in 1..m {
let a = rings[i - 1];
let b = rings[i];
let wt = p.wt[i - 1];
for k in 0..n {
let j = (k + 1) % n;
let tc = 2.0 * PI() * (k + 1) / n;
this.face([a[k], a[j], b[j], b[k]], (if tc.sin() < wt { p.low } else { p.top }) + " beast");
}
}
let c0 = rings[0];
c0.reverse();
this.face(c0, p.cap0 + " beast");
this.face(rings[m - 1], p.cap1 + " beast");
this
}
/// Four legs from the underside of a body to the ground: at x fx (front) /// Four legs from the underside of a body to the ground: at x fx (front)
/// and bx (back), y = +-y, from height z0, radii r0 at the top, r1 at the hoof. /// and bx (back), y = +-y, from height z0, radii r0 at the top, r1 at the hoof.
fn legs(fx, bx, y, z0, r0, r1, mat) { fn legs(fx, bx, y, z0, r0, r1, mat) {
@@ -468,6 +556,335 @@ fn prefab_butterflies(o) {
if high.len() > 0 { s.swim(0.0, 0.0, p.h + 0.4, high); } if high.len() > 0 { s.swim(0.0, 0.0, p.h + 0.4, high); }
} }
// ---------------------------------------------------------------- hamsters
// A hamster is built like the animals above, from lofts in two colours (fur
// on top, white underneath), with beady eyes and a flipbook of running legs.
// A hamster wheel's rungs are a flipbook too (they join the wheel's two
// planes, so no rotation can turn them); its spokes turn by a CSS rotation
// in the wheel's own plane (`vplane_g`, the fan's trick). Every animated
// group carries `iso-a`, so the viewer pauses it.
fn hamster_colours() { ["golden", "grey", "cream", "mixed"] }
/// The fur material of hamster i for a colour choice ("mixed" takes turns).
fn hamster_fur(c, i) {
let k = if c == "mixed" { ["golden", "grey", "cream"][i % 3] } else { c };
if k == "grey" { "hamsterg" } else if k == "cream" { "hamsterc" } else { "hamster" }
}
/// The size of a life-size hamster (about 17 cm) for hamster_parts.
fn hamster_life() { 0.14 }
/// Raw SVG group that maps the vertical plane along (ux, uy) (plan, per
/// metre) onto the screen: u along it, v straight down, in metres. Draw
/// circles and paths in it (wheels, pulleys) and close it with `</g>`.
fn vplane_g(ux, uy) {
let a = dv(ux * 1000.0, uy * 1000.0, 0.0);
let b = dv(0.0, 0.0, -1000.0);
`<g transform="matrix(${round4(a[0] / 1000.0)} ${round4(a[1] / 1000.0)} ${round4(b[0] / 1000.0)} ${round4(b[1] / 1000.0)} 0 0)">`
}
/// One leg of a hamster facing +x (hamster_parts size), in running pose
/// phi (radians: a paw pushes back on the ground and swings forward
/// lifted) or standing. i: 0/1 front left/right, 2/3 hind left/right.
fn hamster_leg(i, phi, run, n, fur) {
let side = if i % 2 == 0 { 1.0 } else { -1.0 };
let front = i < 2;
let ph = phi + (if side > 0.0 { 0.0 } else { 0.8 }) + (if front { 0.0 } else { PI() });
let swing = if run { 0.09 * ph.cos() } else { 0.0 };
let lift = if run { fmax(0.0, -0.06 * ph.sin()) } else { 0.0 };
if front {
let y = side * 0.125;
let fx = 0.18 + swing;
[p_tube([0.15, y, 0.2], [fx, y, lift + 0.035], 0.05, 0.035, n, fur),
p_tube([fx - 0.02, y, lift + 0.018], [fx + 0.05, y, lift + 0.014], 0.026, 0.02, n, "hamsterp")]
} else {
let y = side * 0.17;
let bx = -0.3 + swing;
[p_tube([-0.27, y, 0.22], [bx, y, lift + 0.04], 0.075, 0.045, n, fur),
p_tube([bx - 0.04, y, lift + 0.02], [bx + 0.08, y, lift + 0.015], 0.034, 0.026, n, "hamsterp")]
}
}
/// The parts of a hamster facing +x, feet at z = 0, about 1.2 m long and
/// 0.75 m high (place_parts scales it; hamster_life() is life size): fur
/// material `fur`, white underneath, pink nose, ears and paws. run: the legs
/// in a flipbook of np poses (4), `delay` s in; sleep: lying down with its
/// eyes shut and its legs tucked away. n: sides of body and head (8-10).
fn hamster_parts(run, sleep, n, np, delay, fur) {
let dz = if sleep { -0.07 } else { 0.0 };
let dh = if sleep { -0.05 } else { 0.0 };
let parts = [];
// body: an egg, white underneath
parts.push(#{ t: "loft2", at: [-0.12, 0.0, 0.36 + dz],
spine: [[-0.5, 0.0, 0.33 + dz], [-0.45, 0.0, 0.335 + dz], [-0.34, 0.0, 0.345 + dz], [-0.17, 0.0, 0.355 + dz], [0.02, 0.0, 0.365 + dz], [0.16, 0.0, 0.375 + dz], [0.24, 0.0, 0.385 + dz]],
rs: [[0.07, 0.07], [0.19, 0.19], [0.27, 0.27], [0.31, 0.3], [0.3, 0.29], [0.25, 0.25], [0.17, 0.18]],
n: n, top: fur, low: "hamsterw", wt: [-0.5, -0.5, -0.45, -0.45, -0.45, -0.4], cap0: fur, cap1: fur });
// head with puffed cheek pouches, white muzzle and chin
let hz = 0.42 + dz + dh;
parts.push(#{ t: "loft2", at: [0.38, 0.0, hz],
spine: [[0.12, 0.0, hz], [0.25, 0.0, hz + 0.02], [0.37, 0.0, hz], [0.47, 0.0, hz - 0.04], [0.55, 0.0, hz - 0.09], [0.59, 0.0, hz - 0.11]],
rs: [[0.2, 0.2], [0.255, 0.23], [0.24, 0.21], [0.18, 0.16], [0.1, 0.09], [0.035, 0.035]],
n: n, top: fur, low: "hamsterw", wt: [-0.45, -0.4, -0.15, 0.25, 0.6], cap0: fur, cap1: "hamsterw" });
// nose, buck teeth, whiskers
parts.push(p_tube([0.58, 0.0, hz - 0.105], [0.61, 0.0, hz - 0.115], 0.03, 0.02, 5, "hamsterp"));
for sd in [1.0, -1.0] {
parts.push(p_tube([0.565, sd * 0.012, hz - 0.15], [0.57, sd * 0.012, hz - 0.2], 0.012, 0.011, 4, "tusk"));
parts.push(#{ t: "line", at: [0.58, sd * 0.1, hz - 0.1], cls: "whisker",
pts: [[0.56, sd * 0.06, hz - 0.11], [0.64, sd * 0.17, hz - 0.07], [0.69, sd * 0.24, hz - 0.06]] });
parts.push(#{ t: "line", at: [0.58, sd * 0.1, hz - 0.12], cls: "whisker",
pts: [[0.56, sd * 0.06, hz - 0.13], [0.64, sd * 0.18, hz - 0.15], [0.68, sd * 0.25, hz - 0.18]] });
// round ears: fur behind, pink inside, both anchored on the ear's
// middle (the pink one a little forward), so seen from the front it
// lies on the fur and from behind under it
let ec = [0.22, sd * 0.155, hz + 0.27];
let ear = [];
for k in 0..7 {
let a = PI() * k / 6.0;
ear.push([ec[0] - 0.01 * a.sin(), ec[1] + sd * 0.095 * a.cos(), ec[2] + 0.095 * a.sin() - 0.045]);
}
let back = p_face(ear.map(|v| [v[0] - 0.005, v[1], v[2]]), fur);
back.at = [ec[0] - 0.005, ec[1], ec[2]];
parts.push(back);
let inner = p_face(ear.map(|v| [v[0] + 0.02, ec[1] + (v[1] - ec[1]) * 0.62, ec[2] - 0.012 + (v[2] - ec[2]) * 0.62]), "hamsterp");
inner.at = [ec[0] + 0.03, ec[1], ec[2]];
parts.push(inner);
// eyes: shiny beads on the sides of the head
parts.push(#{ t: "eye", at: [0.43, sd * 0.172, hz + 0.065], r: 0.05, closed: sleep });
}
// tail: a stub
parts.push(p_tube([-0.48, 0.0, 0.37 + dz], [-0.555, 0.0, 0.4 + dz], 0.035, 0.02, 4, fur));
// legs: a flipbook of running poses, or standing; tucked away asleep
if !sleep {
let ln = if n > 8 { 5 } else { 4 };
for i in 0..4 {
if run {
let vars = [];
for k in 0..np { vars.push(hamster_leg(i, 2.0 * PI() * k / np, true, ln, fur)); }
parts.push(#{ t: "flip", at: vars[0][0].at, vars: vars, delay: delay });
} else {
parts += hamster_leg(i, 0.0, false, ln, fur);
}
}
}
parts
}
/// Draws a hamster facing +x with its feet at (x, 0, z): hamster_parts
/// scaled by `size` (hamster_life() is life size), fur material `fur`.
/// run: running (legs in 4 poses, the body bobbing, `delay` s in); sleep:
/// curled up, eyes shut, snoring. Its faces are edged `--bw` wide (set it on
/// a group around it: the default .3 suits a hamster a metre long).
fn hamster_at(x, z, size, run, sleep, fur, delay) {
let parts = place_parts(hamster_parts(run, sleep, if size >= 1.0 { 10 } else { 8 }, 4, delay, fur), size, [x, 0.0, z]);
if run {
this.begin("a runbob", `--hb:-${round4(size * 0.25)}px;animation-delay:-${round4(delay)}s`);
}
this.draw_parts(parts);
if run {
this.end();
}
if sleep {
let zx = round4(size * 1.2);
// (its delay keeps a still view, paused anywhere, on a visible z Z)
this.raw(x + 0.45 * size, 0.0, z + 0.7 * size,
`<g class="iso-a iso-zzz" style="animation-delay:-${round4(0.9 + delay % 1.4)}s;--zx:${round4(size * 1.2)}px;--zy:-${round4(size * 2.9)}px">`
+ `<text class="iso-hamz" font-size="${zx}" x="0" y="0">z</text>`
+ `<text class="iso-hamz" font-size="${round4(zx * 1.4)}" x="${round4(zx * 0.7)}" y="-${round4(zx * 0.8)}">Z</text></g>`);
}
this
}
/// How many times bigger to draw something `l` metres across: the drawing's
/// coordinates are rounded to a tenth of a screen unit (2.5 cm), which turns
/// a life-size hamster or a 30 cm wheel blocky. See fine_begin.
fn fine_k(l) { if l < 2.0 { 10.0 } else { 1.0 } }
/// Opens a group that shows what is drawn in it k times smaller about the
/// point (x, y, z) of this frame (fz: the frame's height, the object's z), so
/// draw a small thing k times bigger about that point. css: the group's
/// style (custom properties such as --bw, k times too). Close it with
/// fine_end(), then set the sort extent with ext_reset() and ext_box(): the
/// drawn one is k times too big.
fn fine_begin(x, y, z, fz, k, css) {
if k == 1.0 {
this.raw(`<g style="${css}">`);
} else {
let l0 = to_local(0.0, 0.0);
let a = dv((x - l0[0]) * 1000.0, (y - l0[1]) * 1000.0, (z + fz) * 1000.0);
let sx = round4(a[0] / 1000.0);
let sy = round4(a[1] / 1000.0);
this.raw(`<g transform="translate(${sx} ${sy}) scale(${round4(1.0 / k)}) translate(${-sx} ${-sy})" style="${css}">`);
}
this
}
fn fine_end() { this.raw("</g>") }
fn meta_hamster() {
#{ label: "Hamster", category: "Animals", icon: "🐹",
help: "Hamsters (golden, grey or cream, white underneath) standing about, running on the spot or curled up asleep. Life size (about 17 cm) at scale 1; scale them up for a giant.",
props: herd_props(1, 0.5, 30.0) + [
#{ key: "pose", type: "choice", def: "standing", choices: ["standing", "running", "asleep"], label: "Pose" },
#{ key: "colour", type: "choice", def: "golden", choices: hamster_colours(), label: "Fur" },
#{ key: "scale", type: "float", def: 1.0, min: 0.5, max: 40.0, step: 0.5, label: "Scale (× life)" },
] }
}
fn prefab_hamster(o) {
let p = o.p;
let n = p.n;
let size = hamster_life() * p.scale;
let k = fine_k(size * 1.2);
let bw = round4(fmin(0.3, size * 0.5) * k);
let seed = o.x * 0.57 + o.y * 1.13;
for i in 0..n {
// a sunflower spread like the herds, each its own sprite
let rr = if n == 1 { 0.0 } else { p.r * ((i + 0.5) / n).sqrt() };
let ang = i * 2.39996 + hash(seed + i) * 0.6;
let w = world_of(o, rr * ang.cos(), rr * ang.sin());
let turn = if n == 1 { 0.0 } else { (hash(seed + i * 3.7) - 0.5) * 140.0 };
frame(w[0], w[1], o.z, o.rot + turn);
let s = sprite();
s.fine_begin(0.0, 0.0, 0.0, o.z, k, `--bw:${bw}`);
s.hamster_at(0.0, 0.0, size * k, p.pose == "running", p.pose == "asleep", hamster_fur(p.colour, i), round4(hash(seed + i * 2.3) * 3.2));
s.fine_end();
s.ext_reset();
s.ext_box(-0.57 * size, -0.31 * size, 0.7 * size, 0.31 * size, 0.0, 0.75 * size);
}
frame(o);
info_row("Animals", `${n} hamster${if n == 1 { "" } else { "s" }}, ${p.pose}`);
}
/// A hamster wheel of radius R drawn into this sprite around (0, 0): the
/// drum (0.7 R wide) across local y, open at -y, the spokes and the axle at
/// +y on an A-frame stand, or without the stand on an axle stub for a cage
/// wall at +y. A hamster with fur `fur` runs in it (run: legs, rungs and
/// spokes turning, `delay` s in) or stands still. Returns #{ turn: seconds
/// per turn, ext: [x0, y0, x1, y1, z0, z1] } for the sort extent (raw SVG
/// adds none). Faces are edged `--bw` wide (see hamster_at).
fn hamster_wheel(R, run, stand, fur, delay) {
let W = R * 0.7;
let N = 24; // rungs
let F = 4; // rung poses per rung step
let turn = N * F * 0.1; // s per turn
let lift = if stand { R * 0.12 } else { R * 0.04 };
let zc = lift + R;
let near = vis(0.0, 1.0); // the stand side is turned to the viewer
let ys = W / 2.0 + R * 0.15; // the stand's plane
let lw = R * 0.05; // the stand's bars
let rimw = round4(R * 0.045);
let hs = R * 0.8; // the hamster's size
let rw = `--rw:${round4(R * 0.028 * px())}`;
// the hamster and where it sorts against the rungs
let ox = -0.06 * hs;
let oz = lift + 0.035 * hs;
let hk = depth(ox, 0.0) + oz + 0.38 * hs;
// rungs: for each pose, the ones behind the hamster and the ones in front
let back = [];
let front = [];
let nposes = if run { F } else { 1 };
for k in 0..nposes {
let b = [];
let f = [];
for j in 0..N {
let a = 2.0 * PI() * (j - k / F.to_float()) / N + 0.1;
let x = R * a.cos();
let z = zc + R * a.sin();
let seg = [[x, -W / 2.0, z], [x, W / 2.0, z]];
if depth(x, 0.0) + z < hk { b.push(seg); } else { f.push(seg); }
}
back.push(b);
front.push(f);
}
// the stand side: the spoked rim turning in its plane, hub, axle, stand
let dsp = "";
for i in 0..6 {
let a = 2.0 * PI() * i / 6.0;
dsp += `M0 0L${round4(R * a.cos())} ${round4(R * a.sin())}`;
}
let plane = vplane_g(1.0, 0.0);
let rim = `<circle class="iso-wheelrim" r="${round4(R)}" style="stroke-width:${rimw}"/>`;
let spokes = plane + rim
+ (if run { `<g class="iso-a iso-wheelspin" style="animation-duration:${round4(turn)}s;animation-delay:-${round4(delay * 3.7 % turn)}s">` } else { "<g>" })
+ `<circle class="iso-swimbox" r="${round4(R)}"/><path class="iso-wheelspoke" style="stroke-width:${round4(rimw * 0.6)}" d="${dsp}"/></g>`
+ `<circle class="iso-wheelhub" r="${round4(R * 0.09)}"/></g>`;
let rim0 = plane + rim + "</g>";
let ra = R * 0.035;
let yend = if stand { ys + lw * 2.0 } else { ys + R * 0.03 };
let holder = |s| {
if stand {
let fx = R * 0.62;
s.box(-fx - lw * 2.0, ys - lw * 1.5, fx + lw * 2.0, ys + lw * 1.5, 0.0, lw, "hwheel beast");
s.beam([-fx, ys, lw], [0.0, ys, zc], [0.0, 1.0, 0.0], lw, "hwheel beast");
s.beam([fx, ys, lw], [0.0, ys, zc], [0.0, 1.0, 0.0], lw, "hwheel beast");
s.beam([-fx * 0.55, ys, zc * 0.42], [fx * 0.55, ys, zc * 0.42], [0.0, 1.0, 0.0], lw * 0.8, "hwheel beast");
} else {
// a bracket for the cage wall
s.box(-R * 0.1, ys, R * 0.1, yend, zc - R * 0.1, zc + R * 0.1, "hwheel beast");
}
s.tube([0.0, W / 2.0, zc], [0.0, yend, zc], ra, ra, 6, "hwheel beast");
};
let s = this;
if near {
s.raw(0.0, -W / 2.0, zc, rim0);
} else {
holder.call(s);
s.raw(0.0, W / 2.0, zc, spokes);
}
for k in 0..nposes {
if back[k].len() > 0 {
s.begin(if run { flip_cls(k, F) } else { "" }, if run { flip_css(k, F, delay) + ";" + rw } else { rw });
s.polyline(back[k], 0.0, "wheelrung");
s.end();
}
}
s.hamster_at(ox, oz, hs, run, false, fur, delay);
for k in 0..nposes {
if front[k].len() > 0 {
s.begin(if run { flip_cls(k, F) } else { "" }, if run { flip_css(k, F, delay) + ";" + rw } else { rw });
s.polyline(front[k], 0.0, "wheelrung");
s.end();
}
}
if near {
s.raw(0.0, W / 2.0, zc, spokes);
holder.call(s);
} else {
s.raw(0.0, -W / 2.0, zc, rim0);
}
#{ turn: turn, ext: [-R, -W / 2.0, R, yend, 0.0, zc + R] }
}
fn meta_hamster_wheel() {
#{ label: "Hamster wheel", category: "Animals", icon: "◎",
help: "A hamster running in a wheel: the drum across local y, open at -y, spokes and axle at +y on an A-frame stand (without the stand: an axle stub for a cage wall at +y). Life size by default; make it big for a hamster power plant.",
props: [
#{ key: "diameter", type: "float", def: 0.3, min: 0.15, max: 12.0, step: 0.05, label: "Diameter (m)" },
#{ key: "running", type: "bool", def: true, label: "Running" },
#{ key: "stand", type: "bool", def: true, label: "Stand" },
#{ key: "colour", type: "choice", def: "golden", choices: ["golden", "grey", "cream"], label: "Fur" },
] }
}
fn prefab_hamster_wheel(o) {
let p = o.p;
let delay = round4(hash(o.x * 0.71 + o.y * 1.31) * 2.0);
let k = fine_k(p.diameter);
let s = sprite();
s.fine_begin(0.0, 0.0, 0.0, o.z, k, `--bw:${round4(fmin(0.3, p.diameter * 0.2) * k)}`);
let w = s.hamster_wheel(p.diameter / 2.0 * k, p.running, p.stand, hamster_fur(p.colour, 0), delay);
s.fine_end();
s.ext_reset();
let e = w.ext.map(|v| v / k);
s.ext_box(e[0], e[1], e[2], e[3], e[4], e[5]);
let turn = w.turn;
let across = if p.diameter < 1.0 { `${(p.diameter * 100.0).round().to_int()} cm` } else { `${num1(p.diameter)} m` };
info_row("Wheel", if p.running { `${across} across, a turn every ${num1(turn)} s` } else { `${across} across, standing still` });
}
// ---------------------------------------------------------------- beehives // ---------------------------------------------------------------- beehives
fn hive_colours() { ["mixed", "white", "yellow", "blue", "green"] } fn hive_colours() { ["mixed", "white", "yellow", "blue", "green"] }
+33
View File
@@ -288,6 +288,15 @@ rabbit = "#8a7a66"
eleph = "#6f6b69" eleph = "#6f6b69"
elephe = "#625e5c" elephe = "#625e5c"
tusk = "#e9e2d0" tusk = "#e9e2d0"
hamster = "#d99a45"
hamsterg = "#8f8a84"
hamsterc = "#e2c58f"
hamsterw = "#ece3d2"
hamsterp = "#e98f9c"
whisker = "rgba(255, 248, 235, .8)"
hamz = "#cfe3ff"
hwheel = "#a7b2c2"
hwhub = "#5d6878"
birdd = "#0b0d10" birdd = "#0b0d10"
birdw = "#e8e6e0" birdw = "#e8e6e0"
bee = "#e7b416" bee = "#e7b416"
@@ -597,6 +606,15 @@ rabbit = "#a08d76"
eleph = "#8a8582" eleph = "#8a8582"
elephe = "#7b7673" elephe = "#7b7673"
tusk = "#f3eee0" tusk = "#f3eee0"
hamster = "#e9a650"
hamsterg = "#a29b93"
hamsterc = "#efd6a3"
hamsterw = "#fbf5ea"
hamsterp = "#f29aa7"
whisker = "rgba(90, 66, 40, .6)"
hamz = "#3b5a8a"
hwheel = "#7d8898"
hwhub = "#5d6878"
birdd = "#343a44" birdd = "#343a44"
birdw = "#f6f4ee" birdw = "#f6f4ee"
bee = "#f0c02a" bee = "#f0c02a"
@@ -647,6 +665,12 @@ rabbit = "rabbit"
eleph = "eleph" eleph = "eleph"
elephe = "elephe" elephe = "elephe"
tusk = "tusk" tusk = "tusk"
hamster = "hamster"
hamsterg = "hamsterg"
hamsterc = "hamsterc"
hamsterw = "hamsterw"
hamsterp = "hamsterp"
hwheel = "hwheel"
hive1 = "hive1" hive1 = "hive1"
hive2 = "hive2" hive2 = "hive2"
hive3 = "hive3" hive3 = "hive3"
@@ -924,6 +948,15 @@ f-conc = "conc"
".iso-svg .iso-route" = "fill: none; stroke: var(--iso-route); stroke-width: 1.2; stroke-dasharray: 5 4; stroke-linecap: round; stroke-linejoin: round; opacity: .75" ".iso-svg .iso-route" = "fill: none; stroke: var(--iso-route); stroke-width: 1.2; stroke-dasharray: 5 4; stroke-linecap: round; stroke-linejoin: round; opacity: .75"
".iso-svg .iso-antler" = "fill: none; stroke: var(--iso-antler); stroke-width: .35; stroke-linecap: round; stroke-linejoin: round" ".iso-svg .iso-antler" = "fill: none; stroke: var(--iso-antler); stroke-width: .35; stroke-linecap: round; stroke-linejoin: round"
".iso-svg .iso-bristle" = "fill: none; stroke: var(--iso-bristle); stroke-width: .5; stroke-linecap: round" ".iso-svg .iso-bristle" = "fill: none; stroke: var(--iso-bristle); stroke-width: .5; stroke-linecap: round"
".iso-svg .iso-hamstereye" = "fill: #120d0a; stroke: none"
".iso-svg .iso-hamsterglint" = "fill: #ffffff; stroke: none"
".iso-svg .iso-hamstereyec" = "fill: none; stroke: #2a1d14; stroke-width: calc(var(--bw, .3) * .5); stroke-linecap: round"
".iso-svg .iso-whisker" = "fill: none; stroke: var(--iso-whisker); stroke-width: calc(var(--bw, .3) * .35); stroke-linecap: round"
".iso-svg .iso-hamz" = "fill: var(--iso-hamz); stroke: none; font-weight: 700; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif"
".iso-svg .iso-wheelrim" = "fill: none; stroke: var(--iso-hwheel)"
".iso-svg .iso-wheelspoke" = "fill: none; stroke: var(--iso-hwheel); stroke-linecap: round"
".iso-svg .iso-wheelhub" = "fill: var(--iso-hwhub); stroke: none"
".iso-svg .iso-wheelrung" = "fill: none; stroke: var(--iso-hwheel); stroke-width: var(--rw, .4); stroke-linecap: round"
".iso-svg .iso-bird, .iso-svg .iso-wing" = "stroke: none" ".iso-svg .iso-bird, .iso-svg .iso-wing" = "stroke: none"
".iso-svg .iso-birdd" = "fill: var(--iso-birdd)" ".iso-svg .iso-birdd" = "fill: var(--iso-birdd)"
".iso-svg .iso-birdw" = "fill: var(--iso-birdw)" ".iso-svg .iso-birdw" = "fill: var(--iso-birdw)"
+7
View File
@@ -145,6 +145,13 @@ turns while grazing, rabbits hopping; Birds and Butterflies as flocks (`swim` sh
out of step); Beehives with bees, Bush, Flowers (bed or meadow), Rocks, Log / stump. out of step); Beehives with bees, Bush, Flowers (bed or meadow), Rocks, Log / stump.
`examples/country`, Country demo in the web Studio. `examples/country`, Country demo in the web Studio.
Hamsters (ported from the thaiai.lol easter egg): Hamster (golden, grey or cream, white underneath;
standing, running on the spot or asleep; life size or scaled up) and Hamster wheel (diameter, stand
or cage-wall axle, running or still). Running legs and the wheel's rungs are flipbooks of four
poses, 0.1 s each; the spokes turn by a CSS rotation in the wheel's plane. Things under 2 m are
drawn ten times bigger inside a group that scales them back, so a life-size hamster keeps its
shape on the drawing's 0.1-unit grid.
Routes (`routes.rhai`, `travel` primitive): cars, buses, motorbikes, cyclists, people, deer and Routes (`routes.rhai`, `travel` primitive): cars, buses, motorbikes, cyclists, people, deer and
elephants along waypoints drawn point by point in the Studio (`shape: "path"`, `pts`); loops or elephants along waypoints drawn point by point in the Studio (`shape: "path"`, `pts`); loops or
there and back, each way on its side of the line, rounded corners. Every straight piece is a there and back, each way on its side of the line, rounded corners. Every straight piece is a
+19
View File
@@ -168,6 +168,25 @@ spread within `r` of the object, each facing its rotation give or take 70°,
each its own sprite. Flocks of birds, butterflies and bees use `swim` with a each its own sprite. Flocks of birds, butterflies and bees use `swim` with a
`shape`. `shape`.
Hamsters add three kinds of parts to `draw_parts`: `loft2` (a loft in two
colours: faces pointing down get the belly colour, see `loft2`), `eye` (a
shiny bead, or shut) and `flip` (a flipbook: one part list per pose, shown
0.1 s each in groups `iso-a iso-flip4 [iso-pose0]`, the step of the viewer's
light animation mode). `hamster_parts(run, sleep, n, np, delay, fur)` builds
one about 1.2 m long, `place_parts(parts, s, o)` scales and moves any part
list, and `s.hamster_at(x, z, size, run, sleep, fur, delay)` draws it
(`hamster_life()` = life size). `s.hamster_wheel(R, run, stand, fur, delay)`
draws a wheel with a hamster in it: the rungs join the wheel's two planes, so
they are a flipbook too, drawn behind or in front of the hamster pose by
pose; the spokes turn by CSS inside `vplane_g(ux, uy)`, a raw group mapping
a vertical plane onto the screen in metres.
Small things lose their shape to the drawing's 0.1-unit grid (2.5 cm): draw
them `fine_k(size)` times bigger between `s.fine_begin(x, y, z, fz, k, css)`
and `s.fine_end()` (a group that scales them back about the point), then set
the sort extent with `ext_reset()` and `ext_box()`. Screen-unit values
inside (stroke widths, `--bw` for the `beast` edges) are k times bigger too.
## Points (`pts`) ## Points (`pts`)
An object's `pts` are plan points relative to its x, y in its turned frame. A prefab that draws An object's `pts` are plan points relative to its x, y in its turned frame. A prefab that draws
+11 -1
View File
@@ -74,7 +74,7 @@
.iso-sprites path, .iso-sprites ellipse, .iso-sprites circle { stroke: var(--iso-edge); stroke-width: var(--iso-edgew); stroke-linejoin: round; } .iso-sprites path, .iso-sprites ellipse, .iso-sprites circle { stroke: var(--iso-edge); stroke-width: var(--iso-edgew); stroke-linejoin: round; }
.iso-ground path, .iso-ground ellipse { stroke: none; } .iso-ground path, .iso-ground ellipse { stroke: none; }
/* animals: each face edged in its own shade (no seams, no wireframe), so a body reads as one surface */ /* animals: each face edged in its own shade (no seams, no wireframe), so a body reads as one surface */
.iso-sprites .iso-beast { stroke-width: .3; } .iso-sprites .iso-beast { stroke-width: var(--bw, .3); }
.iso-sprites .iso-beast.iso-t { stroke: var(--b, #888); } .iso-sprites .iso-beast.iso-t { stroke: var(--b, #888); }
.iso-sprites .iso-beast.iso-l { stroke: color-mix(in srgb, var(--b, #888) var(--iso-kl), var(--iso-shade)); } .iso-sprites .iso-beast.iso-l { stroke: color-mix(in srgb, var(--b, #888) var(--iso-kl), var(--iso-shade)); }
.iso-sprites .iso-beast.iso-c { stroke: color-mix(in srgb, var(--b, #888) var(--iso-kc), var(--iso-shade)); } .iso-sprites .iso-beast.iso-c { stroke: color-mix(in srgb, var(--b, #888) var(--iso-kc), var(--iso-shade)); }
@@ -139,6 +139,13 @@ html[data-theme=light] .iso-svg .iso-glint { display: inline; }
.iso-svg .iso-flap { transform-box: fill-box; transform-origin: center; animation: iso-flap .8s ease-in-out infinite alternate; } .iso-svg .iso-flap { transform-box: fill-box; transform-origin: center; animation: iso-flap .8s ease-in-out infinite alternate; }
.iso-svg .iso-flutter { transform-box: fill-box; transform-origin: center; animation: iso-flap .3s ease-in-out infinite alternate; } .iso-svg .iso-flutter { transform-box: fill-box; transform-origin: center; animation: iso-flap .3s ease-in-out infinite alternate; }
.iso-svg .iso-hop { animation: iso-hop 3.4s ease-out infinite; } .iso-svg .iso-hop { animation: iso-hop 3.4s ease-out infinite; }
/* hamsters: running legs and wheel rungs are flipbooks of 4 poses, 0.1 s each (delays set the turn;
unanimated the first); spokes turn in the wheel's plane; runners bob, sleepers snore */
.iso-svg .iso-flip4 { visibility: hidden; animation: iso-flip4 .4s step-end infinite; }
.iso-svg .iso-flip4.iso-pose0 { visibility: visible; }
.iso-svg .iso-wheelspin { transform-box: fill-box; transform-origin: center; animation: iso-spin 9.6s linear infinite; }
.iso-svg .iso-runbob { animation: iso-runbob .4s ease-in-out infinite; }
.iso-svg .iso-zzz { animation: iso-zzz 3.2s ease-out infinite; }
.iso-svg .iso-park { display: none; } .iso-svg .iso-park { display: none; }
/* route movers: each piece's copy shows only while a mover is on it (keyframes made per piece); /* route movers: each piece's copy shows only while a mover is on it (keyframes made per piece);
not .iso-a, so unanimated views pause them where they are instead of hiding them */ not .iso-a, so unanimated views pause them where they are instead of hiding them */
@@ -168,6 +175,9 @@ html[data-theme=light] .iso-svg .iso-glint { display: inline; }
@keyframes iso-pose2b { 0% { visibility: hidden; } 65%, 100% { visibility: visible; } } @keyframes iso-pose2b { 0% { visibility: hidden; } 65%, 100% { visibility: visible; } }
@keyframes iso-flap { from { transform: scaleX(1); } to { transform: scaleX(.25); } } @keyframes iso-flap { from { transform: scaleX(1); } to { transform: scaleX(.25); } }
@keyframes iso-hop { 0%, 70%, 100% { transform: translate(0, 0); } 78% { transform: translate(0, -4px); } 86% { transform: translate(0, 0); } 91% { transform: translate(0, -2px); } } @keyframes iso-hop { 0%, 70%, 100% { transform: translate(0, 0); } 78% { transform: translate(0, -4px); } 86% { transform: translate(0, 0); } 91% { transform: translate(0, -2px); } }
@keyframes iso-flip4 { 0% { visibility: visible; } 25%, 100% { visibility: hidden; } }
@keyframes iso-runbob { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(0, var(--hb, -1px)); } }
@keyframes iso-zzz { 0% { transform: translate(0, 0); opacity: 0; } 20% { opacity: 1; } 100% { transform: translate(var(--zx, 3px), var(--zy, -7px)); opacity: 0; } }
@keyframes iso-ram { 0%, 20%, 100% { transform: translate(0, 0); } 45%, 55% { transform: translate(var(--dx), var(--dy)); } } @keyframes iso-ram { 0%, 20%, 100% { transform: translate(0, 0); } 45%, 55% { transform: translate(var(--dx), var(--dy)); } }
@keyframes iso-move { from { transform: translate(0, 0); } to { transform: translate(var(--dx), var(--dy)); } } @keyframes iso-move { from { transform: translate(0, 0); } to { transform: translate(var(--dx), var(--dy)); } }
@keyframes iso-shuttle { @keyframes iso-shuttle {