use uzor::render::{RenderContext, TextAlign, TextBaseline};
#[derive(Clone, Copy)]
pub struct Coord {
pub x: usize,
pub y: usize,
pub index: usize,
}
#[derive(Clone, Copy)]
pub struct GridContext {
pub frame: u64,
pub time: f64,
pub cols: usize,
pub rows: usize,
pub aspect: f64,
}
#[derive(Clone, Copy, Default)]
pub struct Cursor {
pub x: f64,
pub y: f64,
pub pressed: bool,
pub inside: bool,
pub intensity: f64,
}
#[derive(Clone, Copy)]
pub struct Cell {
pub ch: char,
pub color: [u8; 3],
pub alpha: f32,
pub scale: f32,
}
impl Default for Cell {
fn default() -> Self {
Self { ch: ' ', color: [244, 244, 245], alpha: 1.0, scale: 1.0 }
}
}
pub trait CellShader {
fn main(&self, coord: Coord, ctx: &GridContext, cursor: &Cursor) -> Cell;
}
pub struct AsciiGrid {
cols: usize,
rows: usize,
buffer: Vec<Cell>,
frame: u64,
cursor: Cursor,
}
impl AsciiGrid {
pub fn new(cols: usize, rows: usize) -> Self {
Self {
cols,
rows,
buffer: vec![Cell::default(); cols * rows],
frame: 0,
cursor: Cursor::default(),
}
}
pub fn cols(&self) -> usize {
self.cols
}
pub fn rows(&self) -> usize {
self.rows
}
pub fn set_cursor(&mut self, cursor: Cursor) {
self.cursor = cursor;
}
pub fn step<S: CellShader>(&mut self, shader: &S, time: f64, aspect: f64) {
self.frame = self.frame.wrapping_add(1);
let ctx = GridContext {
frame: self.frame,
time,
cols: self.cols,
rows: self.rows,
aspect,
};
for y in 0..self.rows {
for x in 0..self.cols {
let index = y * self.cols + x;
self.buffer[index] = shader.main(Coord { x, y, index }, &ctx, &self.cursor);
}
}
}
pub fn cell(&self, x: usize, y: usize) -> Cell {
self.buffer[y * self.cols + x]
}
pub fn for_each<F>(&self, mut f: F)
where
F: FnMut(usize, usize, Cell),
{
for y in 0..self.rows {
for x in 0..self.cols {
f(x, y, self.cell(x, y));
}
}
}
pub fn render(&self, ctx: &mut dyn RenderContext, ox: f64, oy: f64, cell_w: f64, cell_h: f64) {
use std::fmt::Write;
ctx.set_text_align(TextAlign::Center);
ctx.set_text_baseline(TextBaseline::Middle);
let mut col = String::with_capacity(20);
let mut chbuf = [0u8; 4];
let mut cur_alpha = 1.0_f64;
let mut cur_fs = -1_i32;
ctx.set_global_alpha(1.0);
let base = cell_h.max(4.0);
for y in 0..self.rows {
for x in 0..self.cols {
let c = self.cell(x, y);
if c.ch == ' ' || c.alpha <= 0.01 || c.scale <= 0.06 {
continue;
}
let fs = ((base * c.scale as f64).clamp(3.0, base * 1.7)).round() as i32;
if fs != cur_fs {
col.clear();
let _ = write!(col, "{}px monospace", fs);
ctx.set_font(&col);
cur_fs = fs;
}
let a = c.alpha as f64;
if (a - cur_alpha).abs() > 0.004 {
ctx.set_global_alpha(a);
cur_alpha = a;
}
col.clear();
let _ = write!(col, "rgb({},{},{})", c.color[0], c.color[1], c.color[2]);
ctx.set_fill_color(&col);
let s = c.ch.encode_utf8(&mut chbuf);
ctx.fill_text(
s,
ox + x as f64 * cell_w + cell_w / 2.0,
oy + y as f64 * cell_h + cell_h / 2.0,
);
}
}
ctx.set_global_alpha(1.0);
}
}
pub fn sd_circle(px: f64, py: f64, r: f64) -> f64 {
(px * px + py * py).sqrt() - r
}
pub fn sd_box(px: f64, py: f64, bx: f64, by: f64) -> f64 {
let dx = px.abs() - bx;
let dy = py.abs() - by;
let ox = dx.max(0.0);
let oy = dy.max(0.0);
(ox * ox + oy * oy).sqrt() + dx.max(dy).min(0.0)
}
pub const DENSITY: &[u8] = b" .:-=+*oOX#%@";
pub fn density_char(v: f64) -> char {
let n = DENSITY.len();
let i = ((v.clamp(0.0, 1.0)) * (n - 1) as f64).round() as usize;
DENSITY[i.min(n - 1)] as char
}
pub fn hsl(h: f64, s: f64, l: f64) -> [u8; 3] {
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let hp = ((h % 360.0) + 360.0) % 360.0 / 60.0;
let x = c * (1.0 - ((hp % 2.0) - 1.0).abs());
let (r1, g1, b1) = match hp as i32 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
let m = l - c / 2.0;
[
(((r1 + m) * 255.0).clamp(0.0, 255.0)) as u8,
(((g1 + m) * 255.0).clamp(0.0, 255.0)) as u8,
(((b1 + m) * 255.0).clamp(0.0, 255.0)) as u8,
]
}
pub struct GlitchLetter {
pub cols: usize,
pub rows: usize,
mask: Vec<bool>,
rest: char,
glitch_chars: Vec<char>,
base: [u8; 3],
}
impl GlitchLetter {
pub fn from_rows(rows_src: &[&str], rest: char, base: [u8; 3]) -> Self {
let rows = rows_src.len();
let cols = rows_src.iter().map(|r| r.chars().count()).max().unwrap_or(0);
let mut mask = vec![false; cols * rows];
for (y, line) in rows_src.iter().enumerate() {
for (x, c) in line.chars().enumerate() {
mask[y * cols + x] = c == 'X';
}
}
Self {
cols,
rows,
mask,
rest,
glitch_chars: "#@%&$*/\\|=+<>?".chars().collect(),
base,
}
}
pub fn letter_m() -> Self {
Self::from_rows(
&["X...X", "XX.XX", "X.X.X", "X.X.X", "X...X", "X...X", "X...X"],
'#',
[251, 178, 106],
)
}
}
impl CellShader for GlitchLetter {
fn main(&self, coord: Coord, ctx: &GridContext, cursor: &Cursor) -> Cell {
let on = self.mask.get(coord.index).copied().unwrap_or(false);
let it = cursor.intensity.clamp(0.0, 1.0);
let spark = !on && it > 0.15 && fastrand::f64() < 0.02 * it;
if !on && !spark {
return Cell { ch: ' ', color: self.base, alpha: 1.0, scale: 1.0 };
}
let swap = fastrand::f64() < 0.55 * it;
let ch = if swap && !self.glitch_chars.is_empty() {
self.glitch_chars[fastrand::usize(0..self.glitch_chars.len())]
} else {
self.rest
};
let color = if it <= 0.001 {
self.base
} else {
let hue = (ctx.time * 120.0 + coord.index as f64 * 16.0 + it * 200.0) % 360.0;
let cyc = hsl(hue, 0.85, 0.62);
let mix = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * it) as u8;
[mix(self.base[0], cyc[0]), mix(self.base[1], cyc[1]), mix(self.base[2], cyc[2])]
};
Cell { ch, color, alpha: if spark { 0.6 } else { 1.0 }, scale: 1.0 }
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use uzor_export::{render_to_png, ExportSpec};
use super::{AsciiGrid, Cursor, GlitchLetter};
use crate::ascii::{draw_ascii_grid, AsciiGridStyle};
const CELL: f64 = 24.0;
const WIDTH: u32 = 200;
const HEIGHT: u32 = 260;
fn out_dir() -> PathBuf {
PathBuf::from(r"C:\Users\VA PC\CODING\ML_TRADING\nemo\uzor\out")
}
fn write_proof_png(name: &str, bytes: &[u8]) {
let dir = out_dir();
std::fs::create_dir_all(&dir).expect("create uzor/out/ proof directory");
std::fs::write(dir.join(name), bytes).expect("write proof PNG");
}
fn decoded_png_dims(bytes: &[u8]) -> (u32, u32) {
let decoder = png::Decoder::new(bytes);
let reader = decoder.read_info().expect("valid PNG header");
let info = reader.info();
(info.width, info.height)
}
#[test]
fn glitch_letter_at_a_fixed_seed_and_intensity_renders_to_a_valid_png() {
fastrand::seed(20260716);
let shader = GlitchLetter::letter_m();
let mut grid = AsciiGrid::new(shader.cols, shader.rows);
grid.set_cursor(Cursor { x: 2.0, y: 3.0, pressed: false, inside: true, intensity: 0.65 });
grid.step(&shader, 0.0, 1.0);
let spec = ExportSpec { width_px: WIDTH, height_px: HEIGHT, dpr: 1.0, background: Some([11, 11, 14, 255]) };
let bytes = render_to_png(&spec, |ctx| {
draw_ascii_grid(ctx, (10.0, 10.0), &grid, AsciiGridStyle::square(CELL));
})
.unwrap_or_else(|e| panic!("glitch letter proof render should succeed: {e}"));
assert_eq!(decoded_png_dims(&bytes), (WIDTH, HEIGHT));
write_proof_png("text_p4_glitch.png", &bytes);
}
}