Wildlife and nature: animals, beehives, bushes, flowers, rocks, logs
Deploy web Studio / deploy (push) Successful in 1m5s

New stdlib module wildlife.rhai:
- Animals (new palette category): Deer (stag with antlers, does, fawns),
  Wild boar, Fox, Elephant (bull with tusks, cow, calf), Rabbits. Low-poly
  solids from faces (loft, tube), parts drawn back to front by depth, faces
  edged in their own shade (class beast). Herds scatter round a spot, each
  animal its own sprite facing about the object's rotation. Grazing
  animals switch between two head poses (iso-pose2 groups), elephants curl
  their trunks, rabbits hop.
- Birds and Butterflies as flocks: swim() takes a shape (bird, fly, bee);
  wings flap out of step. Fish output unchanged.
- Beehives on stands with supers, lids, entrances and bees flying round;
  Bush (round, flowering, berries); Flowers (bed in rows or meadow); Rocks;
  Log or stump. g-meadow and g-soil ground materials.
- polyline() takes 3D points as documented (antlers).

Theme colours for dark and light, viewer CSS for poses, flapping, hopping.
examples/country: elephants in a paddock with a cut corner, deer at a
forest edge, boar, fox, rabbits, an apiary by a meadow, a pond, a lane,
fence and hedgerow at angles; Country demo in the web Studio and deploy.
MCP guidance, README, SCRIPTING, ROADMAP.

Tests: Rust 78, Studio 122; existing examples draw the same (only the
embedded theme CSS grew).
This commit is contained in:
grabowski committed 2026-10-25 13:57:23 +07:00
1 parent 96d472625f
commit e840d36000
22 files changed
+1209 -14

No files matched your search

+1
View File
@@ -23,6 +23,7 @@ on:
- "examples/house/**" - "examples/house/**"
- "examples/factory/**" - "examples/factory/**"
- "examples/streets/**" - "examples/streets/**"
- "examples/country/**"
- "tools/build_web.py" - "tools/build_web.py"
- "Cargo.toml" - "Cargo.toml"
- "Cargo.lock" - "Cargo.lock"
+11
View File
@@ -79,6 +79,17 @@ The **House** palette has the parts, **Furniture** the interior:
swimming round under lily pads, ripples and reeds; trees, hedges and rows of 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
kind), Wild boar, Fox, Elephant (bull, cow, calf), Rabbits, a flock of Birds
circling and Butterflies; Beehives on stands with bees flying round, Bushes
(round, flowering, berries), Flowers (a bed in rows or a wildflower meadow),
Rocks and a Log or stump. Animals are low-poly solids that read from every
side; a herd scatters about the object's spot, each animal its own sprite.
Grazing animals lower and raise their heads in turn, elephants curl their
trunks, rabbits hop, 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.
**Fences and walls** around the plot: the Fence has six designs (mesh, wooden **Fences and walls** around the plot: the Fence has six designs (mesh, wooden
boards, steel bars, mesh on a stone base, cobble wall, brick wall with boards, steel bars, mesh on a stone base, cobble wall, brick wall with
pillars). Fences and site walls whose ends meet join like house walls: corners pillars). Fences and site walls whose ends meet join like house walls: corners
+4
View File
@@ -206,6 +206,8 @@ pub enum Prim {
/// One fish of a [`Prim::Swim`]: it goes round an ellipse of radii (rx, ry) /// One fish of a [`Prim::Swim`]: it goes round an ellipse of radii (rx, ry)
/// once every `dur` seconds, `delay` seconds into its round at the start, /// once every `dur` seconds, `delay` seconds into its round at the start,
/// counter-clockwise (seen from above) unless `cw`; `len` is its length. /// counter-clockwise (seen from above) unless `cw`; `len` is its length.
/// `shape`: "fish" (or empty), "bird" (wings spread `len` wide, flapping),
/// "bee" or "fly" (a butterfly, fluttering): flocks use the same orbits.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Fish { pub struct Fish {
pub rx: f64, pub rx: f64,
@@ -215,4 +217,6 @@ pub struct Fish {
pub cw: bool, pub cw: bool,
pub len: f64, pub len: f64,
pub cls: String, pub cls: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub shape: String,
} }
+89 -4
View File
@@ -307,11 +307,38 @@ impl Ctx<'_> {
let e = 1.0 + l / rx.min(ry) + 0.05; let e = 1.0 + l / rx.min(ry) + 0.05;
// a fish nose first along +y (counter-clockwise) or -y // a fish nose first along +y (counter-clockwise) or -y
let k = if f.cw { -l } else { l }; let k = if f.cw { -l } else { l };
// body widest behind the head, a narrow tail root, a forked tail fin // body widest behind the head, a narrow tail root, a forked tail fin; birds,
let (b, t) = (body_path(k, l), tail_path(k, l)); // bees and butterflies in their own shapes, wings flapping in a group of their own
// wingbeats out of step: each bird or butterfly starts its own way into one
let beat = r3s((f.delay * 0.731).fract());
let body = match f.shape.as_str() {
"bird" => format!(
"<path class=\"iso-bird iso-{}\" d=\"{}\"/><g class=\"iso-a iso-flap\" style=\"animation-delay:-{beat}s\"><path class=\"iso-bird iso-{}\" d=\"{}\"/></g>",
esc(&f.cls),
bird_body(k, l),
esc(&f.cls),
bird_wings(k, l)
),
"fly" => format!(
"<g class=\"iso-a iso-flutter\" style=\"animation-delay:-{beat}s\"><path class=\"iso-wing iso-{}\" d=\"{}\"/></g>",
esc(&f.cls),
fly_wings(k, l)
),
"bee" => format!(
"<path class=\"iso-bee iso-{}\" d=\"{}\"/>",
esc(&f.cls),
bee_path(k, l)
),
_ => format!(
"<path class=\"iso-fish iso-{}\" d=\"{}{}\"/>",
esc(&f.cls),
body_path(k, l),
tail_path(k, l)
),
};
let _ = write!( let _ = write!(
self.out, self.out,
"<g transform=\"scale({} {})\"><g class=\"iso-a iso-swim\" style=\"animation-duration:{}s;animation-delay:-{}s{}\"><path class=\"iso-swimbox\" d=\"M{} {}L{} {}\"/><g transform=\"translate(1 0) scale({} {})\"><path class=\"iso-fish iso-{}\" d=\"{b}{t}\"/></g></g></g>", "<g transform=\"scale({} {})\"><g class=\"iso-a iso-swim\" style=\"animation-duration:{}s;animation-delay:-{}s{}\"><path class=\"iso-swimbox\" d=\"M{} {}L{} {}\"/><g transform=\"translate(1 0) scale({} {})\">{body}</g></g></g>",
r3s(rx), r3s(rx),
r3s(ry), r3s(ry),
r3s(f.dur.max(1.0)), r3s(f.dur.max(1.0)),
@@ -323,7 +350,6 @@ impl Ctx<'_> {
r3s(e), r3s(e),
r3s(1.0 / rx), r3s(1.0 / rx),
r3s(1.0 / ry), r3s(1.0 / ry),
esc(&f.cls),
); );
} }
self.out.push_str("</g>"); self.out.push_str("</g>");
@@ -788,6 +814,65 @@ fn body_path(k: f64, l: f64) -> String {
} }
/// The forked tail fin behind [`body_path`]. /// The forked tail fin behind [`body_path`].
/// A bird seen from above flying along +y (k > 0) or -y: a slim body ...
fn bird_body(k: f64, l: f64) -> String {
format!(
"M0 {}L{} 0L0 {}L{} 0Z",
r3s(0.22 * k),
r3s(0.05 * l),
r3s(-0.3 * k),
r3s(-0.05 * l)
)
}
/// ... and its wings, `l` from tip to tip, swept back.
fn bird_wings(k: f64, l: f64) -> String {
format!(
"M{} {}L{} {}L0 {}L{} {}L{} {}L{} {}L0 {}L{} {}Z",
r3s(-0.5 * l),
r3s(-0.12 * k),
r3s(-0.22 * l),
r3s(0.1 * k),
r3s(0.04 * k),
r3s(0.22 * l),
r3s(0.1 * k),
r3s(0.5 * l),
r3s(-0.12 * k),
r3s(0.2 * l),
r3s(-0.03 * k),
r3s(-0.07 * k),
r3s(-0.2 * l),
r3s(-0.03 * k)
)
}
/// A butterfly's two pairs of wings, `l` across.
fn fly_wings(k: f64, l: f64) -> String {
let w = |s: f64| {
format!(
"M0 {}L{} {}L{} {}L0 {}Z",
r3s(0.08 * k),
r3s(s * 0.5 * l),
r3s(0.32 * k),
r3s(s * 0.42 * l),
r3s(-0.3 * k),
r3s(-0.12 * k)
)
};
format!("{}{}", w(1.0), w(-1.0))
}
/// A bee: a small oval, `l` long.
fn bee_path(k: f64, l: f64) -> String {
format!(
"M0 {}L{} 0L0 {}L{} 0Z",
r3s(0.5 * k),
r3s(0.3 * l),
r3s(-0.5 * k),
r3s(-0.3 * l)
)
}
fn tail_path(k: f64, l: f64) -> String { fn tail_path(k: f64, l: f64) -> String {
format!( format!(
"M0 {}L{} {}L0 {}L{} {}Z", "M0 {}L{} {}L0 {}L{} {}Z",
+2
View File
@@ -27,6 +27,8 @@ 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 \
scattered within props.r metres of x, y, facing about the rotation; keep them out of roads, ponds and buildings. \
Production lines (category Industry): a producer (props.rate per minute, product) puts goods out on its local +x \ Production lines (category Industry): a producer (props.rate per minute, product) puts goods out on its local +x \
side; a conveyor (rot 0 runs towards +x, 90 +y, 180 -x, 270 -y; w = length, props.speed m/s) starting exactly \ side; a conveyor (rot 0 runs towards +x, 90 +y, 180 -x, 270 -y; w = length, props.speed m/s) starting exactly \
at that side carries them, and a conveyor whose start is at another's end carries on with them (corners join). \ at that side carries them, and a conveyor whose start is at another's end carries on with them (corners join). \
+6 -1
View File
@@ -72,7 +72,7 @@ fn demo_passes_the_check() {
assert!(f.is_empty(), "{f:?}"); assert!(f.is_empty(), "{f:?}");
} }
/// The house, factory and streets demos: every view draws every object without warnings /// The house, factory, streets and country demos: every view draws every object without warnings
/// or depth-sort cycles (walls and belts must not overlap), and the projects pass the check. /// or depth-sort cycles (walls and belts must not overlap), and the projects pass the check.
#[test] #[test]
fn house_demo_renders_every_view_cleanly() { fn house_demo_renders_every_view_cleanly() {
@@ -89,6 +89,11 @@ fn streets_demo_renders_every_view_cleanly() {
example_renders_every_view_cleanly("streets"); example_renders_every_view_cleanly("streets");
} }
#[test]
fn country_demo_renders_every_view_cleanly() {
example_renders_every_view_cleanly("country");
}
fn example_renders_every_view_cleanly(name: &str) { fn example_renders_every_view_cleanly(name: &str) {
let p = Project::load( let p = Project::load(
Path::new(env!("CARGO_MANIFEST_DIR")) Path::new(env!("CARGO_MANIFEST_DIR"))
+1
View File
@@ -470,6 +470,7 @@ pub fn register_sprite(e: &mut Engine) {
.get("cls") .get("cls")
.map(|v| v.to_string()) .map(|v| v.to_string())
.unwrap_or_else(|| "koi".into()), .unwrap_or_else(|| "koi".into()),
shape: m.get("shape").map(|v| v.to_string()).unwrap_or_default(),
}); });
} }
s.0.borrow_mut().swim(n(&x)?, n(&y)?, n(&z)?, fish); s.0.borrow_mut().swim(n(&x)?, n(&y)?, n(&z)?, fish);
+22 -4
View File
@@ -90,13 +90,31 @@ pub fn polys3(d: &Dynamic) -> RR<Vec<Vec<P3>>> {
} }
} }
/// One line or several, of plan points `[x, y]` (put at height z) or 3D points. /// One line or several, of plan points `[x, y]` (put at height z) or 3D
/// points `[x, y, z]` (antlers, branches).
pub fn polys3_or_flat(d: &Dynamic, z: f64) -> RR<Vec<Vec<P3>>> { pub fn polys3_or_flat(d: &Dynamic, z: f64) -> RR<Vec<Vec<P3>>> {
polys(d).map(|ls| { let a = arr(d, "polyline")?;
ls.into_iter() let lines: Vec<&Dynamic> = match a.first() {
.map(|l| l.into_iter().map(|p| [p[0], p[1], z]).collect()) None => return Ok(vec![]),
Some(f) if is_point(f) => vec![d],
_ => a.iter().collect(),
};
lines
.into_iter()
.map(|l| {
arr(l, "polyline")?
.iter()
.map(|q| {
let n = q.read_lock::<Array>().map(|v| v.len()).unwrap_or(0);
if n >= 3 {
p3(q)
} else {
p2(q).map(|p| [p[0], p[1], z])
}
})
.collect() .collect()
}) })
.collect()
} }
/// `[[a, b, c, d], ...]` (segments, opening lists) /// `[[a, b, c, d], ...]` (segments, opening lists)
+1
View File
@@ -9,6 +9,7 @@ pub const MODULES: &[(&str, &str)] = &[
("isokit/util", include_str!("../stdlib/util.rhai")), ("isokit/util", include_str!("../stdlib/util.rhai")),
("isokit/ground", include_str!("../stdlib/ground.rhai")), ("isokit/ground", include_str!("../stdlib/ground.rhai")),
("isokit/streets", include_str!("../stdlib/streets.rhai")), ("isokit/streets", include_str!("../stdlib/streets.rhai")),
("isokit/wildlife", include_str!("../stdlib/wildlife.rhai")),
("isokit/nature", include_str!("../stdlib/nature.rhai")), ("isokit/nature", include_str!("../stdlib/nature.rhai")),
("isokit/building", include_str!("../stdlib/building.rhai")), ("isokit/building", include_str!("../stdlib/building.rhai")),
("isokit/vehicles", include_str!("../stdlib/vehicles.rhai")), ("isokit/vehicles", include_str!("../stdlib/vehicles.rhai")),
+95
View File
@@ -858,6 +858,101 @@ fn piece(id: &str, kind: &str, a: [f64; 2], b: [f64; 2], props: Value) -> Object
o o
} }
#[test]
fn wildlife_and_garden_prefabs_draw_from_every_side() {
let r = Runtime::new(&Bundle::default()).unwrap();
let doc = Doc {
objects: vec![
obj("deer", "deer", -18.0, 8.0, json!({"n": 5, "r": 4})),
obj(
"stag",
"deer",
-8.0,
12.0,
json!({"n": 1, "mix": "stag", "graze": false}),
),
obj("boar", "boar", -18.0, -6.0, json!({"n": 3})),
obj("fox", "fox", -8.0, -2.0, json!({})),
obj(
"el",
"elephant",
6.0,
10.0,
json!({"n": 3, "mix": "herd", "r": 5}),
),
obj("rab", "rabbits", -4.0, -12.0, json!({"n": 4})),
obj("birds", "birds", 0.0, 0.0, json!({"n": 7})),
obj("bf", "butterflies", 10.0, -12.0, json!({"n": 5})),
obj("hives", "beehive", 16.0, -6.0, json!({"n": 4})),
obj("bed", "flowers", 10.0, -12.0, json!({"w": 5, "d": 3})),
obj(
"mead",
"flowers",
20.0,
-14.0,
json!({"w": 8, "d": 5, "kind": "meadow"}),
),
obj("b1", "bush", 3.0, -4.0, json!({})),
obj("b2", "bush", 5.0, -6.0, json!({"kind": "berries"})),
obj("rk", "rocks", 22.0, 6.0, json!({"n": 4})),
obj("lg", "log", 14.0, 4.0, json!({})),
obj("st", "log", 18.0, 2.0, json!({"kind": "stump"})),
],
..Default::default()
};
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"));
// each animal its own sprite: five deer, three boar, one fox, three elephants, four rabbits
for (id, n) in [
("deer", 5),
("stag", 1),
("boar", 3),
("fox", 1),
("el", 3),
("rab", 4),
] {
assert_eq!(sprites_of(&b, id), n, "{id}");
}
// grazing animals carry two head poses taking turns, the first shown unanimated;
// the standing stag and the rabbits have none
assert_eq!(
out.html.matches("iso-a iso-pose2 iso-pose0").count(),
5 + 3 + 1 + 3
);
assert_eq!(out.html.matches("iso-a iso-pose2\"").count(), 5 + 3 + 1 + 3);
assert_eq!(out.html.matches("iso-a iso-hop").count(), 4);
// birds flap, butterflies flutter, bees fly round the hives
assert_eq!(out.html.matches("iso-a iso-flap").count(), 7);
assert_eq!(out.html.matches("iso-a iso-flutter").count(), 5);
assert_eq!(out.html.matches("class=\"iso-bee iso-bee\"").count(), 4 * 8);
// animal faces are edged in their own shade; antlers on the one stag
assert!(out.html.contains("iso-deer iso-beast"));
assert!(out.html.contains("iso-antler"));
}
let b = r.build("site", 0, &cfg(), &doc).unwrap();
// a stag stands about 1.5 m at the head, antlers up to 2.1 m; it fits its sort box
let st = b.boxes["stag"];
assert!(st[5] > 1.9 && st[5] < 2.3, "{st:?}");
// an elephant bull: about 3 m tall at the head
let el = b.boxes["el"];
assert!(el[5] > 2.6 && el[5] < 3.3, "{el:?}");
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("deer", "Herd"), "1 stag, 3 does, 1 fawn");
assert_eq!(row("el", "Herd"), "1 bull, 1 cow, 1 calf");
assert_eq!(row("hives", "Apiary"), "4 hives, 2 supers each");
}
#[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();
+1 -1
View File
@@ -3,7 +3,7 @@
// document order (later objects lie on top). // document order (later objects lie on top).
fn ground_materials() { fn ground_materials() {
["g-grass", "g-grass2", "g-green", "g-garden", "g-asph", "g-conc", "g-pad", "g-path", "g-sand", "g-court", "dirt"] ["g-grass", "g-grass2", "g-green", "g-garden", "g-asph", "g-conc", "g-pad", "g-path", "g-sand", "g-court", "g-meadow", "g-soil", "dirt"]
} }
fn meta_site() { fn meta_site() {
+698
View File
@@ -0,0 +1,698 @@
// isokit/wildlife: animals (deer, wild boar, fox, elephant, rabbits, 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
// 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
// at scattered spots with their own headings, each animal its own sprite.
// Grazing animals take turns between two head poses (a CSS flipbook, paused
// like every animation); birds, butterflies and bees fly round on orbits.
fn animal_kinds() { ["deer", "boar", "fox", "elephant", "rabbits"] }
// ---------------------------------------------------------------- vectors
fn vadd(a, b) { [a[0] + b[0], a[1] + b[1], a[2] + b[2]] }
fn vsub(a, b) { [a[0] - b[0], a[1] - b[1], a[2] - b[2]] }
fn vmul(a, k) { [a[0] * k, a[1] * k, a[2] * k] }
fn vdot(a, b) { a[0] * b[0] + a[1] * b[1] + a[2] * b[2] }
fn vcross(a, b) { [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]] }
fn vunit(a) {
let l = fmax((a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt(), 1e-12);
[a[0] / l, a[1] / l, a[2] / l]
}
// ---------------------------------------------------------------- solids
/// A loft along `spine` (3D points of this frame) with n sides: rings of
/// radii rs (each a number or [across, up]: an oval), side faces between
/// them and a cap at each end; mats: [sides, first cap, last cap]. Bodies,
/// limbs, necks, heads, trunks, logs. The rings keep their turn along a bent
/// spine (parallel transport), so trunks and tails do not twist.
fn loft(spine, rs, n, mats) {
let m = spine.len();
let rings = [];
let v1 = ();
for i in 0..m {
let a = spine[if i == 0 { 0 } else { i - 1 }];
let b = spine[if i == m - 1 { m - 1 } else { i + 1 }];
let u = vunit(vsub(b, a));
if type_of(v1) == "()" {
v1 = if u[2].abs() > 0.95 { vunit(vcross([0.0, 1.0, 0.0], u)) } else { vunit(vcross([0.0, 0.0, 1.0], u)) };
} else {
v1 = vunit(vsub(v1, vmul(u, vdot(v1, u))));
}
let v2 = vcross(u, v1);
let r = rs[i];
let rw = if type_of(r) == "array" { r[0] } else { r };
let rh = if type_of(r) == "array" { r[1] } else { r };
let ring = [];
for k in 0..n {
let t = 2.0 * PI() * (k + 0.5) / n;
ring.push(vadd(spine[i], vadd(vmul(v1, rw * t.cos()), vmul(v2, rh * t.sin()))));
}
rings.push(ring);
}
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
}
/// 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]) }
// 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
// part lists shown in turn: a, b, css).
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_face(pts, mat) { #{ at: pts[0], t: "face", pts: pts, mat: mat } }
fn p_line(pts, cls) { #{ at: pts[pts.len() / 2], t: "line", pts: pts, cls: cls } }
/// The depth of a part's anchor: nearer to the viewer is larger (plan depth
/// plus height: the view looks down along (1, 1, 1) in view space).
fn part_key(p) { depth(p.at[0], p.at[1]) + p.at[2] }
/// Draws parts back to front.
fn draw_parts(parts) {
let ps = parts.map(|p| #{ k: part_key(p), p: p });
ps.sort(|a, b| if a.k < b.k { -1 } else if a.k > b.k { 1 } else { 0 });
for e in ps {
let p = e.p;
if p.t == "loft" {
this.loft(p.spine, p.rs, p.n, p.mats.map(|m| m + " beast"));
} else if p.t == "face" {
this.face(p.pts, p.mat + " beast", #{ two_sided: true });
} else if p.t == "line" {
this.polyline(p.pts, 0.0, p.cls);
} else if p.t == "alt" {
this.begin("a pose2 pose0", p.css);
this.draw_parts(p.a);
this.end();
this.begin("a pose2", p.css);
this.draw_parts(p.b);
this.end();
}
}
this
}
/// 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).
fn hp(base, f, a, b, c) {
let w = [-f[2], 0.0, f[0]];
vadd(base, vadd(vmul(f, a), vadd([0.0, b, 0.0], vmul(w, c))))
}
/// Scale every point of a part list by s (fawns, calves).
fn scale_parts(parts, s) {
parts.map(|p| {
let q = p;
q.at = vmul(p.at, s);
if p.t == "loft" {
q.spine = p.spine.map(|v| 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| vmul(v, s));
} else if p.t == "alt" {
q.a = scale_parts(p.a, s);
q.b = scale_parts(p.b, s);
}
q
})
}
/// 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.
fn legs(fx, bx, y, z0, r0, r1, mat) {
let out = [];
for x in [fx, bx] {
for s in [1.0, -1.0] {
out.push(p_tube([x, s * y, z0], [x + 0.02 * (if x > 0.0 { 1.0 } else { -1.0 }), s * y, 0.0], r0, r1, 4, mat));
}
}
out
}
/// The parts of one animal of `kind` facing +x at (0, 0), and the head
/// poses that take turns when it grazes: #{ body: [...], up: [...], down: [...] }.
fn animal_parts(kind, sub) {
if kind == "deer" { return deer_parts(sub); }
if kind == "boar" { return boar_parts(); }
if kind == "fox" { return fox_parts(); }
if kind == "elephant" { return elephant_parts(sub); }
rabbit_parts()
}
/// A red or roe deer: sub "stag" (antlers), "doe" or "fawn" (drawn smaller).
fn deer_parts(sub) {
let body = [
p_loft([[-0.62, 0.0, 0.99], [-0.55, 0.0, 0.97], [-0.3, 0.0, 0.95], [0.15, 0.0, 0.96], [0.5, 0.0, 1.0], [0.57, 0.0, 1.02]],
[[0.08, 0.1], [0.15, 0.18], [0.19, 0.23], [0.2, 0.24], [0.17, 0.22], [0.1, 0.14]], 8, ["deer", "deerl", "deer"]),
p_tube([-0.55, 0.0, 1.05], [-0.64, 0.0, 0.94], 0.04, 0.03, 4, "deerl"),
];
body += legs(0.38, -0.4, 0.1, 0.8, 0.045, 0.022, "deer");
let head = |base, f| {
let out = [
p_tube(base, hp(base, f, 0.27, 0.0, -0.02), [0.07, 0.08], [0.033, 0.04], 6, "deer"),
p_tube(hp(base, f, 0.25, 0.0, -0.02), hp(base, f, 0.29, 0.0, -0.025), 0.03, 0.022, 4, "nose"),
];
for s in [1.0, -1.0] {
out.push(p_face([hp(base, f, 0.03, s * 0.05, 0.05), hp(base, f, -0.03, s * 0.05, 0.04), hp(base, f, -0.04, s * 0.18, 0.15)], "deer"));
if sub == "stag" {
let a = hp(base, f, -0.01, s * 0.04, 0.07);
let b = hp(base, f, -0.08, s * 0.16, 0.3);
let c = hp(base, f, -0.14, s * 0.22, 0.5);
out.push(p_line([a, b, c, hp(base, f, -0.1, s * 0.3, 0.64)], "antler"));
out.push(p_line([b, hp(base, f, 0.06, s * 0.2, 0.4)], "antler"));
out.push(p_line([c, hp(base, f, -0.02, s * 0.32, 0.58)], "antler"));
}
}
out
};
let up = [p_tube([0.45, 0.0, 1.08], [0.72, 0.0, 1.45], 0.085, 0.06, 6, "deer")] + head.call([0.72, 0.0, 1.47], vunit([0.95, 0.0, -0.3]));
let down = [p_tube([0.47, 0.0, 1.02], [0.8, 0.0, 0.48], 0.085, 0.06, 6, "deer")] + head.call([0.8, 0.0, 0.47], vunit([0.35, 0.0, -0.94]));
let r = #{ body: body, up: up, down: down };
if sub == "fawn" {
r.body = scale_parts(r.body, 0.6);
r.up = scale_parts(r.up, 0.6);
r.down = scale_parts(r.down, 0.6);
}
r
}
/// A wild boar: a heavy front, a long snout, small tusks; it roots with its snout.
fn boar_parts() {
let body = [
p_loft([[-0.66, 0.0, 0.58], [-0.58, 0.0, 0.58], [-0.3, 0.0, 0.6], [0.15, 0.0, 0.64], [0.5, 0.0, 0.66], [0.58, 0.0, 0.67]],
[[0.09, 0.11], [0.17, 0.2], [0.23, 0.27], [0.26, 0.33], [0.24, 0.3], [0.16, 0.2]], 8, ["boar", "boar", "boar"]),
p_tube([-0.58, 0.0, 0.66], [-0.68, 0.0, 0.5], 0.025, 0.015, 4, "boar"),
p_line([[-0.45, 0.0, 0.86], [0.0, 0.0, 0.96], [0.45, 0.0, 0.98]], "bristle"),
];
body += legs(0.36, -0.4, 0.12, 0.42, 0.06, 0.035, "boar");
let head = |base, f| {
let out = [
p_tube(base, hp(base, f, 0.42, 0.0, -0.04), [0.17, 0.19], [0.06, 0.065], 6, "boar"),
p_tube(hp(base, f, 0.41, 0.0, -0.04), hp(base, f, 0.45, 0.0, -0.045), 0.06, 0.055, 6, "nose"),
];
for s in [1.0, -1.0] {
out.push(p_face([hp(base, f, 0.04, s * 0.09, 0.12), hp(base, f, -0.04, s * 0.08, 0.1), hp(base, f, -0.02, s * 0.15, 0.24)], "boar"));
out.push(p_tube(hp(base, f, 0.32, s * 0.06, -0.08), hp(base, f, 0.36, s * 0.09, 0.0), 0.014, 0.008, 4, "tusk"));
}
out
};
let up = head.call([0.6, 0.0, 0.72], vunit([0.9, 0.0, -0.45]));
let down = head.call([0.62, 0.0, 0.62], vunit([0.55, 0.0, -0.84]));
#{ body: body, up: up, down: down }
}
/// A red fox: slim, dark stockings, big ears, a bushy tail with a white tip.
fn fox_parts() {
let body = [
p_loft([[-0.32, 0.0, 0.36], [0.0, 0.0, 0.36], [0.3, 0.0, 0.39], [0.36, 0.0, 0.41]], [[0.08, 0.09], [0.1, 0.11], [0.085, 0.1], [0.05, 0.06]], 8, ["fox", "fox", "foxw"]),
p_loft([[-0.3, 0.0, 0.38], [-0.5, 0.0, 0.33], [-0.68, 0.0, 0.26]], [0.045, 0.085, 0.07], 6, ["fox", "fox", "fox"]),
p_tube([-0.68, 0.0, 0.26], [-0.8, 0.0, 0.22], 0.07, 0.025, 6, "foxw"),
];
body += legs(0.24, -0.24, 0.05, 0.3, 0.025, 0.018, "foxd");
let head = |base, f| {
let out = [
p_tube([0.27, 0.0, 0.42], base, 0.06, 0.055, 6, "fox"),
p_tube(base, hp(base, f, 0.2, 0.0, -0.03), [0.07, 0.06], [0.014, 0.014], 6, "fox"),
p_tube(hp(base, f, 0.18, 0.0, -0.03), hp(base, f, 0.21, 0.0, -0.035), 0.015, 0.01, 4, "nose"),
];
for s in [1.0, -1.0] {
out.push(p_face([hp(base, f, 0.04, s * 0.04, 0.05), hp(base, f, -0.04, s * 0.04, 0.04), hp(base, f, -0.01, s * 0.08, 0.16)], "fox"));
}
out
};
let up = head.call([0.4, 0.0, 0.5], vunit([0.95, 0.0, -0.25]));
let down = head.call([0.42, 0.0, 0.38], vunit([0.5, 0.0, -0.86]));
#{ body: body, up: up, down: down }
}
/// An Asian elephant: sub "bull" has tusks. Its trunk hangs or curls up.
fn elephant_parts(sub) {
let body = [
p_loft([[-1.48, 0.0, 1.85], [-1.3, 0.0, 1.87], [-0.6, 0.0, 1.95], [0.4, 0.0, 2.05], [1.15, 0.0, 2.0], [1.3, 0.0, 2.02]],
[[0.3, 0.38], [0.58, 0.68], [0.75, 0.85], [0.78, 0.9], [0.66, 0.78], [0.42, 0.52]], 8, ["eleph", "eleph", "eleph"]),
p_tube([-1.3, 0.0, 2.1], [-1.4, 0.0, 1.3], 0.04, 0.025, 4, "eleph"),
];
body += legs(0.8, -0.85, 0.34, 1.3, 0.24, 0.21, "eleph");
let base = [1.2, 0.0, 2.25];
let f = vunit([0.6, 0.0, -0.8]);
let head = [p_tube(base, hp(base, f, 0.62, 0.0, 0.0), [0.46, 0.52], [0.3, 0.3], 8, "eleph")];
for s in [1.0, -1.0] {
head.push(p_face([[1.3, s * 0.5, 2.6], [1.02, s * 0.86, 2.52], [0.86, s * 0.98, 2.0], [0.98, s * 0.88, 1.45], [1.26, s * 0.56, 1.62]], "elephe"));
if sub == "bull" {
head.push(p_loft([[1.62, s * 0.2, 1.62], [1.85, s * 0.24, 1.42], [2.08, s * 0.22, 1.42]], [0.055, 0.045, 0.025], 5, ["tusk", "tusk", "tusk"]));
}
}
let t0 = hp(base, f, 0.6, 0.0, 0.0);
let hang = [p_loft([t0, [1.92, 0.0, 1.15], [1.98, 0.0, 0.7], [1.93, 0.0, 0.25]], [0.17, 0.13, 0.1, 0.07], 6, ["eleph", "eleph", "eleph"])];
let curl = [p_loft([t0, [2.0, 0.0, 1.3], [2.2, 0.0, 1.25], [2.28, 0.0, 1.5]], [0.17, 0.13, 0.1, 0.07], 6, ["eleph", "eleph", "eleph"])];
let r = #{ body: body + head, up: hang, down: curl };
if sub == "calf" {
r.body = scale_parts(r.body, 0.55);
r.up = scale_parts(r.up, 0.55);
r.down = scale_parts(r.down, 0.55);
}
r
}
/// A rabbit sitting up, long ears, a white scut.
fn rabbit_parts() {
let body = [
p_loft([[-0.13, 0.0, 0.11], [0.0, 0.0, 0.12], [0.1, 0.0, 0.15]], [[0.075, 0.08], [0.095, 0.1], [0.07, 0.08]], 6, ["rabbit", "rabbit", "rabbit"]),
p_tube([-0.13, 0.0, 0.13], [-0.17, 0.0, 0.12], 0.035, 0.02, 4, "foxw"),
p_tube([0.1, 0.0, 0.2], [0.2, 0.0, 0.17], [0.05, 0.055], [0.025, 0.03], 6, "rabbit"),
];
for s in [1.0, -1.0] {
body.push(p_tube([0.11, s * 0.025, 0.24], [0.05, s * 0.05, 0.37], [0.012, 0.026], [0.008, 0.016], 4, "rabbit"));
}
#{ body: body, up: [], down: [] }
}
/// Draws one animal of `kind` (sub-kind sub) in this sprite's frame;
/// `graze`: its head poses take turns every `cycle` seconds, `delay` in.
fn animal(kind, sub, graze, cycle, delay) {
let a = animal_parts(kind, sub);
let parts = a.body;
if a.up.len() > 0 {
if graze {
let at = a.up[0].at;
parts.push(#{ at: at, t: "alt", a: a.down, b: a.up, css: `animation-duration:${cycle}s;animation-delay:-${delay}s` });
} else {
parts += a.up;
}
}
this.draw_parts(parts)
}
/// The sub-kinds of a herd of `kind`, `mix` as picked in its props.
fn herd_subs(kind, mix, n) {
let out = [];
for i in 0..n {
out.push(if kind == "deer" {
if mix == "herd" { if i == 0 { "stag" } else if i % 3 == 2 { "fawn" } else { "doe" } } else { mix }
} else if kind == "elephant" {
if mix == "herd" { if i == 0 { "bull" } else if i % 3 == 2 { "calf" } else { "cow" } } else { mix }
} else { "" });
}
out
}
/// A herd (or one animal) of `kind`: n animals at scattered spots within r
/// of the object's position, facing its rotation give or take 70 degrees.
fn herd(o, kind, mix, graze) {
let p = o.p;
let n = p.n;
let subs = herd_subs(kind, mix, n);
let seed = o.x * 0.731 + o.y * 1.379;
let cycle = if kind == "elephant" { 7.0 } else { 9.0 };
for i in 0..n {
// a sunflower spread: even, not in rows
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 lx = rr * ang.cos();
let ly = rr * ang.sin();
let w = world_of(o, lx, ly);
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();
if kind == "rabbits" && p.hop {
s.begin("a hop", `animation-delay:-${round1(hash(seed + i * 5.1) * 3.0)}s`);
s.animal(kind, subs[i], false, cycle, 0.0);
s.end();
} else {
s.animal(kind, subs[i], graze, cycle, round1(hash(seed + i * 2.3) * cycle));
}
}
frame(o);
}
fn herd_props(n, r, rmax) {
[
#{ key: "n", type: "int", def: n, min: 1, max: 12, step: 1, label: "How many" },
#{ key: "r", type: "float", def: r, min: 0.5, max: rmax, step: 0.5, label: "Spread (m)" },
]
}
fn meta_deer() {
#{ label: "Deer", category: "Animals", icon: "🦌",
help: "A herd of deer grazing: a stag with antlers, does and fawns (or all of one kind), at scattered spots facing about the object's rotation. Grazing deer lower and raise their heads in turn.",
props: herd_props(4, 4.0, 30.0) + [
#{ key: "mix", type: "choice", def: "herd", choices: ["herd", "stag", "doe", "fawn"], label: "Kind" },
#{ key: "graze", type: "bool", def: true, label: "Grazing" },
] }
}
fn prefab_deer(o) {
herd(o, "deer", o.p.mix, o.p.graze);
let subs = herd_subs("deer", o.p.mix, o.p.n);
let c = |k| subs.filter(|s| s == k).len();
info_row("Herd", herd_text(c, [["stag", "stags"], ["doe", "does"], ["fawn", "fawns"]]));
}
/// "1 stag, 2 does, 1 fawn" from counts per sub-kind.
fn herd_text(c, names) {
let parts = [];
for nm in names {
let k = c.call(nm[0]);
if k > 0 { parts.push(`${k} ${if k == 1 { nm[0] } else { nm[1] }}`); }
}
parts.reduce(|s, x| if s == "" { x } else { s + ", " + x }, "")
}
fn meta_boar() {
#{ label: "Wild boar", category: "Animals", icon: "🐗",
help: "Wild boar rooting about: they push their snouts into the ground and look up in turn.",
props: herd_props(3, 3.0, 20.0) + [ #{ key: "graze", type: "bool", def: true, label: "Rooting" } ] }
}
fn prefab_boar(o) {
herd(o, "boar", "", o.p.graze);
info_row("Animals", `${o.p.n} wild boar`);
}
fn meta_fox() {
#{ label: "Fox", category: "Animals", icon: "🦊",
help: "A red fox (or a few), nosing about now and then.",
props: herd_props(1, 3.0, 20.0) + [ #{ key: "graze", type: "bool", def: true, label: "Sniffing" } ] }
}
fn prefab_fox(o) {
herd(o, "fox", "", o.p.graze);
info_row("Animals", `${o.p.n} fox${if o.p.n == 1 { "" } else { "es" }}`);
}
fn meta_elephant() {
#{ label: "Elephant", category: "Animals", icon: "🐘",
help: "Asian elephants: a bull with tusks, cows and calves (or all of one kind); their trunks hang and curl up in turn.",
props: herd_props(1, 6.0, 40.0) + [
#{ key: "mix", type: "choice", def: "bull", choices: ["herd", "bull", "cow", "calf"], label: "Kind" },
#{ key: "graze", type: "bool", def: true, label: "Trunk moves" },
] }
}
fn prefab_elephant(o) {
herd(o, "elephant", o.p.mix, o.p.graze);
let subs = herd_subs("elephant", o.p.mix, o.p.n);
let c = |k| subs.filter(|s| s == k).len();
info_row("Herd", herd_text(c, [["bull", "bulls"], ["cow", "cows"], ["calf", "calves"]]));
}
fn meta_rabbits() {
#{ label: "Rabbits", category: "Animals", icon: "🐇",
help: "Rabbits sitting about a meadow, hopping now and then.",
props: herd_props(4, 2.0, 15.0) + [ #{ key: "hop", type: "bool", def: true, label: "Hopping" } ] }
}
fn prefab_rabbits(o) {
herd(o, "rabbits", "", false);
info_row("Animals", `${o.p.n} rabbit${if o.p.n == 1 { "" } else { "s" }}`);
}
// ---------------------------------------------------------------- flocks
fn meta_birds() {
#{ label: "Birds", category: "Animals", icon: "🕊",
help: "A flock of birds circling at a height over the spot, wings flapping.",
props: [
#{ key: "n", type: "int", def: 9, min: 1, max: 40, step: 1, label: "How many" },
#{ key: "h", type: "float", def: 14.0, min: 3.0, max: 80.0, step: 0.5, label: "Height (m)" },
#{ key: "r", type: "float", def: 8.0, min: 1.0, max: 60.0, step: 0.5, label: "Circle (m)" },
#{ key: "kind", type: "choice", def: "dark", choices: ["dark", "white"], label: "Birds" },
] }
}
fn prefab_birds(o) {
let p = o.p;
let seed = o.x * 0.53 + o.y * 0.97;
let fl = [];
for i in 0..p.n {
let rr = p.r * (0.55 + 0.45 * hash(seed + i));
fl.push(#{ rx: rr, ry: rr * (0.8 + 0.2 * hash(seed + i * 1.7)), dur: 14.0 + 8.0 * hash(seed + i * 2.9), delay: 20.0 * hash(seed + i * 4.1),
cw: false, len: 0.9, cls: if p.kind == "white" { "birdw" } else { "birdd" }, shape: "bird" });
}
sprite().swim(0.0, 0.0, p.h, fl);
info_row("Flock", `${p.n} birds circling ${num1(p.h)} m up`);
}
fn meta_butterflies() {
#{ label: "Butterflies", category: "Animals", icon: "🦋",
help: "Butterflies fluttering round over flowers.",
props: [
#{ key: "n", type: "int", def: 6, min: 1, max: 30, step: 1, label: "How many" },
#{ key: "r", type: "float", def: 2.0, min: 0.3, max: 15.0, step: 0.1, label: "Circle (m)" },
#{ key: "h", type: "float", def: 0.8, min: 0.2, max: 4.0, step: 0.1, label: "Height (m)" },
] }
}
fn prefab_butterflies(o) {
let p = o.p;
let seed = o.x * 0.41 + o.y * 0.77;
let cls = ["flyo", "flyb", "flyw", "flyy"];
let low = [];
let high = [];
for i in 0..p.n {
let rr = p.r * (0.3 + 0.7 * hash(seed + i));
let b = #{ rx: rr, ry: rr * (0.6 + 0.4 * hash(seed + i * 1.3)), dur: 5.0 + 6.0 * hash(seed + i * 2.1), delay: 10.0 * hash(seed + i * 3.3),
cw: hash(seed + i * 5.9) > 0.5, len: 0.22, cls: cls[i % 4], shape: "fly" };
if i % 2 == 0 { low.push(b); } else { high.push(b); }
}
let s = sprite();
if low.len() > 0 { s.swim(0.0, 0.0, p.h, low); }
if high.len() > 0 { s.swim(0.0, 0.0, p.h + 0.4, high); }
}
// ---------------------------------------------------------------- beehives
fn hive_colours() { ["mixed", "white", "yellow", "blue", "green"] }
fn meta_beehive() {
#{ label: "Beehives", category: "Nature", icon: "🐝",
help: "A row of beehives on stands along local x, entrances to local -y, bees flying round in front.",
props: [
#{ key: "n", type: "int", def: 3, min: 1, max: 12, step: 1, label: "Hives" },
#{ key: "gap", type: "float", def: 0.9, min: 0.6, max: 4.0, step: 0.1, label: "Spacing (m)" },
#{ key: "colour", type: "choice", def: "mixed", choices: hive_colours(), label: "Paint" },
#{ key: "supers", type: "int", def: 2, min: 0, max: 4, step: 1, label: "Supers" },
#{ key: "stand", type: "bool", def: true, label: "Stands" },
#{ key: "bees", type: "bool", def: true, label: "Bees" },
] }
}
fn prefab_beehive(o) {
let p = o.p;
let x0 = -(p.n - 1) * p.gap / 2.0;
let seed = o.x * 0.29 + o.y * 0.61;
for i in 0..p.n {
let x = x0 + i * p.gap;
let col = if p.colour == "mixed" { ["hive1", "hive2", "hive3", "hive4"][i % 4] } else { switch p.colour { "yellow" => "hive2", "blue" => "hive3", "green" => "hive4", _ => "hive1" } };
let s = sprite();
let z = 0.0;
if p.stand {
for lx in [-0.2, 0.2] { for ly in [-0.17, 0.17] { s.box(x + lx - 0.025, ly - 0.025, x + lx + 0.025, ly + 0.025, 0.0, 0.33, "woodd"); } }
s.box(x - 0.27, -0.24, x + 0.27, 0.24, 0.33, 0.37, "wood");
z = 0.37;
}
// brood box with its entrance and landing board, the supers, the lid
s.box(x - 0.25, -0.21, x + 0.25, 0.21, z, z + 0.25, col);
s.box(x - 0.18, -0.29, x + 0.18, -0.21, z, z + 0.02, "wood");
s.open([x - 0.25, -0.21], [x + 0.25, -0.21], [[0.1, 0.4, 0.02, 0.045]], "hivedoor");
let zt = z + 0.25;
for k in 0..p.supers {
s.box(x - 0.25, -0.21, x + 0.25, 0.21, zt + 0.004, zt + 0.17, col);
zt += 0.17;
}
s.box(x - 0.29, -0.25, x + 0.29, 0.25, zt + 0.004, zt + 0.07, "hivelid");
if p.bees {
let bs = [];
for k in 0..8 {
let r = 0.25 + 0.6 * hash(seed + i * 11.0 + k);
bs.push(#{ rx: r, ry: r * 0.7, dur: 1.6 + 2.0 * hash(seed + i * 13.0 + k * 1.9), delay: 4.0 * hash(seed + i * 7.0 + k * 3.1),
cw: k % 2 == 0, len: 0.12, cls: "bee", shape: "bee" });
}
s.swim(x, -0.55, z + 0.35, bs);
}
}
info_row("Apiary", `${p.n} hive${if p.n == 1 { "" } else { "s" }}, ${p.supers} super${if p.supers == 1 { "" } else { "s" }} each`);
}
// ---------------------------------------------------------------- plants
fn flower_colours() { ["mixed", "red", "yellow", "white", "purple", "pink"] }
/// The flower classes for a colour choice.
fn flower_cls(c) {
if c == "mixed" { ["flr", "fly", "flw", "flp", "flk"] } else { [switch c { "red" => "flr", "yellow" => "fly", "white" => "flw", "purple" => "flp", _ => "flk" }] }
}
/// Small diamonds at plan points pts, height z, one path per class.
fn blossoms(pts, z, size, classes) {
let by = classes.map(|c| []);
for i in 0..pts.len() {
let q = pts[i];
let k = i % classes.len();
let d = by[k];
d.push([[q[0] - size, q[1], z], [q[0], q[1] - size, z], [q[0] + size, q[1], z], [q[0], q[1] + size, z]]);
by[k] = d;
}
for k in 0..classes.len() {
if by[k].len() > 0 { this.poly(by[k], classes[k]); }
}
this
}
fn meta_bush() {
#{ label: "Bush", category: "Nature", icon: "🌿",
help: "A shrub: round and green, in flower, or with berries.",
props: [
#{ key: "r", type: "float", def: 0.9, min: 0.3, max: 4.0, step: 0.1, label: "Size (m)" },
#{ key: "kind", type: "choice", def: "round", choices: ["round", "flowering", "berries"], label: "Kind" },
#{ key: "colour", type: "choice", def: "mixed", choices: flower_colours(), label: "Flowers" },
] }
}
fn prefab_bush(o) {
let p = o.p;
let r = p.r;
let s = sprite();
s.ext_box(-r, -r, r, r, 0.0, r * 1.5);
let R = r * px() * 0.95;
let seed = o.x * 0.37 + o.y * 0.83;
// three overlapping clumps, the back ones first, a light top
let clumps = [[-0.35 * r, 0.25 * r, 0.75], [0.3 * r, 0.3 * r, 0.8], [0.0, -0.25 * r, 0.9]];
clumps.sort(|a, b| { let da = depth(a[0], a[1]); let db = depth(b[0], b[1]); if da < db { -1 } else if da > db { 1 } else { 0 } });
for c in clumps {
s.circle(c[0], c[1], r * 0.62, R * c[2], "bush l");
}
s.circle(0.0, 0.0, r * 0.9, R * 0.55, "bush t", -R * 0.2, -R * 0.25);
if p.kind != "round" {
// blossoms or berries dotted over the crown: small, spread up and down its face
let cls = if p.kind == "berries" { ["berry"] } else { flower_cls(p.colour) };
let dot = R * (if p.kind == "berries" { 0.07 } else { 0.09 });
for i in 0..22 {
let a = i * 2.39996 + hash(seed + i);
let d = r * (0.15 + 0.5 * hash(seed + i * 1.9));
let z = r * (0.5 + 0.7 * hash(seed + i * 3.7));
s.circle(d * a.cos(), d * a.sin(), z, dot, cls[i % cls.len()]);
}
}
}
fn meta_flowers() {
#{ label: "Flowers", category: "Nature", icon: "✿", shape: "rect",
help: "A flower bed (rows in dark soil) or a wildflower meadow. Drag a rectangle on the map.",
props: [
#{ key: "w", type: "float", def: 4.0, min: 0.5, max: 200.0, step: 0.5, label: "Width (m)" },
#{ key: "d", type: "float", def: 2.0, min: 0.5, max: 200.0, step: 0.5, label: "Length (m)" },
#{ key: "kind", type: "choice", def: "bed", choices: ["bed", "meadow"], label: "Kind" },
#{ key: "colour", type: "choice", def: "mixed", choices: flower_colours(), label: "Flowers" },
#{ key: "density", type: "float", def: 3.0, min: 0.5, max: 12.0, step: 0.5, label: "Per m²" },
] }
}
fn prefab_flowers(o) {
let p = o.p;
let w = p.w / 2.0;
let d = p.d / 2.0;
layer("ground");
let s = sprite();
let bed = p.kind == "bed";
s.flat(rect(-w, -d, w, d), 0.02, if bed { "g-soil t" } else { "g-meadow t" });
let n = fmin(600.0, (p.w * p.d * p.density).round()).to_int();
let seed = o.x * 0.19 + o.y * 0.47;
let pts = [];
if bed {
// rows across the bed's length, a little uneven
let rows = fmax(1.0, (p.d / 0.45).floor()).to_int();
let per = fmax(1.0, (n.to_float() / rows.to_float()).round()).to_int();
for j in 0..rows {
let y = -d + (j + 0.5) * p.d / rows;
for i in 0..per {
let x = -w + (i + 0.5) * p.w / per;
pts.push([x + (hash(seed + i * 3.1 + j) - 0.5) * 0.15, y + (hash(seed + i + j * 5.3) - 0.5) * 0.12]);
}
}
} else {
for i in 0..n {
pts.push([-w + p.w * hash(seed + i * 1.37), -d + p.d * hash(seed + i * 2.71)]);
}
}
s.blossoms(pts.map(|q| [q[0] + 0.04, q[1] - 0.04]), 0.05, 0.07, ["leafy"]);
s.blossoms(pts, if bed { 0.32 } else { 0.22 }, 0.06, flower_cls(p.colour));
info_row("Flowers", `${pts.len()} on ${num1(p.w * p.d)} m²`);
}
// ---------------------------------------------------------------- rocks and logs
fn meta_rocks() {
#{ label: "Rocks", category: "Nature", icon: "🪨",
help: "Boulders lying about: low-poly stones of varied size.",
props: [
#{ key: "n", type: "int", def: 3, min: 1, max: 20, step: 1, label: "How many" },
#{ key: "r", type: "float", def: 0.6, min: 0.1, max: 5.0, step: 0.1, label: "Size (m)" },
#{ key: "spread", type: "float", def: 1.5, min: 0.0, max: 20.0, step: 0.1, label: "Spread (m)" },
] }
}
fn prefab_rocks(o) {
let p = o.p;
let seed = o.x * 0.67 + o.y * 0.23;
for i in 0..p.n {
let rr = if p.n == 1 { 0.0 } else { p.spread * ((i + 0.5) / p.n).sqrt() };
let a = i * 2.39996 + hash(seed + i);
let size = p.r * (0.55 + 0.6 * hash(seed + i * 4.3));
sprite().rock(rr * a.cos(), rr * a.sin(), size, seed + i * 9.1);
}
}
/// A low-poly boulder of radius r at (x, y): jittered rings from the ground
/// up, widest a little above it, and a flat-ish top.
fn rock(x, y, r, seed) {
let n = 7;
let h = r * (0.75 + 0.35 * hash(seed));
let rings = [[0.0, 0.9], [0.35, 1.0], [0.75, 0.78], [1.0, 0.4]];
let pts = [];
for ring in rings {
let q = [];
for k in 0..n {
let a = 2.0 * PI() * (k + 0.3 * hash(seed + k)) / n;
let rk = r * ring[1] * (0.85 + 0.25 * hash(seed + k * 1.7 + ring[0]));
q.push([x + rk * a.cos(), y + rk * a.sin() * 0.85, h * ring[0]]);
}
pts.push(q);
}
for i in 1..pts.len() {
let lo = pts[i - 1];
let hi = pts[i];
for k in 0..n {
let j = (k + 1) % n;
this.face([lo[k], lo[j], hi[j], hi[k]], "rock beast");
}
}
this.face(pts[pts.len() - 1], "rock beast");
this
}
fn meta_log() {
#{ label: "Log", category: "Nature", icon: "🪵",
help: "A fallen log along local x, or a tree stump.",
props: [
#{ key: "kind", type: "choice", def: "log", choices: ["log", "stump"], label: "Kind" },
#{ key: "w", type: "float", def: 3.0, min: 0.3, max: 20.0, step: 0.1, label: "Length (m)" },
#{ key: "r", type: "float", def: 0.25, min: 0.05, max: 1.5, step: 0.05, label: "Radius (m)" },
] }
}
fn prefab_log(o) {
let p = o.p;
let s = sprite();
if p.kind == "stump" {
let h = fmax(0.25, p.r * 1.6);
s.loft([[0.0, 0.0, 0.0], [0.0, 0.0, h]], [p.r * 1.15, p.r], 9, ["bark", "bark", "woodend"]);
} else {
let w = p.w / 2.0;
s.loft([[-w, 0.0, p.r * 0.92], [w, 0.0, p.r * 0.92]], [p.r, p.r * 0.9], 9, ["bark", "woodend", "woodend"]);
s.tube([w * 0.3, 0.0, p.r * 1.5], [w * 0.3 + 0.2, 0.22, p.r * 2.1], p.r * 0.25, p.r * 0.15, 5, "bark");
}
}
+117
View File
@@ -272,6 +272,45 @@ mach3 = "#5b6370"
mach4 = "#b8692b" mach4 = "#b8692b"
robot = "#e07a1f" robot = "#e07a1f"
robotb = "#3a3f48" robotb = "#3a3f48"
# wildlife and garden
deer = "#8a5a32"
deerl = "#d6c4a6"
nose = "#2a2420"
antler = "#c9b48e"
boar = "#5a4b42"
bristle = "#2c2622"
fox = "#c45f25"
foxw = "#e9e1d4"
foxd = "#3a2a22"
rabbit = "#8a7a66"
eleph = "#6f6b69"
elephe = "#625e5c"
tusk = "#e9e2d0"
birdd = "#0b0d10"
birdw = "#e8e6e0"
bee = "#e7b416"
flyo = "#f08a24"
flyb = "#4f86d9"
flyw = "#f2efe6"
flyy = "#f5d33f"
hive1 = "#e6e1d3"
hive2 = "#e2b84f"
hive3 = "#7aa3c4"
hive4 = "#86b07f"
hivelid = "#9aa1a8"
hivedoor = "#1d1a17"
bush = "#2f6a3a"
berry = "#b8303a"
flr = "#d8434a"
fly = "#e8c33a"
flw = "#ece8de"
flp = "#8a5bc4"
flk = "#e27aa5"
leafy = "#2d5e30"
soil = "#4a3828"
meadow = "#2f5a2e"
bark = "#5a3f28"
woodend = "#cfae7c"
# streets: sidewalk paving, kerb stone faces, roundabout apron # streets: sidewalk paving, kerb stone faces, roundabout apron
walk = "#555c6b" walk = "#555c6b"
kerbf = "#7d8494" kerbf = "#7d8494"
@@ -542,6 +581,44 @@ mach3 = "#8a93a1"
mach4 = "#e08a43" mach4 = "#e08a43"
robot = "#f08c2b" robot = "#f08c2b"
robotb = "#5a616d" robotb = "#5a616d"
deer = "#a8703f"
deerl = "#ecdfc8"
nose = "#3a302a"
antler = "#d8c39c"
boar = "#5e5048"
bristle = "#3a322c"
fox = "#d9732f"
foxw = "#f4efe6"
foxd = "#4a3428"
rabbit = "#a08d76"
eleph = "#8a8582"
elephe = "#7b7673"
tusk = "#f3eee0"
birdd = "#343a44"
birdw = "#f6f4ee"
bee = "#f0c02a"
flyo = "#f59536"
flyb = "#5b93e6"
flyw = "#fbf8f0"
flyy = "#f7dc52"
hive1 = "#f1ece0"
hive2 = "#ecc561"
hive3 = "#8fb6d6"
hive4 = "#97c290"
hivelid = "#b6bcc2"
hivedoor = "#2a2520"
bush = "#4f9a48"
berry = "#d03e48"
flr = "#e45a5f"
fly = "#f1d04e"
flw = "#fbf9f3"
flp = "#9b6fd6"
flk = "#ee93b8"
leafy = "#4a8a40"
soil = "#7a5a42"
meadow = "#78ab55"
bark = "#6e4e32"
woodend = "#ddbf8e"
walk = "#d9d6cf" walk = "#d9d6cf"
kerbf = "#a5a9b1" kerbf = "#a5a9b1"
apron = "#b9ad98" apron = "#b9ad98"
@@ -556,6 +633,29 @@ g-conc = "conc"
g-pad = "pad" g-pad = "pad"
g-path = "path" g-path = "path"
g-walk = "walk" g-walk = "walk"
deer = "deer"
deerl = "deerl"
nose = "nose"
boar = "boar"
fox = "fox"
foxw = "foxw"
foxd = "foxd"
rabbit = "rabbit"
eleph = "eleph"
elephe = "elephe"
tusk = "tusk"
hive1 = "hive1"
hive2 = "hive2"
hive3 = "hive3"
hive4 = "hive4"
hivelid = "hivelid"
hivedoor = "hivedoor"
bush = "bush"
rock = "rock"
bark = "bark"
woodend = "woodend"
g-soil = "soil"
g-meadow = "meadow"
g-apron = "apron" g-apron = "apron"
plinth = "plinth" plinth = "plinth"
wall = "wall" wall = "wall"
@@ -818,6 +918,23 @@ f-conc = "conc"
".iso-svg .iso-win" = "fill: var(--iso-win); stroke: none" ".iso-svg .iso-win" = "fill: var(--iso-win); stroke: none"
".iso-svg .iso-school" = "pointer-events: none" ".iso-svg .iso-school" = "pointer-events: none"
".iso-svg .iso-swimbox" = "fill: none; stroke: none" ".iso-svg .iso-swimbox" = "fill: none; stroke: none"
".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-bird, .iso-svg .iso-wing" = "stroke: none"
".iso-svg .iso-birdd" = "fill: var(--iso-birdd)"
".iso-svg .iso-birdw" = "fill: var(--iso-birdw)"
".iso-svg .iso-bee" = "fill: var(--iso-bee); stroke: none"
".iso-svg .iso-flyo" = "fill: var(--iso-flyo)"
".iso-svg .iso-flyb" = "fill: var(--iso-flyb)"
".iso-svg .iso-flyw" = "fill: var(--iso-flyw)"
".iso-svg .iso-flyy" = "fill: var(--iso-flyy)"
".iso-svg .iso-berry" = "fill: var(--iso-berry); stroke: none"
".iso-svg .iso-flr" = "fill: var(--iso-flr); stroke: none"
".iso-svg .iso-fly" = "fill: var(--iso-fly); stroke: none"
".iso-svg .iso-flw" = "fill: var(--iso-flw); stroke: none"
".iso-svg .iso-flp" = "fill: var(--iso-flp); stroke: none"
".iso-svg .iso-flk" = "fill: var(--iso-flk); stroke: none"
".iso-svg .iso-leafy" = "fill: var(--iso-leafy); stroke: none"
".iso-svg .iso-kerbf" = "fill: var(--iso-kerbf); stroke: none" ".iso-svg .iso-kerbf" = "fill: var(--iso-kerbf); stroke: none"
".iso-svg .iso-lane" = "fill: none; stroke: var(--iso-dash); stroke-width: .6; stroke-dasharray: 4 6" ".iso-svg .iso-lane" = "fill: none; stroke: var(--iso-dash); stroke-width: .6; stroke-dasharray: 4 6"
".iso-svg .iso-giveway" = "fill: none; stroke: var(--iso-dash); stroke-width: .9; stroke-dasharray: 2 2" ".iso-svg .iso-giveway" = "fill: none; stroke: var(--iso-dash); stroke-width: .9; stroke-dasharray: 2 2"
+7 -1
View File
@@ -83,7 +83,7 @@ Each phase ends with tests, a Studio/viewer check in Edge, commit and push.
8. Waypoint animation: a person or car moving along a path of waypoints 8. Waypoint animation: a person or car moving along a path of waypoints
9. ~~Build your own house~~ (done, see above) 9. ~~Build your own house~~ (done, see above)
10. ~~Diagonal streets, walls, fences, hedges~~ (done, see below) 10. ~~Diagonal streets, walls, fences, hedges~~ (done, see below)
11. Wildlife and nature: deer, bushes, beehives and more 11. ~~Wildlife and nature: deer, bushes, beehives and more~~ (done, see below)
Done since: the middle mouse button only moves the camera; fences and site walls join like house Done since: the middle mouse button only moves the camera; fences and site walls join like house
walls (`line_joint`); fence designs (mesh, wood, bars, half stone, cobble, brick with pillars); walls (`line_joint`); fence designs (mesh, wood, bars, half stone, cobble, brick with pillars);
@@ -132,6 +132,12 @@ against everything on either side. Studio: runs and drags keep to x or y or go d
(Ctrl: any angle), a road run turning 45° gets a 45° curve, Rotation takes any angle. The (Ctrl: any angle), a road run turning 45° gets a 45° curve, Rotation takes any angle. The
streets demo has a diagonal lane. streets demo has a diagonal lane.
Wildlife and nature (`wildlife.rhai`): Deer, Wild boar, Fox, Elephant, Rabbits as low-poly solids
(`loft`, `tube`, parts drawn back to front), herds scattered round a spot, two head poses taking
turns while grazing, rabbits hopping; Birds and Butterflies as flocks (`swim` shapes, wings flapping
out of step); Beehives with bees, Bush, Flowers (bed or meadow), Rocks, Log / stump.
`examples/country`, Country demo in the web Studio.
## Phase 2: richer editing ## Phase 2: richer editing
- [x] Interior prefabs (see Build your own house); a building `rooms` prop is still open - [x] Interior prefabs (see Build your own house); a building `rooms` prop is still open
+23 -2
View File
@@ -88,11 +88,11 @@ shaded automatically by face direction.
| `flat(poly or polys, z, cls[, id])` | flat polygon(s); later ones cut holes | | `flat(poly or polys, z, cls[, id])` | flat polygon(s); later ones cut holes |
| `open(a, b, [[u0, u1, z0, z1], …], cls[, id])` | windows / doors on the face a→b (outward normal on the right) | | `open(a, b, [[u0, u1, z0, z1], …], cls[, id])` | windows / doors on the face a→b (outward normal on the right) |
| `lines(segs, z, cls)`, `disc(cx, cy, z, r, cls[, id])` | markings | | `lines(segs, z, cls)`, `disc(cx, cy, z, r, cls[, id])` | markings |
| `polyline(lines, z, cls)` | open lines through plan points at height z (or 3D points): a dashed line stays one dash run round a curve | | `polyline(lines, z, cls)` | open lines through plan points at height z, or 3D points `[x, y, z]` (antlers): a dashed line stays one dash run round a curve |
| `circle(x, y, z, r_px, cls[, dx, dy])`, `stem(x, y, z0, z1, w_px, cls)` | screen-space details (crowns, poles) | | `circle(x, y, z, r_px, cls[, dx, dy])`, `stem(x, y, z0, z1, w_px, cls)` | screen-space details (crowns, poles) |
| `person(x, y, z[, cls])`, `sit(x, y, z, cls)`, `head(x, y, z, r)`, `led(x, y, z[, cls])` | figures | | `person(x, y, z[, cls])`, `sit(x, y, z, cls)`, `head(x, y, z, r)`, `led(x, y, z[, cls])` | figures |
| `puffs(x, y, z[, kind[, n, dur]])`, `fan(x, y, z, r)` | animated steam / smoke / fans | | `puffs(x, y, z[, kind[, n, dur]])`, `fan(x, y, z, r)` | animated steam / smoke / fans |
| `swim(x, y, z, [#{ rx, ry, dur, delay, cw, len, cls }, ...])` | fish going round ellipses (radii rx, ry along the object's axes) about (x, y) on the plane at z, one round per `dur` s, clockwise when `cw`; `cls` koi / koiw / koig (the pond) | | `swim(x, y, z, [#{ rx, ry, dur, delay, cw, len, cls[, shape] }, ...])` | fish going round ellipses (radii rx, ry along the object's axes) about (x, y) on the plane at z, one round per `dur` s, clockwise when `cw`; `cls` koi / koiw / koig (the pond); `shape` "bird" (wings `len` across, flapping), "fly" (a butterfly) or "bee" makes it a flock |
| `seq([[x, y, \|q\| …], …])` | items drawn back to front by their anchor | | `seq([[x, y, \|q\| …], …])` | items drawn back to front by their anchor |
| `group(cls[, css], \|q\| …)`, `begin(cls[, css])` … `end()` | `<g>` around parts (animation) | | `group(cls[, css], \|q\| …)`, `begin(cls[, css])` … `end()` | `<g>` around parts (animation) |
| `ext(x, y, z)`, `ext_box(…)` | widen the sort box | | `ext(x, y, z)`, `ext_box(…)` | widen the sort box |
@@ -145,6 +145,27 @@ orders them, the one on the viewer's side going later. A long diagonal wall
therefore sorts right against everything on either side of it, and mitred therefore sorts right against everything on either side of it, and mitred
pieces against each other. pieces against each other.
## Animals
`wildlife.rhai` draws animals as low-poly solids from faces. `s.loft(spine,
rs, n, [side, cap0, cap1])` sweeps an n-sided ring (radius r, or `[across,
up]`) along 3D points, keeping its turn round bends; `s.tube(a, b, ra, rb, n,
mat)` is a loft of two rings. `face` shades each face by its direction and
leaves out the ones turned away, so a solid needs no sorting of its own
faces; separate parts (legs, body, neck, head, antlers) are drawn back to
front by `draw_parts`, which sorts them by the depth of their anchors in the
view (`depth(x, y) + z`). The class `beast` edges each face in its own shade
instead of the dark outline, so a body reads as one surface.
`animal_parts(kind, sub)` returns `#{ body, up, down }` for an animal facing
+x: the parts that stay and two head poses. With grazing on, the poses sit in
two groups `iso-a iso-pose2 iso-pose0` and `iso-a iso-pose2` that take turns
(the CSS shows the first unanimated); each animal starts at its own point of
the cycle. `herd(o, kind, mix, graze)` scatters `n` animals in a sunflower
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
`shape`.
## Production lines ## Production lines
`producer`, `conveyor` and `robot_arm` (stdlib/industry.rhai) work together `producer`, `conveyor` and `robot_arm` (stdlib/industry.rhai) work together
+29
View File
@@ -0,0 +1,29 @@
# isokit country demo: animals and plants of the countryside, and fences, hedges and a lane at an angle.
# Open it with `isokit studio examples/country`.
title = "isokit country"
note = "A corner of the countryside: elephants in a paddock with a cut corner, deer grazing at the forest edge, wild boar, a fox, rabbits, an apiary by a wildflower meadow, a pond. The lane bends at 45 degrees, the hedgerow runs along it. Tap anything for its numbers. Drag to pan, Ctrl + wheel or pinch to zoom."
default_level = "site"
default_rotation = 0
prelude = []
theme = []
[[level]]
id = "site"
label = "Site"
title = "Countryside"
[[param]]
name = "labels"
type = "bool"
default = true
label = "Labels"
[[param]]
name = "running"
type = "bool"
default = true
label = "Animation"
[build]
out = "dist/index.html"
budget = 6000
+44
View File
@@ -0,0 +1,44 @@
{
"version": 1,
"objects": [
{"id": "ground", "type": "area", "name": "Meadows", "x": 0, "y": 0, "rot": 0, "props": {"w": 96, "d": 74, "mat": "g-grass"}},
{"id": "lane-w", "type": "road", "name": "Lane (west)", "x": -30.0, "y": -30.0, "rot": 0.0, "props": {"w": 36.0, "d": 4.5, "dashed": false, "kerb": false}},
{"id": "lane-d", "type": "road", "name": "Lane (diagonal)", "x": 3.0, "y": -15.0, "rot": 45.0, "props": {"w": 42.426, "d": 4.5, "dashed": false, "kerb": false}},
{"id": "lane-n", "type": "road", "name": "Lane (north)", "x": 18.0, "y": 18.5, "rot": 90.0, "props": {"w": 37.0, "d": 4.5, "dashed": false, "kerb": false}},
{"id": "paddock-1", "type": "fence", "name": "Paddock fence", "x": -33.0, "y": -6.0, "rot": 0.0, "props": {"w": 22.0, "style": "wood", "h": 1.6}},
{"id": "paddock-2", "type": "fence", "name": "Paddock fence", "x": -18.0, "y": -2.0, "rot": 45.0, "props": {"w": 11.314, "style": "wood", "h": 1.6}},
{"id": "paddock-3", "type": "fence", "name": "Paddock fence", "x": -14.0, "y": 14.0, "rot": 90.0, "props": {"w": 24.0, "style": "wood", "h": 1.6}},
{"id": "paddock-4", "type": "fence", "name": "Paddock fence", "x": -29.0, "y": 26.0, "rot": 180.0, "props": {"w": 30.0, "style": "wood", "h": 1.6}},
{"id": "paddock-5", "type": "fence", "name": "Paddock fence", "x": -44.0, "y": 10.0, "rot": 270.0, "props": {"w": 32.0, "style": "wood", "h": 1.6}},
{"id": "elephants", "type": "elephant", "name": "Elephants", "x": -29, "y": 11, "rot": 330, "props": {"n": 3, "mix": "herd", "r": 6}},
{"id": "pad-rocks", "type": "rocks", "name": "Rocks", "x": -38, "y": 20, "rot": 0, "props": {"n": 3, "r": 0.8, "spread": 2}},
{"id": "pad-tree", "type": "tree", "x": -20, "y": 21, "rot": 0, "props": {"r": 2.2}},
{"id": "forest-1", "type": "tree_row", "name": "Forest edge", "x": 36, "y": 34, "rot": 0, "props": {"w": 26, "spacing": 4, "r": 2.0}},
{"id": "forest-2", "type": "tree_row", "x": 36, "y": 29, "rot": 0, "props": {"w": 24, "spacing": 5, "r": 1.8}},
{"id": "tree-1", "type": "tree", "x": 24, "y": 24, "rot": 0, "props": {"r": 1.6}},
{"id": "tree-2", "type": "tree", "x": 41, "y": 22, "rot": 0, "props": {"r": 2.0}},
{"id": "tree-3", "type": "tree", "x": -6, "y": 34, "rot": 0, "props": {"r": 1.8}},
{"id": "tree-4", "type": "tree", "x": 4, "y": 33, "rot": 0, "props": {"r": 2.1}},
{"id": "tree-5", "type": "tree", "x": -2, "y": 29, "rot": 0, "props": {"r": 1.5}},
{"id": "deer", "type": "deer", "name": "Deer", "x": 30, "y": 14, "rot": 200, "props": {"n": 6, "r": 5}},
{"id": "boar", "type": "boar", "name": "Wild boar", "x": 40, "y": 28, "rot": 170, "props": {"n": 3, "r": 2.5}},
{"id": "log", "type": "log", "name": "Fallen log", "x": 8, "y": 28, "rot": 20, "props": {"w": 4, "r": 0.3}},
{"id": "stump", "type": "log", "x": 12, "y": 31, "rot": 0, "props": {"kind": "stump", "r": 0.4}},
{"id": "fox", "type": "fox", "name": "Fox", "x": 6, "y": 16, "rot": 300, "props": {"n": 1}},
{"id": "hedge-1", "type": "hedge", "name": "Hedgerow", "x": 2.0, "y": -8.0, "rot": 45.0, "props": {"w": 22.627, "d": 0.9, "h": 1.5}},
{"id": "hedge-2", "type": "hedge", "name": "Hedgerow", "x": 10.0, "y": 5.0, "rot": 90.0, "props": {"w": 10.0, "d": 0.9, "h": 1.5}},
{"id": "bush-1", "type": "bush", "x": -8, "y": -18, "rot": 0, "props": {"r": 1.0, "kind": "berries"}},
{"id": "bush-2", "type": "bush", "x": 12, "y": 12, "rot": 0, "props": {"r": 0.9, "kind": "flowering", "colour": "white"}},
{"id": "bush-3", "type": "bush", "x": 2, "y": -4, "rot": 0, "props": {"r": 0.7}},
{"id": "meadow", "type": "flowers", "name": "Wildflower meadow", "x": 4, "y": -29, "rot": 0, "props": {"w": 18, "d": 7, "kind": "meadow", "density": 5}},
{"id": "hives", "type": "beehive", "name": "Apiary", "x": 31, "y": -22, "rot": 0, "props": {"n": 4, "gap": 1.0}},
{"id": "butterflies", "type": "butterflies", "x": 4, "y": -29, "rot": 0, "props": {"n": 8, "r": 4}},
{"id": "bed", "type": "flowers", "name": "Flower bed", "x": 31, "y": -27, "rot": 0, "props": {"w": 6, "d": 2, "kind": "bed", "colour": "mixed"}},
{"id": "rabbits", "type": "rabbits", "name": "Rabbits", "x": -28, "y": -20, "rot": 0, "props": {"n": 5, "r": 2.5}},
{"id": "bush-4", "type": "bush", "x": -20, "y": -22, "rot": 0, "props": {"r": 1.2}},
{"id": "bush-5", "type": "bush", "x": -36, "y": -18, "rot": 0, "props": {"r": 0.8, "kind": "flowering", "colour": "pink"}},
{"id": "pond", "type": "pond", "name": "Pond", "x": 36, "y": -6, "rot": 0, "props": {"w": 11, "d": 8, "fish": 6, "lilies": 4}},
{"id": "pond-rocks", "type": "rocks", "x": 42, "y": -1, "rot": 0, "props": {"n": 2, "r": 0.6, "spread": 1}},
{"id": "birds", "type": "birds", "name": "Birds", "x": 0, "y": 0, "rot": 0, "props": {"n": 10, "h": 18, "r": 14}}
]
}
+1
View File
@@ -150,6 +150,7 @@ def main():
(web / "house.json").write_text(json.dumps(demo_files("house"), ensure_ascii=False), encoding="utf-8") (web / "house.json").write_text(json.dumps(demo_files("house"), ensure_ascii=False), encoding="utf-8")
(web / "factory.json").write_text(json.dumps(demo_files("factory"), ensure_ascii=False), encoding="utf-8") (web / "factory.json").write_text(json.dumps(demo_files("factory"), ensure_ascii=False), encoding="utf-8")
(web / "streets.json").write_text(json.dumps(demo_files("streets"), ensure_ascii=False), encoding="utf-8") (web / "streets.json").write_text(json.dumps(demo_files("streets"), ensure_ascii=False), encoding="utf-8")
(web / "country.json").write_text(json.dumps(demo_files("country"), ensure_ascii=False), encoding="utf-8")
h = hashlib.sha256() h = hashlib.sha256()
for p in sorted((out / "assets").rglob("*")): for p in sorted((out / "assets").rglob("*")):
+22
View File
@@ -555,6 +555,28 @@ with sync_playwright() as p:
pg.wait_for_timeout(700) pg.wait_for_timeout(700)
check(len(doc()["objects"]) == n5, "four undos take the diagonals back") check(len(doc()["objects"]) == n5, "four undos take the diagonals back")
# animals and an apiary from the palette: a deer herd grazes, the hives get bees
pal = pg.evaluate("() => [...document.querySelectorAll('.st-cat')].map(e => e.dataset.cat)")
check("Animals" in pal, "the palette has an Animals category")
pg.locator('.st-tile[data-type="deer"]').click()
pg.mouse.click(*at(44, 28))
pg.wait_for_timeout(900)
deer = doc()["objects"][-1]
pg.locator('.st-tile[data-type="beehive"]').click()
pg.mouse.click(*at(52, 18))
pg.wait_for_timeout(900)
hives = doc()["objects"][-1]
grazing = pg.evaluate(f"() => document.querySelectorAll('[data-obj=\"{deer['id']}\"] .iso-pose2.iso-pose0').length")
bees = pg.evaluate(f"() => document.querySelectorAll('[data-obj=\"{hives['id']}\"] .iso-bee').length")
herd = pg.evaluate(f"() => Object.fromEntries((__studio.view.info('{deer['id']}') || {{rows: []}}).rows)['Herd'] || ''")
check(deer["type"] == "deer" and hives["type"] == "beehive" and grazing == 4 and bees == 24 and herd == "1 stag, 2 does, 1 fawn",
f"a deer herd and beehives placed from the palette: {grazing} grazing, {bees} bees, herd {herd}")
pg.screenshot(path=str(OUT / "studio-wildlife.jpg"), type="jpeg", quality=80)
for _ in range(2):
pg.locator("#undo").click()
pg.wait_for_timeout(700)
check(len(doc()["objects"]) == n5, "two undos take the animals back")
# an agent edits over MCP: the open Studio follows # an agent edits over MCP: the open Studio follows
init = mcp({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "test", "version": "0"}}}) init = mcp({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "test", "version": "0"}}})
check(init["result"]["serverInfo"]["name"] == "isokit", "MCP initialize") check(init["result"]["serverInfo"]["name"] == "isokit", "MCP initialize")
+16
View File
@@ -342,6 +342,22 @@ try:
check(meets == "east-ave, park-rd" and ring.startswith("4:"), f"streets know what they meet: cross-st meets {meets}; roundabout arms {ring}") check(meets == "east-ave, park-rd" and ring.startswith("4:"), f"streets know what they meet: cross-st meets {meets}; roundabout arms {ring}")
shot(pg, "web-streets.jpg") shot(pg, "web-streets.jpg")
# ---------------------------------------------------------------- the country demo: animals, an apiary, diagonals
pg.locator("#wb-projects").click()
pg.locator("#wb-ncountry").click()
pg.wait_for_load_state()
ready(pg)
pg.wait_for_timeout(500)
nc = len(doc(pg)["objects"])
beasts = pg.evaluate("() => new Set([...document.querySelectorAll('.iso-svg .iso-beast')].map(e => e.closest('[data-obj]')?.dataset.obj)).size")
poses = pg.evaluate("() => document.querySelectorAll('.iso-svg .iso-pose2.iso-pose0').length")
check(title(pg) == "isokit country" and nc > 30 and beasts >= 8 and poses >= 12 and pg.evaluate("() => __studio.S.warnings.length") == 0,
f"Country demo opens the countryside ({nc} objects, {beasts} animal and rock groups, {poses} grazing animals, no warnings)")
herd = pg.evaluate("() => Object.fromEntries((__studio.view.info('elephants') || {rows: []}).rows)['Herd'] || ''")
rot = pg.evaluate("() => __studio.S.doc.objects.find(o => o.id === 'lane-d').rot")
check(herd == "1 bull, 1 cow, 1 calf" and abs(rot - 45) < 1e-6, f"elephants: {herd}; the lane bends at {rot} degrees")
shot(pg, "web-country.jpg")
# ---------------------------------------------------------------- a system asking for less motion # ---------------------------------------------------------------- a system asking for less motion
# (Windows: Animation effects off): the view starts still, but the button stays and turns it on # (Windows: Animation effects off): the view starts still, but the button stays and turns it on
rctx = b.new_context(viewport={"width": 1200, "height": 800}, reduced_motion="reduce") rctx = b.new_context(viewport={"width": 1200, "height": 800}, reduced_motion="reduce")
+17
View File
@@ -73,6 +73,12 @@
.iso-svg .iso-r { fill: color-mix(in srgb, var(--b, #888) var(--iso-kr), var(--iso-shade)); } .iso-svg .iso-r { fill: color-mix(in srgb, var(--b, #888) var(--iso-kr), var(--iso-shade)); }
.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 */
.iso-sprites .iso-beast { stroke-width: .3; }
.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-c { stroke: color-mix(in srgb, var(--b, #888) var(--iso-kc), var(--iso-shade)); }
.iso-sprites .iso-beast.iso-r { stroke: color-mix(in srgb, var(--b, #888) var(--iso-kr), var(--iso-shade)); }
@supports not (color: color-mix(in srgb, red 50%, blue)) { @supports not (color: color-mix(in srgb, red 50%, blue)) {
.iso-svg .iso-l, .iso-svg .iso-c, .iso-svg .iso-r { fill: var(--b, #888); } .iso-svg .iso-l, .iso-svg .iso-c, .iso-svg .iso-r { fill: var(--b, #888); }
.iso-svg .iso-r { fill-opacity: .82; } .iso-svg .iso-r { fill-opacity: .82; }
@@ -126,6 +132,13 @@ html[data-theme=light] .iso-svg .iso-glint { display: inline; }
unanimated (reduced motion, ghosted) only the first */ unanimated (reduced motion, ghosted) only the first */
.iso-svg .iso-flip { visibility: hidden; animation: iso-flip16 4s step-end infinite; } .iso-svg .iso-flip { visibility: hidden; animation: iso-flip16 4s step-end infinite; }
.iso-svg .iso-flip.iso-pose0 { visibility: visible; } .iso-svg .iso-flip.iso-pose0 { visibility: visible; }
/* animals: two poses taking turns (grazing, looking up; delays set the turn); unanimated the first.
Birds flap and butterflies flutter (wings scaled across their span); rabbits hop now and then */
.iso-svg .iso-pose2 { visibility: hidden; animation: iso-pose2b 9s step-end infinite; }
.iso-svg .iso-pose2.iso-pose0 { visibility: visible; animation-name: iso-pose2a; }
.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-hop { animation: iso-hop 3.4s ease-out infinite; }
.iso-svg .iso-park { display: none; } .iso-svg .iso-park { display: none; }
/* paused off-screen (iso-off), when stopped (iso-still), with animation off and during pan/zoom gestures */ /* paused off-screen (iso-off), when stopped (iso-still), with animation off and during pan/zoom gestures */
.iso-off .iso-a, .iso-still .iso-a, .iso-off .iso-drive, .iso-still .iso-drive, .iso-off .iso-ledr, .iso-off .iso-ledo, .iso-still .iso-ledr, .iso-still .iso-ledo, .iso-off .iso-a, .iso-still .iso-a, .iso-off .iso-drive, .iso-still .iso-drive, .iso-off .iso-ledr, .iso-off .iso-ledo, .iso-still .iso-ledr, .iso-still .iso-ledo,
@@ -147,6 +160,10 @@ html[data-theme=light] .iso-svg .iso-glint { display: inline; }
@keyframes iso-fadeout { 0%, 65% { opacity: 1; } 100% { opacity: 0; } } @keyframes iso-fadeout { 0%, 65% { opacity: 1; } 100% { opacity: 0; } }
@keyframes iso-fadeio { 0%, 100% { opacity: 0; } 30%, 70% { opacity: 1; } } @keyframes iso-fadeio { 0%, 100% { opacity: 0; } 30%, 70% { opacity: 1; } }
@keyframes iso-flip16 { 0% { visibility: visible; } 6.25%, 100% { visibility: hidden; } } @keyframes iso-flip16 { 0% { visibility: visible; } 6.25%, 100% { visibility: hidden; } }
@keyframes iso-pose2a { 0% { visibility: visible; } 65%, 100% { visibility: hidden; } }
@keyframes iso-pose2b { 0% { visibility: hidden; } 65%, 100% { visibility: visible; } }
@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-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 {
+2 -1
View File
@@ -367,7 +367,7 @@ function injectUI() {
'<h3>Projects</h3>' + '<h3>Projects</h3>' +
'<div class="wb-cur" id="wb-cur"></div>' + '<div class="wb-cur" id="wb-cur"></div>' +
'<ul class="wb-plist" id="wb-plist"></ul>' + '<ul class="wb-plist" id="wb-plist"></ul>' +
'<div class="wb-new"><input id="wb-ntitle" placeholder="Title of a new project" autocomplete="off"><button type="button" id="wb-ncreate">New project</button><button type="button" id="wb-ndemo" title="A fresh copy of the demo scene">Demo copy</button><button type="button" id="wb-nhouse" title="A house built from parts: walls, windows, doors, floors, furniture">House demo</button><button type="button" id="wb-nfactory" title="Presses making goods at a set rate, conveyors carrying them">Factory demo</button><button type="button" id="wb-nstreets" title="Streets with sidewalks, a rounded corner, junctions and a roundabout">Streets demo</button></div>' + '<div class="wb-new"><input id="wb-ntitle" placeholder="Title of a new project" autocomplete="off"><button type="button" id="wb-ncreate">New project</button><button type="button" id="wb-ndemo" title="A fresh copy of the demo scene">Demo copy</button><button type="button" id="wb-nhouse" title="A house built from parts: walls, windows, doors, floors, furniture">House demo</button><button type="button" id="wb-nfactory" title="Presses making goods at a set rate, conveyors carrying them">Factory demo</button><button type="button" id="wb-nstreets" title="Streets with sidewalks, a rounded corner, junctions and a roundabout">Streets demo</button><button type="button" id="wb-ncountry" title="Elephants, deer, boar, beehives and a wildflower meadow; a lane, fence and hedgerow at an angle">Country demo</button></div>' +
'<div class="wb-drop" id="wb-drop">Import: drop a project folder or .zip here, or <button type="button" id="wb-izip" title="Import a project .zip into this browser">.zip…</button><button type="button" id="wb-idir" title="Copy a project folder into this browser">folder…</button>' + '<div class="wb-drop" id="wb-drop">Import: drop a project folder or .zip here, or <button type="button" id="wb-izip" title="Import a project .zip into this browser">.zip…</button><button type="button" id="wb-idir" title="Copy a project folder into this browser">folder…</button>' +
(canOpenFolder ? '<span class="st-grow"></span><button type="button" id="wb-ofolder" title="Edit the files in the folder on disk directly; changes made there show up live">Open folder live…</button>' : "") + "</div>" + (canOpenFolder ? '<span class="st-grow"></span><button type="button" id="wb-ofolder" title="Edit the files in the folder on disk directly; changes made there show up live">Open folder live…</button>' : "") + "</div>" +
'<p class="st-help">Projects are kept in this browser. ' + (canOpenFolder ? "A folder opened live is edited on disk, so agents (<b>isokit mcp</b>), editors and git see the same files." : "To edit a folder on disk directly, use Chrome or Edge.") + "</p>" + '<p class="st-help">Projects are kept in this browser. ' + (canOpenFolder ? "A folder opened live is edited on disk, so agents (<b>isokit mcp</b>), editors and git see the same files." : "To edit a folder on disk directly, use Chrome or Edge.") + "</p>" +
@@ -420,6 +420,7 @@ function wireProjects() {
$("wb-nhouse").addEventListener("click", async function () { openProject((await createProject(await demoFiles("house.json"))).id); }); $("wb-nhouse").addEventListener("click", async function () { openProject((await createProject(await demoFiles("house.json"))).id); });
$("wb-nfactory").addEventListener("click", async function () { openProject((await createProject(await demoFiles("factory.json"))).id); }); $("wb-nfactory").addEventListener("click", async function () { openProject((await createProject(await demoFiles("factory.json"))).id); });
$("wb-nstreets").addEventListener("click", async function () { openProject((await createProject(await demoFiles("streets.json"))).id); }); $("wb-nstreets").addEventListener("click", async function () { openProject((await createProject(await demoFiles("streets.json"))).id); });
$("wb-ncountry").addEventListener("click", async function () { openProject((await createProject(await demoFiles("country.json"))).id); });
$("wb-izip").addEventListener("click", function () { $("wb-fzip").click(); }); $("wb-izip").addEventListener("click", function () { $("wb-fzip").click(); });
$("wb-idir").addEventListener("click", function () { $("wb-fdir").click(); }); $("wb-idir").addEventListener("click", function () { $("wb-fdir").click(); });
$("wb-fzip").addEventListener("change", function () { var f = this.files[0]; if (f) importZip(f); this.value = ""; }); $("wb-fzip").addEventListener("change", function () { var f = this.files[0]; if (f) importZip(f); this.value = ""; });