Files
isokit/crates/isokit-script/stdlib/wildlife.rhai
T
grabowski 9619d0f267 Wildlife: hamster and hamster wheel prefabs
Ported from the thaiai.lol easter egg (thaiai iso/hamster/lib/hamster.rhai);
the data-centre joke parts stay there.

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

1116 lines
50 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// isokit/wildlife: animals (deer, wild boar, fox, elephant, rabbits,
// hamsters and a hamster wheel, a flock of birds, butterflies) and beehives,
// bushes, flowers, rocks and logs.
//
// Animals are low-poly solids drawn face by face (`face`: the engine culls
// 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); running hamsters move their legs in a flipbook of
// four poses; birds, butterflies and bees fly round on orbits.
fn animal_kinds() { ["deer", "boar", "fox", "elephant", "rabbits"] }
// ---------------------------------------------------------------- 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 rings = loft_rings(spine, rs, n);
let m = spine.len();
for i in 1..m {
let p = rings[i - 1];
let q = rings[i];
for k in 0..n {
let j = (k + 1) % n;
this.face([p[k], p[j], q[j], q[k]], mats[0]);
}
}
let c0 = rings[0];
c0.reverse();
this.face(c0, mats[1]);
this.face(rings[m - 1], mats[2]);
this
}
/// The rings of a loft: n points round each spine point, the first one to
/// the spine's left (towards +x on an upright spine), then up and round.
fn loft_rings(spine, rs, n) {
let m = spine.len();
let 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);
}
rings
}
/// 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), "alt" (two
// part lists shown in turn: a, b, css), "loft2" (a loft in two colours,
// see `loft2`), "eye" (a bead of radius r at `at`, or shut when `closed`)
// or "flip" (a flipbook: vars, a part list per pose, shown 0.1 s each,
// `delay` s in; 4 poses).
fn p_loft(spine, rs, n, mats) { #{ at: spine[spine.len() / 2], t: "loft", spine: spine, rs: rs, n: n, mats: mats } }
fn p_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();
} else if p.t == "loft2" {
this.loft2(p);
} else if p.t == "eye" {
let c = p.at;
if p.closed {
let d = p.r * 0.9;
this.polyline([[c[0] - d, c[1], c[2] + d * 0.15], [c[0], c[1], c[2] - d * 0.3], [c[0] + d, c[1], c[2] + d * 0.15]], 0.0, "hamstereyec");
} else {
let r = p.r * px();
this.circle(c[0], c[1], c[2], r, "hamstereye");
this.circle(c[0], c[1], c[2], r * 0.36, "hamsterglint", -r * 0.3, -r * 0.34);
}
} else if p.t == "flip" {
let np = p.vars.len();
for k in 0..np {
this.begin(flip_cls(k, np), flip_css(k, np, p.delay));
this.draw_parts(p.vars[k]);
this.end();
}
}
}
this
}
/// Class and style of pose k in a flipbook of np poses shown 0.1 s each,
/// the whole book `delay` s in; pose 0 shows when unanimated. The viewer
/// styles books of 4 poses (`iso-flip4`).
fn flip_cls(k, np) { if k == 0 { `a flip${np} pose0` } else { `a flip${np}` } }
fn flip_css(k, np, delay) { `animation-delay:-${round4(((np - k) % np) * 0.1 + delay)}s` }
fn round4(x) { (x * 10000.0).round() / 10000.0 }
/// A point of a head: `a` along its way f, `b` to its left, `c` up from it,
/// 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
})
}
/// Parts scaled by s about the origin, then moved by the 3D offset o (a
/// hamster of any size anywhere); every kind of part.
fn place_parts(parts, s, o) {
parts.map(|p| {
let q = p;
q.at = vadd(o, vmul(p.at, s));
if p.t == "loft" || p.t == "loft2" {
q.spine = p.spine.map(|v| vadd(o, vmul(v, s)));
q.rs = p.rs.map(|r| if type_of(r) == "array" { [r[0] * s, r[1] * s] } else { r * s });
} else if p.t == "face" || p.t == "line" {
q.pts = p.pts.map(|v| vadd(o, vmul(v, s)));
} else if p.t == "eye" {
q.r = p.r * s;
} else if p.t == "alt" {
q.a = place_parts(p.a, s, o);
q.b = place_parts(p.b, s, o);
} else if p.t == "flip" {
q.vars = p.vars.map(|v| place_parts(v, s, o));
}
q
})
}
/// A loft (see `loft`) in two colours, from a part map: the side faces of
/// segment i whose middle points down by more than wt[i] (the sine of the
/// angle below level; -1 none, 1 all) get `low` (belly, chin, muzzle), the
/// others `top`; the end caps cap0 and cap1. Faces carry `beast`.
fn loft2(p) {
let rings = loft_rings(p.spine, p.rs, p.n);
let n = p.n;
let m = rings.len();
for i in 1..m {
let a = rings[i - 1];
let b = rings[i];
let wt = p.wt[i - 1];
for k in 0..n {
let j = (k + 1) % n;
let tc = 2.0 * PI() * (k + 1) / n;
this.face([a[k], a[j], b[j], b[k]], (if tc.sin() < wt { p.low } else { p.top }) + " beast");
}
}
let c0 = rings[0];
c0.reverse();
this.face(c0, p.cap0 + " beast");
this.face(rings[m - 1], p.cap1 + " beast");
this
}
/// Four legs from the underside of a body to the ground: at x fx (front)
/// 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); }
}
// ---------------------------------------------------------------- hamsters
// A hamster is built like the animals above, from lofts in two colours (fur
// on top, white underneath), with beady eyes and a flipbook of running legs.
// A hamster wheel's rungs are a flipbook too (they join the wheel's two
// planes, so no rotation can turn them); its spokes turn by a CSS rotation
// in the wheel's own plane (`vplane_g`, the fan's trick). Every animated
// group carries `iso-a`, so the viewer pauses it.
fn hamster_colours() { ["golden", "grey", "cream", "mixed"] }
/// The fur material of hamster i for a colour choice ("mixed" takes turns).
fn hamster_fur(c, i) {
let k = if c == "mixed" { ["golden", "grey", "cream"][i % 3] } else { c };
if k == "grey" { "hamsterg" } else if k == "cream" { "hamsterc" } else { "hamster" }
}
/// The size of a life-size hamster (about 17 cm) for hamster_parts.
fn hamster_life() { 0.14 }
/// Raw SVG group that maps the vertical plane along (ux, uy) (plan, per
/// metre) onto the screen: u along it, v straight down, in metres. Draw
/// circles and paths in it (wheels, pulleys) and close it with `</g>`.
fn vplane_g(ux, uy) {
let a = dv(ux * 1000.0, uy * 1000.0, 0.0);
let b = dv(0.0, 0.0, -1000.0);
`<g transform="matrix(${round4(a[0] / 1000.0)} ${round4(a[1] / 1000.0)} ${round4(b[0] / 1000.0)} ${round4(b[1] / 1000.0)} 0 0)">`
}
/// One leg of a hamster facing +x (hamster_parts size), in running pose
/// phi (radians: a paw pushes back on the ground and swings forward
/// lifted) or standing. i: 0/1 front left/right, 2/3 hind left/right.
fn hamster_leg(i, phi, run, n, fur) {
let side = if i % 2 == 0 { 1.0 } else { -1.0 };
let front = i < 2;
let ph = phi + (if side > 0.0 { 0.0 } else { 0.8 }) + (if front { 0.0 } else { PI() });
let swing = if run { 0.09 * ph.cos() } else { 0.0 };
let lift = if run { fmax(0.0, -0.06 * ph.sin()) } else { 0.0 };
if front {
let y = side * 0.125;
let fx = 0.18 + swing;
[p_tube([0.15, y, 0.2], [fx, y, lift + 0.035], 0.05, 0.035, n, fur),
p_tube([fx - 0.02, y, lift + 0.018], [fx + 0.05, y, lift + 0.014], 0.026, 0.02, n, "hamsterp")]
} else {
let y = side * 0.17;
let bx = -0.3 + swing;
[p_tube([-0.27, y, 0.22], [bx, y, lift + 0.04], 0.075, 0.045, n, fur),
p_tube([bx - 0.04, y, lift + 0.02], [bx + 0.08, y, lift + 0.015], 0.034, 0.026, n, "hamsterp")]
}
}
/// The parts of a hamster facing +x, feet at z = 0, about 1.2 m long and
/// 0.75 m high (place_parts scales it; hamster_life() is life size): fur
/// material `fur`, white underneath, pink nose, ears and paws. run: the legs
/// in a flipbook of np poses (4), `delay` s in; sleep: lying down with its
/// eyes shut and its legs tucked away. n: sides of body and head (8-10).
fn hamster_parts(run, sleep, n, np, delay, fur) {
let dz = if sleep { -0.07 } else { 0.0 };
let dh = if sleep { -0.05 } else { 0.0 };
let parts = [];
// body: an egg, white underneath
parts.push(#{ t: "loft2", at: [-0.12, 0.0, 0.36 + dz],
spine: [[-0.5, 0.0, 0.33 + dz], [-0.45, 0.0, 0.335 + dz], [-0.34, 0.0, 0.345 + dz], [-0.17, 0.0, 0.355 + dz], [0.02, 0.0, 0.365 + dz], [0.16, 0.0, 0.375 + dz], [0.24, 0.0, 0.385 + dz]],
rs: [[0.07, 0.07], [0.19, 0.19], [0.27, 0.27], [0.31, 0.3], [0.3, 0.29], [0.25, 0.25], [0.17, 0.18]],
n: n, top: fur, low: "hamsterw", wt: [-0.5, -0.5, -0.45, -0.45, -0.45, -0.4], cap0: fur, cap1: fur });
// head with puffed cheek pouches, white muzzle and chin
let hz = 0.42 + dz + dh;
parts.push(#{ t: "loft2", at: [0.38, 0.0, hz],
spine: [[0.12, 0.0, hz], [0.25, 0.0, hz + 0.02], [0.37, 0.0, hz], [0.47, 0.0, hz - 0.04], [0.55, 0.0, hz - 0.09], [0.59, 0.0, hz - 0.11]],
rs: [[0.2, 0.2], [0.255, 0.23], [0.24, 0.21], [0.18, 0.16], [0.1, 0.09], [0.035, 0.035]],
n: n, top: fur, low: "hamsterw", wt: [-0.45, -0.4, -0.15, 0.25, 0.6], cap0: fur, cap1: "hamsterw" });
// nose, buck teeth, whiskers
parts.push(p_tube([0.58, 0.0, hz - 0.105], [0.61, 0.0, hz - 0.115], 0.03, 0.02, 5, "hamsterp"));
for sd in [1.0, -1.0] {
parts.push(p_tube([0.565, sd * 0.012, hz - 0.15], [0.57, sd * 0.012, hz - 0.2], 0.012, 0.011, 4, "tusk"));
parts.push(#{ t: "line", at: [0.58, sd * 0.1, hz - 0.1], cls: "whisker",
pts: [[0.56, sd * 0.06, hz - 0.11], [0.64, sd * 0.17, hz - 0.07], [0.69, sd * 0.24, hz - 0.06]] });
parts.push(#{ t: "line", at: [0.58, sd * 0.1, hz - 0.12], cls: "whisker",
pts: [[0.56, sd * 0.06, hz - 0.13], [0.64, sd * 0.18, hz - 0.15], [0.68, sd * 0.25, hz - 0.18]] });
// round ears: fur behind, pink inside, both anchored on the ear's
// middle (the pink one a little forward), so seen from the front it
// lies on the fur and from behind under it
let ec = [0.22, sd * 0.155, hz + 0.27];
let ear = [];
for k in 0..7 {
let a = PI() * k / 6.0;
ear.push([ec[0] - 0.01 * a.sin(), ec[1] + sd * 0.095 * a.cos(), ec[2] + 0.095 * a.sin() - 0.045]);
}
let back = p_face(ear.map(|v| [v[0] - 0.005, v[1], v[2]]), fur);
back.at = [ec[0] - 0.005, ec[1], ec[2]];
parts.push(back);
let inner = p_face(ear.map(|v| [v[0] + 0.02, ec[1] + (v[1] - ec[1]) * 0.62, ec[2] - 0.012 + (v[2] - ec[2]) * 0.62]), "hamsterp");
inner.at = [ec[0] + 0.03, ec[1], ec[2]];
parts.push(inner);
// eyes: shiny beads on the sides of the head
parts.push(#{ t: "eye", at: [0.43, sd * 0.172, hz + 0.065], r: 0.05, closed: sleep });
}
// tail: a stub
parts.push(p_tube([-0.48, 0.0, 0.37 + dz], [-0.555, 0.0, 0.4 + dz], 0.035, 0.02, 4, fur));
// legs: a flipbook of running poses, or standing; tucked away asleep
if !sleep {
let ln = if n > 8 { 5 } else { 4 };
for i in 0..4 {
if run {
let vars = [];
for k in 0..np { vars.push(hamster_leg(i, 2.0 * PI() * k / np, true, ln, fur)); }
parts.push(#{ t: "flip", at: vars[0][0].at, vars: vars, delay: delay });
} else {
parts += hamster_leg(i, 0.0, false, ln, fur);
}
}
}
parts
}
/// Draws a hamster facing +x with its feet at (x, 0, z): hamster_parts
/// scaled by `size` (hamster_life() is life size), fur material `fur`.
/// run: running (legs in 4 poses, the body bobbing, `delay` s in); sleep:
/// curled up, eyes shut, snoring. Its faces are edged `--bw` wide (set it on
/// a group around it: the default .3 suits a hamster a metre long).
fn hamster_at(x, z, size, run, sleep, fur, delay) {
let parts = place_parts(hamster_parts(run, sleep, if size >= 1.0 { 10 } else { 8 }, 4, delay, fur), size, [x, 0.0, z]);
if run {
this.begin("a runbob", `--hb:-${round4(size * 0.25)}px;animation-delay:-${round4(delay)}s`);
}
this.draw_parts(parts);
if run {
this.end();
}
if sleep {
let zx = round4(size * 1.2);
// (its delay keeps a still view, paused anywhere, on a visible z Z)
this.raw(x + 0.45 * size, 0.0, z + 0.7 * size,
`<g class="iso-a iso-zzz" style="animation-delay:-${round4(0.9 + delay % 1.4)}s;--zx:${round4(size * 1.2)}px;--zy:-${round4(size * 2.9)}px">`
+ `<text class="iso-hamz" font-size="${zx}" x="0" y="0">z</text>`
+ `<text class="iso-hamz" font-size="${round4(zx * 1.4)}" x="${round4(zx * 0.7)}" y="-${round4(zx * 0.8)}">Z</text></g>`);
}
this
}
/// How many times bigger to draw something `l` metres across: the drawing's
/// coordinates are rounded to a tenth of a screen unit (2.5 cm), which turns
/// a life-size hamster or a 30 cm wheel blocky. See fine_begin.
fn fine_k(l) { if l < 2.0 { 10.0 } else { 1.0 } }
/// Opens a group that shows what is drawn in it k times smaller about the
/// point (x, y, z) of this frame (fz: the frame's height, the object's z), so
/// draw a small thing k times bigger about that point. css: the group's
/// style (custom properties such as --bw, k times too). Close it with
/// fine_end(), then set the sort extent with ext_reset() and ext_box(): the
/// drawn one is k times too big.
fn fine_begin(x, y, z, fz, k, css) {
if k == 1.0 {
this.raw(`<g style="${css}">`);
} else {
let l0 = to_local(0.0, 0.0);
let a = dv((x - l0[0]) * 1000.0, (y - l0[1]) * 1000.0, (z + fz) * 1000.0);
let sx = round4(a[0] / 1000.0);
let sy = round4(a[1] / 1000.0);
this.raw(`<g transform="translate(${sx} ${sy}) scale(${round4(1.0 / k)}) translate(${-sx} ${-sy})" style="${css}">`);
}
this
}
fn fine_end() { this.raw("</g>") }
fn meta_hamster() {
#{ label: "Hamster", category: "Animals", icon: "🐹",
help: "Hamsters (golden, grey or cream, white underneath) standing about, running on the spot or curled up asleep. Life size (about 17 cm) at scale 1; scale them up for a giant.",
props: herd_props(1, 0.5, 30.0) + [
#{ key: "pose", type: "choice", def: "standing", choices: ["standing", "running", "asleep"], label: "Pose" },
#{ key: "colour", type: "choice", def: "golden", choices: hamster_colours(), label: "Fur" },
#{ key: "scale", type: "float", def: 1.0, min: 0.5, max: 40.0, step: 0.5, label: "Scale (× life)" },
] }
}
fn prefab_hamster(o) {
let p = o.p;
let n = p.n;
let size = hamster_life() * p.scale;
let k = fine_k(size * 1.2);
let bw = round4(fmin(0.3, size * 0.5) * k);
let seed = o.x * 0.57 + o.y * 1.13;
for i in 0..n {
// a sunflower spread like the herds, each its own sprite
let rr = if n == 1 { 0.0 } else { p.r * ((i + 0.5) / n).sqrt() };
let ang = i * 2.39996 + hash(seed + i) * 0.6;
let w = world_of(o, rr * ang.cos(), rr * ang.sin());
let turn = if n == 1 { 0.0 } else { (hash(seed + i * 3.7) - 0.5) * 140.0 };
frame(w[0], w[1], o.z, o.rot + turn);
let s = sprite();
s.fine_begin(0.0, 0.0, 0.0, o.z, k, `--bw:${bw}`);
s.hamster_at(0.0, 0.0, size * k, p.pose == "running", p.pose == "asleep", hamster_fur(p.colour, i), round4(hash(seed + i * 2.3) * 3.2));
s.fine_end();
s.ext_reset();
s.ext_box(-0.57 * size, -0.31 * size, 0.7 * size, 0.31 * size, 0.0, 0.75 * size);
}
frame(o);
info_row("Animals", `${n} hamster${if n == 1 { "" } else { "s" }}, ${p.pose}`);
}
/// A hamster wheel of radius R drawn into this sprite around (0, 0): the
/// drum (0.7 R wide) across local y, open at -y, the spokes and the axle at
/// +y on an A-frame stand, or without the stand on an axle stub for a cage
/// wall at +y. A hamster with fur `fur` runs in it (run: legs, rungs and
/// spokes turning, `delay` s in) or stands still. Returns #{ turn: seconds
/// per turn, ext: [x0, y0, x1, y1, z0, z1] } for the sort extent (raw SVG
/// adds none). Faces are edged `--bw` wide (see hamster_at).
fn hamster_wheel(R, run, stand, fur, delay) {
let W = R * 0.7;
let N = 24; // rungs
let F = 4; // rung poses per rung step
let turn = N * F * 0.1; // s per turn
let lift = if stand { R * 0.12 } else { R * 0.04 };
let zc = lift + R;
let near = vis(0.0, 1.0); // the stand side is turned to the viewer
let ys = W / 2.0 + R * 0.15; // the stand's plane
let lw = R * 0.05; // the stand's bars
let rimw = round4(R * 0.045);
let hs = R * 0.8; // the hamster's size
let rw = `--rw:${round4(R * 0.028 * px())}`;
// the hamster and where it sorts against the rungs
let ox = -0.06 * hs;
let oz = lift + 0.035 * hs;
let hk = depth(ox, 0.0) + oz + 0.38 * hs;
// rungs: for each pose, the ones behind the hamster and the ones in front
let back = [];
let front = [];
let nposes = if run { F } else { 1 };
for k in 0..nposes {
let b = [];
let f = [];
for j in 0..N {
let a = 2.0 * PI() * (j - k / F.to_float()) / N + 0.1;
let x = R * a.cos();
let z = zc + R * a.sin();
let seg = [[x, -W / 2.0, z], [x, W / 2.0, z]];
if depth(x, 0.0) + z < hk { b.push(seg); } else { f.push(seg); }
}
back.push(b);
front.push(f);
}
// the stand side: the spoked rim turning in its plane, hub, axle, stand
let dsp = "";
for i in 0..6 {
let a = 2.0 * PI() * i / 6.0;
dsp += `M0 0L${round4(R * a.cos())} ${round4(R * a.sin())}`;
}
let plane = vplane_g(1.0, 0.0);
let rim = `<circle class="iso-wheelrim" r="${round4(R)}" style="stroke-width:${rimw}"/>`;
let spokes = plane + rim
+ (if run { `<g class="iso-a iso-wheelspin" style="animation-duration:${round4(turn)}s;animation-delay:-${round4(delay * 3.7 % turn)}s">` } else { "<g>" })
+ `<circle class="iso-swimbox" r="${round4(R)}"/><path class="iso-wheelspoke" style="stroke-width:${round4(rimw * 0.6)}" d="${dsp}"/></g>`
+ `<circle class="iso-wheelhub" r="${round4(R * 0.09)}"/></g>`;
let rim0 = plane + rim + "</g>";
let ra = R * 0.035;
let yend = if stand { ys + lw * 2.0 } else { ys + R * 0.03 };
let holder = |s| {
if stand {
let fx = R * 0.62;
s.box(-fx - lw * 2.0, ys - lw * 1.5, fx + lw * 2.0, ys + lw * 1.5, 0.0, lw, "hwheel beast");
s.beam([-fx, ys, lw], [0.0, ys, zc], [0.0, 1.0, 0.0], lw, "hwheel beast");
s.beam([fx, ys, lw], [0.0, ys, zc], [0.0, 1.0, 0.0], lw, "hwheel beast");
s.beam([-fx * 0.55, ys, zc * 0.42], [fx * 0.55, ys, zc * 0.42], [0.0, 1.0, 0.0], lw * 0.8, "hwheel beast");
} else {
// a bracket for the cage wall
s.box(-R * 0.1, ys, R * 0.1, yend, zc - R * 0.1, zc + R * 0.1, "hwheel beast");
}
s.tube([0.0, W / 2.0, zc], [0.0, yend, zc], ra, ra, 6, "hwheel beast");
};
let s = this;
if near {
s.raw(0.0, -W / 2.0, zc, rim0);
} else {
holder.call(s);
s.raw(0.0, W / 2.0, zc, spokes);
}
for k in 0..nposes {
if back[k].len() > 0 {
s.begin(if run { flip_cls(k, F) } else { "" }, if run { flip_css(k, F, delay) + ";" + rw } else { rw });
s.polyline(back[k], 0.0, "wheelrung");
s.end();
}
}
s.hamster_at(ox, oz, hs, run, false, fur, delay);
for k in 0..nposes {
if front[k].len() > 0 {
s.begin(if run { flip_cls(k, F) } else { "" }, if run { flip_css(k, F, delay) + ";" + rw } else { rw });
s.polyline(front[k], 0.0, "wheelrung");
s.end();
}
}
if near {
s.raw(0.0, W / 2.0, zc, spokes);
holder.call(s);
} else {
s.raw(0.0, -W / 2.0, zc, rim0);
}
#{ turn: turn, ext: [-R, -W / 2.0, R, yend, 0.0, zc + R] }
}
fn meta_hamster_wheel() {
#{ label: "Hamster wheel", category: "Animals", icon: "◎",
help: "A hamster running in a wheel: the drum across local y, open at -y, spokes and axle at +y on an A-frame stand (without the stand: an axle stub for a cage wall at +y). Life size by default; make it big for a hamster power plant.",
props: [
#{ key: "diameter", type: "float", def: 0.3, min: 0.15, max: 12.0, step: 0.05, label: "Diameter (m)" },
#{ key: "running", type: "bool", def: true, label: "Running" },
#{ key: "stand", type: "bool", def: true, label: "Stand" },
#{ key: "colour", type: "choice", def: "golden", choices: ["golden", "grey", "cream"], label: "Fur" },
] }
}
fn prefab_hamster_wheel(o) {
let p = o.p;
let delay = round4(hash(o.x * 0.71 + o.y * 1.31) * 2.0);
let k = fine_k(p.diameter);
let s = sprite();
s.fine_begin(0.0, 0.0, 0.0, o.z, k, `--bw:${round4(fmin(0.3, p.diameter * 0.2) * k)}`);
let w = s.hamster_wheel(p.diameter / 2.0 * k, p.running, p.stand, hamster_fur(p.colour, 0), delay);
s.fine_end();
s.ext_reset();
let e = w.ext.map(|v| v / k);
s.ext_box(e[0], e[1], e[2], e[3], e[4], e[5]);
let turn = w.turn;
let across = if p.diameter < 1.0 { `${(p.diameter * 100.0).round().to_int()} cm` } else { `${num1(p.diameter)} m` };
info_row("Wheel", if p.running { `${across} across, a turn every ${num1(turn)} s` } else { `${across} across, standing still` });
}
// ---------------------------------------------------------------- beehives
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");
}
}