/*
* OrganiComplex
*
* Interactive complex-valued cellular automaton on 2D and 3D grids in search
* of that stuff - emergence, open-endedness, organicity etc.
*
* https://sunkware.org/organicomplex
*
* mediator@sunkware.org
*
* Copyright (c) 2026 Sunkware
*
* OrganiComplex is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OrganiComplex is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OrganiComplex. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::{
base::{
ARGB,
RGB
},
fontset::CFont,
master::{
ERR_ALREADY_SET,
ERR_NOT_SET_CANNOT_RUN,
Master
},
play::Context
};
use super::{
ColorMode,
EditMode
};
const MID: &str = "AutomatonRender";
const TABLINE_COLOR: ARGB = ARGB{a: 0xFF, r: 0x80, g: 0x80, b: 0x80};
const MAGLINE_COLOR: ARGB = ARGB{a: 0x80, r: 0xFF, g: 0xFF, b: 0xFF};
const BRUSHLINE_COLOR: ARGB = ARGB{a: 0x80, r: 0xC0, g: 0xC0, b: 0xC0};
const NBHOOD_GRID_COLOR: ARGB = ARGB{a: 0xFF, r: 0x40, g: 0x40, b: 0x40};
const NBHOOD_ON_COLOR: ARGB = ARGB{a: 0xFF, r: 0xFF, g: 0xFF, b: 0xFF};
const NBHOOD_OFF_COLOR: ARGB = ARGB{a: 0xFF, r: 0, g: 0, b: 0};
const POINTER_COLOR: ARGB = ARGB{a: 0xFF, r: 0xFF, g: 0xFF, b: 0xFF};
const NBHOOD_POINTER_COLOR: ARGB = ARGB{a: 0xFF, r: 0xFF, g: 0, b: 0xFF};
const TEXT_COLOR: RGB = RGB{r: 0xFF, g: 0xFF, b: 0xFF};
const ESCAPE_HINT_COLOR: RGB = RGB{r: 0x80, g: 0x80, b: 0x80};
const TEXT_SHADE: u8 = 0xE0;
const INFO_MARGIN: i32 = 4;
pub enum MRender {
Unset,
Set {
topleft_x: i32,
topleft_y: i32
}
}
fn scale_for_tab(x: f64) -> usize {
(0x200isize + (x as isize)).max(0).min(0x3FF) as usize
}
impl MRender {
pub fn new() -> Box<Self> {
Box::new(Self::Unset)
}
}
impl Master for MRender {
fn id(&self) -> String {
String::from(MID)
}
fn start(&mut self, Context{sys, ..}: &mut Context) -> Result<(), String> {
match self {
&mut Self::Unset => {
let topleft_x: i32 = -(sys.width() >> 1);
let topleft_y: i32 = -(sys.height() >> 1);
*self = Self::Set{topleft_x, topleft_y};
Ok(())
},
&mut Self::Set{..} => Err(format!("{} {}", MID, ERR_ALREADY_SET))
}
}
fn run(&mut self, Context{sys, precalc, fontset, lingua, state, ..}: &mut Context) -> Result<(), String> {
match self {
&mut Self::Set{topleft_x, topleft_y} => {
match &mut state.automaton {
Some(state) => {
if !sys.draw_locked() { // only when new frame is required, to decrease load
sys.clear_screen().unwrap_or(());
let perpetuator = state.neighbourhood.perpetuator();
let cell_size = state.cell_size;
let cells = & state.field.cells;
let fpu = state.pointer_field_u;
let fpv = state.pointer_field_y / cell_size;
let fpw = state.pointer_field_x / cell_size;
let ndv = (state.pointer_nbhood_y / state.nbhood_cell_size - state.nbhood_radius) as isize;
let ndw = (state.pointer_nbhood_x / state.nbhood_cell_size - state.nbhood_radius) as isize;
let mgy = topleft_y + ((state.field.height() - 1 + state.field.width()) as i32) * cell_size;
let mgx = topleft_x + ((state.field.height() - 1 + state.field.length()) as i32) * cell_size;
let reim_to_rgb = match state.color_mode {
ColorMode::HueLum => & precalc.reim_to_huelum_rgb,
ColorMode::Lum => & precalc.reim_to_lum_rgb,
ColorMode::Hue => & precalc.reim_to_hue_rgb
};
sys.draw_line(mgx, topleft_y, mgx, sys.height() >> 1, TABLINE_COLOR).unwrap_or(());
sys.draw_line(topleft_x, mgy, sys.width() >> 1, mgy, TABLINE_COLOR).unwrap_or(());
// Field
if state.show_field {
let clip_max_x = topleft_x + sys.width() + cell_size;
let clip_max_y = topleft_y + sys.height() + cell_size;
let lum_amplif = match state.scale_lum {
false => 1.0,
true => 4.0 / (state.amplification * perpetuator)
} * 512.0;
let alpha_mask: u8 = if cells.len() > 1 {0} else {0xFF};
// Field itself
for (u, layer) in cells.iter().enumerate() {
let rx = topleft_x + ((u as i32) * cell_size);
let mut y = topleft_y + ((u as i32) * cell_size);
for (_v, row) in layer.iter().enumerate() {
let mut x = rx;
for (_w, cell) in row.iter().enumerate() {
let re = scale_for_tab(cell.amph.re * lum_amplif);
let im = scale_for_tab(cell.amph.im * lum_amplif);
// DEBUG (test color wheel)
// let re = (0x200isize + ((((_w as f64) / 256.0 - 1.0) * (0x200 as f64)) as isize)) as usize;
// let im = (0x200isize + (((1.0 - (_v as f64) / 256.0) * (0x200 as f64)) as isize)) as usize;
let color = reim_to_rgb[re][im];
let color = ARGB{a: color.lum() | alpha_mask, r: color.r, g: color.g, b: color.b};
sys.draw_rect_size(x, y, cell_size, cell_size, color, true).unwrap_or(());
x += cell_size;
if x > clip_max_x {
break;
}
}
y += cell_size;
if y > clip_max_y {
break;
}
}
}
// Pointer
sys.draw_rect_size(
topleft_x + (fpu + fpw) * cell_size,
topleft_y + (fpu + fpv) * cell_size,
cell_size,
cell_size,
POINTER_COLOR,
true
)?;
// "Magnifying glass"
if state.show_maglass {
let maglass_radius = state.maglass_radius;
sys.draw_rect_size(
topleft_x + (fpu + fpw - maglass_radius) * cell_size,
topleft_y + (fpu + fpv - maglass_radius) * cell_size,
(2 * maglass_radius + 1) * cell_size,
(2 * maglass_radius + 1) * cell_size,
MAGLINE_COLOR,
false
).unwrap_or(());
let maglass_cell_size = state.maglass_cell_size;
let width_binmask = (state.field.width() - 1) as i32;
let length_binmask = (state.field.length() - 1) as i32;
for dv in -state.maglass_radius..=state.maglass_radius {
for dw in -state.maglass_radius..=state.maglass_radius {
let v = (fpv + dv) & width_binmask;
let w = (fpw + dw) & length_binmask;
let cell = & cells[fpu as usize][v as usize][w as usize];
let re = scale_for_tab(cell.amph.re * lum_amplif);
let im = scale_for_tab(cell.amph.im * lum_amplif);
let color = reim_to_rgb[re][im];
let color = ARGB{a: 0xFF, r: color.r, g: color.g, b: color.b};
sys.draw_rect_size(
mgx + 1 + (dw + maglass_radius) * maglass_cell_size,
mgy + 1 + (dv + maglass_radius) * maglass_cell_size,
maglass_cell_size,
maglass_cell_size,
color,
true
).unwrap_or(());
}
}
sys.draw_rect_size(
mgx + 1 + maglass_radius * maglass_cell_size,
mgy + 1 + maglass_radius * maglass_cell_size,
maglass_cell_size,
maglass_cell_size,
POINTER_COLOR,
false
).unwrap_or(());
if state.brush_radius < maglass_radius {
sys.draw_rect_size(
mgx + 1 + (maglass_radius - state.brush_radius) * maglass_cell_size,
mgy + 1 + (maglass_radius - state.brush_radius) * maglass_cell_size,
(2 * state.brush_radius + 1) * maglass_cell_size,
(2 * state.brush_radius + 1) * maglass_cell_size,
BRUSHLINE_COLOR,
false
).unwrap_or(());
}
}
// Brush
sys.draw_rect_size(
topleft_x + (fpu + fpw - state.brush_radius) * cell_size,
topleft_y + (fpu + fpv - state.brush_radius) * cell_size,
(2 * state.brush_radius + 1) * cell_size,
(2 * state.brush_radius + 1) * cell_size,
BRUSHLINE_COLOR,
false
).unwrap_or(());
}
// Neighbourhood
if state.show_nbhood {
let nbhood_topleft_x = topleft_x + sys.width() - (2 * state.nbhood_radius + 1) * state.nbhood_cell_size;
let nbhood_topleft_y = topleft_y + 1;
// Grid
sys.draw_rect_size(
nbhood_topleft_x - 1,
nbhood_topleft_y - 1,
(2 * state.nbhood_radius + 1) * state.nbhood_cell_size + 1,
(2 * state.nbhood_radius + 1) * state.nbhood_cell_size + 1,
NBHOOD_OFF_COLOR,
true
).unwrap_or(());
for v in 0..=(2 * state.nbhood_radius + 1) {
for w in 0..=(2 * state.nbhood_radius + 1) {
sys.draw_line(
nbhood_topleft_x - 1,
nbhood_topleft_y - 1 + v * state.nbhood_cell_size,
nbhood_topleft_x - 1 + (2 * state.nbhood_radius + 1) * state.nbhood_cell_size + 1,
nbhood_topleft_y - 1 + v * state.nbhood_cell_size,
NBHOOD_GRID_COLOR
).unwrap_or(());
sys.draw_line(
nbhood_topleft_x - 1 + w * state.nbhood_cell_size,
nbhood_topleft_y - 1 + v,
nbhood_topleft_x - 1 + w * state.nbhood_cell_size,
nbhood_topleft_y - 1 + (2 * state.nbhood_radius + 1) * state.nbhood_cell_size + 1,
NBHOOD_GRID_COLOR
).unwrap_or(());
}
}
for &(du, dv, dw) in & state.neighbourhood.0 {
if du == state.nbhood_du {
let v = state.nbhood_radius + (dv as i32);
let w = state.nbhood_radius + (dw as i32);
sys.draw_rect_size(
nbhood_topleft_x + w * state.nbhood_cell_size,
nbhood_topleft_y + v * state.nbhood_cell_size,
state.nbhood_cell_size - 1,
state.nbhood_cell_size - 1,
NBHOOD_ON_COLOR,
true
).unwrap_or(());
}
}
sys.draw_line(
nbhood_topleft_x + state.nbhood_radius * state.nbhood_cell_size,
nbhood_topleft_y + state.nbhood_radius * state.nbhood_cell_size,
nbhood_topleft_x + (state.nbhood_radius + 1) * state.nbhood_cell_size,
nbhood_topleft_y + (state.nbhood_radius + 1) * state.nbhood_cell_size,
NBHOOD_GRID_COLOR
).unwrap_or(());
sys.draw_line(
nbhood_topleft_x + (state.nbhood_radius + 1) * state.nbhood_cell_size,
nbhood_topleft_y + state.nbhood_radius * state.nbhood_cell_size,
nbhood_topleft_x + state.nbhood_radius * state.nbhood_cell_size,
nbhood_topleft_y + (state.nbhood_radius + 1) * state.nbhood_cell_size,
NBHOOD_GRID_COLOR
).unwrap_or(());
sys.draw_rect_size(
nbhood_topleft_x + state.pointer_nbhood_x - 2,
nbhood_topleft_y + state.pointer_nbhood_y - 2,
5,
5,
NBHOOD_POINTER_COLOR,
true
).unwrap_or(());
}
// Palette & brush
if state.show_palette {
let palette_centre_x = topleft_x + sys.width() - 0x80;
let palette_centre_y = topleft_y + sys.height() - 0x80;
for y in -0x80..=0x80 {
let im = (0x200i32 + (y << 2)).max(0).min(0x3FF) as usize;
for x in -0x80..=0x80 {
let re = (0x200i32 + (x << 2)).max(0).min(0x3FF) as usize;
let color = reim_to_rgb[re][im];
sys.draw_pixel_opaque(palette_centre_x + x, palette_centre_y - y, color).unwrap_or(());
}
}
sys.draw_rect_size(
palette_centre_x + ((state.brush.re * 128.0) as i32) - 1,
palette_centre_y - ((state.brush.im * 128.0) as i32) - 1,
3,
3,
POINTER_COLOR,
true
).unwrap_or(());
}
// Parameters & settings
if state.show_settings_stats {
lingua.set_prefix("automaton");
let font = fontset.get(CFont::Info);
let fh = font.height();
let info_x = mgx + INFO_MARGIN;
let mut info_y = topleft_y;
// Dimensions
sys.draw_text(font, format!("{}Z x {}Y x {}X", state.field.height(), state.field.width(), state.field.length()), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh << 1;
// Modes
sys.draw_text(font, format!("{}: {}", lingua.get1_or_echo("edit mode"), if state.edit_mode == EditMode::Field {lingua.get1_or_echo("edit mode field")} else {lingua.get1_or_echo("edit mode neighbourhood")}), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("{}: {}", lingua.get1_or_echo("color mode"),
match state.color_mode {
ColorMode::HueLum => lingua.get1_or_echo("color mode hue luminance"),
ColorMode::Hue => lingua.get1_or_echo("color mode hue only"),
ColorMode::Lum => lingua.get1_or_echo("color mode luminance only")
}), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh << 1;
// Parameters
sys.draw_text(font, format!("{} = {:.3} ({} = {:.7})", lingua.get1_or_echo("amplification"), state.amplification, lingua.get1_or_echo("scalemult"), state.amplification * perpetuator), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("{} = {:.3}", lingua.get1_or_echo("noise"), state.noise), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("{} = {:.3}", lingua.get1_or_echo("synchronicity"), state.synchronicity), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y+= fh << 1;
// Brush
sys.draw_text(font, format!("{}: {} = {}", lingua.get1_or_echo("brush"), lingua.get1_or_echo("radius"), state.brush_radius), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("Re = {:.7}, Im = {:.7}", state.brush.re, state.brush.im), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("Mod = {:.7}, Arg = {:.7}°", state.brush.abs().0, 180.0 * state.brush.arg().0 / std::f64::consts::PI), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
let re = scale_for_tab(state.brush.re * 512.0);
let im = scale_for_tab(state.brush.im * 512.0);
let brush_color = ARGB::from(reim_to_rgb[re][im]);
sys.draw_rect_size(info_x, info_y, 100, fh, brush_color, true).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("{}: {} {}", lingua.get1_or_echo("palette"), lingua.get1_or_echo("show"), if state.show_palette {lingua.get1_or_echo("ON")} else {lingua.get1_or_echo("OFF")}), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh << 1;
// Some settings
sys.draw_text(font, format!("{}: {} {}, {} = {}", lingua.get1_or_echo("field"), lingua.get1_or_echo("show"), if state.show_field {lingua.get1_or_echo("ON")} else {lingua.get1_or_echo("OFF")}, lingua.get1_or_echo("cell size"), state.cell_size), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("{}: {}", lingua.get1_or_echo("field update"), if state.manual_step {lingua.get1_or_echo("field update manual")} else {lingua.get1_or_echo("field update auto")}), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("{}: {}", lingua.get1_or_echo("luminance scaling"), if state.scale_lum {lingua.get1_or_echo("ON")} else {lingua.get1_or_echo("OFF")}), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("{}: {} {}, {} = {}, {} = {}", lingua.get1_or_echo("maglass"), lingua.get1_or_echo("show"), if state.show_maglass {lingua.get1_or_echo("ON")} else {lingua.get1_or_echo("OFF")}, lingua.get1_or_echo("radius"), state.maglass_radius, lingua.get1_or_echo("cell size"), state.maglass_cell_size), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("{}: {}, {} {}, {} {}, {} = {}, {} = {}", lingua.get1_or_echo("neighbourhood"), state.neighbourhood.0.len(), lingua.get1_or_echo("symm"), if state.neighbourhood.is_symmetric() {"+"} else {"–"}, lingua.get1_or_echo("show"), if state.show_nbhood {lingua.get1_or_echo("ON")} else {lingua.get1_or_echo("OFF")}, lingua.get1_or_echo("radius"), state.nbhood_radius, lingua.get1_or_echo("cell size"), state.nbhood_cell_size), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("{} = {:.7}", lingua.get1_or_echo("perpetuator"), perpetuator), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
let info_x = topleft_x + INFO_MARGIN;
let mut info_y = mgy + INFO_MARGIN;
// Probe at field pointer
if state.show_field {
if (fpu >= 0) && (fpu < (state.field.height() as i32)) && (fpv >= 0) && (fpv < (state.field.width() as i32)) && (fpw >= 0) && (fpw < (state.field.length() as i32)) {
let amph = cells[fpu as usize][fpv as usize][fpw as usize].amph;
sys.draw_text(font, format!("{} ({}, {}, {})", lingua.get1_or_echo("field cell"), fpu, fpv, fpw), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("Re = {:.7}, Im = {:.7}", amph.re, amph.im), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("Mod = {:.7}, Arg = {:.7}°", amph.abs().0, 180.0 * amph.arg().0 / std::f64::consts::PI), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh << 1;
}
}
// Probe at neighbourhood pointer
if state.show_nbhood {
let triple = (state.nbhood_du, ndv, ndw);
sys.draw_text(font, format!("{} ({}, {}, {}) : {}", lingua.get1_or_echo("neighbourhood cell"), state.nbhood_du, ndv, ndw, if state.neighbourhood.0.contains(&triple) {lingua.get1_or_echo("ON")} else {lingua.get1_or_echo("OFF")}), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh << 1;
}
// Some statistics
sys.draw_text(font, format!("{} = {}", lingua.get1_or_echo("step"), state.field.step), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("{} = {:.3}", lingua.get1_or_echo("steps-per-sec"), state.sps()), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
info_y += fh;
sys.draw_text(font, format!("{} = {:.7}", lingua.get1_or_echo("energy"), state.field.energy), 0, TEXT_COLOR, TEXT_SHADE, 0, info_x, info_y).unwrap_or(());
// Escape hint
sys.draw_text(font, lingua.get1_or_echo("escape hint"), 0, ESCAPE_HINT_COLOR, TEXT_SHADE, 0, topleft_x, topleft_y + sys.height() - fh).unwrap_or(());
lingua.clear_prefix();
}
}
Ok(())
},
None => {
Err(String::from("automaton state is absent"))
}
}
},
&mut Self::Unset => Err(format!("{} {}", MID, ERR_NOT_SET_CANNOT_RUN))
}
}
}