use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use wasm_bindgen::prelude::*;
use web_sys::ImageData;
use crate::error::{CanvasError, WasmError, WasmResult};
use crate::tile::TileCoord;
#[allow(dead_code)]
pub const DEFAULT_CANVAS_BUFFER_SIZE_MB: usize = 50;
pub const MAX_VIEWPORT_HISTORY: usize = 50;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RenderQuality {
Low,
Medium,
High,
Ultra,
}
impl RenderQuality {
pub const fn resolution_multiplier(&self) -> f64 {
match self {
Self::Low => 0.5,
Self::Medium => 1.0,
Self::High => 1.5,
Self::Ultra => 2.0,
}
}
pub const fn interpolation_quality(&self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Ultra => "high",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ViewportTransform {
pub tx: f64,
pub ty: f64,
pub sx: f64,
pub sy: f64,
pub rotation: f64,
}
impl ViewportTransform {
pub const fn new(sx: f64, _shy: f64, _shx: f64, sy: f64, tx: f64, ty: f64) -> Self {
Self {
tx,
ty,
sx,
sy,
rotation: 0.0,
}
}
pub const fn identity() -> Self {
Self {
tx: 0.0,
ty: 0.0,
sx: 1.0,
sy: 1.0,
rotation: 0.0,
}
}
pub const fn translate(tx: f64, ty: f64) -> Self {
Self {
tx,
ty,
sx: 1.0,
sy: 1.0,
rotation: 0.0,
}
}
pub const fn scale(sx: f64, sy: f64) -> Self {
Self {
tx: 0.0,
ty: 0.0,
sx,
sy,
rotation: 0.0,
}
}
pub const fn uniform_scale(s: f64) -> Self {
Self::scale(s, s)
}
pub const fn rotate(rotation: f64) -> Self {
Self {
tx: 0.0,
ty: 0.0,
sx: 1.0,
sy: 1.0,
rotation,
}
}
pub fn transform_point(&self, x: f64, y: f64) -> (f64, f64) {
let cos_r = self.rotation.cos();
let sin_r = self.rotation.sin();
let x_scaled = x * self.sx;
let y_scaled = y * self.sy;
let x_rotated = x_scaled * cos_r - y_scaled * sin_r;
let y_rotated = x_scaled * sin_r + y_scaled * cos_r;
(x_rotated + self.tx, y_rotated + self.ty)
}
pub fn inverse_transform_point(&self, x: f64, y: f64) -> (f64, f64) {
let cos_r = self.rotation.cos();
let sin_r = self.rotation.sin();
let x_translated = x - self.tx;
let y_translated = y - self.ty;
let x_rotated = x_translated * cos_r + y_translated * sin_r;
let y_rotated = -x_translated * sin_r + y_translated * cos_r;
(x_rotated / self.sx, y_rotated / self.sy)
}
pub fn compose(&self, other: &Self) -> Self {
Self {
tx: self.tx + other.tx * self.sx,
ty: self.ty + other.ty * self.sy,
sx: self.sx * other.sx,
sy: self.sy * other.sy,
rotation: self.rotation + other.rotation,
}
}
}
impl Default for ViewportTransform {
fn default() -> Self {
Self::identity()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ViewportState {
pub canvas_width: u32,
pub canvas_height: u32,
pub transform: ViewportTransform,
pub visible_bounds: (f64, f64, f64, f64),
pub zoom_level: u32,
}
impl ViewportState {
pub fn new(canvas_width: u32, canvas_height: u32) -> Self {
Self {
canvas_width,
canvas_height,
transform: ViewportTransform::identity(),
visible_bounds: (0.0, 0.0, canvas_width as f64, canvas_height as f64),
zoom_level: 0,
}
}
pub fn update_transform(&mut self, transform: ViewportTransform) {
self.transform = transform;
self.update_visible_bounds();
}
fn update_visible_bounds(&mut self) {
let (min_x, min_y) = self.transform.inverse_transform_point(0.0, 0.0);
let (max_x, max_y) = self
.transform
.inverse_transform_point(self.canvas_width as f64, self.canvas_height as f64);
self.visible_bounds = (
min_x.min(max_x),
min_y.min(max_y),
min_x.max(max_x),
min_y.max(max_y),
);
}
pub fn pan(&mut self, dx: f64, dy: f64) {
self.transform.tx += dx;
self.transform.ty += dy;
self.update_visible_bounds();
}
pub fn zoom(&mut self, factor: f64, center_x: f64, center_y: f64) {
let factor = factor.abs().max(0.01);
let (world_x, world_y) = self.transform.inverse_transform_point(center_x, center_y);
self.transform.sx *= factor;
self.transform.sy *= factor;
let (new_screen_x, new_screen_y) = self.transform.transform_point(world_x, world_y);
self.transform.tx += center_x - new_screen_x;
self.transform.ty += center_y - new_screen_y;
self.update_visible_bounds();
}
pub fn fit_to_bounds(&mut self, bounds: (f64, f64, f64, f64)) {
let (min_x, min_y, max_x, max_y) = bounds;
let width = max_x - min_x;
let height = max_y - min_y;
let scale_x = self.canvas_width as f64 / width;
let scale_y = self.canvas_height as f64 / height;
let scale = scale_x.min(scale_y);
self.transform.sx = scale;
self.transform.sy = scale;
self.transform.tx = -min_x * scale;
self.transform.ty = -min_y * scale;
self.update_visible_bounds();
}
pub fn visible_tiles(&self, tile_size: u32) -> Vec<TileCoord> {
let (min_x, min_y, max_x, max_y) = self.visible_bounds;
let min_tile_x = (min_x / tile_size as f64).floor() as u32;
let min_tile_y = (min_y / tile_size as f64).floor() as u32;
let max_tile_x = (max_x / tile_size as f64).ceil() as u32;
let max_tile_y = (max_y / tile_size as f64).ceil() as u32;
let mut tiles = Vec::new();
for y in min_tile_y..=max_tile_y {
for x in min_tile_x..=max_tile_x {
tiles.push(TileCoord::new(self.zoom_level, x, y));
}
}
tiles
}
}
pub struct ViewportHistory {
history: VecDeque<ViewportState>,
current_index: usize,
max_size: usize,
}
impl ViewportHistory {
pub fn new(max_size: usize) -> Self {
Self {
history: VecDeque::new(),
current_index: 0,
max_size,
}
}
pub fn push(&mut self, state: ViewportState) {
while self.history.len() > self.current_index + 1 {
self.history.pop_back();
}
self.history.push_back(state);
if self.history.len() > self.max_size {
self.history.pop_front();
} else {
self.current_index = self.history.len() - 1;
}
}
pub fn undo(&mut self) -> Option<&ViewportState> {
if self.current_index > 0 {
self.current_index -= 1;
self.history.get(self.current_index)
} else {
None
}
}
pub fn redo(&mut self) -> Option<&ViewportState> {
if self.current_index + 1 < self.history.len() {
self.current_index += 1;
self.history.get(self.current_index)
} else {
None
}
}
pub fn current(&self) -> Option<&ViewportState> {
self.history.get(self.current_index)
}
pub const fn can_undo(&self) -> bool {
self.current_index > 0
}
pub fn can_redo(&self) -> bool {
self.current_index + 1 < self.history.len()
}
pub const fn current_index(&self) -> usize {
self.current_index
}
pub fn clear(&mut self) {
self.history.clear();
self.current_index = 0;
}
}
pub struct CanvasBuffer {
data: Vec<u8>,
width: u32,
height: u32,
dirty: bool,
}
impl CanvasBuffer {
pub fn new(width: u32, height: u32) -> WasmResult<Self> {
if width == 0 || height == 0 {
return Err(WasmError::InvalidOperation {
operation: "CanvasBuffer::new".to_string(),
reason: "Width and height must be greater than zero".to_string(),
});
}
let size = (width as usize) * (height as usize) * 4;
let data = vec![0u8; size];
Ok(Self {
data,
width,
height,
dirty: true,
})
}
pub fn resize(&mut self, width: u32, height: u32) -> WasmResult<()> {
if width == 0 || height == 0 {
return Err(WasmError::InvalidOperation {
operation: "CanvasBuffer::resize".to_string(),
reason: "Width and height must be greater than zero".to_string(),
});
}
if width != self.width || height != self.height {
let size = (width as usize) * (height as usize) * 4;
self.data.resize(size, 0);
self.width = width;
self.height = height;
self.dirty = true;
}
Ok(())
}
pub fn clear(&mut self) {
self.data.fill(0);
self.dirty = true;
}
pub fn clear_with_color(&mut self, r: u8, g: u8, b: u8, a: u8) {
for chunk in self.data.chunks_exact_mut(4) {
chunk[0] = r;
chunk[1] = g;
chunk[2] = b;
chunk[3] = a;
}
self.dirty = true;
}
pub fn draw_tile(
&mut self,
tile_data: &[u8],
tile_x: u32,
tile_y: u32,
tile_width: u32,
tile_height: u32,
) -> WasmResult<()> {
let expected_size = (tile_width * tile_height * 4) as usize;
if tile_data.len() != expected_size {
return Err(WasmError::Canvas(CanvasError::BufferSizeMismatch {
expected: expected_size,
actual: tile_data.len(),
}));
}
if tile_x + tile_width > self.width || tile_y + tile_height > self.height {
return Err(WasmError::Canvas(CanvasError::InvalidDimensions {
width: tile_x + tile_width,
height: tile_y + tile_height,
reason: "Tile extends beyond buffer bounds".to_string(),
}));
}
for y in 0..tile_height {
let src_offset = (y * tile_width * 4) as usize;
let dst_offset = (((tile_y + y) * self.width + tile_x) * 4) as usize;
let row_size = (tile_width * 4) as usize;
self.data[dst_offset..dst_offset + row_size]
.copy_from_slice(&tile_data[src_offset..src_offset + row_size]);
}
self.dirty = true;
Ok(())
}
pub fn composite_tile(
&mut self,
tile_data: &[u8],
tile_width: u32,
tile_height: u32,
dst_x: u32,
dst_y: u32,
opacity: f32,
) -> WasmResult<()> {
let expected_size = (tile_width * tile_height * 4) as usize;
if tile_data.len() != expected_size {
return Err(WasmError::Canvas(CanvasError::BufferSizeMismatch {
expected: expected_size,
actual: tile_data.len(),
}));
}
let opacity = opacity.clamp(0.0, 1.0);
let max_x = (dst_x + tile_width).min(self.width);
let max_y = (dst_y + tile_height).min(self.height);
if dst_x >= self.width || dst_y >= self.height {
return Ok(()); }
for y in dst_y..max_y {
for x in dst_x..max_x {
let src_idx = (((y - dst_y) * tile_width + (x - dst_x)) * 4) as usize;
let dst_idx = ((y * self.width + x) * 4) as usize;
if src_idx + 3 < tile_data.len() && dst_idx + 3 < self.data.len() {
let src_alpha = (f32::from(tile_data[src_idx + 3]) / 255.0) * opacity;
let dst_alpha = 1.0 - src_alpha;
for c in 0..3 {
let src_val = f32::from(tile_data[src_idx + c]);
let dst_val = f32::from(self.data[dst_idx + c]);
let blended = (src_val * src_alpha + dst_val * dst_alpha).clamp(0.0, 255.0);
self.data[dst_idx + c] = blended as u8;
}
let new_alpha = (src_alpha
+ dst_alpha * f32::from(self.data[dst_idx + 3]) / 255.0)
.clamp(0.0, 1.0);
self.data[dst_idx + 3] = (new_alpha * 255.0) as u8;
}
}
}
self.dirty = true;
Ok(())
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn mark_clean(&mut self) {
self.dirty = false;
}
pub const fn is_dirty(&self) -> bool {
self.dirty
}
pub const fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
pub fn to_image_data(&self) -> Result<ImageData, JsValue> {
let clamped = wasm_bindgen::Clamped(self.data.as_slice());
ImageData::new_with_u8_clamped_array_and_sh(clamped, self.width, self.height)
}
}
pub struct ProgressiveRenderer {
queue: Vec<(TileCoord, u32)>, rendering: Vec<TileCoord>,
completed: Vec<TileCoord>,
max_parallel: usize,
}
impl ProgressiveRenderer {
pub fn new(max_parallel: usize) -> Self {
Self {
queue: Vec::new(),
rendering: Vec::new(),
completed: Vec::new(),
max_parallel,
}
}
pub fn add_tiles(&mut self, tiles: Vec<TileCoord>, priority: u32) {
for coord in tiles {
if !self.is_completed(&coord) && !self.is_rendering(&coord) {
self.queue.push((coord, priority));
}
}
self.queue.sort_by_key(|x| std::cmp::Reverse(x.1));
}
pub fn next_batch(&mut self) -> Vec<TileCoord> {
let available = self.max_parallel.saturating_sub(self.rendering.len());
let mut batch = Vec::new();
for _ in 0..available {
if let Some((coord, _)) = self.queue.pop() {
self.rendering.push(coord);
batch.push(coord);
} else {
break;
}
}
batch
}
pub fn mark_completed(&mut self, coord: TileCoord) {
if let Some(pos) = self.rendering.iter().position(|c| *c == coord) {
self.rendering.remove(pos);
self.completed.push(coord);
}
}
pub fn is_completed(&self, coord: &TileCoord) -> bool {
self.completed.contains(coord)
}
pub fn is_rendering(&self, coord: &TileCoord) -> bool {
self.rendering.contains(coord)
}
pub fn clear(&mut self) {
self.queue.clear();
self.rendering.clear();
self.completed.clear();
}
pub fn stats(&self) -> ProgressiveRenderStats {
ProgressiveRenderStats {
queued: self.queue.len(),
rendering: self.rendering.len(),
completed: self.completed.len(),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ProgressiveRenderStats {
pub queued: usize,
pub rendering: usize,
pub completed: usize,
}
impl ProgressiveRenderStats {
pub const fn total(&self) -> usize {
self.queued + self.rendering + self.completed
}
pub fn completion_percentage(&self) -> f64 {
let total = self.total();
if total == 0 {
100.0
} else {
(self.completed as f64 / total as f64) * 100.0
}
}
}
pub struct CanvasRenderer {
front_buffer: CanvasBuffer,
back_buffer: CanvasBuffer,
progressive: ProgressiveRenderer,
viewport: ViewportState,
history: ViewportHistory,
quality: RenderQuality,
}
impl CanvasRenderer {
pub fn new(width: u32, height: u32, max_parallel: usize) -> WasmResult<Self> {
Ok(Self {
front_buffer: CanvasBuffer::new(width, height)?,
back_buffer: CanvasBuffer::new(width, height)?,
progressive: ProgressiveRenderer::new(max_parallel),
viewport: ViewportState::new(width, height),
history: ViewportHistory::new(MAX_VIEWPORT_HISTORY),
quality: RenderQuality::Medium,
})
}
pub fn resize(&mut self, width: u32, height: u32) -> WasmResult<()> {
self.front_buffer.resize(width, height)?;
self.back_buffer.resize(width, height)?;
self.viewport.canvas_width = width;
self.viewport.canvas_height = height;
Ok(())
}
pub fn begin_frame(&mut self) {
self.back_buffer.clear();
}
pub fn draw_tile(
&mut self,
coord: TileCoord,
tile_data: &[u8],
tile_width: u32,
tile_height: u32,
) -> WasmResult<()> {
let world_x = f64::from(coord.x * tile_width);
let world_y = f64::from(coord.y * tile_height);
let (screen_x, screen_y) = self.viewport.transform.transform_point(world_x, world_y);
self.back_buffer.draw_tile(
tile_data,
screen_x as u32,
screen_y as u32,
tile_width,
tile_height,
)?;
self.progressive.mark_completed(coord);
Ok(())
}
pub fn swap_buffers(&mut self) {
std::mem::swap(&mut self.front_buffer, &mut self.back_buffer);
self.front_buffer.mark_clean();
}
pub fn front_buffer_image_data(&self) -> Result<ImageData, JsValue> {
self.front_buffer.to_image_data()
}
pub fn update_viewport(&mut self, state: ViewportState) {
self.history.push(self.viewport.clone());
self.viewport = state;
}
pub fn pan(&mut self, dx: f64, dy: f64) {
let old_state = self.viewport.clone();
self.viewport.pan(dx, dy);
if old_state != self.viewport {
self.history.push(old_state);
}
}
pub fn zoom(&mut self, factor: f64, center_x: f64, center_y: f64) {
let old_state = self.viewport.clone();
self.viewport.zoom(factor, center_x, center_y);
if old_state != self.viewport {
self.history.push(old_state);
}
}
pub fn undo(&mut self) -> bool {
if let Some(state) = self.history.undo() {
self.viewport = state.clone();
true
} else {
false
}
}
pub fn redo(&mut self) -> bool {
if let Some(state) = self.history.redo() {
self.viewport = state.clone();
true
} else {
false
}
}
pub fn set_quality(&mut self, quality: RenderQuality) {
self.quality = quality;
}
pub const fn viewport(&self) -> &ViewportState {
&self.viewport
}
pub fn progressive_stats(&self) -> ProgressiveRenderStats {
self.progressive.stats()
}
}
pub struct AnimationManager {
frame_times: VecDeque<f64>,
max_samples: usize,
last_frame: Option<f64>,
target_fps: f64,
}
impl AnimationManager {
pub fn new(target_fps: f64) -> Self {
Self {
frame_times: VecDeque::new(),
max_samples: 60,
last_frame: None,
target_fps,
}
}
pub fn record_frame(&mut self, timestamp: f64) {
if let Some(last) = self.last_frame {
let frame_time = timestamp - last;
self.frame_times.push_back(frame_time);
if self.frame_times.len() > self.max_samples {
self.frame_times.pop_front();
}
}
self.last_frame = Some(timestamp);
}
pub fn current_fps(&self) -> f64 {
if self.frame_times.is_empty() {
return 0.0;
}
let avg_frame_time: f64 =
self.frame_times.iter().sum::<f64>() / self.frame_times.len() as f64;
if avg_frame_time > 0.0 {
1000.0 / avg_frame_time
} else {
0.0
}
}
pub fn average_frame_time(&self) -> f64 {
if self.frame_times.is_empty() {
return 0.0;
}
self.frame_times.iter().sum::<f64>() / self.frame_times.len() as f64
}
pub fn is_below_target(&self) -> bool {
self.current_fps() < self.target_fps
}
pub fn stats(&self) -> AnimationStats {
AnimationStats {
current_fps: self.current_fps(),
average_frame_time_ms: self.average_frame_time(),
target_fps: self.target_fps,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct AnimationStats {
pub current_fps: f64,
pub average_frame_time_ms: f64,
pub target_fps: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_viewport_transform() {
let transform = ViewportTransform::translate(10.0, 20.0);
let (x, y) = transform.transform_point(0.0, 0.0);
assert_eq!(x, 10.0);
assert_eq!(y, 20.0);
}
#[test]
fn test_viewport_transform_inverse() {
let transform = ViewportTransform::translate(10.0, 20.0);
let (x, y) = transform.inverse_transform_point(10.0, 20.0);
assert!((x - 0.0).abs() < 0.001);
assert!((y - 0.0).abs() < 0.001);
}
#[test]
fn test_viewport_state() {
let mut state = ViewportState::new(800, 600);
state.pan(10.0, 20.0);
assert_eq!(state.transform.tx, 10.0);
assert_eq!(state.transform.ty, 20.0);
}
#[test]
fn test_viewport_history() {
let mut history = ViewportHistory::new(10);
let state1 = ViewportState::new(800, 600);
let mut state2 = ViewportState::new(800, 600);
state2.pan(10.0, 20.0);
history.push(state1);
history.push(state2);
assert!(history.can_undo());
history.undo();
assert!(history.can_redo());
}
#[test]
fn test_canvas_buffer() {
let mut buffer = CanvasBuffer::new(256, 256).expect("Failed to create buffer");
assert!(buffer.is_dirty());
buffer.mark_clean();
assert!(!buffer.is_dirty());
buffer.clear();
assert!(buffer.is_dirty());
}
#[test]
fn test_progressive_renderer() {
let mut renderer = ProgressiveRenderer::new(4);
let tiles = vec![
TileCoord::new(0, 0, 0),
TileCoord::new(0, 1, 0),
TileCoord::new(0, 0, 1),
];
renderer.add_tiles(tiles, 10);
let batch = renderer.next_batch();
assert!(!batch.is_empty());
assert!(batch.len() <= 4);
}
#[test]
fn test_animation_manager() {
let mut manager = AnimationManager::new(60.0);
manager.record_frame(0.0);
manager.record_frame(16.67); manager.record_frame(33.34);
let fps = manager.current_fps();
assert!(fps > 50.0 && fps < 70.0);
}
#[test]
fn test_render_quality() {
assert_eq!(RenderQuality::Low.resolution_multiplier(), 0.5);
assert_eq!(RenderQuality::Medium.resolution_multiplier(), 1.0);
assert_eq!(RenderQuality::High.resolution_multiplier(), 1.5);
assert_eq!(RenderQuality::Ultra.resolution_multiplier(), 2.0);
}
}