// 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 ``. 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); `` } /// 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, `` + `z` + `Z`); } 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(``); } 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(``); } this } fn fine_end() { this.raw("") } 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 = ``; let spokes = plane + rim + (if run { `` } else { "" }) + `` + ``; let rim0 = plane + rim + ""; 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"); } }