Multi-select, select joined walls and fences, pond with swimming fish
Deploy web Studio / deploy (push) Successful in 1m27s
Deploy web Studio / deploy (push) Successful in 1m27s
Selection: S.sel stays the object picked last; S.selIds holds everything selected. - In the object list, Ctrl+click adds or removes an object and Shift+click selects a range. - On the map, Ctrl/Shift+click toggles an object and Shift+drag draws a box that selects what lies wholly inside it (locked and hidden objects are left out). - Ctrl+A selects everything that is not locked; Esc selects nothing. - Clicking a wall or fence a second time selects every piece of the same type joined to it; another click goes back to that one piece. - Dragging one selected object moves them all, together with the windows and doors on them, as one undo step. Arrow keys, R (turns the group about its middle), Ctrl+D, Delete, Lock and Hide all work on the whole selection. - When all selected objects share a type, the property form edits them all; fields where they differ show "mixed". - The agent's system prompt lists the selected objects. The viewer outlines several objects with selectObj([ids]) and passes the click event to onPick. A batch op now answers `ids`, one per operation, so the copies from a group duplicate get selected. Pond (Nature): natural, round or square, with a stone or grass edge, fish, lily pads, spreading ripples and reed clumps. The fish come from a new Swim primitive (script: s.swim). One matrix maps the object's frame to the screen. Each fish turns round its ellipse with a CSS rotation, so off-screen pausing and reduced motion apply as for the other animations. The house demo gets a koi pond in the back garden. Tests: ponds in every shape and rotation have the right number of fish, no NaN and no depth-sort cycles, and stay inside their rectangle. check_studio covers list and map multi-select, a group drag undone in one step, joined-fence selection, a mixed-value form applied to all, box select, delete and undo, Ctrl+A, Esc, and a new pond's fish.
This commit is contained in:
1 parent
e4017ce2a8
commit
19f4c83ae8
21 files changed
+759
-59
No files matched your search
@@ -28,6 +28,17 @@ pub fn r1s(v: f64) -> String {
|
||||
s
|
||||
}
|
||||
|
||||
/// `v` rounded to 0.001, without trailing zeros or `-0` (`NaN` when not finite):
|
||||
/// for small sizes in a local frame (fish, scaled groups).
|
||||
pub fn r3s(v: f64) -> String {
|
||||
if !v.is_finite() {
|
||||
return "NaN".into();
|
||||
}
|
||||
let t = (v * 1000.0).round() / 1000.0;
|
||||
let s = format!("{:.3}", if t == 0.0 { 0.0 } else { t });
|
||||
s.trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
}
|
||||
|
||||
/// Round to 0.1 as a number.
|
||||
pub fn round1(v: f64) -> f64 {
|
||||
(v * 10.0 + 0.5).floor() / 10.0
|
||||
|
||||
@@ -23,7 +23,7 @@ pub mod scene;
|
||||
pub mod sort;
|
||||
pub mod sprite;
|
||||
|
||||
pub use prim::{Prim, Style, Zs, P2, P3};
|
||||
pub use prim::{Fish, Prim, Style, Zs, P2, P3};
|
||||
pub use proj::{shade, Proj, C, CZ};
|
||||
pub use render::{render, RenderOpts, Rendered};
|
||||
pub use scene::{Info, Layer, LayerOpts, Scene, Tag};
|
||||
|
||||
@@ -179,6 +179,15 @@ pub enum Prim {
|
||||
},
|
||||
/// Spinning fan lying in the plane at height at.z (CSS animation).
|
||||
Fan { at: P3, r: f64 },
|
||||
/// Fish swimming round ellipses on the plane at height z (CSS
|
||||
/// animation): centre c, the ellipses' x axis along u (a world unit
|
||||
/// vector, the object's local x).
|
||||
Swim {
|
||||
c: P2,
|
||||
z: f64,
|
||||
u: P2,
|
||||
fish: Vec<Fish>,
|
||||
},
|
||||
/// Raw SVG translated to a world point (or untranslated without one).
|
||||
/// `{uid}` in the text is replaced by the view's unique id.
|
||||
Raw { at: Option<P3>, svg: String },
|
||||
@@ -191,3 +200,17 @@ pub enum Prim {
|
||||
/// Items drawn back to front by the depth of their plan anchor.
|
||||
Seq { items: Vec<(P2, Vec<Prim>)> },
|
||||
}
|
||||
|
||||
/// One fish of a [`Prim::Swim`]: it goes round an ellipse of radii (rx, ry)
|
||||
/// once every `dur` seconds, `delay` seconds into its round at the start,
|
||||
/// counter-clockwise (seen from above) unless `cw`; `len` is its length.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Fish {
|
||||
pub rx: f64,
|
||||
pub ry: f64,
|
||||
pub dur: f64,
|
||||
pub delay: f64,
|
||||
pub cw: bool,
|
||||
pub len: f64,
|
||||
pub cls: String,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Projection of a scene to SVG for one view rotation.
|
||||
|
||||
use crate::fmt::{esc, hash, r1, r1s, round1};
|
||||
use crate::fmt::{esc, hash, r1, r1s, r3s, round1};
|
||||
use crate::prim::{Prim, Style, P2};
|
||||
use crate::proj::{shade, Proj, C};
|
||||
use crate::scene::{Info, Scene};
|
||||
@@ -279,6 +279,56 @@ impl Ctx<'_> {
|
||||
self.out.push_str("\"/>");
|
||||
}
|
||||
|
||||
/// Fish in the plane at height z: one matrix maps the object's frame
|
||||
/// (metres, x along u) to the screen; each fish sits at (1, 0) of a
|
||||
/// unit circle that a CSS rotation turns and a scale stretches to its
|
||||
/// ellipse. An invisible diagonal centres the turning group's box on
|
||||
/// the circle's middle (`transform-box: fill-box`, as for the fan).
|
||||
fn swim(&mut self, c: P2, z: f64, u: P2, fish: &[crate::prim::Fish]) {
|
||||
if fish.is_empty() {
|
||||
return;
|
||||
}
|
||||
let p = self.p;
|
||||
let o = p.s(c[0], c[1], z);
|
||||
let a = p.s(c[0] + u[0], c[1] + u[1], z);
|
||||
let b = p.s(c[0] - u[1], c[1] + u[0], z);
|
||||
let _ = write!(
|
||||
self.out,
|
||||
"<g class=\"iso-school\" transform=\"matrix({} {} {} {} {} {})\">",
|
||||
r3s(a[0] - o[0]),
|
||||
r3s(a[1] - o[1]),
|
||||
r3s(b[0] - o[0]),
|
||||
r3s(b[1] - o[1]),
|
||||
r1s(o[0]),
|
||||
r1s(o[1])
|
||||
);
|
||||
for f in fish {
|
||||
let (rx, ry, l) = (f.rx.max(0.05), f.ry.max(0.05), f.len.max(0.05));
|
||||
let e = 1.0 + l / rx.min(ry) + 0.05;
|
||||
// a fish nose first along +y (counter-clockwise) or -y
|
||||
let k = if f.cw { -l } else { l };
|
||||
// body widest behind the head, a narrow tail root, a forked tail fin
|
||||
let (b, t) = (body_path(k, l), tail_path(k, l));
|
||||
let _ = write!(
|
||||
self.out,
|
||||
"<g transform=\"scale({} {})\"><g class=\"iso-a iso-swim\" style=\"animation-duration:{}s;animation-delay:-{}s{}\"><path class=\"iso-swimbox\" d=\"M{} {}L{} {}\"/><g transform=\"translate(1 0) scale({} {})\"><path class=\"iso-fish iso-{}\" d=\"{b}{t}\"/></g></g></g>",
|
||||
r3s(rx),
|
||||
r3s(ry),
|
||||
r3s(f.dur.max(1.0)),
|
||||
r3s(f.delay.max(0.0)),
|
||||
if f.cw { ";animation-direction:reverse" } else { "" },
|
||||
r3s(-e),
|
||||
r3s(-e),
|
||||
r3s(e),
|
||||
r3s(e),
|
||||
r3s(1.0 / rx),
|
||||
r3s(1.0 / ry),
|
||||
esc(&f.cls),
|
||||
);
|
||||
}
|
||||
self.out.push_str("</g>");
|
||||
}
|
||||
|
||||
fn disc(&mut self, c: P2, z: f64, r: f64, cls: &str, id: Option<&str>) {
|
||||
let s = self.p.s(c[0], c[1], z);
|
||||
let rx = r * C * SQ2;
|
||||
@@ -461,6 +511,7 @@ impl Ctx<'_> {
|
||||
-a
|
||||
);
|
||||
}
|
||||
Prim::Swim { c, z, u, fish } => self.swim(*c, *z, *u, fish),
|
||||
Prim::Raw { at, svg } => {
|
||||
let svg = svg.replace("{uid}", self.uid);
|
||||
match at {
|
||||
@@ -696,6 +747,40 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
|
||||
}
|
||||
}
|
||||
|
||||
/// A fish body nose first along +y (k = length, negative: along -y), l wide.
|
||||
fn body_path(k: f64, l: f64) -> String {
|
||||
format!(
|
||||
"M0 {}C{} {} {} {} {} {}L{} {}C{} {} {} {} 0 {}Z",
|
||||
r3s(0.5 * k),
|
||||
r3s(0.24 * l),
|
||||
r3s(0.38 * k),
|
||||
r3s(0.24 * l),
|
||||
r3s(-0.1 * k),
|
||||
r3s(0.05 * l),
|
||||
r3s(-0.28 * k),
|
||||
r3s(-0.05 * l),
|
||||
r3s(-0.28 * k),
|
||||
r3s(-0.24 * l),
|
||||
r3s(-0.1 * k),
|
||||
r3s(-0.24 * l),
|
||||
r3s(0.38 * k),
|
||||
r3s(0.5 * k)
|
||||
)
|
||||
}
|
||||
|
||||
/// The forked tail fin behind [`body_path`].
|
||||
fn tail_path(k: f64, l: f64) -> String {
|
||||
format!(
|
||||
"M0 {}L{} {}L0 {}L{} {}Z",
|
||||
r3s(-0.24 * k),
|
||||
r3s(0.2 * l),
|
||||
r3s(-0.54 * k),
|
||||
r3s(-0.42 * k),
|
||||
r3s(-0.2 * l),
|
||||
r3s(-0.54 * k)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! an object placed at (x, y, z) and turned in 90 degree steps draws around
|
||||
//! its own origin, and the sprite stores world coordinates.
|
||||
|
||||
use crate::prim::{Prim, Style, Zs, P2, P3};
|
||||
use crate::prim::{Fish, Prim, Style, Zs, P2, P3};
|
||||
use crate::proj::C;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -476,6 +476,18 @@ impl Sprite {
|
||||
self.push(Prim::Fan { at, r })
|
||||
}
|
||||
|
||||
/// Fish swimming round ellipses about (cx, cy) on the plane at height z;
|
||||
/// the ellipses follow the frame's axes.
|
||||
pub fn swim(&mut self, cx: f64, cy: f64, z: f64, fish: Vec<Fish>) -> &mut Self {
|
||||
let rx = fish.iter().map(|f| f.rx + f.len).fold(0.0, f64::max);
|
||||
let ry = fish.iter().map(|f| f.ry + f.len).fold(0.0, f64::max);
|
||||
self.ext_box(cx - rx, cy - ry, cx + rx, cy + ry, z, z);
|
||||
let c = self.xf.p(cx, cy);
|
||||
let z = self.xf.z(z);
|
||||
let u = self.xf.v(1.0, 0.0);
|
||||
self.push(Prim::Swim { c, z, u, fish })
|
||||
}
|
||||
|
||||
pub fn raw(&mut self, at: Option<P3>, svg: impl Into<String>) -> &mut Self {
|
||||
let at = at.map(|a| self.xf.p3(a));
|
||||
self.push(Prim::Raw {
|
||||
|
||||
@@ -23,7 +23,8 @@ pub fn meta<S: Store>(ws: &Workspace<S>) -> Value {
|
||||
}
|
||||
|
||||
/// One operation, or `{ "ops": [...] }` as one undo step. Answers the new
|
||||
/// state with `ok` and the id of the object made or changed, or `error`.
|
||||
/// state with `ok`, the id of the object made or changed (the last one) and
|
||||
/// `ids` (one per operation that made or changed an object), or `error`.
|
||||
pub fn op<S: Store>(ws: &mut Workspace<S>, body: &Value) -> Value {
|
||||
let r = if let Some(list) = body.get("ops").and_then(|v| v.as_array()) {
|
||||
let ops: Result<Vec<Op>, String> = list
|
||||
@@ -31,18 +32,19 @@ pub fn op<S: Store>(ws: &mut Workspace<S>, body: &Value) -> Value {
|
||||
.map(|v| serde_json::from_value::<Op>(v.clone()).map_err(|e| e.to_string()))
|
||||
.collect();
|
||||
ops.and_then(|l| ws.apply_all(l))
|
||||
.map(|o| o.last().and_then(|x| x.id.clone()))
|
||||
.map(|o| o.into_iter().filter_map(|x| x.id).collect::<Vec<_>>())
|
||||
} else {
|
||||
serde_json::from_value::<Op>(body.clone())
|
||||
.map_err(|e| format!("bad operation: {e}"))
|
||||
.and_then(|op| ws.apply(op))
|
||||
.map(|o| o.id)
|
||||
.map(|o| o.id.into_iter().collect())
|
||||
};
|
||||
match r {
|
||||
Ok(id) => {
|
||||
Ok(ids) => {
|
||||
let mut s = state(ws);
|
||||
s["ok"] = json!(true);
|
||||
s["id"] = json!(id);
|
||||
s["id"] = json!(ids.last());
|
||||
s["ids"] = json!(ids);
|
||||
s
|
||||
}
|
||||
Err(e) => json!({ "error": e }),
|
||||
|
||||
@@ -432,6 +432,32 @@ pub fn register_sprite(e: &mut Engine) {
|
||||
ok(s)
|
||||
},
|
||||
);
|
||||
// fish round ellipses: list of #{ rx, ry, dur, delay, cw, len, cls }
|
||||
e.register_fn(
|
||||
"swim",
|
||||
|s: &mut SpriteH, x: D, y: D, z: D, list: Array| -> RR<SpriteH> {
|
||||
let mut fish = Vec::with_capacity(list.len());
|
||||
for f in &list {
|
||||
let Some(m) = f.read_lock::<Map>() else {
|
||||
return err("swim: expected a list of maps");
|
||||
};
|
||||
fish.push(isokit_core::Fish {
|
||||
rx: fnum(&m, "rx"),
|
||||
ry: fnum(&m, "ry"),
|
||||
dur: fnum(&m, "dur"),
|
||||
delay: fnum(&m, "delay"),
|
||||
cw: m.get("cw").and_then(|v| v.as_bool().ok()).unwrap_or(false),
|
||||
len: m.get("len").and_then(|v| n(v).ok()).unwrap_or(0.4),
|
||||
cls: m
|
||||
.get("cls")
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| "koi".into()),
|
||||
});
|
||||
}
|
||||
s.0.borrow_mut().swim(n(&x)?, n(&y)?, n(&z)?, fish);
|
||||
ok(s)
|
||||
},
|
||||
);
|
||||
e.register_fn("raw", |s: &mut SpriteH, svg: &str| -> RR<SpriteH> {
|
||||
s.0.borrow_mut().raw(None, svg);
|
||||
ok(s)
|
||||
|
||||
@@ -485,6 +485,63 @@ fn fences_join_like_walls_in_every_design() {
|
||||
.contains("iso-seam"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ponds_have_fish_swimming_in_every_shape() {
|
||||
let r = Runtime::new(&Bundle::default()).unwrap();
|
||||
for (shape, edge, rot) in [
|
||||
("natural", "stone", 0.0),
|
||||
("round", "grass", 90.0),
|
||||
("square", "stone", 0.0),
|
||||
] {
|
||||
let mut doc = Doc::default();
|
||||
let mut o = obj(
|
||||
"pond",
|
||||
"pond",
|
||||
3.0,
|
||||
-2.0,
|
||||
json!({"w": 9, "d": 5, "shape": shape, "edge": edge, "fish": 6, "lilies": 4}),
|
||||
);
|
||||
o.rot = rot;
|
||||
doc.objects.push(o);
|
||||
for view in 0..4u8 {
|
||||
let b = r.build("site", view, &cfg(), &doc).unwrap();
|
||||
assert!(
|
||||
b.scene.warnings.is_empty(),
|
||||
"{shape}: {:?}",
|
||||
b.scene.warnings
|
||||
);
|
||||
let out = render(&b.scene, view, &RenderOpts::default());
|
||||
assert_eq!(
|
||||
out.html.matches("iso-a iso-swim").count(),
|
||||
6,
|
||||
"{shape}/{view}"
|
||||
);
|
||||
assert!(!out.html.contains("NaN"), "{shape}/{view}");
|
||||
assert!(out.html.contains("iso-fish iso-koi") && out.html.contains("iso-lily"));
|
||||
assert!(out.html.contains("iso-a iso-rip"));
|
||||
assert_eq!(out.cycles, 0);
|
||||
}
|
||||
// the pond stays inside its 9 x 5 m (turned: 5 x 9 m) rectangle; the reeds stand on its edge
|
||||
let bx = r.build("site", 0, &cfg(), &doc).unwrap().boxes["pond"];
|
||||
let (hw, hd) = if rot == 0.0 { (4.5, 2.5) } else { (2.5, 4.5) };
|
||||
assert!(
|
||||
bx[0] >= 3.0 - hw - 1e-6
|
||||
&& bx[3] <= 3.0 + hw + 1e-6
|
||||
&& bx[1] >= -2.0 - hd - 1e-6
|
||||
&& bx[4] <= -2.0 + hd + 1e-6,
|
||||
"{shape}: {bx:?}"
|
||||
);
|
||||
}
|
||||
// no fish: no school
|
||||
let mut doc = Doc::default();
|
||||
doc.objects
|
||||
.push(obj("p", "pond", 0.0, 0.0, json!({"fish": 0})));
|
||||
let b = r.build("site", 0, &cfg(), &doc).unwrap();
|
||||
assert!(!render(&b.scene, 0, &RenderOpts::default())
|
||||
.html
|
||||
.contains("iso-swim"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn site_walls_join_at_corners() {
|
||||
let r = Runtime::new(&Bundle::default()).unwrap();
|
||||
|
||||
@@ -49,3 +49,137 @@ fn prefab_hedge(o) {
|
||||
let p = o.p;
|
||||
sprite().box(-p.w / 2.0, -p.d / 2.0, p.w / 2.0, p.d / 2.0, 0.0, p.h, "hedge");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,6 +251,17 @@ lshade = "#d9c9a0"
|
||||
roofr = "#8a4a3a"
|
||||
cobble = "#6c675f"
|
||||
capst = "#8c877e"
|
||||
pondw = "#24607a"
|
||||
ponddp = "#1a4c63"
|
||||
pondbank = "#4a3f2f"
|
||||
lily = "#3f7a3a"
|
||||
lilyf = "#e7a1c0"
|
||||
reed = "#4f6e30"
|
||||
cattail = "#5a3e2a"
|
||||
koi = "#e8742a"
|
||||
koiw = "#e9e4da"
|
||||
koig = "#d9a63a"
|
||||
ripple = "#8cc3d8"
|
||||
|
||||
[palette.light]
|
||||
sky1 = "#a9d4fb"
|
||||
@@ -497,6 +508,17 @@ lshade = "#f2e6c8"
|
||||
roofr = "#b8613f"
|
||||
cobble = "#aaa195"
|
||||
capst = "#d8d2c6"
|
||||
pondw = "#5aa7c4"
|
||||
ponddp = "#4791b0"
|
||||
pondbank = "#8d7a5c"
|
||||
lily = "#6fae5a"
|
||||
lilyf = "#f4b6d2"
|
||||
reed = "#5e8a3a"
|
||||
cattail = "#7a5536"
|
||||
koi = "#f08a3c"
|
||||
koiw = "#fbf8f2"
|
||||
koig = "#efc35a"
|
||||
ripple = "#d6f1fa"
|
||||
|
||||
[materials]
|
||||
warn = "#e0412f"
|
||||
@@ -723,6 +745,13 @@ lshade = "lshade"
|
||||
roofr = "roofr"
|
||||
cobble = "cobble"
|
||||
capst = "capst"
|
||||
pondw = "pondw"
|
||||
ponddp = "ponddp"
|
||||
pondbank = "pondbank"
|
||||
lily = "lily"
|
||||
lilyf = "lilyf"
|
||||
reed = "reed"
|
||||
cattail = "cattail"
|
||||
doorw = "furnw"
|
||||
doorg = "door"
|
||||
f-oak = "oak"
|
||||
@@ -748,6 +777,14 @@ f-conc = "conc"
|
||||
".iso-svg .iso-glassw" = "--b: var(--iso-glassw); fill-opacity: var(--iso-glasswo)"
|
||||
".iso-svg path.iso-glassw.iso-t" = "fill-opacity: .5"
|
||||
".iso-svg .iso-win" = "fill: var(--iso-win); stroke: none"
|
||||
".iso-svg .iso-school" = "pointer-events: none"
|
||||
".iso-svg .iso-swimbox" = "fill: none; stroke: none"
|
||||
".iso-svg .iso-fish" = "stroke: none; fill-opacity: .92"
|
||||
".iso-svg .iso-koi" = "fill: var(--iso-koi)"
|
||||
".iso-svg .iso-koiw" = "fill: var(--iso-koiw)"
|
||||
".iso-svg .iso-koig" = "fill: var(--iso-koig)"
|
||||
".iso-svg .iso-ripple" = "fill: none; stroke: var(--iso-ripple); stroke-width: .5"
|
||||
".iso-svg path.iso-reed, .iso-svg path.iso-cattail" = "stroke: none"
|
||||
".iso-svg .iso-win.iso-lit" = "fill: var(--iso-winlit)"
|
||||
".iso-svg .iso-glass" = "fill: var(--iso-glass); fill-opacity: var(--iso-glasso); stroke: none"
|
||||
".iso-svg .iso-door" = "fill: var(--iso-door); stroke: none"
|
||||
|
||||
Reference in new issue
Block a user