Deploy web Studio / deploy (push) Successful in 1m5s
Object frames turn by any angle (quarter turns stay exact, so scenes along the axes render byte for byte as before); a box in a turned frame is a prism with its own corners. Rotation is kept as given (normalised to 0..360), no longer snapped to quarter turns. Depth sort: sprites whose plan outline is not their box carry their convex outline; when boxes overlap, an outline edge that separates two sprites orders them. A long diagonal wall sorts right against both sides. Joints (line_meet, end_cut, cut_box in util.rhai): where a piece off the axes is involved, corners are mitred and T-junctions slanted; fences put a corner post or pillar in its own sprite and mitre stone bases; Y joints let the lowest id run through. Streets: every end part is a cut e + k*w, so side streets come in at any angle (slanted sidewalk gaps), two streets meet in a mitre with seam patches; fixes a curve's outer sidewalk that was skipped when its kerb and back lines had different point counts. Studio: runs and drags keep to x/y or snap to 45 degree diagonals, Ctrl gives any angle; a road run turning 45 degrees gets a 45 degree curve; Rotation is a number box. Streets demo: a diagonal lane with a hedge. MCP guidance, SCRIPTING/README/ROADMAP. Tests: Rust 76, Studio and web checks green.
194 lines
8.2 KiB
Plaintext
194 lines
8.2 KiB
Plaintext
// isokit/nature: trees, hedges, shrubs.
|
|
|
|
/// A tree with a round crown of radius r (m) standing at (x, y) of this sprite.
|
|
fn tree_at(x, y, r) {
|
|
let h = 1.4 + r * 0.6;
|
|
this.ext_box(x - r, y - r, x + r, y + r, 0.0, h + r * 1.8);
|
|
this.stem(x, y, 0.0, h, 0.7, "trunk");
|
|
let R = r * px() * 1.18;
|
|
this.circle(x, y, h + r * 0.9, R, "leaf l");
|
|
this.circle(x, y, h + r * 0.9, R * 0.58, "leaf t", -R * 0.28, -R * 0.3);
|
|
this
|
|
}
|
|
|
|
fn meta_tree() {
|
|
#{ label: "Tree", category: "Nature", icon: "🌳",
|
|
props: [ #{ key: "r", type: "float", def: 1.6, min: 0.5, max: 6.0, step: 0.1, label: "Crown radius (m)" } ] }
|
|
}
|
|
fn prefab_tree(o) {
|
|
sprite().tree_at(0.0, 0.0, o.p.r);
|
|
}
|
|
|
|
fn meta_tree_row() {
|
|
#{ label: "Row of trees", category: "Nature", icon: "🌲", shape: "line",
|
|
props: [
|
|
#{ key: "w", type: "float", def: 30.0, min: 2.0, max: 500.0, step: 1.0, label: "Length (m)" },
|
|
#{ key: "spacing", type: "float", def: 6.0, min: 2.0, max: 30.0, step: 0.5, label: "Spacing (m)" },
|
|
#{ key: "r", type: "float", def: 1.5, min: 0.5, max: 6.0, step: 0.1, label: "Crown radius (m)" },
|
|
] }
|
|
}
|
|
fn prefab_tree_row(o) {
|
|
let p = o.p;
|
|
let n = (p.w / p.spacing).floor().to_int() + 1;
|
|
let step = if n > 1 { p.w / (n - 1) } else { 0.0 };
|
|
for i in 0..n {
|
|
// one sprite per tree; small deterministic size variation
|
|
sprite().tree_at(-p.w / 2.0 + i * step, 0.0, p.r * (0.85 + 0.3 * hash(i + p.w)));
|
|
}
|
|
}
|
|
|
|
fn meta_hedge() {
|
|
#{ label: "Hedge", category: "Nature", icon: "▬", shape: "line",
|
|
props: [
|
|
#{ key: "w", type: "float", def: 10.0, min: 0.5, max: 300.0, step: 0.5, label: "Length (m)" },
|
|
#{ key: "d", type: "float", def: 0.6, min: 0.3, max: 3.0, step: 0.1, label: "Thickness (m)" },
|
|
#{ key: "h", type: "float", def: 1.4, min: 0.3, max: 4.0, step: 0.1, label: "Height (m)" },
|
|
] }
|
|
}
|
|
fn prefab_hedge(o) {
|
|
let p = o.p;
|
|
let c0 = hedge_end(o, -1.0);
|
|
let c1 = hedge_end(o, 1.0);
|
|
sprite().cut_box(c0.x, c0.k, c1.x, c1.k, -p.d / 2.0, p.d / 2.0, 0.0, p.h, "hedge");
|
|
}
|
|
|
|
/// Where hedge o ends at e: mitred or slanted where a diagonal hedge meets it.
|
|
fn hedge_end(o, e) {
|
|
let m = line_meet(o, e, ["hedge"], []);
|
|
if m.kind == "corner" || m.kind == "tee" { end_cut(o.p.w, e, m) } else { end_cut(o.p.w, e, #{ ext: 0.0, k: 0.0 }) }
|
|
}
|
|
|
|
// ---------------------------------------------------------------- pond
|
|
|
|
fn meta_pond() {
|
|
#{ label: "Pond", category: "Nature", icon: "◍", shape: "rect",
|
|
help: "A garden pond with fish swimming round under lily pads, reeds at the back and a stone or grass edge. Drag a rectangle on the map.",
|
|
props: [
|
|
#{ key: "w", type: "float", def: 6.0, min: 1.5, max: 60.0, step: 0.5, label: "Width (m)" },
|
|
#{ key: "d", type: "float", def: 4.0, min: 1.5, max: 60.0, step: 0.5, label: "Length (m)" },
|
|
#{ key: "shape", type: "choice", def: "natural", choices: ["natural", "round", "square"], label: "Shape" },
|
|
#{ key: "fish", type: "int", def: 5, min: 0, max: 16, step: 1, label: "Fish" },
|
|
#{ key: "lilies", type: "int", def: 3, min: 0, max: 12, step: 1, label: "Lily pads" },
|
|
#{ key: "reeds", type: "bool", def: true, label: "Reeds" },
|
|
#{ key: "edge", type: "choice", def: "stone", choices: ["stone", "grass"], label: "Edge" },
|
|
] }
|
|
}
|
|
|
|
/// The pond's outline inset by `ins` metres: a kidney ("natural"), an
|
|
/// ellipse ("round") or a rectangle with round corners ("square").
|
|
fn pond_outline(p, ins) {
|
|
let a = p.w / 2.0 - ins;
|
|
let b = p.d / 2.0 - ins;
|
|
let pts = [];
|
|
if p.shape == "square" {
|
|
let r = fmax(0.05, fmin(a, b) * 0.35);
|
|
for k in 0..4 {
|
|
let cx = if k == 0 || k == 3 { a - r } else { -a + r };
|
|
let cy = if k < 2 { b - r } else { -b + r };
|
|
for j in 0..7 {
|
|
let t = (k * 90 + j * 15) * PI() / 180.0;
|
|
pts.push([cx + r * t.cos(), cy + r * t.sin()]);
|
|
}
|
|
}
|
|
return pts;
|
|
}
|
|
for j in 0..48 {
|
|
let t = j * PI() / 24.0;
|
|
let k = 1.0;
|
|
if p.shape == "natural" {
|
|
// a dent at the back (+y) and a slight wobble, never past the box
|
|
let dt = (t - PI() / 2.0) / 0.6;
|
|
k = (1.0 - 0.3 * (-dt * dt).exp()) * (0.965 + 0.035 * (3.0 * t + 1.0).sin());
|
|
}
|
|
pts.push([a * k * t.cos(), b * k * t.sin()]);
|
|
}
|
|
pts
|
|
}
|
|
|
|
fn prefab_pond(o) {
|
|
let p = o.p;
|
|
let a = p.w / 2.0;
|
|
let b = p.d / 2.0;
|
|
let rim = if p.edge == "stone" { fmin(0.35, fmin(a, b) * 0.18) } else { fmin(0.2, fmin(a, b) * 0.1) };
|
|
layer("ground");
|
|
let s = sprite();
|
|
let outer = pond_outline(p, 0.0);
|
|
let inner = pond_outline(p, rim);
|
|
if p.edge == "stone" {
|
|
s.flat([outer], 0.03, "capst t");
|
|
// joints across the stone rim
|
|
let segs = [];
|
|
let step = if outer.len() > 30 { 2 } else { 1 };
|
|
for j in range(0, outer.len(), step) { segs.push([outer[j][0], outer[j][1], inner[j][0], inner[j][1]]); }
|
|
s.lines(segs, 0.031, "seam");
|
|
} else {
|
|
s.flat([outer], 0.02, "pondbank t");
|
|
}
|
|
s.flat([inner], 0.032, "pondw t");
|
|
s.flat([pond_outline(p, rim + fmin(a, b) * 0.28)], 0.033, "ponddp t");
|
|
// the water the fish keep to: an ellipse well inside, off the natural shape's dent
|
|
let wa = (a - rim) * 0.72;
|
|
let wb = (b - rim) * 0.72;
|
|
let oy = if p.shape == "natural" { -0.12 * b } else { 0.0 };
|
|
if p.shape == "natural" { wb *= 0.82; }
|
|
let n = p.fish;
|
|
for i in 0..n {
|
|
let f = 0.45 + 0.55 * ((i * 37 + 11) % 10) / 10.0;
|
|
let rx = wa * f;
|
|
let ry = wb * f;
|
|
let cx = 0.0;
|
|
// long ponds: smaller rounds spread along the long side
|
|
if rx > ry * 1.6 { cx = (((i * 53) % 9) / 8.0 - 0.5) * (rx - ry * 1.6) * 2.0; rx = ry * 1.6; }
|
|
if ry > rx * 1.6 { ry = rx * 1.6; }
|
|
let len = fmin(0.9, fmax(0.3, fmin(a, b) * 0.24)) * (0.85 + 0.1 * (i % 3));
|
|
let dur = fmax(8.0, 2.0 * PI() * (rx + ry) / 2.0 / 0.32);
|
|
s.swim(cx, oy, 0.036, [#{ rx: rx, ry: ry, dur: dur, delay: (i * 7.3) % dur, cw: i % 3 == 1, len: len,
|
|
cls: ["koi", "koiw", "koig"][i % 3] }]);
|
|
}
|
|
// lily pads over the fish, some in flower; rings spreading where a fish came up
|
|
for i in 0..p.lilies {
|
|
let t = i * 2.4 + 0.7;
|
|
let q = 0.35 + 0.4 * ((i * 29 + 3) % 10) / 10.0;
|
|
let x = wa * q * t.cos();
|
|
let y = oy + wb * q * t.sin();
|
|
s.disc(x, y, 0.04, fmin(0.4, fmax(0.18, fmin(a, b) * 0.1)), "lily t");
|
|
if i % 2 == 0 { s.disc(x + 0.08, y + 0.05, 0.05, 0.07, "lilyf t"); }
|
|
}
|
|
if n > 0 {
|
|
for i in 0..fmin(3, n).to_int() {
|
|
let t = i * 2.1 + 1.9;
|
|
s.begin("a rip", `animation-delay:-${i * 1.7}s`);
|
|
s.disc(wa * 0.5 * t.cos(), oy + wb * 0.5 * t.sin(), 0.04, fmin(0.6, fmin(a, b) * 0.15), "ripple");
|
|
s.end();
|
|
}
|
|
}
|
|
// reeds in clumps on the back edge, upright, so with the other sprites
|
|
if p.reeds {
|
|
layer("main");
|
|
for t in [1.15, 1.95, 2.55] {
|
|
let q = pond_outline(p, rim * 0.5 + 0.3);
|
|
let j = ((t / (2.0 * PI())) * q.len()).round().to_int() % q.len();
|
|
let c = q[j];
|
|
// tapered blades leaning out from the clump, cattail heads on some
|
|
let blades = [];
|
|
let heads = [];
|
|
for k in 0..7 {
|
|
let x = c[0] + ((k * 37) % 7 - 3) * 0.04;
|
|
let y = c[1] + ((k * 53) % 5 - 2) * 0.04;
|
|
let h = 0.75 + ((k * 31) % 6) * 0.09;
|
|
let lx = ((k * 17) % 7 - 3) * 0.06;
|
|
let ly = ((k * 23) % 5 - 2) * 0.06;
|
|
blades.push([[x - 0.06, y, 0.03], [x + 0.06, y, 0.03], [x + lx, y + ly, h]]);
|
|
blades.push([[x, y - 0.06, 0.03], [x, y + 0.06, 0.03], [x + lx, y + ly, h]]);
|
|
if k % 3 == 0 {
|
|
let f = 0.62;
|
|
let hx = x + lx * f;
|
|
let hy = y + ly * f;
|
|
heads.push([[hx - 0.035, hy, h * f], [hx + 0.035, hy, h * f], [hx + 0.035 + lx * 0.18, hy + ly * 0.18, h * (f + 0.18)], [hx - 0.035 + lx * 0.18, hy + ly * 0.18, h * (f + 0.18)]]);
|
|
}
|
|
}
|
|
sprite().poly(blades, "reed t").poly(heads, "cattail t");
|
|
}
|
|
}
|
|
}
|