use noise::{Fbm, MultiFractal, Perlin};
use crate::{
generator::{TextureError, TextureGenerator, TextureMap, Workspace, validate_dimensions},
noise::{ToroidalNoise, normalize, sample_grid_into},
surface::{SurfaceCell, SurfaceSample, generate_surface},
};
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum EncausticPattern {
Checkerboard,
Octagon,
Diamond,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct EncausticConfig {
pub seed: u32,
pub scale: f64,
pub pattern: EncausticPattern,
pub grout_width: f64,
pub glaze_roughness: f64,
pub color_a: [f32; 3],
pub color_b: [f32; 3],
pub color_grout: [f32; 3],
pub normal_strength: f32,
}
impl Default for EncausticConfig {
fn default() -> Self {
Self {
seed: 47,
scale: 5.0,
pattern: EncausticPattern::Octagon,
grout_width: 0.06,
glaze_roughness: 0.04,
color_a: [0.72, 0.38, 0.22],
color_b: [0.22, 0.35, 0.65],
color_grout: [0.82, 0.80, 0.75],
normal_strength: 3.0,
}
}
}
pub struct EncausticGenerator {
config: EncausticConfig,
glaze_noise: ToroidalNoise<Fbm<Perlin>>,
}
impl EncausticGenerator {
pub fn new(config: EncausticConfig) -> Self {
let glaze_fbm: Fbm<Perlin> = Fbm::new(config.seed).set_octaves(4);
let glaze_noise = ToroidalNoise::new(glaze_fbm, config.scale * 1.5);
Self {
config,
glaze_noise,
}
}
}
struct EncausticCell<'a> {
config: &'a EncausticConfig,
glaze_grid: &'a [f64],
scale_f: f64,
grout_half: f64,
width: usize,
}
impl SurfaceCell for EncausticCell<'_> {
fn sample(&self, x: u32, y: u32, u: f64, v: f64) -> SurfaceSample {
let c = self.config;
let idx = y as usize * self.width + x as usize;
let cell_u = u * self.scale_f;
let cell_v = v * self.scale_f;
let ci = cell_u.floor() as i64;
let cj = cell_v.floor() as i64;
let cx = cell_u.fract() - 0.5;
let cy = cell_v.fract() - 0.5;
let glaze = normalize(self.glaze_grid[idx]);
let region = classify(&c.pattern, cx, cy, ci, cj, self.grout_half);
let (color, h_val, rough_val) = match region {
Region::TileA | Region::TileB => {
let base = match region {
Region::TileA => &c.color_a,
_ => &c.color_b,
};
let perturb = (glaze - 0.5) * c.glaze_roughness * 0.6;
let color = [
(base[0] + perturb as f32).clamp(0.0, 1.0),
(base[1] + perturb as f32).clamp(0.0, 1.0),
(base[2] + perturb as f32).clamp(0.0, 1.0),
];
(color, 0.85 + glaze * 0.15, 0.20 + glaze * 0.05)
}
Region::Grout => (c.color_grout, 0.0_f64, 0.85_f64),
};
SurfaceSample::matte(h_val, color, rough_val as f32)
}
}
impl EncausticGenerator {
fn generate_inner(
&self,
width: u32,
height: u32,
mut ws: Option<&mut Workspace>,
) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let c = &self.config;
let mut glaze_grid = ws.as_deref_mut().map_or_else(Vec::new, |w| w.take_grid());
sample_grid_into(&self.glaze_noise, width, height, &mut glaze_grid);
let cell = EncausticCell {
config: c,
glaze_grid: &glaze_grid,
scale_f: c.scale.round().max(1.0),
grout_half: (c.grout_width * 0.5).clamp(0.0, 0.49),
width: width as usize,
};
let result = generate_surface(width, height, c.normal_strength, ws.as_deref_mut(), &cell);
if let Some(ws) = ws {
ws.return_grid(glaze_grid);
}
result
}
}
impl TextureGenerator for EncausticGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
self.generate_inner(width, height, None)
}
fn generate_with_workspace(
&self,
width: u32,
height: u32,
workspace: &mut Workspace,
) -> Result<TextureMap, TextureError> {
self.generate_inner(width, height, Some(workspace))
}
}
enum Region {
TileA,
TileB,
Grout,
}
fn classify(
pattern: &EncausticPattern,
cx: f64,
cy: f64,
ci: i64,
cj: i64,
grout_half: f64,
) -> Region {
match pattern {
EncausticPattern::Checkerboard => classify_checkerboard(cx, cy, ci, cj, grout_half),
EncausticPattern::Octagon => classify_octagon(cx, cy, grout_half),
EncausticPattern::Diamond => classify_diamond(cx, cy, grout_half),
}
}
fn classify_checkerboard(cx: f64, cy: f64, ci: i64, cj: i64, grout_half: f64) -> Region {
if cx.abs() > 0.5 - grout_half || cy.abs() > 0.5 - grout_half {
return Region::Grout;
}
if (ci + cj).rem_euclid(2) == 0 {
Region::TileA
} else {
Region::TileB
}
}
fn classify_octagon(cx: f64, cy: f64, grout_half: f64) -> Region {
let cut = 0.22_f64;
let ax = cx.abs();
let ay = cy.abs();
let in_grout_border = ax > 0.5 - grout_half || ay > 0.5 - grout_half;
let corner_threshold = 0.5 - cut - grout_half;
let in_corner = ax > corner_threshold && ay > corner_threshold;
let oct_inner = 0.5 - grout_half; let in_oct_square = ax < oct_inner && ay < oct_inner;
let diagonal_limit = (0.5 - grout_half) + (0.5 - grout_half - cut);
let in_oct = in_oct_square && (ax + ay) < diagonal_limit && !in_corner;
if in_grout_border {
Region::Grout
} else if in_corner {
Region::TileB
} else if in_oct {
Region::TileA
} else {
Region::Grout
}
}
fn classify_diamond(cx: f64, cy: f64, grout_half: f64) -> Region {
use std::f64::consts::FRAC_1_SQRT_2;
const INV_SQRT2: f64 = FRAC_1_SQRT_2;
let rx = (cx + cy) * INV_SQRT2;
let ry = (cx - cy) * INV_SQRT2;
let half = 0.5 * INV_SQRT2;
let grout_r = grout_half * INV_SQRT2;
if rx.abs() < half - grout_r && ry.abs() < half - grout_r {
Region::TileA
} else {
Region::Grout
}
}