Wildlife: hamster and hamster wheel prefabs
Ported from the thaiai.lol easter egg (thaiai iso/hamster/lib/hamster.rhai); the data-centre joke parts stay there. - hamster (Animals): golden, grey or cream with a white belly, pink nose, ears and paws, buck teeth and whiskers; standing, running on the spot or asleep (eyes shut, z Z); n and spread like the herds; life size (about 17 cm) at scale 1, up to 40 times. - hamster_wheel (Animals): diameter (0.3 m life size, up to 12 m), running, A-frame stand or a cage-wall axle stub, fur colour. Running legs and the rungs are flipbooks of 4 poses, 0.1 s each; the spokes turn by a CSS rotation in a matrix that maps the wheel's plane onto the screen. - draw_parts learns loft2 (two-tone lofts), eye and flip parts; place_parts scales and moves any part list; loft and loft2 share loft_rings (the other animals draw byte for byte as before). - Things under 2 m are drawn ten times bigger in a group that scales them back (fine_begin / fine_end), so a life-size hamster keeps its shape on the drawing's 0.1-unit grid; sort boxes stay real size. - The legs push back on the ground and swing forward lifted (the original lifted them on the way back). - Theme: hamster, hamsterg, hamsterc, hamsterw, hamsterp, hwheel, hwhub, whisker and hamz in both palettes, materials, rules for eyes, whiskers, rims, spokes, hub, rungs and the z Z. Viewer CSS: iso-flip4, iso-wheelspin, iso-runbob, iso-zzz; beast edges take --bw (default .3). - Test hamsters_run_in_wheels_from_every_side: no sort cycles in any rotation, flipbook counts, scaled groups, real-size sort boxes, tooltips. - README, ROADMAP, SCRIPTING and the MCP instructions list them.
This commit is contained in:
1 parent
c8becc2e59
commit
9619d0f267
8 files changed
+609
-29
No files matched your search
@@ -1,12 +1,14 @@
|
||||
// isokit/wildlife: animals (deer, wild boar, fox, elephant, rabbits, a flock
|
||||
// of birds, butterflies) and beehives, bushes, flowers, rocks and logs.
|
||||
// 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); birds, butterflies and bees fly round on orbits.
|
||||
// like every animation); running hamsters move their legs in a flipbook of
|
||||
// four poses; birds, butterflies and bees fly round on orbits.
|
||||
|
||||
fn animal_kinds() { ["deer", "boar", "fox", "elephant", "rabbits"] }
|
||||
|
||||
@@ -30,6 +32,26 @@ fn vunit(a) {
|
||||
/// 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 = ();
|
||||
@@ -53,27 +75,18 @@ fn loft(spine, rs, n, mats) {
|
||||
}
|
||||
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
|
||||
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) or "alt" (two
|
||||
// part lists shown in turn: a, b, css).
|
||||
// (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 } }
|
||||
@@ -102,11 +115,38 @@ fn draw_parts(parts) {
|
||||
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) {
|
||||
@@ -132,6 +172,54 @@ fn scale_parts(parts, s) {
|
||||
})
|
||||
}
|
||||
|
||||
/// Parts scaled by s about the origin, then moved by the 3D offset o (a
|
||||
/// hamster of any size anywhere); every kind of part.
|
||||
fn place_parts(parts, s, o) {
|
||||
parts.map(|p| {
|
||||
let q = p;
|
||||
q.at = vadd(o, vmul(p.at, s));
|
||||
if p.t == "loft" || p.t == "loft2" {
|
||||
q.spine = p.spine.map(|v| vadd(o, vmul(v, s)));
|
||||
q.rs = p.rs.map(|r| if type_of(r) == "array" { [r[0] * s, r[1] * s] } else { r * s });
|
||||
} else if p.t == "face" || p.t == "line" {
|
||||
q.pts = p.pts.map(|v| vadd(o, vmul(v, s)));
|
||||
} else if p.t == "eye" {
|
||||
q.r = p.r * s;
|
||||
} else if p.t == "alt" {
|
||||
q.a = place_parts(p.a, s, o);
|
||||
q.b = place_parts(p.b, s, o);
|
||||
} else if p.t == "flip" {
|
||||
q.vars = p.vars.map(|v| place_parts(v, s, o));
|
||||
}
|
||||
q
|
||||
})
|
||||
}
|
||||
|
||||
/// A loft (see `loft`) in two colours, from a part map: the side faces of
|
||||
/// segment i whose middle points down by more than wt[i] (the sine of the
|
||||
/// angle below level; -1 none, 1 all) get `low` (belly, chin, muzzle), the
|
||||
/// others `top`; the end caps cap0 and cap1. Faces carry `beast`.
|
||||
fn loft2(p) {
|
||||
let rings = loft_rings(p.spine, p.rs, p.n);
|
||||
let n = p.n;
|
||||
let m = rings.len();
|
||||
for i in 1..m {
|
||||
let a = rings[i - 1];
|
||||
let b = rings[i];
|
||||
let wt = p.wt[i - 1];
|
||||
for k in 0..n {
|
||||
let j = (k + 1) % n;
|
||||
let tc = 2.0 * PI() * (k + 1) / n;
|
||||
this.face([a[k], a[j], b[j], b[k]], (if tc.sin() < wt { p.low } else { p.top }) + " beast");
|
||||
}
|
||||
}
|
||||
let c0 = rings[0];
|
||||
c0.reverse();
|
||||
this.face(c0, p.cap0 + " beast");
|
||||
this.face(rings[m - 1], p.cap1 + " beast");
|
||||
this
|
||||
}
|
||||
|
||||
/// Four legs from the underside of a body to the ground: at x fx (front)
|
||||
/// 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) {
|
||||
@@ -468,6 +556,335 @@ fn prefab_butterflies(o) {
|
||||
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"] }
|
||||
|
||||
Reference in new issue
Block a user