use crate::canvas::CanvasDimensions;
use crate::error::AvengerWgpuError;
use crate::marks::basic_mark::{BasicMarkBatch, BasicMarkShader};
use avenger::marks::group::GroupBounds;
use avenger::marks::image::ImageMark;
use avenger::marks::value::{ImageAlign, ImageBaseline};
use etagere::Size;
use itertools::izip;
use wgpu::{Extent3d, FilterMode, VertexBufferLayout};
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ImageUniform {
pub size: [f32; 2],
pub origin: [f32; 2],
pub group_size: [f32; 2],
pub scale: f32,
pub clip: f32,
}
impl ImageUniform {
pub fn new(dimensions: CanvasDimensions, group_bounds: GroupBounds, clip: bool) -> Self {
Self {
size: dimensions.size,
scale: dimensions.scale,
origin: [group_bounds.x, group_bounds.y],
group_size: [
group_bounds.width.unwrap_or(0.0),
group_bounds.height.unwrap_or(0.0),
],
clip: if clip && group_bounds.width.is_some() && group_bounds.height.is_some() {
1.0
} else {
0.0
},
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ImageVertex {
pub position: [f32; 2],
pub tex_coord: [f32; 2],
}
const VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
0 => Float32x2, 1 => Float32x2, ];
impl ImageVertex {
pub fn desc() -> VertexBufferLayout<'static> {
VertexBufferLayout {
array_stride: std::mem::size_of::<ImageVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &VERTEX_ATTRIBUTES,
}
}
}
pub struct ImageShader {
verts: Vec<ImageVertex>,
indices: Vec<u16>,
uniform: ImageUniform,
smooth: bool,
shader: String,
vertex_entry_point: String,
fragment_entry_point: String,
batches: Vec<BasicMarkBatch>,
texture_size: Extent3d,
}
impl ImageShader {
pub fn from_image_mark(
mark: &ImageMark,
dimensions: CanvasDimensions,
group_bounds: GroupBounds,
) -> Result<Self, AvengerWgpuError> {
let mut verts: Vec<ImageVertex> = Vec::new();
let mut indices: Vec<u16> = Vec::new();
let mut batches: Vec<BasicMarkBatch> = Vec::new();
let aspect = mark.aspect;
let limits = wgpu::Limits::downlevel_webgl2_defaults();
let texture_size = Extent3d {
width: limits.max_texture_dimension_1d,
height: limits.max_texture_dimension_2d,
depth_or_array_layers: 1,
};
let mut texture_image = image::RgbaImage::new(texture_size.width, texture_size.height);
let mut atlas_allocator = etagere::AtlasAllocator::new(Size::new(
texture_size.width as i32,
texture_size.height as i32,
));
let mut start_index = indices.len() as u32;
for (img, x, y, width, height, baseline, align) in izip!(
mark.image_iter(),
mark.x_iter(),
mark.y_iter(),
mark.width_iter(),
mark.height_iter(),
mark.baseline_iter(),
mark.align_iter(),
) {
let Some(rgba_image) = img.to_image() else {
continue;
};
let allocation = match atlas_allocator
.allocate(Size::new(img.width as i32, img.height as i32))
{
Some(allocation) => allocation,
None => {
batches.push(BasicMarkBatch {
indices_range: start_index..indices.len() as u32,
image: Some(image::DynamicImage::ImageRgba8(texture_image)),
});
atlas_allocator = etagere::AtlasAllocator::new(Size::new(
texture_size.width as i32,
texture_size.height as i32,
));
let Some(allocation) =
atlas_allocator.allocate(Size::new(img.width as i32, img.height as i32))
else {
if img.width > texture_size.width || img.height > texture_size.height {
return Err(AvengerWgpuError::ImageAllocationError(format!(
"Image dimensions ({}, {}) exceed the maximum size of ({}, {})",
img.width, img.height, texture_size.width, texture_size.height
)));
} else {
return Err(AvengerWgpuError::ImageAllocationError(
"Unknown error".to_string(),
));
}
};
texture_image = image::RgbaImage::new(texture_size.width, texture_size.height);
start_index = indices.len() as u32;
allocation
}
};
let p0 = allocation.rectangle.min;
let p1 = allocation.rectangle.max;
let x0 = p0.x;
let x1 = p1.x.min(x0 + img.width as i32);
let y0 = p0.y;
let y1 = p1.y.min(y0 + img.height as i32);
for (src_x, dest_x) in (x0..x1).enumerate() {
for (src_y, dest_y) in (y0..y1).enumerate() {
texture_image.put_pixel(
dest_x as u32,
dest_y as u32,
*rgba_image.get_pixel(src_x as u32, src_y as u32),
);
}
}
let tex_x0 = x0 as f32 / texture_size.width as f32;
let tex_x1 = x1 as f32 / texture_size.width as f32;
let tex_y0 = y0 as f32 / texture_size.height as f32;
let tex_y1 = y1 as f32 / texture_size.height as f32;
let offset = verts.len() as u16;
let left = match *align {
ImageAlign::Left => *x,
ImageAlign::Center => *x - *width / 2.0,
ImageAlign::Right => *x - *width,
};
let top = match *baseline {
ImageBaseline::Top => *y,
ImageBaseline::Middle => *y - *height / 2.0,
ImageBaseline::Bottom => *y - *height,
};
let (left, top, width, height) = if aspect {
let img_aspect = img.width as f32 / img.height as f32;
let outline_aspect = *width / *height;
if img_aspect > outline_aspect {
let aspect_height = *width / img_aspect;
let aspect_top = top + (*height - aspect_height) / 2.0;
(left, aspect_top, *width, aspect_height)
} else if img_aspect < outline_aspect {
let aspect_width = *height * img_aspect;
let aspect_left = left + (*width - aspect_width) / 2.0;
(aspect_left, top, aspect_width, *height)
} else {
(left, top, *width, *height)
}
} else {
(left, top, *width, *height)
};
verts.push(ImageVertex {
position: [left, top],
tex_coord: [tex_x0, tex_y0],
});
verts.push(ImageVertex {
position: [left, top + height],
tex_coord: [tex_x0, tex_y1],
});
verts.push(ImageVertex {
position: [left + width, top + height],
tex_coord: [tex_x1, tex_y1],
});
verts.push(ImageVertex {
position: [left + width, top],
tex_coord: [tex_x1, tex_y0],
});
indices.push(offset);
indices.push(offset + 1);
indices.push(offset + 2);
indices.push(offset);
indices.push(offset + 2);
indices.push(offset + 3);
}
batches.push(BasicMarkBatch {
indices_range: start_index..indices.len() as u32,
image: Some(image::DynamicImage::ImageRgba8(texture_image)),
});
Ok(Self {
verts,
indices,
uniform: ImageUniform::new(dimensions, group_bounds, mark.clip),
smooth: mark.smooth,
batches,
texture_size,
shader: include_str!("image.wgsl").to_string(),
vertex_entry_point: "vs_main".to_string(),
fragment_entry_point: "fs_main".to_string(),
})
}
}
impl BasicMarkShader for ImageShader {
type Vertex = ImageVertex;
type Uniform = ImageUniform;
fn verts(&self) -> &[Self::Vertex] {
self.verts.as_slice()
}
fn indices(&self) -> &[u16] {
self.indices.as_slice()
}
fn uniform(&self) -> Self::Uniform {
self.uniform
}
fn shader(&self) -> &str {
self.shader.as_str()
}
fn vertex_entry_point(&self) -> &str {
self.vertex_entry_point.as_str()
}
fn fragment_entry_point(&self) -> &str {
self.fragment_entry_point.as_str()
}
fn vertex_desc(&self) -> VertexBufferLayout<'static> {
ImageVertex::desc()
}
fn batches(&self) -> &[BasicMarkBatch] {
self.batches.as_slice()
}
fn texture_size(&self) -> Extent3d {
self.texture_size
}
fn mag_filter(&self) -> FilterMode {
if self.smooth {
FilterMode::Linear
} else {
FilterMode::Nearest
}
}
}