|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub trait FrameSource {
|
|
|
| fn size(&self) -> usize;
|
|
|
|
|
|
|
| fn next_frame(&mut self) -> Option<&[u8]>;
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub struct TestPattern {
|
| size: usize,
|
| buf: Vec<u8>,
|
| frame: u32,
|
| }
|
|
|
| impl TestPattern {
|
| pub fn new(size: usize) -> Self {
|
| Self {
|
| size,
|
| buf: vec![0; size * size * 3],
|
| frame: 0,
|
| }
|
| }
|
|
|
| fn render(&mut self) {
|
| let size = self.size;
|
| let t = self.frame as f32 * 0.02;
|
|
|
|
|
| let discs = [
|
| (0.30 + 0.18 * t.cos(), 0.30 + 0.18 * t.sin(), 0.16, [230u8, 60, 50]),
|
| (0.70 + 0.15 * (t * 0.7 + 2.0).cos(), 0.35 + 0.15 * (t * 0.7).sin(), 0.13, [60, 200, 90]),
|
| (0.50 + 0.20 * (t * 0.5 + 4.0).sin(), 0.72 + 0.10 * (t * 0.9).cos(), 0.15, [70, 110, 235]),
|
| ];
|
|
|
| for y in 0..size {
|
| let v = y as f32 / size as f32;
|
| for x in 0..size {
|
| let u = x as f32 / size as f32;
|
|
|
|
|
| let mut rgb = [
|
| (40.0 + 60.0 * v) as u8,
|
| (50.0 + 40.0 * (1.0 - v)) as u8,
|
| (70.0 + 50.0 * v) as u8,
|
| ];
|
|
|
|
|
|
|
| if u > 0.72 && v > 0.72 {
|
| let cell = ((x / 8) + (y / 8)) % 2;
|
| let shade = if cell == 0 { 200 } else { 90 };
|
| rgb = [shade, shade, shade];
|
| }
|
|
|
| for &(cx, cy, r, color) in &discs {
|
| let dx = u - cx;
|
| let dy = v - cy;
|
| if dx * dx + dy * dy < r * r {
|
| rgb = color;
|
| }
|
| }
|
|
|
| let i = (y * size + x) * 3;
|
| self.buf[i] = rgb[0];
|
| self.buf[i + 1] = rgb[1];
|
| self.buf[i + 2] = rgb[2];
|
| }
|
| }
|
| }
|
| }
|
|
|
| impl FrameSource for TestPattern {
|
| fn size(&self) -> usize {
|
| self.size
|
| }
|
|
|
| fn next_frame(&mut self) -> Option<&[u8]> {
|
| self.render();
|
| self.frame = self.frame.wrapping_add(1);
|
| Some(&self.buf)
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn square_downscale_rgba(rgba: &[u8], width: usize, height: usize, size: usize) -> Vec<u8> {
|
| let side = width.min(height);
|
| let x0 = (width - side) / 2;
|
| let y0 = (height - side) / 2;
|
|
|
| let mut out = vec![0u8; size * size * 3];
|
| for oy in 0..size {
|
| let sy0 = y0 + oy * side / size;
|
| let sy1 = (y0 + (oy + 1) * side / size).max(sy0 + 1);
|
| for ox in 0..size {
|
| let sx0 = x0 + ox * side / size;
|
| let sx1 = (x0 + (ox + 1) * side / size).max(sx0 + 1);
|
|
|
| let mut acc = [0u32; 3];
|
| let mut n = 0u32;
|
| for sy in sy0..sy1.min(height) {
|
| for sx in sx0..sx1.min(width) {
|
| let i = (sy * width + sx) * 4;
|
| acc[0] += rgba[i] as u32;
|
| acc[1] += rgba[i + 1] as u32;
|
| acc[2] += rgba[i + 2] as u32;
|
| n += 1;
|
| }
|
| }
|
| let n = n.max(1);
|
| let o = (oy * size + ox) * 3;
|
| for c in 0..3 {
|
| out[o + c] = (acc[c] / n) as u8;
|
| }
|
| }
|
| }
|
| out
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| #[cfg(feature = "capture")]
|
| pub struct ScreenCapture {
|
| monitor: xcap::Monitor,
|
| size: usize,
|
| buf: Vec<u8>,
|
| }
|
|
|
| #[cfg(feature = "capture")]
|
| impl ScreenCapture {
|
|
|
| pub fn new(size: usize, index: usize) -> Result<Self, Box<dyn std::error::Error>> {
|
| let monitors = xcap::Monitor::all()?;
|
| if monitors.is_empty() {
|
| return Err("no monitors found".into());
|
| }
|
| for (i, m) in monitors.iter().enumerate() {
|
| log::info!(
|
| "monitor {i}: {}x{}{}",
|
| m.width().unwrap_or(0),
|
| m.height().unwrap_or(0),
|
| if i == index { " (selected)" } else { "" }
|
| );
|
| }
|
| let monitor = monitors
|
| .into_iter()
|
| .nth(index)
|
| .ok_or("monitor index out of range")?;
|
| Ok(Self {
|
| monitor,
|
| size,
|
| buf: vec![128; size * size * 3],
|
| })
|
| }
|
| }
|
|
|
| #[cfg(feature = "capture")]
|
| impl FrameSource for ScreenCapture {
|
| fn size(&self) -> usize {
|
| self.size
|
| }
|
|
|
| fn next_frame(&mut self) -> Option<&[u8]> {
|
|
|
|
|
| match self.monitor.capture_image() {
|
| Ok(image) => {
|
| let (w, h) = (image.width() as usize, image.height() as usize);
|
| self.buf = square_downscale_rgba(&image.into_raw(), w, h, self.size);
|
| Some(&self.buf)
|
| }
|
| Err(e) => {
|
| log::warn!("screen capture failed: {e}");
|
| None
|
| }
|
| }
|
| }
|
| }
|
|
|
| #[cfg(test)]
|
| mod tests {
|
| use super::*;
|
|
|
| #[test]
|
| fn downscale_centre_crops_and_averages() {
|
|
|
|
|
| let (w, h) = (40usize, 20usize);
|
| let mut rgba = vec![0u8; w * h * 4];
|
| for y in 0..h {
|
| for x in 0..w {
|
| let i = (y * w + x) * 4;
|
| let c = if x < w / 2 { [255, 0, 0] } else { [0, 0, 255] };
|
| rgba[i..i + 3].copy_from_slice(&c);
|
| rgba[i + 3] = 255;
|
| }
|
| }
|
|
|
| let out = square_downscale_rgba(&rgba, w, h, 4);
|
| assert_eq!(out.len(), 4 * 4 * 3);
|
|
|
| let px = |x: usize, y: usize| {
|
| let i = (y * 4 + x) * 3;
|
| [out[i], out[i + 1], out[i + 2]]
|
| };
|
| assert_eq!(px(0, 0), [255, 0, 0], "left edge should be red");
|
| assert_eq!(px(3, 0), [0, 0, 255], "right edge should be blue");
|
| }
|
|
|
| #[test]
|
| fn downscale_averages_rather_than_point_samples() {
|
|
|
|
|
| let (w, h) = (64usize, 64usize);
|
| let mut rgba = vec![255u8; w * h * 4];
|
| for y in 0..h {
|
| for x in 0..w {
|
| let v = if x % 2 == 0 { 0 } else { 255 };
|
| let i = (y * w + x) * 4;
|
| rgba[i..i + 3].copy_from_slice(&[v, v, v]);
|
| }
|
| }
|
| let out = square_downscale_rgba(&rgba, w, h, 8);
|
| for px in out.chunks_exact(3) {
|
| assert!(
|
| (100..=155).contains(&px[0]),
|
| "expected mid-grey from averaging, got {}",
|
| px[0]
|
| );
|
| }
|
| }
|
|
|
| #[test]
|
| fn test_pattern_has_spatial_structure() {
|
| let mut src = TestPattern::new(224);
|
| let frame = src.next_frame().unwrap().to_vec();
|
| assert_eq!(frame.len(), 224 * 224 * 3);
|
|
|
|
|
|
|
| let mean = frame.iter().map(|&b| b as f64).sum::<f64>() / frame.len() as f64;
|
| let var = frame
|
| .iter()
|
| .map(|&b| (b as f64 - mean).powi(2))
|
| .sum::<f64>()
|
| / frame.len() as f64;
|
| assert!(var > 400.0, "test pattern is too flat: variance {var:.1}");
|
| }
|
|
|
| #[test]
|
| fn test_pattern_animates() {
|
| let mut src = TestPattern::new(64);
|
| let a = src.next_frame().unwrap().to_vec();
|
| for _ in 0..20 {
|
| src.next_frame();
|
| }
|
| let b = src.next_frame().unwrap().to_vec();
|
| assert_ne!(a, b, "frames are identical; the scene is not moving");
|
| }
|
| }
|
|
|