From 3b828cbe55096c0ff07bb7715502e8919c963c13 Mon Sep 17 00:00:00 2001 From: Juergen Stuber Date: Fri, 13 Mar 2026 14:02:01 +0100 Subject: [PATCH] Add bitart app --- README.txt | 3 + src/bin/bitart/expression.rs | 301 +++++++++++++++++++++++++++++++++++ src/bin/bitart/main.rs | 159 ++++++++++++++++++ 3 files changed, 463 insertions(+) create mode 100644 src/bin/bitart/expression.rs create mode 100644 src/bin/bitart/main.rs diff --git a/README.txt b/README.txt index 83d905d..6216eee 100644 --- a/README.txt +++ b/README.txt @@ -34,6 +34,9 @@ colorcode - Show a color coded resistor thiele - Thieles talmønstre, quadratic residues of Gaussian integers modulo parameter is the number of seconds between pattern changes +bitart - Evaluate random expressions over x and y (with thanks to suetanvil) + parameter is the number of seconds between pattern changes + Compile with "cargo build --release". diff --git a/src/bin/bitart/expression.rs b/src/bin/bitart/expression.rs new file mode 100644 index 0000000..b5ef131 --- /dev/null +++ b/src/bin/bitart/expression.rs @@ -0,0 +1,301 @@ +use core::fmt; + +use std::rc::Rc; + +use rand::Rng; + +#[derive(Clone, Debug)] +pub struct Env { + pub x: i64, + pub y: i64, +} + +#[derive(Clone, Copy, Debug)] +pub struct Literal(i64); +impl Literal { + fn eval(&self) -> i64 { + self.0 + } +} +impl fmt::Display for Literal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Clone, Copy, Debug)] +pub enum Variable { + X, + Y, +} +impl Variable { + const VALUES: &[Variable] = &[Variable::X, Variable::Y]; + + fn random(rng: &mut R) -> Variable { + let values = Self::VALUES; + let i = rng.random_range(0..values.len()); + values[i] + } + fn eval(&self, env: &Env) -> i64 { + match self { + Variable::X => env.x, + Variable::Y => env.y, + } + } +} +impl fmt::Display for Variable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Variable::X => write!(f, "x"), + Variable::Y => write!(f, "y"), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum UnaryOperator { + Negation, + Complement, +} + +impl UnaryOperator { + const VALUES: &[UnaryOperator] = &[UnaryOperator::Negation, UnaryOperator::Complement]; + + fn random(rng: &mut R) -> UnaryOperator { + let values = Self::VALUES; + let i = rng.random_range(0..values.len()); + values[i] + } + fn apply(&self, operand: i64) -> i64 { + match self { + UnaryOperator::Negation => -operand, + UnaryOperator::Complement => !operand, + } + } +} + +impl fmt::Display for UnaryOperator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UnaryOperator::Negation => write!(f, "-"), + // We use C syntax for `Display`. + UnaryOperator::Complement => write!(f, "~"), + } + } +} + +#[derive(Clone, Debug)] +pub struct UnaryOperation { + operator: UnaryOperator, + operand: Expression, +} +impl UnaryOperation { + fn eval(&self, env: &Env) -> i64 { + self.operator.apply(self.operand.eval(env)) + } +} +impl fmt::Display for UnaryOperation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", self.operator, self.operand) + } +} + +#[derive(Clone, Copy, Debug)] +pub enum BinaryOperator { + Add, + Sub, + Mul, + Div, + Rem, + And, + Or, + Xor, +} +impl BinaryOperator { + const VALUES: &[BinaryOperator] = &[ + BinaryOperator::Add, + BinaryOperator::Sub, + BinaryOperator::Mul, + BinaryOperator::Div, + BinaryOperator::Rem, + BinaryOperator::And, + BinaryOperator::Or, + BinaryOperator::Xor, + ]; + + fn random(rng: &mut R) -> BinaryOperator { + let values = Self::VALUES; + let i = rng.random_range(0..values.len()); + values[i] + } + fn apply(&self, operand0: i64, operand1: i64) -> i64 { + fn safe_div(operand0: i64, operand1: i64) -> i64 { + if operand1 != 0 { + operand0 / operand1 + } else { + 0 + } + } + fn safe_rem(operand0: i64, operand1: i64) -> i64 { + if operand1 != 0 { + operand0 % operand1 + } else { + 0 + } + } + match self { + BinaryOperator::Add => operand0 + operand1, + BinaryOperator::Sub => operand0 - operand1, + BinaryOperator::Mul => operand0 * operand1, + BinaryOperator::Div => safe_div(operand0, operand1), + BinaryOperator::Rem => safe_rem(operand0, operand1), + BinaryOperator::And => operand0 & operand1, + BinaryOperator::Or => operand0 | operand1, + BinaryOperator::Xor => operand0 ^ operand1, + } + } +} +impl fmt::Display for BinaryOperator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BinaryOperator::Add => write!(f, "+"), + BinaryOperator::Sub => write!(f, "-"), + BinaryOperator::Mul => write!(f, "*"), + BinaryOperator::Div => write!(f, "/"), + BinaryOperator::Rem => write!(f, "%"), + BinaryOperator::And => write!(f, "&"), + BinaryOperator::Or => write!(f, "|"), + BinaryOperator::Xor => write!(f, "^"), + } + } +} + +#[derive(Clone, Debug)] +pub struct BinaryOperation { + operator: BinaryOperator, + operands: [Expression; 2], +} +impl BinaryOperation { + fn eval(&self, env: &Env) -> i64 { + self.operator + .apply(self.operands[0].eval(env), self.operands[1].eval(env)) + } +} +impl fmt::Display for BinaryOperation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "({} {} {})", + self.operands[0], self.operator, self.operands[1] + ) + } +} + +#[derive(Clone, Debug)] +pub enum InnerExpression { + Literal(Literal), + Variable(Variable), + UnaryOperation(UnaryOperation), + BinaryOperation(BinaryOperation), +} +impl InnerExpression { + pub fn eval(&self, env: &Env) -> i64 { + match self { + InnerExpression::Literal(literal) => literal.eval(), + InnerExpression::Variable(variable) => variable.eval(env), + InnerExpression::UnaryOperation(op) => op.eval(env), + InnerExpression::BinaryOperation(op) => op.eval(env), + } + } +} +impl fmt::Display for InnerExpression { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InnerExpression::Literal(literal) => write!(f, "{}", literal), + InnerExpression::Variable(variable) => write!(f, "{}", variable), + InnerExpression::UnaryOperation(op) => write!(f, "{}", op), + InnerExpression::BinaryOperation(op) => write!(f, "{}", op), + } + } +} + +pub type Expression = Rc; + +fn literal(n: i64) -> Expression { + Rc::new(InnerExpression::Literal(Literal(n))) +} +fn variable(v: Variable) -> Expression { + Rc::new(InnerExpression::Variable(v)) +} +fn unary_operation(operator: UnaryOperator, operand: Expression) -> Expression { + Rc::new(InnerExpression::UnaryOperation(UnaryOperation { + operator, + operand, + })) +} +fn binary_operation( + operator: BinaryOperator, + operand0: Expression, + operand1: Expression, +) -> Expression { + let operands = [operand0, operand1]; + Rc::new(InnerExpression::BinaryOperation(BinaryOperation { + operator, + operands, + })) +} + +#[derive(Clone, Debug)] +pub struct RandomExpressionBuilder { + unary_rate: f64, + variable_rate: f64, + max_literal: i64, + depth: usize, +} +impl RandomExpressionBuilder { + pub fn build(rng: &mut R) -> Expression { + let builder = Self::new(); + builder.build_recursive(rng, builder.depth, true) + } + fn new() -> RandomExpressionBuilder { + // Default values + let unary_rate = 0.3; + let variable_rate = 0.5; + let max_literal = 24; + let depth = 3; + RandomExpressionBuilder { + unary_rate, + variable_rate, + max_literal, + depth, + } + } + fn build_recursive(&self, rng: &mut R, depth: usize, left: bool) -> Expression { + if depth == 0 { + self.build_leaf(rng, left) + } else if rng.random::() < self.unary_rate { + self.build_unary(rng, depth) + } else { + self.build_binary(rng, depth) + } + } + fn build_leaf(&self, rng: &mut R, left: bool) -> Expression { + // Force a variable in a left leaf. + if left || rng.random::() < self.variable_rate { + variable(Variable::random(rng)) + } else { + literal(rng.random_range(1..=self.max_literal)) + } + } + fn build_unary(&self, rng: &mut R, depth: usize) -> Expression { + let op = UnaryOperator::random(rng); + let arg = self.build_recursive(rng, depth - 1, true); + unary_operation(op, arg) + } + fn build_binary(&self, rng: &mut R, depth: usize) -> Expression { + let op = BinaryOperator::random(rng); + let arg0 = self.build_recursive(rng, depth - 1, true); + let arg1 = self.build_recursive(rng, depth - 1, false); + binary_operation(op, arg0, arg1) + } +} diff --git a/src/bin/bitart/main.rs b/src/bin/bitart/main.rs new file mode 100644 index 0000000..31a0d82 --- /dev/null +++ b/src/bin/bitart/main.rs @@ -0,0 +1,159 @@ +use std::collections::HashMap; +use std::env::args; +use std::io::stdout; +use std::io::Write; +use std::thread::sleep; +use std::time::Duration; + +use rand::rng; + +use lowdim::bb2d; +use lowdim::p2d; +use lowdim::Array2d; +use lowdim::BBox2d; +use lowdim::Point2d; + +use pixelfoo_apps::color::Color; + +mod expression; +use expression::Env; +use expression::RandomExpressionBuilder; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Square { + Common, + Uncommon, +} +impl Square { + fn color(&self) -> Color { + match self { + Square::Common => Color::new(0xd2, 0xd4, 0xbc), + Square::Uncommon => Color::black(), + } + } +} + +#[derive(Clone, Debug)] +struct Board { + map: Array2d, +} +impl Board { + pub fn with(bbox: BBox2d, f: impl FnMut(Point2d) -> Square) -> Board { + let map = Array2d::with(bbox, f); + Board { map } + } + pub fn bbox(&self) -> BBox2d { + self.map.bbox() + } +} + +fn send( + w: &mut T, + old_board: &Board, + new_board: &Board, + alpha: f64, +) -> std::io::Result<()> { + for y in old_board.bbox().y_range() { + for x in old_board.bbox().x_range() { + let old_color = old_board.map[p2d(x, y)].color(); + let new_color = new_board.map[p2d(x, y)].color(); + let color = old_color.interpolate(new_color, alpha); + + w.write_all(&color.rgb())?; + } + } + w.flush() +} + +const DEFAULT_ARG: u64 = 10; + +fn main() -> std::io::Result<()> { + let args = args().collect::>(); + eprintln!("executing {}", args[0]); + + let x_size = args[1].parse::().unwrap(); + let y_size = args[2].parse::().unwrap(); + let arg = if let Some(s) = args.get(3) { + s.parse::().unwrap_or(DEFAULT_ARG) + } else { + DEFAULT_ARG + }; + eprintln!("screen size {}x{}, arg {}", x_size, y_size, arg); + + let mut rng = rng(); + let bbox = bb2d(0..x_size, 0..y_size); + + let min_percent = 30; + let max_percent = 70; + + let frames_per_second = 25; + let delay = Duration::from_millis(1000 / frames_per_second); + let frame_seconds = if arg > 0 { arg } else { DEFAULT_ARG }; + let frame_time_count = frame_seconds * frames_per_second; + + let fade_time_count = (2 * frames_per_second).min(frame_time_count / 3); + let fade_alpha_step = 1.0 / (fade_time_count as f64); + + let mut old_board = Board::with(bbox, |_p| Square::Uncommon); + let mut new_board = Board::with(bbox, |_p| Square::Uncommon); + + let mut time_count = frame_time_count; + loop { + if time_count >= frame_time_count { + time_count = 0; + + // Pick a random expression. + loop { + let expression = RandomExpressionBuilder::build(&mut rng); + let values = Array2d::with(bbox, |p| { + let x = p.x(); + let y = p.y(); + let env = Env { x, y }; + expression.eval(&env) + }); + let mut histogram = HashMap::new(); + for p in bbox { + let entry = histogram.entry(values[p]).or_insert(0); + *entry += 1; + } + + // Find the most common value and its count. + let mut max_count = 0; + let mut max_count_value = 0; + for (value, count) in histogram { + if count > max_count { + max_count = count; + max_count_value = value; + } + } + + let percent = 100 * max_count / bbox.area(); + + if (min_percent..=max_percent).contains(&percent) { + eprintln!("chose expression {expression}"); + + old_board = new_board; + new_board = Board::with(bbox, |p| { + // Use the `onebit` scheme for now. + if values[p] == max_count_value { + Square::Common + } else { + Square::Uncommon + } + }); + break; + } + } + } + + let alpha = ((time_count as f64) * fade_alpha_step).min(1.0); + + let mut buf = Vec::with_capacity((x_size * y_size * 3) as usize); + send(&mut buf, &old_board, &new_board, alpha)?; + stdout().write_all(&buf)?; + stdout().flush()?; + + sleep(delay); + time_count += 1; + } +}