Wildlife and nature: animals, beehives, bushes, flowers, rocks, logs
Deploy web Studio / deploy (push) Successful in 1m5s
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:
1 parent
96d472625f
commit
e840d36000
22 files changed
+1209
-14
No files matched your search
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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). \
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")),
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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}}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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("*")):
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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 = ""; });
|
||||||
|
|||||||
Reference in new issue
Block a user