Files
photocraft/crates/algo/src/render2.rs
a41800c466 fix: resolve edge-case crashes, unbounded allocations, and UI sync defects (#1010)
* fix: resolve edge-case crashes, unbounded allocations, and UI sync defects (#915, #916, #917, #918, #920, #921, #923, #924, #925, #926)

* Review fixups: drop hunks covered elsewhere, simplify shortcut conflicts

- Drop the brush preview spacing_enabled hunk (#941), the vanishing.rs
  overflow guards (#869) and the artboard_cmds.rs Custom background change
  (conflicts with #1032); ui.window.open already landed in #1133.
- Revert the Measurement Log export change: an empty selection keeps
  exporting the whole log instead of an empty CSV.
- Drop the reach().min(1024) cap: effects still sample past a clamped reach,
  which would give tile seams.
- Shortcut dialog: one pass, newly assigned shortcuts first, taking theirs
  from any holder (overrides included); unchanged overrides only clear
  defaults. More test cases (no-op OK, giving a shortcut back).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:09:38 -04:00

1536 lines
66 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Procedural renderers of Filter › Render: Flame, Picture Frame and Tree.
//!
//! Each renderer turns its options into a list of anti-aliased vector
//! primitives ([`Prim`]: tapered capsules and shaded ellipses, blended
//! normally or additively). [`composite`] rasterizes the list over a surface
//! tile by tile (in parallel on native targets), in straight sRGB converted to
//! and from the surface's own colour model and depth, mixed by the selection.
//!
//! The shapes are our own designs (L-system-like recursive branching for
//! trees, turbulent flame strands, ornaments walked along the frame edge),
//! built from observation of what the options do, not from any reference
//! implementation.
use photocraft_geom::Rect;
use photocraft_raster::{Surface, from_rgba_into, to_rgba};
use serde::{Deserialize, Serialize};
// ---------------------------------------------------------------------------
// Primitives and compositing
// ---------------------------------------------------------------------------
/// Geometry of a primitive (document pixel coordinates).
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Shape {
/// A segment from `a` to `b` whose radius goes from `ra` to `rb`; colour goes `c0` → `c1` along it.
Capsule { a: (f32, f32), b: (f32, f32), ra: f32, rb: f32 },
/// An ellipse rotated by `angle` radians; colour goes `c0` (centre) → `c1` (rim).
Ellipse { c: (f32, f32), rx: f32, ry: f32, angle: f32 },
}
/// One drawable primitive. Colours are straight sRGB RGBA (0–1).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Prim {
pub shape: Shape,
pub c0: [f32; 4],
pub c1: [f32; 4],
/// Edge softness in pixels (≥ 1 for anti-aliasing; larger for glows).
pub soft: f32,
pub blend: Blend,
}
/// How a primitive combines with what is under it.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Blend {
/// Source over.
#[default]
Normal,
/// Additive light (glows).
Add,
/// Per-channel maximum of the premultiplied values (overlapping strands don't build up).
Lighten,
}
impl Prim {
pub fn capsule(a: (f32, f32), b: (f32, f32), ra: f32, rb: f32, c0: [f32; 4], c1: [f32; 4]) -> Prim {
Prim { shape: Shape::Capsule { a, b, ra, rb }, c0, c1, soft: 1.0, blend: Blend::Normal }
}
pub fn ellipse(c: (f32, f32), rx: f32, ry: f32, angle: f32, c0: [f32; 4], c1: [f32; 4]) -> Prim {
Prim { shape: Shape::Ellipse { c, rx, ry, angle }, c0, c1, soft: 1.0, blend: Blend::Normal }
}
pub fn disc(c: (f32, f32), r: f32, col: [f32; 4]) -> Prim {
Prim::ellipse(c, r, r, 0.0, col, col)
}
fn with_soft(mut self, s: f32) -> Prim {
self.soft = s;
self
}
fn additive(mut self) -> Prim {
self.blend = Blend::Add;
self
}
fn lighten(mut self) -> Prim {
self.blend = Blend::Lighten;
self
}
/// Pixel bounds this primitive can touch.
pub fn bounds(&self) -> Rect {
let s = self.soft.max(1.0) + 1.0;
let (x0, y0, x1, y1) = match self.shape {
Shape::Capsule { a, b, ra, rb } => {
let r = ra.max(rb) + s;
(a.0.min(b.0) - r, a.1.min(b.1) - r, a.0.max(b.0) + r, a.1.max(b.1) + r)
}
Shape::Ellipse { c, rx, ry, angle } => {
let (sn, cs) = angle.sin_cos();
let ex = ((rx * cs).powi(2) + (ry * sn).powi(2)).sqrt() + s;
let ey = ((rx * sn).powi(2) + (ry * cs).powi(2)).sqrt() + s;
(c.0 - ex, c.1 - ey, c.0 + ex, c.1 + ey)
}
};
if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
return Rect::EMPTY;
}
let cl = |v: f32| v.clamp(-1.0e8, 1.0e8) as i32;
Rect::new(cl(x0.floor()), cl(y0.floor()), cl(x1.ceil()), cl(y1.ceil()))
}
/// Coverage (0–1) and colour position (0–1) at a pixel centre.
#[inline]
fn eval(&self, x: f32, y: f32) -> (f32, f32) {
let soft = self.soft.max(1.0);
match self.shape {
Shape::Capsule { a, b, ra, rb } => {
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let l2 = dx * dx + dy * dy;
let t = if l2 > 1e-9 { (((x - a.0) * dx + (y - a.1) * dy) / l2).clamp(0.0, 1.0) } else { 0.0 };
let (px, py) = (a.0 + dx * t - x, a.1 + dy * t - y);
let d = (px * px + py * py).sqrt();
let r = ra + (rb - ra) * t;
// Hairlines thinner than a pixel fade instead of vanishing.
let thin = (2.0 * r).clamp(0.0, 1.0);
(((r.max(0.5) - d) / soft + 0.5).clamp(0.0, 1.0) * thin, t)
}
Shape::Ellipse { c, rx, ry, angle } => {
let (s, co) = angle.sin_cos();
let (dx, dy) = (x - c.0, y - c.1);
let (u, v) = (dx * co + dy * s, -dx * s + dy * co);
let (rx, ry) = (rx.max(0.05), ry.max(0.05));
let e = ((u / rx).powi(2) + (v / ry).powi(2)).sqrt();
let edge = (1.0 - e) * rx.min(ry);
let thin = (2.0 * rx.min(ry)).clamp(0.0, 1.0);
((edge / soft + 0.5).clamp(0.0, 1.0) * thin, e.min(1.0))
}
}
}
}
#[inline]
fn lerp4(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] {
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t, a[3] + (b[3] - a[3]) * t]
}
/// Rasterizes `prims` (in order) into a premultiplied RGBA buffer covering `t`.
fn raster_tile(prims: &[Prim], idx: &[u32], t: Rect) -> Vec<[f32; 4]> {
let w = t.width() as usize;
let mut buf = vec![[0.0f32; 4]; w * t.height() as usize];
for &k in idx {
let p = &prims[k as usize];
let r = p.bounds().intersect(&t);
if r.is_empty() {
continue;
}
for y in r.y0..r.y1 {
let row = (y - t.y0) as usize * w;
for x in r.x0..r.x1 {
let (cov, ct) = p.eval(x as f32 + 0.5, y as f32 + 0.5);
if cov <= 0.0 {
continue;
}
let c = if p.c0 == p.c1 { p.c0 } else { lerp4(p.c0, p.c1, ct) };
let a = (c[3] * cov).clamp(0.0, 1.0);
if a <= 0.0 {
continue;
}
let d = &mut buf[row + (x - t.x0) as usize];
match p.blend {
Blend::Add => {
d[0] += c[0] * a;
d[1] += c[1] * a;
d[2] += c[2] * a;
d[3] += a * (1.0 - d[3]);
}
Blend::Lighten => {
d[0] = d[0].max(c[0] * a);
d[1] = d[1].max(c[1] * a);
d[2] = d[2].max(c[2] * a);
d[3] = d[3].max(a);
}
Blend::Normal => {
let k = 1.0 - a;
d[0] = c[0] * a + d[0] * k;
d[1] = c[1] * a + d[1] * k;
d[2] = c[2] * a + d[2] * k;
d[3] = a + d[3] * k;
}
}
}
}
}
buf
}
/// Size of the tiles [`composite`] works in.
const CTILE: i32 = 128;
/// Draws `prims` over `surface` inside `clip`, mixed by the selection coverage
/// (channel 0 of `selection`). Returns the bounds of what was drawn.
pub fn composite(surface: &mut Surface, prims: &[Prim], clip: Rect, selection: Option<&Surface>) -> Rect {
let mut used = Rect::EMPTY;
let bounds: Vec<Rect> = prims.iter().map(|p| p.bounds().intersect(&clip)).collect();
for b in &bounds {
if !b.is_empty() {
used = if used.is_empty() { *b } else { used.union(b) };
}
}
if used.is_empty() {
return used;
}
// Bin primitives into tiles (order preserved within a tile).
let tx0 = used.x0.div_euclid(CTILE);
let ty0 = used.y0.div_euclid(CTILE);
let tx1 = (used.x1 - 1).div_euclid(CTILE);
let ty1 = (used.y1 - 1).div_euclid(CTILE);
let (nx, ny) = ((tx1 - tx0 + 1) as usize, (ty1 - ty0 + 1) as usize);
let mut bins: Vec<Vec<u32>> = vec![Vec::new(); nx * ny];
for (k, b) in bounds.iter().enumerate() {
if b.is_empty() {
continue;
}
for ty in b.y0.div_euclid(CTILE)..=(b.y1 - 1).div_euclid(CTILE) {
for tx in b.x0.div_euclid(CTILE)..=(b.x1 - 1).div_euclid(CTILE) {
bins[(ty - ty0) as usize * nx + (tx - tx0) as usize].push(k as u32);
}
}
}
let fmt = surface.format();
let n = fmt.channels();
let jobs: Vec<(Rect, &Vec<u32>)> = bins
.iter()
.enumerate()
.filter(|(_, b)| !b.is_empty())
.map(|(i, b)| {
let (tx, ty) = (tx0 + (i % nx) as i32, ty0 + (i / nx) as i32);
(Rect::new(tx * CTILE, ty * CTILE, (tx + 1) * CTILE, (ty + 1) * CTILE).intersect(&clip), b)
})
.filter(|(r, _)| !r.is_empty())
.collect();
let src: &Surface = surface;
let run = |(t, idx): &(Rect, &Vec<u32>)| -> (Rect, Vec<f32>) {
let paint = raster_tile(prims, idx, *t);
let mut data = src.read_region(*t);
let sel = selection.map(|s| s.read_region(*t));
let sn = selection.map_or(1, Surface::channels);
let mut tmp = [0.0f32; 8];
for (i, px) in data.chunks_exact_mut(n).enumerate() {
let s = paint[i];
let k = sel.as_ref().map_or(1.0, |v| v[i * sn].clamp(0.0, 1.0));
let sa = s[3].clamp(0.0, 1.0) * k;
if sa <= 0.0 {
continue;
}
let inv = 1.0 / s[3].max(1e-6);
let sc = [(s[0] * inv).clamp(0.0, 1.0), (s[1] * inv).clamp(0.0, 1.0), (s[2] * inv).clamp(0.0, 1.0)];
let d = to_rgba(&fmt, px);
let da = if fmt.alpha { d[3] } else { 1.0 };
let oa = sa + da * (1.0 - sa);
let mix = |sv: f32, dv: f32| if oa > 1e-7 { (sv * sa + dv * da * (1.0 - sa)) / oa } else { 0.0 };
let o = [mix(sc[0], d[0]), mix(sc[1], d[1]), mix(sc[2], d[2]), oa];
let m = from_rgba_into(&fmt, o, &mut tmp);
px.copy_from_slice(&tmp[..m.min(n)]);
}
(*t, data)
};
#[cfg(not(target_arch = "wasm32"))]
let results: Vec<(Rect, Vec<f32>)> = {
use rayon::prelude::*;
jobs.par_iter().map(run).collect()
};
#[cfg(target_arch = "wasm32")]
let results: Vec<(Rect, Vec<f32>)> = jobs.iter().map(run).collect();
for (t, data) in results {
surface.write_region(t, &data);
}
used
}
// ---------------------------------------------------------------------------
// Randomness
// ---------------------------------------------------------------------------
/// Small deterministic PRNG (SplitMix64).
#[derive(Clone, Debug)]
pub struct Rng(u64);
impl Rng {
pub fn new(seed: u32, salt: u64) -> Rng {
Rng(((seed as u64) << 32) ^ salt.wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ 0x2545_F491_4F6C_DD1D)
}
pub fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
/// Uniform in [0, 1).
pub fn f(&mut self) -> f32 {
(self.next_u64() >> 40) as f32 / (1u64 << 24) as f32
}
/// Uniform in [a, b).
pub fn range(&mut self, a: f32, b: f32) -> f32 {
a + (b - a) * self.f()
}
/// Uniform in [-1, 1).
pub fn sym(&mut self) -> f32 {
self.f() * 2.0 - 1.0
}
}
/// Smooth 1-D value noise in [-1, 1].
fn noise1(x: f32, seed: u64) -> f32 {
let h = |i: i64| -> f32 {
let mut r = Rng((i as u64).wrapping_mul(0xD6E8_FEB8_6659_FD93) ^ seed);
r.sym()
};
let i = x.floor();
let f = x - i;
let i = i as i64;
let t = f * f * (3.0 - 2.0 * f);
h(i) + (h(i + 1) - h(i)) * t
}
/// Two octaves of [`noise1`].
fn fbm1(x: f32, seed: u64) -> f32 {
(noise1(x, seed) + 0.5 * noise1(x * 2.13 + 7.1, seed ^ 0xA5A5)) / 1.5
}
// ---------------------------------------------------------------------------
// Polylines
// ---------------------------------------------------------------------------
/// A polyline parameterized by arc length.
struct Spine {
pts: Vec<(f32, f32)>,
cum: Vec<f32>,
}
impl Spine {
fn new(pts: Vec<(f32, f32)>) -> Spine {
let mut cum = vec![0.0];
for w in pts.windows(2) {
let l = ((w[1].0 - w[0].0).powi(2) + (w[1].1 - w[0].1).powi(2)).sqrt();
cum.push(cum.last().copied().unwrap_or(0.0) + l);
}
Spine { pts, cum }
}
fn len(&self) -> f32 {
self.cum.last().copied().unwrap_or(0.0)
}
/// Point and unit tangent at arc length `s`.
fn at(&self, s: f32) -> ((f32, f32), (f32, f32)) {
let n = self.pts.len();
if n == 0 {
return ((0.0, 0.0), (1.0, 0.0));
}
if n == 1 {
return (self.pts[0], (0.0, -1.0));
}
let s = s.clamp(0.0, self.len());
let k = match self.cum.binary_search_by(|v| v.partial_cmp(&s).unwrap_or(std::cmp::Ordering::Less)) {
Ok(i) => i.min(n - 2),
Err(i) => i.saturating_sub(1).min(n - 2),
};
let (a, b) = (self.pts[k], self.pts[k + 1]);
let seg = (self.cum[k + 1] - self.cum[k]).max(1e-6);
let t = ((s - self.cum[k]) / seg).clamp(0.0, 1.0);
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let l = (dx * dx + dy * dy).sqrt().max(1e-6);
((a.0 + dx * t, a.1 + dy * t), (dx / l, dy / l))
}
}
// ---------------------------------------------------------------------------
// Flame
// ---------------------------------------------------------------------------
/// Flame placement (Photoshop's six flame types).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub enum FlameType {
/// One flame whose spine is the path (path start = flame base).
#[default]
OneFlameAlongPath,
/// Flames at `interval` along the path, rising from its left side.
MultipleFlamesAlongPath,
/// Flames at `interval` along the path, pointing along the path's direction.
MultipleFlamesPathDirections,
/// Like `multipleFlamesAlongPath` with strongly varying lengths.
MultipleFlamesVariousLength,
/// A candle flame at the start of each subpath.
CandleLight,
/// Flames at `interval` along the path, all pointing at `angle`.
MultipleFlamesOneDirection,
}
/// Overall motion of the flame lines.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub enum FlameStyle {
#[default]
Normal,
Violent,
Flat,
}
/// How the flame lines converge towards the tip.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub enum FlameShape {
#[default]
Parallel,
ToCenter,
Spread,
Oval,
Pointed,
}
/// Flame options (Photoshop's Flame dialog, Basic + Advanced).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct FlameSpec {
pub flame_type: FlameType,
/// Flame length in px (1–1000).
pub length: f32,
pub randomize_length: bool,
/// Flame width in px (1–1000).
pub width: f32,
/// Direction for `multipleFlamesOneDirection` (degrees, 0 = up, clockwise).
pub angle: f32,
/// Spacing between flames along the path in px (1–1000).
pub interval: f32,
/// Spread the flames evenly so a closed path loops seamlessly.
pub adjust_interval_for_loops: bool,
/// `Some` = custom flame colour; `None` = natural flame colours.
pub color: Option<[f32; 4]>,
/// 0–100.
pub turbulent: f32,
/// 0–100.
pub jag: f32,
/// 0–100 %.
pub opacity: f32,
/// Flame lines (complexity) 1–100.
pub flame_lines: f32,
/// 0–100: how unevenly the lines start along the flame.
pub flame_bottom_alignment: f32,
pub flame_style: FlameStyle,
pub flame_shape: FlameShape,
pub randomize_shapes: bool,
/// 0–100: arrangement seed.
pub seed: u32,
/// 0 (draft) … 4 (fine): segments per flame line.
pub quality: u32,
}
impl Default for FlameSpec {
fn default() -> Self {
FlameSpec {
flame_type: FlameType::OneFlameAlongPath,
length: 150.0,
randomize_length: false,
width: 40.0,
angle: 0.0,
interval: 60.0,
adjust_interval_for_loops: true,
color: None,
turbulent: 25.0,
jag: 25.0,
opacity: 75.0,
flame_lines: 20.0,
flame_bottom_alignment: 20.0,
flame_style: FlameStyle::Normal,
flame_shape: FlameShape::Parallel,
randomize_shapes: false,
seed: 0,
quality: 2,
}
}
}
/// Natural flame colour at position `t` along the flame (0 = base, 1 = tip).
fn flame_colour(t: f32, custom: Option<[f32; 4]>) -> [f32; 4] {
let a = (1.0 - t).powf(0.7) * 0.9 + 0.1 * (1.0 - t);
match custom {
Some(c) => {
// Hot near-white core → the colour → a darker rim.
let core = [c[0] * 0.4 + 0.6, c[1] * 0.4 + 0.6, c[2] * 0.4 + 0.6];
let rim = [c[0] * 0.6, c[1] * 0.6, c[2] * 0.6];
let k = t.clamp(0.0, 1.0);
let rgb = if k < 0.35 {
let u = k / 0.35;
[core[0] + (c[0] - core[0]) * u, core[1] + (c[1] - core[1]) * u, core[2] + (c[2] - core[2]) * u]
} else {
let u = (k - 0.35) / 0.65;
[c[0] + (rim[0] - c[0]) * u, c[1] + (rim[1] - c[1]) * u, c[2] + (rim[2] - c[2]) * u]
};
[rgb[0], rgb[1], rgb[2], a * c[3]]
}
None => {
const STOPS: [(f32, [f32; 3]); 5] =
[(0.0, [1.0, 0.98, 0.85]), (0.2, [1.0, 0.86, 0.35]), (0.45, [1.0, 0.55, 0.08]), (0.75, [0.9, 0.25, 0.02]), (1.0, [0.6, 0.08, 0.0])];
let k = t.clamp(0.0, 1.0);
let mut rgb = STOPS[4].1;
for w in STOPS.windows(2) {
if k <= w[1].0 {
let u = (k - w[0].0) / (w[1].0 - w[0].0);
rgb = [w[0].1[0] + (w[1].1[0] - w[0].1[0]) * u, w[0].1[1] + (w[1].1[1] - w[0].1[1]) * u, w[0].1[2] + (w[1].1[2] - w[0].1[2]) * u];
break;
}
}
[rgb[0], rgb[1], rgb[2], a]
}
}
}
/// One flame: its spine (base → tip), how wide it is, and its random stream.
fn draw_flame(out: &mut Vec<Prim>, spine: &Spine, width: f32, spec: &FlameSpec, rng: &mut Rng) {
let len = spine.len();
if len < 1.0 {
return;
}
let lines = (spec.flame_lines.clamp(1.0, 100.0) * 0.6 + 3.0).round() as usize;
let segs = [6usize, 10, 16, 24, 36][spec.quality.min(4) as usize].max((len / 12.0) as usize).min(160);
let (mut turb, mut jag) = (spec.turbulent.clamp(0.0, 100.0) / 100.0, spec.jag.clamp(0.0, 100.0) / 100.0);
match spec.flame_style {
FlameStyle::Violent => {
turb = (turb * 1.8 + 0.2).min(2.0);
jag = (jag * 1.8 + 0.15).min(2.0);
}
FlameStyle::Flat => {
turb *= 0.35;
jag *= 0.35;
}
FlameStyle::Normal => {}
}
let shape = if spec.randomize_shapes {
[FlameShape::Parallel, FlameShape::ToCenter, FlameShape::Spread, FlameShape::Oval, FlameShape::Pointed][(rng.next_u64() % 5) as usize]
} else {
spec.flame_shape
};
let opacity = spec.opacity.clamp(0.0, 100.0) / 100.0;
let half = width.max(1.0) / 2.0;
let seed = rng.next_u64();
// The whole flame sways; each line adds its own flicker on top.
let sway = |u: f32| turb * half * 1.6 * fbm1(u * 1.7, seed ^ 0x5A5A) * u.powf(1.4);
// A soft additive body glow along the spine.
for k in 0..6 {
let u = k as f32 / 6.0 * 0.75 + 0.05;
let ((px, py), (dx, dy)) = spine.at(u * len);
let o = sway(u);
let c = flame_colour(u, spec.color);
let r = half * (1.25 - u) + 2.0;
out.push(
Prim::ellipse((px - dy * o, py + dx * o), r, r * 1.2, dy.atan2(dx), [c[0], c[1], c[2], 0.16 * opacity], [c[0], c[1] * 0.6, c[2] * 0.3, 0.0])
.with_soft(r.max(2.0))
.additive(),
);
}
let line_r = (half / lines as f32 * 2.2).clamp(0.8, 10.0);
for li in 0..lines {
let lane = if lines == 1 { 0.0 } else { li as f32 / (lines - 1) as f32 * 2.0 - 1.0 };
let lseed = seed ^ (li as u64).wrapping_mul(0x51_7CC1_B727_220A);
let start = (spec.flame_bottom_alignment.clamp(0.0, 100.0) / 100.0) * rng.f() * 0.4;
// Outer lines are shorter, giving the flame its tongue shape.
let reach = (1.0 - 0.55 * lane.abs().powf(1.5)) * rng.range(0.7, 1.0);
let end = (start + reach * (1.0 - start)).clamp(start + 0.05, 1.0);
let lr = line_r * rng.range(0.6, 1.3);
let mut prev: Option<((f32, f32), f32, f32)> = None;
for k in 0..=segs {
let u = start + (end - start) * k as f32 / segs as f32;
let s = u * len;
let ((px, py), (dx, dy)) = spine.at(s);
let (nx, ny) = (-dy, dx);
let spread = match shape {
FlameShape::Parallel => 1.0 - 0.6 * u,
FlameShape::ToCenter => 1.0 - u,
FlameShape::Spread => 1.0 + u,
FlameShape::Oval => (std::f32::consts::PI * (0.15 + 0.85 * u)).sin() * 1.3,
FlameShape::Pointed => (1.0 - u).powi(2),
};
let flick = turb * half * 0.5 * fbm1(s / (len * 0.2 + 6.0) + li as f32 * 0.37, lseed) * u + jag * half * 0.25 * noise1(s / 5.0, lseed ^ 0x77) * u;
let off = lane * half * spread + sway(u) + flick;
let p = (px + nx * off, py + ny * off);
let r = lr * (1.0 - u * 0.75);
// Lines fade out towards their own tip.
let fade_t = ((u - start) / (end - start).max(1e-3)).clamp(0.0, 1.0);
if let Some((q, rq, uq)) = prev {
// Fade in over the first tenth (a soft base) and out towards the line's tip.
let f = |c: [f32; 4], uu: f32, t: f32| [c[0], c[1], c[2], c[3] * opacity * (1.0 - t.powi(3)) * ((uu - start) / 0.08).clamp(0.0, 1.0).sqrt()];
out.push(
Prim::capsule(q, p, rq, r, f(flame_colour(uq, spec.color), uq, fade_t), f(flame_colour(u, spec.color), u, fade_t))
.with_soft(1.0 + lr * 0.6)
.lighten(),
);
}
prev = Some((p, r, u));
}
}
}
/// Builds flame primitives. `paths` are flattened polylines in document pixels.
pub fn flame(spec: &FlameSpec, paths: &[Vec<(f32, f32)>]) -> Vec<Prim> {
let mut out = Vec::new();
let mut rng = Rng::new(spec.seed, 0xF1A3);
let length = spec.length.clamp(1.0, 1000.0);
let width = spec.width.clamp(1.0, 1000.0);
let interval = spec.interval.clamp(1.0, 1000.0);
let pick_len =
|rng: &mut Rng, var: f32| if spec.randomize_length || var > 0.0 { length * rng.range(1.0 - var.max(0.4), 1.0 + var.max(0.4) * 0.5) } else { length };
for path in paths.iter().filter(|p| !p.is_empty()) {
let spine = Spine::new(path.clone());
let total = spine.len();
match spec.flame_type {
FlameType::OneFlameAlongPath if path.len() >= 2 && total >= 1.0 => draw_flame(&mut out, &spine, width, spec, &mut rng),
FlameType::CandleLight | FlameType::OneFlameAlongPath => {
// A candle: a short teardrop rising straight up from the start.
let base = path[0];
let h = length.min(400.0);
let candle = Spine::new(vec![base, (base.0, base.1 - h)]);
let c = FlameSpec { flame_shape: FlameShape::Oval, turbulent: spec.turbulent * 0.3, jag: spec.jag * 0.2, ..spec.clone() };
draw_flame(&mut out, &candle, (width * 0.6).max(4.0), &c, &mut rng);
let core = flame_colour(0.05, spec.color);
out.push(
Prim::ellipse(
(base.0, base.1 - h * 0.28),
(width * 0.12).max(1.5),
h * 0.22,
0.0,
[core[0], core[1], core[2], 0.9 * spec.opacity.clamp(0.0, 100.0) / 100.0],
[core[0], core[1], core[2], 0.0],
)
.with_soft(2.0)
.additive(),
);
}
_ => {
let closed = path.len() > 2 && {
let (a, b) = (path[0], path[path.len() - 1]);
(a.0 - b.0).abs() < 1.0 && (a.1 - b.1).abs() < 1.0
};
let count = if spec.adjust_interval_for_loops && closed {
let c = (total / interval).round();
if c.is_finite() && c > 1.0 { (c as usize).min(2048) } else { 1 }
} else {
let c = (total / interval).floor();
if c.is_finite() && c >= 0.0 { (c as usize).saturating_add(1).min(2048) } else { 1 }
};
let step = if spec.adjust_interval_for_loops && closed { total / count as f32 } else { interval };
for k in 0..count {
if out.len() >= 65536 {
break;
}
let s = k as f32 * step;
let ((x, y), (dx, dy)) = spine.at(s);
let dir = match spec.flame_type {
FlameType::MultipleFlamesPathDirections => (dx, dy),
FlameType::MultipleFlamesOneDirection => {
let a = spec.angle.to_radians();
(a.sin(), -a.cos())
}
// Left of the path direction (up for a left-to-right path).
_ => (dy, -dx),
};
let var = if spec.flame_type == FlameType::MultipleFlamesVariousLength { 0.7 } else { 0.0 };
let l = pick_len(&mut rng, var);
let fl = Spine::new(vec![(x, y), (x + dir.0 * l, y + dir.1 * l)]);
draw_flame(&mut out, &fl, width, spec, &mut rng);
}
}
}
}
out
}
/// The path Flame uses when the document has none: a gentle arc across the lower part of the canvas.
pub fn default_flame_path(canvas: Rect) -> Vec<(f32, f32)> {
let (w, h) = (canvas.width() as f32, canvas.height() as f32);
(0..=32)
.map(|k| {
let t = k as f32 / 32.0;
(canvas.x0 as f32 + w * (0.15 + 0.7 * t), canvas.y0 as f32 + h * (0.8 - 0.08 * (std::f32::consts::PI * t).sin()))
})
.collect()
}
// ---------------------------------------------------------------------------
// Tree
// ---------------------------------------------------------------------------
/// Leaf form of a tree type.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Leaf {
None,
Round,
Oval,
Needle,
Heart,
Blossom,
Long,
Frond,
}
/// Branch architecture of a tree type.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Habit {
/// Recursive branching (deciduous trees).
Branching,
/// A dominant stem with whorls of side branches (conifers).
Conifer,
/// A tall trunk crowned with fronds.
Palm,
/// Several straight jointed stalks.
Bamboo,
}
/// One base tree type.
struct TreeKind {
name: &'static str,
habit: Habit,
levels: u32,
children: u32,
/// Branch spread (degrees from the parent).
spread: f32,
/// Child / parent length ratio.
ratio: f32,
/// Trunk share of the total height.
trunk: f32,
/// Downward bend per unit length at deep levels (willow > 0, upright < 0).
droop: f32,
/// Random bend.
wiggle: f32,
/// Trunk radius as a fraction of the height.
thick: f32,
leaf: Leaf,
leaf_col: [f32; 3],
bark: [f32; 3],
/// Fruit or blossom accent dots.
accent: Option<[f32; 3]>,
}
#[allow(clippy::too_many_arguments)]
const fn tk(
name: &'static str,
habit: Habit,
levels: u32,
children: u32,
spread: f32,
ratio: f32,
trunk: f32,
droop: f32,
wiggle: f32,
thick: f32,
leaf: Leaf,
leaf_col: [f32; 3],
bark: [f32; 3],
accent: Option<[f32; 3]>,
) -> TreeKind {
TreeKind { name, habit, levels, children, spread, ratio, trunk, droop, wiggle, thick, leaf, leaf_col, bark, accent }
}
const BROWN: [f32; 3] = [0.36, 0.25, 0.16];
const GREY_BARK: [f32; 3] = [0.45, 0.42, 0.38];
const GREEN: [f32; 3] = [0.24, 0.52, 0.16];
const DARK_GREEN: [f32; 3] = [0.12, 0.36, 0.14];
/// The base tree types (index = `baseTreeType` − 1).
const TREES: [TreeKind; 34] = [
tk("Oak", Habit::Branching, 5, 3, 38.0, 0.72, 0.35, 0.05, 0.35, 0.035, Leaf::Round, GREEN, BROWN, None),
tk("Maple", Habit::Branching, 5, 3, 34.0, 0.74, 0.32, 0.0, 0.25, 0.03, Leaf::Heart, [0.3, 0.55, 0.15], BROWN, None),
tk("Pine", Habit::Conifer, 3, 2, 70.0, 0.5, 0.15, 0.25, 0.1, 0.025, Leaf::Needle, DARK_GREEN, BROWN, None),
tk("Palm", Habit::Palm, 1, 9, 0.0, 0.0, 0.85, 0.0, 0.15, 0.022, Leaf::Frond, [0.25, 0.5, 0.18], [0.55, 0.45, 0.32], None),
tk("Birch", Habit::Branching, 5, 2, 28.0, 0.75, 0.4, 0.1, 0.3, 0.02, Leaf::Oval, [0.45, 0.65, 0.2], [0.88, 0.86, 0.82], None),
tk("Weeping Willow", Habit::Branching, 5, 3, 40.0, 0.78, 0.3, 0.9, 0.2, 0.035, Leaf::Long, [0.45, 0.62, 0.22], GREY_BARK, None),
tk("Cherry Blossom", Habit::Branching, 5, 3, 42.0, 0.7, 0.3, 0.0, 0.45, 0.03, Leaf::Blossom, [0.98, 0.75, 0.82], [0.3, 0.2, 0.17], None),
tk("Bamboo", Habit::Bamboo, 1, 5, 6.0, 0.0, 1.0, 0.0, 0.05, 0.012, Leaf::Long, [0.35, 0.6, 0.2], [0.55, 0.65, 0.3], None),
tk("Cypress", Habit::Branching, 5, 3, 10.0, 0.8, 0.1, -0.1, 0.1, 0.02, Leaf::Needle, DARK_GREEN, BROWN, None),
tk("Baobab", Habit::Branching, 3, 4, 50.0, 0.55, 0.65, 0.0, 0.3, 0.09, Leaf::Round, [0.35, 0.55, 0.2], [0.55, 0.47, 0.4], None),
tk("Ginkgo", Habit::Branching, 4, 3, 30.0, 0.72, 0.35, 0.0, 0.2, 0.025, Leaf::Heart, [0.75, 0.75, 0.2], GREY_BARK, None),
tk("Spruce", Habit::Conifer, 3, 2, 80.0, 0.45, 0.1, 0.45, 0.08, 0.025, Leaf::Needle, [0.1, 0.3, 0.2], BROWN, None),
tk("Bare Winter Tree", Habit::Branching, 6, 2, 34.0, 0.74, 0.3, 0.05, 0.35, 0.03, Leaf::None, GREEN, [0.3, 0.25, 0.22], None),
tk("Apple", Habit::Branching, 5, 3, 46.0, 0.68, 0.3, 0.1, 0.4, 0.03, Leaf::Round, GREEN, BROWN, Some([0.85, 0.1, 0.1])),
tk("Acacia", Habit::Branching, 4, 3, 55.0, 0.75, 0.45, -0.25, 0.25, 0.03, Leaf::Round, [0.35, 0.5, 0.18], BROWN, None),
tk("Poplar", Habit::Branching, 5, 3, 12.0, 0.8, 0.15, -0.15, 0.1, 0.025, Leaf::Oval, [0.3, 0.55, 0.15], GREY_BARK, None),
tk("Elm", Habit::Branching, 5, 2, 24.0, 0.8, 0.3, 0.15, 0.25, 0.032, Leaf::Oval, GREEN, BROWN, None),
tk("Magnolia", Habit::Branching, 4, 3, 40.0, 0.7, 0.3, 0.0, 0.3, 0.03, Leaf::Blossom, [0.98, 0.9, 0.92], [0.35, 0.28, 0.25], Some([0.85, 0.5, 0.65])),
tk("Banana", Habit::Palm, 1, 7, 0.0, 0.0, 0.6, 0.0, 0.05, 0.04, Leaf::Frond, [0.45, 0.7, 0.2], [0.5, 0.6, 0.3], None),
tk("Bonsai", Habit::Branching, 4, 3, 60.0, 0.6, 0.45, 0.2, 0.7, 0.05, Leaf::Round, DARK_GREEN, [0.4, 0.3, 0.22], None),
tk("Jacaranda", Habit::Branching, 5, 3, 44.0, 0.72, 0.3, 0.0, 0.35, 0.03, Leaf::Blossom, [0.6, 0.45, 0.85], BROWN, None),
tk("Autumn Maple", Habit::Branching, 5, 3, 34.0, 0.74, 0.32, 0.0, 0.25, 0.03, Leaf::Heart, [0.85, 0.3, 0.1], BROWN, None),
tk("Golden Autumn", Habit::Branching, 5, 3, 36.0, 0.72, 0.33, 0.05, 0.3, 0.03, Leaf::Round, [0.9, 0.7, 0.15], BROWN, None),
tk("Olive", Habit::Branching, 4, 3, 48.0, 0.68, 0.35, 0.1, 0.6, 0.04, Leaf::Long, [0.5, 0.58, 0.42], GREY_BARK, Some([0.25, 0.28, 0.12])),
tk("Fir", Habit::Conifer, 3, 2, 75.0, 0.5, 0.12, 0.15, 0.05, 0.025, Leaf::Needle, [0.12, 0.38, 0.22], BROWN, None),
tk("Cedar", Habit::Conifer, 3, 3, 88.0, 0.6, 0.2, 0.0, 0.15, 0.03, Leaf::Needle, [0.2, 0.4, 0.25], BROWN, None),
tk("Lemon", Habit::Branching, 4, 3, 45.0, 0.68, 0.3, 0.05, 0.35, 0.03, Leaf::Oval, DARK_GREEN, BROWN, Some([0.98, 0.88, 0.15])),
tk("Wisteria", Habit::Branching, 5, 3, 45.0, 0.72, 0.3, 0.7, 0.3, 0.03, Leaf::Blossom, [0.7, 0.55, 0.9], [0.35, 0.3, 0.27], None),
tk("Young Sapling", Habit::Branching, 3, 2, 30.0, 0.7, 0.45, 0.0, 0.2, 0.015, Leaf::Oval, [0.4, 0.7, 0.2], [0.45, 0.35, 0.22], None),
tk("Dead Tree", Habit::Branching, 5, 2, 45.0, 0.68, 0.4, 0.1, 0.8, 0.035, Leaf::None, GREEN, [0.32, 0.29, 0.26], None),
tk("Coconut Palm", Habit::Palm, 1, 11, 0.0, 0.0, 0.88, 0.0, 0.35, 0.018, Leaf::Frond, [0.3, 0.55, 0.15], [0.6, 0.5, 0.36], Some([0.4, 0.3, 0.15])),
tk("Shrub", Habit::Branching, 4, 4, 55.0, 0.7, 0.05, 0.0, 0.35, 0.02, Leaf::Round, GREEN, BROWN, None),
tk("Plum Blossom", Habit::Branching, 5, 2, 48.0, 0.7, 0.3, 0.1, 0.6, 0.03, Leaf::Blossom, [0.95, 0.6, 0.7], [0.25, 0.18, 0.15], None),
tk("Cloud Pine", Habit::Branching, 4, 3, 70.0, 0.65, 0.35, 0.2, 0.5, 0.035, Leaf::Needle, [0.15, 0.4, 0.2], [0.35, 0.27, 0.2], None),
];
/// Names of the base tree types (1-based `baseTreeType` = index + 1).
pub fn tree_type_names() -> Vec<&'static str> {
TREES.iter().map(|t| t.name).collect()
}
/// Tree options (Photoshop's Tree dialog).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct TreeSpec {
/// 1–34.
pub base_tree_type: u32,
/// 1 (light from the left) … 3 (above) … 5 (right).
pub light_direction: u32,
/// 0–100.
pub leaves_amount: f32,
/// 0–200 %.
pub leaves_size: f32,
/// 50–300 %: how high the branching starts.
pub branches_height: f32,
/// 50–200 %.
pub branches_thickness: f32,
/// Use the type's leaf colour; otherwise `leaves_color`.
pub default_leaves: bool,
pub leaves_color: [f32; 4],
/// Use `branches_color` instead of the type's bark.
pub custom_branch_color: bool,
pub branches_color: [f32; 4],
/// Flat shading (no light variation).
pub flat_shading: bool,
pub seed: u32,
/// Base position as a fraction of the canvas.
pub x: f32,
pub y: f32,
/// Height as a fraction of the canvas height.
pub size: f32,
}
impl Default for TreeSpec {
fn default() -> Self {
TreeSpec {
base_tree_type: 1,
light_direction: 3,
leaves_amount: 50.0,
leaves_size: 100.0,
branches_height: 100.0,
branches_thickness: 100.0,
default_leaves: true,
leaves_color: [0.24, 0.52, 0.16, 1.0],
custom_branch_color: false,
branches_color: [0.36, 0.25, 0.16, 1.0],
flat_shading: false,
seed: 0,
x: 0.5,
y: 0.95,
size: 0.8,
}
}
}
struct TreeCtx<'a> {
kind: &'a TreeKind,
spec: &'a TreeSpec,
rng: Rng,
branches: Vec<Prim>,
leaves: Vec<(f32, Prim)>,
bark: [f32; 3],
leaf: [f32; 3],
light: (f32, f32),
centre: (f32, f32),
leaf_px: f32,
}
fn shade(c: [f32; 3], k: f32) -> [f32; 4] {
[(c[0] * k).clamp(0.0, 1.0), (c[1] * k).clamp(0.0, 1.0), (c[2] * k).clamp(0.0, 1.0), 1.0]
}
impl TreeCtx<'_> {
/// Brightness of a leaf at `p` (lit side of the crown brighter).
fn light_at(&mut self, p: (f32, f32)) -> f32 {
if self.spec.flat_shading {
return 1.0;
}
let (dx, dy) = (p.0 - self.centre.0, p.1 - self.centre.1);
let l = (dx * dx + dy * dy).sqrt().max(1.0);
0.78 + 0.32 * ((dx * self.light.0 + dy * self.light.1) / l) + 0.12 * self.rng.sym()
}
fn add_leaf(&mut self, p: (f32, f32), dir: f32) {
let r = &mut self.rng;
let s = self.leaf_px * r.range(0.7, 1.2);
let ang = dir + r.sym() * 1.2;
let k = self.light_at(p);
let col = self.leaf;
let prim = match self.kind.leaf {
Leaf::None => return,
Leaf::Round => Prim::ellipse(p, s, s * 0.85, ang, shade(col, k * 1.1), shade(col, k * 0.8)),
Leaf::Oval => Prim::ellipse(p, s * 1.2, s * 0.55, ang, shade(col, k * 1.1), shade(col, k * 0.8)),
Leaf::Long => Prim::ellipse(p, s * 1.6, s * 0.28, ang, shade(col, k * 1.1), shade(col, k * 0.8)),
Leaf::Heart => Prim::ellipse(p, s * 0.9, s * 1.1, ang, shade(col, k * 1.1), shade(col, k * 0.75)),
Leaf::Needle => {
// A tuft of needles fanning out from the twig.
let n = 7;
for j in 0..n {
let a = ang + (j as f32 / (n - 1) as f32 - 0.5) * 2.4 + self.rng.sym() * 0.15;
let l = s * self.rng.range(1.1, 1.7);
let q = (p.0 + a.cos() * l, p.1 + a.sin() * l);
let kk = k + 0.1 * self.rng.sym();
self.leaves.push((kk, Prim::capsule(p, q, (s * 0.1).max(0.45), (s * 0.05).max(0.3), shade(col, kk * 0.85), shade(col, kk * 1.1))));
}
return;
}
Leaf::Blossom => Prim::ellipse(p, s * 0.75, s * 0.75, 0.0, shade(col, k * 1.08), shade(col, k * 0.85)),
Leaf::Frond => Prim::ellipse(p, s * 1.6, s * 0.3, ang, shade(col, k), shade(col, k * 0.8)),
};
let order = k + self.rng.f() * 0.05;
self.leaves.push((order, prim));
if let Some(acc) = self.kind.accent
&& self.rng.f() < 0.12
{
let q = (p.0 + self.rng.sym() * s, p.1 + self.rng.sym() * s);
let r = (s * 0.45).max(1.0);
self.leaves.push((k + 1.0, Prim::ellipse(q, r, r, 0.0, shade(acc, k * 1.15), shade(acc, k * 0.75))));
}
}
/// A curved branch from `p` heading `ang` (radians, 0 = right, y down).
/// Returns its end point and end heading.
fn limb(&mut self, p: (f32, f32), ang: f32, len: f32, r0: f32, r1: f32, depth01: f32) -> ((f32, f32), f32) {
let segs = ((len / 6.0) as usize).clamp(2, 10);
let (mut q, mut a) = (p, ang);
let droop = self.kind.droop * depth01;
let col0 = shade(self.bark, 0.9 + 0.2 * depth01);
for k in 0..segs {
let t0 = k as f32 / segs as f32;
let t1 = (k + 1) as f32 / segs as f32;
a += self.kind.wiggle * self.rng.sym() * 0.25;
// Gravity pulls the heading towards straight down (π/2).
let down = std::f32::consts::FRAC_PI_2;
let diff = (down - a).sin().atan2((down - a).cos());
a += diff * droop * 0.12;
let step = len / segs as f32;
let nq = (q.0 + a.cos() * step, q.1 + a.sin() * step);
let (ra, rb) = (r0 + (r1 - r0) * t0, r0 + (r1 - r0) * t1);
self.branches.push(Prim::capsule(q, nq, ra, rb, col0, col0));
if depth01 > 0.99 && self.kind.leaf != Leaf::None && k > 0 && self.rng.f() < self.spec.leaves_amount.clamp(0.0, 100.0) / 100.0 * 0.9 {
self.add_leaf(nq, a);
}
q = nq;
}
(q, a)
}
fn branch(&mut self, p: (f32, f32), ang: f32, len: f32, r: f32, level: u32) {
let levels = self.kind.levels;
let depth01 = level as f32 / levels.max(1) as f32;
let r_end = r * 0.62;
let (end, a) = self.limb(p, ang, len, r, r_end, depth01);
if level >= levels {
let amount = self.spec.leaves_amount.clamp(0.0, 100.0) / 100.0;
if self.kind.leaf != Leaf::None {
let n = (amount * 9.0).round() as usize;
for _ in 0..n {
let d = len * 0.5 * self.rng.f();
let th = self.rng.f() * std::f32::consts::TAU;
self.add_leaf((end.0 + th.cos() * d, end.1 + th.sin() * d), a);
}
}
return;
}
let spread = self.kind.spread.to_radians();
let n = self.kind.children;
for c in 0..n {
let side = if n == 1 { 0.0 } else { c as f32 / (n - 1) as f32 * 2.0 - 1.0 };
let ca = a + side * spread + self.rng.sym() * spread * 0.35;
let cl = len * self.kind.ratio * self.rng.range(0.8, 1.1);
let cr = r_end * self.rng.range(0.75, 0.95);
self.branch(end, ca, cl, cr, level + 1);
}
}
}
/// Builds tree primitives inside `canvas`.
pub fn tree(spec: &TreeSpec, canvas: Rect) -> Vec<Prim> {
let kind = &TREES[(spec.base_tree_type.clamp(1, TREES.len() as u32) - 1) as usize];
let h = canvas.height() as f32 * spec.size.clamp(0.05, 2.0);
let base = (canvas.x0 as f32 + canvas.width() as f32 * spec.x, canvas.y0 as f32 + canvas.height() as f32 * spec.y);
let la = match spec.light_direction.clamp(1, 5) {
1 => 180.0f32,
2 => 225.0,
3 => 270.0,
4 => 315.0,
_ => 0.0,
}
.to_radians();
let leaf = if spec.default_leaves { kind.leaf_col } else { [spec.leaves_color[0], spec.leaves_color[1], spec.leaves_color[2]] };
let bark = if spec.custom_branch_color { [spec.branches_color[0], spec.branches_color[1], spec.branches_color[2]] } else { kind.bark };
let thick = spec.branches_thickness.clamp(50.0, 200.0) / 100.0;
let bh = spec.branches_height.clamp(50.0, 300.0) / 100.0;
let mut t = TreeCtx {
kind,
spec,
rng: Rng::new(spec.seed, 0x7EE + spec.base_tree_type as u64),
branches: Vec::new(),
leaves: Vec::new(),
bark,
leaf,
light: (la.cos(), la.sin()),
centre: (base.0, base.1 - h * 0.65),
leaf_px: (h * 0.012 * spec.leaves_size.clamp(0.0, 200.0) / 100.0).max(0.6),
};
let up = -std::f32::consts::FRAC_PI_2;
let r0 = (h * kind.thick * thick).max(0.8);
match kind.habit {
Habit::Branching => {
let trunk = (h * kind.trunk * bh).min(h * 0.92);
let (top, a) = t.limb(base, up, trunk.max(1.0), r0, r0 * 0.75, 0.0);
// Crown length so the tree reaches about `h`.
let geo: f32 = (0..kind.levels).map(|k| kind.ratio.powi(k as i32)).sum();
let first = ((h - trunk).max(h * 0.08) / geo.max(0.5)) * 1.05;
let n = kind.children.max(2);
for c in 0..n {
let side = c as f32 / (n - 1) as f32 * 2.0 - 1.0;
let sp = kind.spread.to_radians() * 0.8;
let ca = a + side * sp + t.rng.sym() * 0.15;
let fl = first * t.rng.range(0.85, 1.05);
t.branch(top, ca, fl, r0 * 0.7, 1);
}
}
Habit::Conifer => {
let (top, _) = t.limb(base, up, h, r0, r0 * 0.1, 0.0);
let start = (h * kind.trunk * bh).min(h * 0.8);
let whorls = (14.0 * (h / 300.0).sqrt().clamp(0.6, 2.5)) as usize;
let spread = kind.spread.to_radians();
for k in 0..whorls {
let f = k as f32 / whorls as f32;
let y = base.1 - start - (h - start) * f;
let x = base.0 + (top.0 - base.0) * ((base.1 - y) / h);
let span = (h - start) * 0.42 * (1.0 - f).powf(0.9) + h * 0.03;
for side in [-1.0f32, 1.0] {
let ang = up + side * spread + t.rng.sym() * 0.1;
t.branch((x, y), ang, span, (r0 * 0.35 * (1.0 - f)).max(0.6), kind.levels);
if kind.levels >= 2 {
let (s2, c2) = (ang + side * 0.5).sin_cos();
let mid = (x + c2 * span * 0.5, y + s2 * span * 0.5);
t.branch(mid, ang + side * 0.6, span * 0.4, (r0 * 0.15).max(0.5), kind.levels);
}
}
}
}
Habit::Palm => {
let trunk = (h * kind.trunk * bh.min(1.15)).max(1.0);
let lean = t.rng.sym() * kind.wiggle;
let segs = 12;
let mut p = base;
for k in 0..segs {
let f = k as f32 / segs as f32;
let a = up + lean * f;
let q = (p.0 + a.cos() * trunk / segs as f32, p.1 + a.sin() * trunk / segs as f32);
let r = r0 * (1.0 - 0.35 * f);
let ring = shade(bark, if k % 2 == 0 { 1.0 } else { 0.85 });
t.branches.push(Prim::capsule(p, q, r, r * 0.97, ring, ring));
p = q;
}
let fronds = kind.children;
let flen = h * 0.38 * (spec.leaves_size.clamp(10.0, 200.0) / 100.0).sqrt();
for k in 0..fronds {
let a = up + (k as f32 / fronds as f32 - 0.5) * std::f32::consts::PI * 1.7 + t.rng.sym() * 0.2;
let mut q = p;
let mut ang = a;
let segs = 10;
let leaflets = (spec.leaves_amount.clamp(0.0, 100.0) / 100.0 * 3.0).round() as usize;
for s in 0..segs {
// Fronds arch over and droop towards the ground.
let d = std::f32::consts::FRAC_PI_2 - ang;
ang += 0.06 * d.sin().atan2(d.cos());
let nq = (q.0 + ang.cos() * flen / segs as f32, q.1 + ang.sin() * flen / segs as f32);
let k2 = t.light_at(nq);
t.leaves
.push((k2 - 0.1, Prim::capsule(q, nq, (r0 * 0.15).max(0.6), (r0 * 0.1).max(0.4), shade(t.leaf, k2 * 0.8), shade(t.leaf, k2 * 0.8))));
for j in 0..leaflets {
let along = j as f32 / leaflets as f32;
let at = (q.0 + (nq.0 - q.0) * along, q.1 + (nq.1 - q.1) * along);
for side in [-1.0f32, 1.0] {
let la = ang + side * 1.1;
let ll = flen * 0.22 * (1.0 - s as f32 / segs as f32 * 0.7);
let tip = (at.0 + la.cos() * ll, at.1 + la.sin() * ll + ll * 0.25);
t.leaves.push((k2, Prim::capsule(at, tip, (ll * 0.08).max(0.5), 0.3, shade(t.leaf, k2), shade(t.leaf, k2 * 0.9))));
}
}
q = nq;
}
}
if let Some(acc) = kind.accent {
for _ in 0..4 {
let c = (p.0 + t.rng.sym() * r0 * 2.0, p.1 + t.rng.f() * r0 * 2.0);
t.leaves.push((2.0, Prim::disc(c, r0 * 1.1, shade(acc, 1.0))));
}
}
}
Habit::Bamboo => {
let stalks = kind.children;
for k in 0..stalks {
let x = base.0 + (k as f32 - (stalks - 1) as f32 / 2.0) * r0 * 5.0 + t.rng.sym() * r0;
let hh = h * t.rng.range(0.7, 1.0);
let a = up + t.rng.sym() * kind.spread.to_radians() * 0.5;
let joints = 8;
let mut p = (x, base.1);
for j in 0..joints {
let q = (p.0 + a.cos() * hh / joints as f32, p.1 + a.sin() * hh / joints as f32);
t.branches.push(Prim::capsule(p, q, r0, r0, shade(bark, 1.0), shade(bark, 0.85)));
t.branches.push(Prim::capsule((q.0 - r0 * 1.1, q.1), (q.0 + r0 * 1.1, q.1), r0 * 0.25, r0 * 0.25, shade(bark, 0.7), shade(bark, 0.7)));
if j >= joints / 3 {
let leaves = (spec.leaves_amount.clamp(0.0, 100.0) / 100.0 * 4.0).round() as usize;
for _ in 0..leaves {
let side = if t.rng.f() < 0.5 { -1.0 } else { 1.0 };
let la = a + side * t.rng.range(0.6, 1.4);
let ll = t.leaf_px * 2.2;
t.add_leaf((q.0 + la.cos() * ll, q.1 + la.sin() * ll), la);
}
}
p = q;
}
}
}
}
// Back leaves first: darker ones underneath, lit ones on top.
t.leaves.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut out = t.branches;
out.extend(t.leaves.into_iter().map(|(_, p)| p));
out
}
// ---------------------------------------------------------------------------
// Picture Frame
// ---------------------------------------------------------------------------
/// Picture frame designs (index = `frame` − 1).
pub const FRAME_STYLES: [&str; 16] = [
"vineWithFlowers",
"vineWithLeaves",
"ivy",
"roses",
"daisies",
"berries",
"bamboo",
"waves",
"zigzag",
"dots",
"rope",
"scallops",
"doubleLine",
"snowflakes",
"stars",
"hearts",
];
/// Picture Frame options.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct FrameSpec {
/// 1-based design (see [`FRAME_STYLES`]).
pub frame: u32,
/// Distance of the frame from the canvas edge, % of the shorter side (0–30).
pub margin: f32,
/// Ornament size 1–100.
pub size: f32,
/// Ornament spacing / density 1–100 (higher = denser).
pub arrangement: f32,
/// Number of parallel lines for line designs (1–5).
pub lines: u32,
/// Line / vine thickness 1–100.
pub thickness: f32,
/// 0–100 %: fade the ornaments towards the inside.
pub fade: f32,
pub vine_color: [f32; 4],
pub flower_color: [f32; 4],
pub leaf_color: [f32; 4],
pub seed: u32,
}
impl Default for FrameSpec {
fn default() -> Self {
FrameSpec {
frame: 1,
margin: 4.0,
size: 50.0,
arrangement: 50.0,
lines: 1,
thickness: 30.0,
fade: 0.0,
vine_color: [0.25, 0.4, 0.15, 1.0],
flower_color: [0.9, 0.3, 0.45, 1.0],
leaf_color: [0.3, 0.6, 0.2, 1.0],
seed: 0,
}
}
}
/// The frame rectangle walked by arc length (clockwise from the top-left).
struct Border {
x0: f32,
y0: f32,
w: f32,
h: f32,
}
impl Border {
fn len(&self) -> f32 {
2.0 * (self.w + self.h)
}
/// Point, unit tangent and inward normal at arc length `s`.
fn at(&self, s: f32) -> ((f32, f32), (f32, f32), (f32, f32)) {
let s = s.rem_euclid(self.len());
let (w, h) = (self.w, self.h);
if s < w {
((self.x0 + s, self.y0), (1.0, 0.0), (0.0, 1.0))
} else if s < w + h {
((self.x0 + w, self.y0 + s - w), (0.0, 1.0), (-1.0, 0.0))
} else if s < 2.0 * w + h {
((self.x0 + w - (s - w - h), self.y0 + h), (-1.0, 0.0), (0.0, -1.0))
} else {
((self.x0, self.y0 + h - (s - 2.0 * w - h)), (0.0, -1.0), (1.0, 0.0))
}
}
}
fn flower(out: &mut Vec<Prim>, c: (f32, f32), r: f32, petals: usize, petal: [f32; 4], centre: [f32; 4], rot: f32) {
for k in 0..petals {
let a = rot + k as f32 / petals as f32 * std::f32::consts::TAU;
let p = (c.0 + a.cos() * r * 0.55, c.1 + a.sin() * r * 0.55);
out.push(Prim::ellipse(p, r * 0.5, r * 0.26, a, petal, shade([petal[0], petal[1], petal[2]], 0.8)));
}
out.push(Prim::ellipse(c, r * 0.28, r * 0.28, 0.0, centre, shade([centre[0], centre[1], centre[2]], 0.75)));
}
fn leaf(out: &mut Vec<Prim>, p: (f32, f32), ang: f32, len: f32, col: [f32; 4]) {
let c = (p.0 + ang.cos() * len * 0.5, p.1 + ang.sin() * len * 0.5);
out.push(Prim::ellipse(c, len * 0.5, len * 0.22, ang, shade([col[0], col[1], col[2]], 1.12), shade([col[0], col[1], col[2]], 0.8)));
}
fn star(out: &mut Vec<Prim>, c: (f32, f32), r: f32, points: usize, col: [f32; 4], rot: f32) {
for k in 0..points {
let a = rot + k as f32 / points as f32 * std::f32::consts::TAU;
out.push(Prim::capsule(c, (c.0 + a.cos() * r, c.1 + a.sin() * r), r * 0.28, 0.4, col, col));
}
}
/// Builds picture-frame primitives inside `canvas`.
pub fn picture_frame(spec: &FrameSpec, canvas: Rect) -> Vec<Prim> {
let mut out = Vec::new();
let short = (canvas.width().min(canvas.height()) as f32).max(8.0);
let margin = short * spec.margin.clamp(0.0, 30.0) / 100.0;
let unit = short * (0.008 + spec.size.clamp(1.0, 100.0) / 100.0 * 0.05);
let thick = (unit * 0.04 + short * 0.00012 * spec.thickness.clamp(1.0, 100.0)).max(0.6);
let spacing = unit * (4.0 - 3.2 * spec.arrangement.clamp(1.0, 100.0) / 100.0);
let inset = margin + unit;
let b = Border {
x0: canvas.x0 as f32 + inset,
y0: canvas.y0 as f32 + inset,
w: (canvas.width() as f32 - 2.0 * inset).max(1.0),
h: (canvas.height() as f32 - 2.0 * inset).max(1.0),
};
let total = b.len();
let count = (total / spacing).round().max(4.0) as usize;
let step = total / count as f32;
let mut rng = Rng::new(spec.seed, 0xF8A3E + spec.frame as u64);
let (vine, fl, lf) = (spec.vine_color, spec.flower_color, spec.leaf_color);
let style = FRAME_STYLES[(spec.frame.clamp(1, FRAME_STYLES.len() as u32) - 1) as usize];
let lines = spec.lines.clamp(1, 5) as usize;
let white = [1.0, 1.0, 1.0, 1.0];
let yellow = [0.98, 0.82, 0.2, 1.0];
// A wavy vine along the border.
let vine_path = |out: &mut Vec<Prim>, amp: f32, wl: f32, col: [f32; 4], r: f32| {
let n = (total / 3.0).ceil().max(8.0) as usize;
let mut prev: Option<(f32, f32)> = None;
for k in 0..=n {
let s = k as f32 / n as f32 * total;
let (p, _, nn) = b.at(s);
let o = amp * (s / wl * std::f32::consts::TAU).sin();
let q = (p.0 + nn.0 * o, p.1 + nn.1 * o);
if let Some(pp) = prev {
out.push(Prim::capsule(pp, q, r, r, col, col));
}
prev = Some(q);
}
};
let wave_off = |s: f32, amp: f32, wl: f32| amp * (s / wl * std::f32::consts::TAU).sin();
match style {
"vineWithFlowers" | "vineWithLeaves" | "ivy" | "roses" | "daisies" | "berries" => {
let amp = unit * 0.5;
let wl = step * 2.0;
vine_path(&mut out, amp, wl, vine, thick);
for k in 0..count {
let s = k as f32 * step + step * 0.5;
let (p, t, nn) = b.at(s);
let o = wave_off(s, amp, wl);
let q = (p.0 + nn.0 * o, p.1 + nn.1 * o);
let side = if k % 2 == 0 { 1.0 } else { -1.0 };
let ta = t.1.atan2(t.0);
let la = ta + side * 0.9 + rng.sym() * 0.3;
let ll = unit * rng.range(0.9, 1.2);
match style {
"vineWithFlowers" => {
leaf(&mut out, q, la, ll, lf);
if k % 2 == 0 {
let c = (q.0 + nn.0 * unit * 0.7 * side, q.1 + nn.1 * unit * 0.7 * side);
flower(&mut out, c, unit * 0.65, 5, fl, yellow, rng.f() * 6.3);
}
}
"vineWithLeaves" => {
leaf(&mut out, q, la, ll, lf);
leaf(&mut out, q, ta - side * 0.9, ll * 0.8, lf);
}
"ivy" => {
for j in 0..3 {
let a = la + (j as f32 - 1.0) * 0.7;
leaf(&mut out, q, a, ll * 0.75, shade([lf[0], lf[1], lf[2]], 0.75 + 0.15 * j as f32));
}
}
"roses" => {
leaf(&mut out, q, la, ll, lf);
if k % 3 == 0 {
let r = unit * 0.7;
for (j, f) in [1.0f32, 0.75, 0.5, 0.28].iter().enumerate() {
let c = shade([fl[0], fl[1], fl[2]], 0.75 + 0.1 * j as f32);
out.push(Prim::ellipse(q, r * f, r * f * 0.9, rng.f() * 3.0, c, shade([fl[0], fl[1], fl[2]], 0.55)));
}
}
}
"daisies" => {
leaf(&mut out, q, la, ll * 0.8, lf);
if k % 2 == 1 {
flower(&mut out, q, unit * 0.6, 12, white, yellow, rng.f());
}
}
_ => {
leaf(&mut out, q, la, ll, lf);
for _ in 0..3 {
let c = (q.0 + rng.sym() * unit * 0.4, q.1 + rng.sym() * unit * 0.4);
out.push(Prim::ellipse(c, unit * 0.2, unit * 0.2, 0.0, shade([fl[0], fl[1], fl[2]], 1.2), shade([fl[0], fl[1], fl[2]], 0.6)));
}
}
}
}
}
"bamboo" => {
for edge in 0..4 {
let (s0, l) = match edge {
0 => (0.0, b.w),
1 => (b.w, b.h),
2 => (b.w + b.h, b.w),
_ => (2.0 * b.w + b.h, b.h),
};
let (a, _, _) = b.at(s0 + 0.01);
let (z, _, _) = b.at(s0 + l - 0.01);
let joints = (l / (spacing * 2.0)).round().max(1.0) as usize;
for j in 0..joints {
let f0 = j as f32 / joints as f32;
let f1 = (j + 1) as f32 / joints as f32;
let p0 = (a.0 + (z.0 - a.0) * f0, a.1 + (z.1 - a.1) * f0);
let p1 = (a.0 + (z.0 - a.0) * f1, a.1 + (z.1 - a.1) * f1);
out.push(Prim::capsule(p0, p1, unit * 0.35, unit * 0.35, vine, shade([vine[0], vine[1], vine[2]], 0.8)));
out.push(Prim::disc(p1, unit * 0.4, shade([vine[0], vine[1], vine[2]], 0.7)));
}
// A few leaves per edge.
for _ in 0..(joints / 2).max(1) {
let f = rng.f();
let p = (a.0 + (z.0 - a.0) * f, a.1 + (z.1 - a.1) * f);
leaf(&mut out, p, rng.f() * 6.3, unit * 1.4, lf);
}
}
}
"waves" | "zigzag" | "doubleLine" | "rope" => {
for li in 0..lines {
let off = (li as f32 - (lines - 1) as f32 / 2.0) * thick * 4.0;
let n = (total / 2.0).ceil().max(8.0) as usize;
let mut prev: Option<(f32, f32)> = None;
for k in 0..=n {
let s = k as f32 / n as f32 * total;
let (p, _, nn) = b.at(s);
let o = off
+ match style {
"waves" => wave_off(s + li as f32 * step * 0.25, unit * 0.4, step),
"zigzag" => {
let ph = (s / step).fract();
unit * 0.4 * (4.0 * (ph - 0.5).abs() - 1.0)
}
_ => 0.0,
};
let q = (p.0 + nn.0 * o, p.1 + nn.1 * o);
if style == "rope" {
if k % 2 == 0 {
let (_, t, _) = b.at(s);
out.push(Prim::ellipse(
q,
unit * 0.45,
unit * 0.2,
t.1.atan2(t.0) + 0.7,
shade([vine[0], vine[1], vine[2]], 1.1),
shade([vine[0], vine[1], vine[2]], 0.65),
));
}
} else if let Some(pp) = prev {
out.push(Prim::capsule(pp, q, thick, thick, vine, vine));
}
prev = Some(q);
}
if style == "doubleLine" {
// A second rule inside each line and corner squares.
let inner = Border {
x0: b.x0 + unit * 0.6 + off,
y0: b.y0 + unit * 0.6 + off,
w: b.w - 2.0 * (unit * 0.6 + off),
h: b.h - 2.0 * (unit * 0.6 + off),
};
for e in 0..4 {
let (p, _, _) = inner.at([0.0, inner.w, inner.w + inner.h, 2.0 * inner.w + inner.h][e]);
let (q, _, _) = inner.at([inner.w, inner.w + inner.h, 2.0 * inner.w + inner.h, inner.len() - 0.001][e]);
out.push(Prim::capsule(p, q, thick * 0.6, thick * 0.6, vine, vine));
}
}
}
if style == "doubleLine" {
for e in 0..4 {
let (p, _, _) = b.at([0.0, b.w, b.w + b.h, 2.0 * b.w + b.h][e]);
out.push(Prim::ellipse(p, unit * 0.45, unit * 0.45, std::f32::consts::FRAC_PI_4, fl, fl));
}
}
}
"dots" | "scallops" | "snowflakes" | "stars" | "hearts" => {
for k in 0..count {
let s = k as f32 * step;
let (p, t, nn) = b.at(s);
let ta = t.1.atan2(t.0);
match style {
"dots" => {
let r = unit * if k % 2 == 0 { 0.4 } else { 0.22 };
out.push(Prim::ellipse(p, r, r, 0.0, shade([fl[0], fl[1], fl[2]], 1.15), shade([fl[0], fl[1], fl[2]], 0.75)));
}
"scallops" => {
let (c0, _, _) = b.at(s - step * 0.5);
let mid = ((c0.0 + p.0) / 2.0, (c0.1 + p.1) / 2.0);
let n = 8;
for j in 0..n {
let a0 = std::f32::consts::PI * j as f32 / n as f32;
let a1 = std::f32::consts::PI * (j + 1) as f32 / n as f32;
let r = step * 0.5;
let pt = |a: f32| (mid.0 - t.0 * r * a.cos() - nn.0 * r * a.sin(), mid.1 - t.1 * r * a.cos() - nn.1 * r * a.sin());
out.push(Prim::capsule(pt(a0), pt(a1), thick, thick, vine, vine));
}
}
"snowflakes" => {
let r = unit * 0.6;
let c = [0.85, 0.92, 1.0, 1.0];
for j in 0..6 {
let a = ta + j as f32 * std::f32::consts::FRAC_PI_3;
let e = (p.0 + a.cos() * r, p.1 + a.sin() * r);
out.push(Prim::capsule(p, e, (thick * 0.6).max(0.6), (thick * 0.4).max(0.5), c, c));
let m = (p.0 + a.cos() * r * 0.6, p.1 + a.sin() * r * 0.6);
for sd in [-0.7f32, 0.7] {
let b2 = (m.0 + (a + sd).cos() * r * 0.3, m.1 + (a + sd).sin() * r * 0.3);
out.push(Prim::capsule(m, b2, (thick * 0.4).max(0.5), 0.4, c, c));
}
}
}
"stars" => star(&mut out, p, unit * 0.55, 5, if k % 2 == 0 { yellow } else { fl }, ta - std::f32::consts::FRAC_PI_2),
_ => {
// Heart: two lobes and a point towards the inside.
let r = unit * 0.3;
let (tx, ty) = t;
let l = (p.0 - tx * r * 0.8 - nn.0 * r * 0.3, p.1 - ty * r * 0.8 - nn.1 * r * 0.3);
let rr = (p.0 + tx * r * 0.8 - nn.0 * r * 0.3, p.1 + ty * r * 0.8 - nn.1 * r * 0.3);
let tip = (p.0 + nn.0 * r * 1.6, p.1 + nn.1 * r * 1.6);
out.push(Prim::capsule(l, tip, r, 0.6, fl, fl));
out.push(Prim::capsule(rr, tip, r, 0.6, fl, fl));
}
}
}
}
_ => {}
}
// Fade: lower the opacity of ornaments that reach further inside.
let fade = spec.fade.clamp(0.0, 100.0) / 100.0;
if fade > 0.0 {
let cx = canvas.x0 as f32 + canvas.width() as f32 / 2.0;
let cy = canvas.y0 as f32 + canvas.height() as f32 / 2.0;
for p in &mut out {
let c = match p.shape {
Shape::Capsule { a, .. } => a,
Shape::Ellipse { c, .. } => c,
};
let edge = ((c.0 - cx).abs() / (b.w / 2.0)).max((c.1 - cy).abs() / (b.h / 2.0)).clamp(0.0, 1.0);
let k = 1.0 - fade * (1.0 - edge) * 8.0;
p.c0[3] *= k.clamp(0.0, 1.0);
p.c1[3] *= k.clamp(0.0, 1.0);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use photocraft_color::{ColorMode, PixelFormat, SampleType};
#[test]
fn composite_draws_a_disc_and_respects_the_clip() {
let mut s = Surface::new(PixelFormat::new(ColorMode::Rgb, SampleType::U8, true));
let p = [Prim::disc((20.0, 20.0), 8.0, [1.0, 0.0, 0.0, 1.0])];
let used = composite(&mut s, &p, Rect::new(0, 0, 20, 40), None);
assert_eq!(used.x1, 20);
let c = s.pixel(18, 20);
assert!(c[0] > 0.99 && c[3] > 0.99);
assert_eq!(s.pixel(22, 20)[3], 0.0, "clipped");
assert_eq!(s.pixel(2, 2)[3], 0.0);
}
#[test]
fn renderers_are_deterministic_and_seeded() {
let canvas = Rect::new(0, 0, 400, 300);
let a = tree(&TreeSpec::default(), canvas);
assert_eq!(a, tree(&TreeSpec::default(), canvas));
assert_ne!(a, tree(&TreeSpec { seed: 7, ..Default::default() }, canvas));
for k in 1..=34 {
assert!(!tree(&TreeSpec { base_tree_type: k, ..Default::default() }, canvas).is_empty(), "tree {k}");
}
for k in 1..=FRAME_STYLES.len() as u32 {
assert!(!picture_frame(&FrameSpec { frame: k, ..Default::default() }, canvas).is_empty(), "frame {k}");
}
let path = default_flame_path(canvas);
for t in [
FlameType::OneFlameAlongPath,
FlameType::MultipleFlamesAlongPath,
FlameType::MultipleFlamesPathDirections,
FlameType::MultipleFlamesVariousLength,
FlameType::CandleLight,
FlameType::MultipleFlamesOneDirection,
] {
assert!(!flame(&FlameSpec { flame_type: t, ..Default::default() }, std::slice::from_ref(&path)).is_empty(), "{t:?}");
}
}
}