pub struct PhotonBuf {
pub r: Vec<f32>,
pub g: Vec<f32>,
pub b: Vec<f32>,
pub width: usize,
pub height: usize,
}
impl PhotonBuf {
pub fn new(width: usize, height: usize) -> Self {
let n = width * height;
Self {
r: vec![0.0_f32; n],
g: vec![0.0_f32; n],
b: vec![0.0_f32; n],
width,
height,
}
}
pub fn resize(&mut self, width: usize, height: usize) {
let n = width * height;
self.r.resize(n, 0.0);
self.g.resize(n, 0.0);
self.b.resize(n, 0.0);
self.r[..n].fill(0.0);
self.g[..n].fill(0.0);
self.b[..n].fill(0.0);
self.width = width;
self.height = height;
}
#[inline]
pub fn clear(&mut self) {
self.r.fill(0.0);
self.g.fill(0.0);
self.b.fill(0.0);
}
#[inline]
pub unsafe fn add_unchecked(&mut self, idx: usize, r: f32, g: f32, b: f32) {
*self.r.get_unchecked_mut(idx) += r;
*self.g.get_unchecked_mut(idx) += g;
*self.b.get_unchecked_mut(idx) += b;
}
#[inline]
pub fn add(&mut self, idx: usize, r: f32, g: f32, b: f32) {
if idx < self.r.len() {
self.r[idx] += r;
self.g[idx] += g;
self.b[idx] += b;
}
}
pub fn flow(&mut self, strength: f32) {
if strength <= 0.0 {
return;
}
let w = self.width;
let h = self.height;
let s = strength.clamp(0.0, 1.0);
let k = s * 0.5; let c = 1.0 - s;
for row in 0..h {
let base = row * w;
for col in 0..w {
let i = base + col;
let l_val_r = if col > 0 { unsafe { *self.r.get_unchecked(i - 1) } } else { 0.0 };
let r_val_r = if col + 1 < w { unsafe { *self.r.get_unchecked(i + 1) } } else { 0.0 };
let l_val_g = if col > 0 { unsafe { *self.g.get_unchecked(i - 1) } } else { 0.0 };
let r_val_g = if col + 1 < w { unsafe { *self.g.get_unchecked(i + 1) } } else { 0.0 };
let l_val_b = if col > 0 { unsafe { *self.b.get_unchecked(i - 1) } } else { 0.0 };
let r_val_b = if col + 1 < w { unsafe { *self.b.get_unchecked(i + 1) } } else { 0.0 };
unsafe {
*self.r.get_unchecked_mut(i) =
*self.r.get_unchecked(i) * c + (l_val_r + r_val_r) * k;
*self.g.get_unchecked_mut(i) =
*self.g.get_unchecked(i) * c + (l_val_g + r_val_g) * k;
*self.b.get_unchecked_mut(i) =
*self.b.get_unchecked(i) * c + (l_val_b + r_val_b) * k;
}
}
}
for col in 0..w {
for row in 0..h {
let i = row * w + col;
let u_val_r = if row > 0 { unsafe { *self.r.get_unchecked(i - w) } } else { 0.0 };
let d_val_r = if row + 1 < h { unsafe { *self.r.get_unchecked(i + w) } } else { 0.0 };
let u_val_g = if row > 0 { unsafe { *self.g.get_unchecked(i - w) } } else { 0.0 };
let d_val_g = if row + 1 < h { unsafe { *self.g.get_unchecked(i + w) } } else { 0.0 };
let u_val_b = if row > 0 { unsafe { *self.b.get_unchecked(i - w) } } else { 0.0 };
let d_val_b = if row + 1 < h { unsafe { *self.b.get_unchecked(i + w) } } else { 0.0 };
unsafe {
*self.r.get_unchecked_mut(i) =
*self.r.get_unchecked(i) * c + (u_val_r + d_val_r) * k;
*self.g.get_unchecked_mut(i) =
*self.g.get_unchecked(i) * c + (u_val_g + d_val_g) * k;
*self.b.get_unchecked_mut(i) =
*self.b.get_unchecked(i) * c + (u_val_b + d_val_b) * k;
}
}
}
}
pub fn drain_toon(&self, buf: &mut [u32], bands: u32) {
let n = self.r.len().min(buf.len());
if bands < 2 {
for i in 0..n {
if self.r[i] <= 0.0 && self.g[i] <= 0.0 && self.b[i] <= 0.0 {
continue; }
let r = (self.r[i].clamp(0.0, 1.0) * 255.0) as u32;
let g = (self.g[i].clamp(0.0, 1.0) * 255.0) as u32;
let b = (self.b[i].clamp(0.0, 1.0) * 255.0) as u32;
buf[i] = (r << 16) | (g << 8) | b;
}
return;
}
for i in 0..n {
unsafe {
if *self.r.get_unchecked(i) <= 0.0
&& *self.g.get_unchecked(i) <= 0.0
&& *self.b.get_unchecked(i) <= 0.0
{
continue;
}
let r = snap(*self.r.get_unchecked(i), bands);
let g = snap(*self.g.get_unchecked(i), bands);
let b = snap(*self.b.get_unchecked(i), bands);
*buf.get_unchecked_mut(i) =
((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
}
}
}
pub fn drain_additive(&self, buf: &mut [u32]) {
let n = self.r.len().min(buf.len());
for i in 0..n {
if self.r[i] <= 0.0 && self.g[i] <= 0.0 && self.b[i] <= 0.0 {
continue;
}
let r = (self.r[i].clamp(0.0, 1.0) * 255.0) as u32;
let g = (self.g[i].clamp(0.0, 1.0) * 255.0) as u32;
let b = (self.b[i].clamp(0.0, 1.0) * 255.0) as u32;
let src = (r << 16) | (g << 8) | b;
buf[i] = add_sat(buf[i], src);
}
}
}
#[inline]
fn snap(v: f32, bands: u32) -> u8 {
let t = v.clamp(0.0, 1.0);
let out = if bands == 3 {
if t < 0.25 { 20u8 } else if t < 0.60 { 127u8 } else { 255u8 } } else {
let n = bands as f32;
let band = (t * n).floor().min(n - 1.0);
((band + 0.5) / n * 255.0) as u8
};
out
}
#[inline]
fn add_sat(a: u32, b: u32) -> u32 {
let r = ((a >> 16 & 0xFF) + (b >> 16 & 0xFF)).min(0xFF);
let g = ((a >> 8 & 0xFF) + (b >> 8 & 0xFF)).min(0xFF);
let bl= ((a & 0xFF) + (b & 0xFF)).min(0xFF);
(r << 16) | (g << 8) | bl
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_and_drain_white_lit() {
let mut pb = PhotonBuf::new(4, 1);
pb.add(0, 1.0, 1.0, 1.0); pb.add(1, 0.5, 0.5, 0.5); pb.add(2, 0.1, 0.1, 0.1); let mut buf = vec![0u32; 4];
pb.drain_toon(&mut buf, 3);
assert_eq!(buf[0] & 0xFF, 0xFF, "full energy → lit band (white)");
let mid = buf[1] & 0xFF;
assert!(mid > 50 && mid < 200, "half energy → mid band, got {mid}");
let dark = buf[2] & 0xFF;
assert!(dark < 50, "dark energy → shadow band, got {dark}");
assert_eq!(buf[3], 0, "zero energy → black");
}
#[test]
fn flow_conserves_energy_approx() {
let mut pb = PhotonBuf::new(3, 3);
pb.add(4, 1.0, 0.0, 0.0);
let sum_before: f32 = pb.r.iter().sum();
pb.flow(0.2);
let sum_after: f32 = pb.r.iter().sum();
assert!((sum_after - sum_before).abs() < sum_before * 0.05,
"flow should approximately conserve energy: {sum_before} vs {sum_after}");
}
}