use crate::render::wgpu::sprite_texture_atlas::SpriteTextureAtlas;
use nalgebra_glm::Vec2;
pub const SHAPE_TEXTURE_SIZE: u32 = 128;
pub const SHAPE_SLOT_CIRCLE: u32 = 120;
pub const SHAPE_SLOT_SOFT_CIRCLE: u32 = 121;
pub const SHAPE_SLOT_RING: u32 = 122;
pub const SHAPE_SLOT_TRIANGLE: u32 = 123;
pub const SHAPE_SLOT_CAPSULE: u32 = 124;
pub const SHAPE_SLOT_OUTLINED_RECT: u32 = 125;
pub const SHAPE_SLOT_ANTIALIASED_RECT: u32 = 126;
pub fn upload_builtin_shapes(queue: &wgpu::Queue, atlas: &mut SpriteTextureAtlas) {
atlas.upload_texture(
queue,
SHAPE_SLOT_CIRCLE,
&generate_circle_texture(SHAPE_TEXTURE_SIZE),
SHAPE_TEXTURE_SIZE,
SHAPE_TEXTURE_SIZE,
);
atlas.upload_texture(
queue,
SHAPE_SLOT_SOFT_CIRCLE,
&generate_soft_circle_texture(SHAPE_TEXTURE_SIZE),
SHAPE_TEXTURE_SIZE,
SHAPE_TEXTURE_SIZE,
);
atlas.upload_texture(
queue,
SHAPE_SLOT_RING,
&generate_ring_texture(SHAPE_TEXTURE_SIZE),
SHAPE_TEXTURE_SIZE,
SHAPE_TEXTURE_SIZE,
);
atlas.upload_texture(
queue,
SHAPE_SLOT_TRIANGLE,
&generate_triangle_texture(SHAPE_TEXTURE_SIZE),
SHAPE_TEXTURE_SIZE,
SHAPE_TEXTURE_SIZE,
);
atlas.upload_texture(
queue,
SHAPE_SLOT_CAPSULE,
&generate_capsule_texture(SHAPE_TEXTURE_SIZE),
SHAPE_TEXTURE_SIZE,
SHAPE_TEXTURE_SIZE,
);
atlas.upload_texture(
queue,
SHAPE_SLOT_OUTLINED_RECT,
&generate_outlined_rect_texture(SHAPE_TEXTURE_SIZE),
SHAPE_TEXTURE_SIZE,
SHAPE_TEXTURE_SIZE,
);
atlas.upload_texture(
queue,
SHAPE_SLOT_ANTIALIASED_RECT,
&generate_antialiased_rect_texture(SHAPE_TEXTURE_SIZE),
SHAPE_TEXTURE_SIZE,
SHAPE_TEXTURE_SIZE,
);
}
pub fn shape_uv() -> (Vec2, Vec2) {
let slot_size = crate::render::wgpu::sprite_texture_atlas::SPRITE_ATLAS_SLOT_SIZE;
let half_texel_x = 0.5 / slot_size.0 as f32;
let half_texel_y = 0.5 / slot_size.1 as f32;
let uv_min = Vec2::new(half_texel_x, half_texel_y);
let uv_max = Vec2::new(
SHAPE_TEXTURE_SIZE as f32 / slot_size.0 as f32 - half_texel_x,
SHAPE_TEXTURE_SIZE as f32 / slot_size.1 as f32 - half_texel_y,
);
(uv_min, uv_max)
}
fn generate_circle_texture(size: u32) -> Vec<u8> {
generate_circle_texture_with_aa(size, 1.0)
}
pub fn generate_circle_texture_with_aa(size: u32, aa_pixels: f32) -> Vec<u8> {
let mut data = vec![0u8; (size * size * 4) as usize];
let center = size as f32 / 2.0 - 0.5;
let radius = size as f32 / 2.0 - 1.0;
let aa = aa_pixels.max(0.001);
for pixel_y in 0..size {
for pixel_x in 0..size {
let distance_x = pixel_x as f32 - center;
let distance_y = pixel_y as f32 - center;
let distance = (distance_x * distance_x + distance_y * distance_y).sqrt();
let index = ((pixel_y * size + pixel_x) * 4) as usize;
if distance < radius + aa {
let alpha = ((radius + aa - distance) / aa).clamp(0.0, 1.0);
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (alpha * 255.0) as u8;
}
}
}
data
}
fn generate_soft_circle_texture(size: u32) -> Vec<u8> {
let mut data = vec![0u8; (size * size * 4) as usize];
let center = size as f32 / 2.0 - 0.5;
let radius = size as f32 / 2.0 - 1.0;
for pixel_y in 0..size {
for pixel_x in 0..size {
let distance_x = pixel_x as f32 - center;
let distance_y = pixel_y as f32 - center;
let distance = (distance_x * distance_x + distance_y * distance_y).sqrt();
let index = ((pixel_y * size + pixel_x) * 4) as usize;
if distance < radius {
let alpha = (1.0 - distance / radius).powf(1.5);
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (alpha * 255.0) as u8;
}
}
}
data
}
fn generate_ring_texture(size: u32) -> Vec<u8> {
generate_ring_texture_with_aa(size, 0.4, 1.0)
}
pub fn generate_ring_texture_with_aa(size: u32, thickness: f32, aa_pixels: f32) -> Vec<u8> {
let mut data = vec![0u8; (size * size * 4) as usize];
let center = size as f32 / 2.0 - 0.5;
let outer_radius = size as f32 / 2.0 - 1.0;
let inner_radius = outer_radius * (1.0 - thickness.clamp(0.0, 1.0));
let aa = aa_pixels.max(0.001);
for pixel_y in 0..size {
for pixel_x in 0..size {
let distance_x = pixel_x as f32 - center;
let distance_y = pixel_y as f32 - center;
let distance = (distance_x * distance_x + distance_y * distance_y).sqrt();
let index = ((pixel_y * size + pixel_x) * 4) as usize;
let outer_edge = ((outer_radius + aa - distance) / aa).clamp(0.0, 1.0);
let inner_edge = ((distance - inner_radius + aa) / aa).clamp(0.0, 1.0);
let alpha = outer_edge * inner_edge;
if alpha > 0.0 {
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (alpha * 255.0) as u8;
}
}
}
data
}
fn generate_triangle_texture(size: u32) -> Vec<u8> {
let mut data = vec![0u8; (size * size * 4) as usize];
let margin = 2.0;
let top = nalgebra_glm::Vec2::new(size as f32 / 2.0, margin);
let bottom_left = nalgebra_glm::Vec2::new(margin, size as f32 - margin);
let bottom_right = nalgebra_glm::Vec2::new(size as f32 - margin, size as f32 - margin);
for pixel_y in 0..size {
for pixel_x in 0..size {
let point = nalgebra_glm::Vec2::new(pixel_x as f32 + 0.5, pixel_y as f32 + 0.5);
let distance = signed_distance_to_triangle(point, top, bottom_left, bottom_right);
let index = ((pixel_y * size + pixel_x) * 4) as usize;
if distance < 1.0 {
let alpha = (1.0 - distance).clamp(0.0, 1.0);
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (alpha * 255.0) as u8;
}
}
}
data
}
fn signed_distance_to_triangle(
point: nalgebra_glm::Vec2,
vertex_a: nalgebra_glm::Vec2,
vertex_b: nalgebra_glm::Vec2,
vertex_c: nalgebra_glm::Vec2,
) -> f32 {
let edge_ab = vertex_b - vertex_a;
let edge_bc = vertex_c - vertex_b;
let edge_ca = vertex_a - vertex_c;
let to_a = point - vertex_a;
let to_b = point - vertex_b;
let to_c = point - vertex_c;
let cross_ab = edge_ab.x * to_a.y - edge_ab.y * to_a.x;
let cross_bc = edge_bc.x * to_b.y - edge_bc.y * to_b.x;
let cross_ca = edge_ca.x * to_c.y - edge_ca.y * to_c.x;
let inside = cross_ab >= 0.0 && cross_bc >= 0.0 && cross_ca >= 0.0;
let distance_to_edge = |edge: nalgebra_glm::Vec2, to_vertex: nalgebra_glm::Vec2| -> f32 {
let edge_length_squared = edge.x * edge.x + edge.y * edge.y;
let parameter =
((to_vertex.x * edge.x + to_vertex.y * edge.y) / edge_length_squared).clamp(0.0, 1.0);
let closest = nalgebra_glm::Vec2::new(
to_vertex.x - parameter * edge.x,
to_vertex.y - parameter * edge.y,
);
(closest.x * closest.x + closest.y * closest.y).sqrt()
};
let minimum_distance = distance_to_edge(edge_ab, to_a)
.min(distance_to_edge(edge_bc, to_b))
.min(distance_to_edge(edge_ca, to_c));
if inside {
-minimum_distance
} else {
minimum_distance
}
}
fn generate_capsule_texture(size: u32) -> Vec<u8> {
let mut data = vec![0u8; (size * size * 4) as usize];
let margin = 2.0;
let cap_radius = size as f32 / 4.0;
let center_y = size as f32 / 2.0 - 0.5;
let left_center = margin + cap_radius;
let right_center = size as f32 - margin - cap_radius;
for pixel_y in 0..size {
for pixel_x in 0..size {
let px = pixel_x as f32;
let py = pixel_y as f32;
let dy = py - center_y;
let distance = if px < left_center {
let dx = px - left_center;
(dx * dx + dy * dy).sqrt() - cap_radius
} else if px > right_center {
let dx = px - right_center;
(dx * dx + dy * dy).sqrt() - cap_radius
} else {
dy.abs() - cap_radius
};
let index = ((pixel_y * size + pixel_x) * 4) as usize;
if distance < 1.0 {
let alpha = (1.0 - distance).clamp(0.0, 1.0);
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (alpha * 255.0) as u8;
}
}
}
data
}
pub fn generate_linear_gradient_texture(
width: u32,
height: u32,
color_start: [f32; 4],
color_end: [f32; 4],
horizontal: bool,
) -> Vec<u8> {
let mut data = vec![0u8; (width * height * 4) as usize];
for pixel_y in 0..height {
for pixel_x in 0..width {
let parameter = if horizontal {
pixel_x as f32 / (width - 1).max(1) as f32
} else {
pixel_y as f32 / (height - 1).max(1) as f32
};
let red = color_start[0] + (color_end[0] - color_start[0]) * parameter;
let green = color_start[1] + (color_end[1] - color_start[1]) * parameter;
let blue = color_start[2] + (color_end[2] - color_start[2]) * parameter;
let alpha = color_start[3] + (color_end[3] - color_start[3]) * parameter;
let index = ((pixel_y * width + pixel_x) * 4) as usize;
data[index] = (red * 255.0) as u8;
data[index + 1] = (green * 255.0) as u8;
data[index + 2] = (blue * 255.0) as u8;
data[index + 3] = (alpha * 255.0) as u8;
}
}
data
}
pub fn generate_radial_gradient_texture(
size: u32,
color_center: [f32; 4],
color_edge: [f32; 4],
) -> Vec<u8> {
let mut data = vec![0u8; (size * size * 4) as usize];
let center = size as f32 / 2.0 - 0.5;
let radius = size as f32 / 2.0;
for pixel_y in 0..size {
for pixel_x in 0..size {
let distance_x = pixel_x as f32 - center;
let distance_y = pixel_y as f32 - center;
let distance = (distance_x * distance_x + distance_y * distance_y).sqrt();
let parameter = (distance / radius).clamp(0.0, 1.0);
let red = color_center[0] + (color_edge[0] - color_center[0]) * parameter;
let green = color_center[1] + (color_edge[1] - color_center[1]) * parameter;
let blue = color_center[2] + (color_edge[2] - color_center[2]) * parameter;
let alpha = color_center[3] + (color_edge[3] - color_center[3]) * parameter;
let index = ((pixel_y * size + pixel_x) * 4) as usize;
data[index] = (red * 255.0) as u8;
data[index + 1] = (green * 255.0) as u8;
data[index + 2] = (blue * 255.0) as u8;
data[index + 3] = (alpha * 255.0) as u8;
}
}
data
}
pub fn generate_gradient_lut(stops: &[(f32, [f32; 4])]) -> Vec<u8> {
generate_multi_stop_gradient_texture(256, 1, stops, true)
}
pub fn generate_multi_stop_gradient_texture(
width: u32,
height: u32,
stops: &[(f32, [f32; 4])],
horizontal: bool,
) -> Vec<u8> {
let mut data = vec![0u8; (width * height * 4) as usize];
if stops.is_empty() {
return data;
}
if stops.len() == 1 {
let color = stops[0].1;
for pixel_y in 0..height {
for pixel_x in 0..width {
let index = ((pixel_y * width + pixel_x) * 4) as usize;
data[index] = (color[0] * 255.0) as u8;
data[index + 1] = (color[1] * 255.0) as u8;
data[index + 2] = (color[2] * 255.0) as u8;
data[index + 3] = (color[3] * 255.0) as u8;
}
}
return data;
}
for pixel_y in 0..height {
for pixel_x in 0..width {
let parameter = if horizontal {
pixel_x as f32 / (width - 1).max(1) as f32
} else {
pixel_y as f32 / (height - 1).max(1) as f32
};
let (red, green, blue, alpha) = sample_gradient_stops(stops, parameter);
let index = ((pixel_y * width + pixel_x) * 4) as usize;
data[index] = (red * 255.0) as u8;
data[index + 1] = (green * 255.0) as u8;
data[index + 2] = (blue * 255.0) as u8;
data[index + 3] = (alpha * 255.0) as u8;
}
}
data
}
fn sample_gradient_stops(stops: &[(f32, [f32; 4])], parameter: f32) -> (f32, f32, f32, f32) {
if parameter <= stops[0].0 {
let color = stops[0].1;
return (color[0], color[1], color[2], color[3]);
}
if parameter >= stops[stops.len() - 1].0 {
let color = stops[stops.len() - 1].1;
return (color[0], color[1], color[2], color[3]);
}
for index in 0..stops.len() - 1 {
let (position_a, color_a) = stops[index];
let (position_b, color_b) = stops[index + 1];
if parameter >= position_a && parameter <= position_b {
let segment_length = position_b - position_a;
let local_parameter = if segment_length > 0.0 {
(parameter - position_a) / segment_length
} else {
0.0
};
return (
color_a[0] + (color_b[0] - color_a[0]) * local_parameter,
color_a[1] + (color_b[1] - color_a[1]) * local_parameter,
color_a[2] + (color_b[2] - color_a[2]) * local_parameter,
color_a[3] + (color_b[3] - color_a[3]) * local_parameter,
);
}
}
let color = stops[stops.len() - 1].1;
(color[0], color[1], color[2], color[3])
}
fn generate_antialiased_rect_texture(size: u32) -> Vec<u8> {
let mut data = vec![0u8; (size * size * 4) as usize];
let edge_width = size as f32 * 0.1;
for pixel_y in 0..size {
for pixel_x in 0..size {
let distance_to_left = pixel_x as f32;
let distance_to_right = (size - 1 - pixel_x) as f32;
let distance_to_top = pixel_y as f32;
let distance_to_bottom = (size - 1 - pixel_y) as f32;
let distance_to_nearest_edge = distance_to_left
.min(distance_to_right)
.min(distance_to_top)
.min(distance_to_bottom);
let alpha = (distance_to_nearest_edge / edge_width).clamp(0.0, 1.0);
let index = ((pixel_y * size + pixel_x) * 4) as usize;
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (alpha * 255.0) as u8;
}
}
data
}
pub fn generate_rounded_rect_texture(size: u32, corner_radius: f32) -> Vec<u8> {
generate_rounded_rect_texture_with_aa(size, corner_radius, 1.0)
}
pub fn generate_rounded_rect_texture_with_aa(
size: u32,
corner_radius: f32,
aa_pixels: f32,
) -> Vec<u8> {
let mut data = vec![0u8; (size * size * 4) as usize];
let half = size as f32 / 2.0;
let rect_half = half - 1.0;
let radius = corner_radius.min(rect_half);
let aa = aa_pixels.max(0.001);
for pixel_y in 0..size {
for pixel_x in 0..size {
let px = pixel_x as f32 - half + 0.5;
let py = pixel_y as f32 - half + 0.5;
let inner_half = rect_half - radius;
let qx = px.abs() - inner_half;
let qy = py.abs() - inner_half;
let distance = if qx > 0.0 && qy > 0.0 {
(qx * qx + qy * qy).sqrt() - radius
} else {
qx.max(qy).max(0.0) - radius + qx.min(0.0).max(qy.min(0.0))
};
let index = ((pixel_y * size + pixel_x) * 4) as usize;
if distance < aa {
let alpha = ((aa - distance) / aa).clamp(0.0, 1.0);
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (alpha * 255.0) as u8;
}
}
}
data
}
pub fn generate_filled_polygon_texture(width: u32, height: u32, points: &[[f32; 2]]) -> Vec<u8> {
generate_filled_polygon_texture_with_aa(width, height, points, 1.0)
}
pub fn generate_filled_polygon_texture_with_aa(
width: u32,
height: u32,
points: &[[f32; 2]],
aa_pixels: f32,
) -> Vec<u8> {
let mut data = vec![0u8; (width * height * 4) as usize];
if points.len() < 3 {
return data;
}
let aa = aa_pixels.max(0.001);
for pixel_y in 0..height {
for pixel_x in 0..width {
let px = (pixel_x as f32 + 0.5) / width as f32;
let py = (pixel_y as f32 + 0.5) / height as f32;
let mut winding_number = 0i32;
let mut min_distance = f32::MAX;
let point_count = points.len();
for edge_index in 0..point_count {
let next_index = (edge_index + 1) % point_count;
let ax = points[edge_index][0];
let ay = points[edge_index][1];
let bx = points[next_index][0];
let by = points[next_index][1];
let edge_x = bx - ax;
let edge_y = by - ay;
let to_point_x = px - ax;
let to_point_y = py - ay;
let edge_length_squared = edge_x * edge_x + edge_y * edge_y;
let parameter = if edge_length_squared > 0.0 {
((to_point_x * edge_x + to_point_y * edge_y) / edge_length_squared)
.clamp(0.0, 1.0)
} else {
0.0
};
let closest_x = to_point_x - parameter * edge_x;
let closest_y = to_point_y - parameter * edge_y;
let segment_distance = (closest_x * closest_x + closest_y * closest_y).sqrt();
min_distance = min_distance.min(segment_distance);
if ay <= py {
if by > py && edge_x * to_point_y - edge_y * to_point_x > 0.0 {
winding_number += 1;
}
} else if by <= py && edge_x * to_point_y - edge_y * to_point_x < 0.0 {
winding_number -= 1;
}
}
let pixel_size = 1.0 / width.max(height) as f32;
let normalized_distance = min_distance / pixel_size;
let index = ((pixel_y * width + pixel_x) * 4) as usize;
if winding_number != 0 {
let alpha = (normalized_distance.min(aa) / aa).clamp(0.0, 1.0);
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (alpha * 255.0) as u8;
} else if normalized_distance < aa {
let alpha = ((aa - normalized_distance) / aa).clamp(0.0, 1.0);
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (alpha * 255.0) as u8;
}
}
}
data
}
pub fn generate_ring_texture_with_thickness(size: u32, thickness: f32) -> Vec<u8> {
let mut data = vec![0u8; (size * size * 4) as usize];
let center = size as f32 / 2.0 - 0.5;
let outer_radius = size as f32 / 2.0 - 1.0;
let inner_radius = outer_radius * (1.0 - thickness.clamp(0.0, 1.0));
for pixel_y in 0..size {
for pixel_x in 0..size {
let distance_x = pixel_x as f32 - center;
let distance_y = pixel_y as f32 - center;
let distance = (distance_x * distance_x + distance_y * distance_y).sqrt();
let index = ((pixel_y * size + pixel_x) * 4) as usize;
let outer_edge = (outer_radius + 1.0 - distance).clamp(0.0, 1.0);
let inner_edge = (distance - inner_radius + 1.0).clamp(0.0, 1.0);
let alpha = outer_edge * inner_edge;
if alpha > 0.0 {
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (alpha * 255.0) as u8;
}
}
}
data
}
pub fn generate_outlined_rect_texture_with_border(size: u32, border_width: f32) -> Vec<u8> {
let mut data = vec![0u8; (size * size * 4) as usize];
let margin = 2.0;
for pixel_y in 0..size {
for pixel_x in 0..size {
let px = pixel_x as f32;
let py = pixel_y as f32;
let distance_to_left = px - margin;
let distance_to_right = (size as f32 - margin) - px;
let distance_to_top = py - margin;
let distance_to_bottom = (size as f32 - margin) - py;
let distance_to_nearest_edge = distance_to_left
.min(distance_to_right)
.min(distance_to_top)
.min(distance_to_bottom);
let outside = distance_to_nearest_edge < 0.0;
let inside_border = distance_to_nearest_edge < border_width;
let index = ((pixel_y * size + pixel_x) * 4) as usize;
if outside {
let alpha = (1.0 + distance_to_nearest_edge).clamp(0.0, 1.0);
if alpha > 0.0 {
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (alpha * 255.0) as u8;
}
} else if inside_border {
let inner_alpha = (border_width - distance_to_nearest_edge).clamp(0.0, 1.0);
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (inner_alpha * 255.0) as u8;
}
}
}
data
}
pub fn boolean_union(a: &[u8], b: &[u8], width: u32, height: u32) -> Vec<u8> {
let pixel_count = (width * height) as usize;
let expected_length = pixel_count * 4;
assert!(
a.len() >= expected_length && b.len() >= expected_length,
"boolean_union: buffer too short (expected {expected_length}, got a={} b={})",
a.len(),
b.len()
);
let mut output = vec![0u8; expected_length];
for pixel_index in 0..pixel_count {
let byte_offset = pixel_index * 4;
let alpha_a = a[byte_offset + 3] as f32 / 255.0;
let alpha_b = b[byte_offset + 3] as f32 / 255.0;
let alpha_out = alpha_a.max(alpha_b);
if alpha_out > 0.0 {
let weight_a = alpha_a / (alpha_a + alpha_b).max(0.001);
let weight_b = 1.0 - weight_a;
output[byte_offset] =
(a[byte_offset] as f32 * weight_a + b[byte_offset] as f32 * weight_b) as u8;
output[byte_offset + 1] =
(a[byte_offset + 1] as f32 * weight_a + b[byte_offset + 1] as f32 * weight_b) as u8;
output[byte_offset + 2] =
(a[byte_offset + 2] as f32 * weight_a + b[byte_offset + 2] as f32 * weight_b) as u8;
}
output[byte_offset + 3] = (alpha_out * 255.0) as u8;
}
output
}
pub fn boolean_subtract(a: &[u8], b: &[u8], width: u32, height: u32) -> Vec<u8> {
let pixel_count = (width * height) as usize;
let expected_length = pixel_count * 4;
assert!(
a.len() >= expected_length && b.len() >= expected_length,
"boolean_subtract: buffer too short (expected {expected_length}, got a={} b={})",
a.len(),
b.len()
);
let mut output = vec![0u8; expected_length];
for pixel_index in 0..pixel_count {
let byte_offset = pixel_index * 4;
let alpha_a = a[byte_offset + 3] as f32 / 255.0;
let alpha_b = b[byte_offset + 3] as f32 / 255.0;
let alpha_out = (alpha_a - alpha_b).max(0.0);
output[byte_offset] = a[byte_offset];
output[byte_offset + 1] = a[byte_offset + 1];
output[byte_offset + 2] = a[byte_offset + 2];
output[byte_offset + 3] = (alpha_out * 255.0) as u8;
}
output
}
pub fn boolean_intersect(a: &[u8], b: &[u8], width: u32, height: u32) -> Vec<u8> {
let pixel_count = (width * height) as usize;
let expected_length = pixel_count * 4;
assert!(
a.len() >= expected_length && b.len() >= expected_length,
"boolean_intersect: buffer too short (expected {expected_length}, got a={} b={})",
a.len(),
b.len()
);
let mut output = vec![0u8; expected_length];
for pixel_index in 0..pixel_count {
let byte_offset = pixel_index * 4;
let alpha_a = a[byte_offset + 3] as f32 / 255.0;
let alpha_b = b[byte_offset + 3] as f32 / 255.0;
let alpha_out = alpha_a.min(alpha_b);
if alpha_out > 0.0 {
let weight_a = alpha_a / (alpha_a + alpha_b).max(0.001);
let weight_b = 1.0 - weight_a;
output[byte_offset] =
(a[byte_offset] as f32 * weight_a + b[byte_offset] as f32 * weight_b) as u8;
output[byte_offset + 1] =
(a[byte_offset + 1] as f32 * weight_a + b[byte_offset + 1] as f32 * weight_b) as u8;
output[byte_offset + 2] =
(a[byte_offset + 2] as f32 * weight_a + b[byte_offset + 2] as f32 * weight_b) as u8;
}
output[byte_offset + 3] = (alpha_out * 255.0) as u8;
}
output
}
pub fn apply_alpha_mask(source: &[u8], mask: &[u8], width: u32, height: u32) -> Vec<u8> {
let pixel_count = (width * height) as usize;
let expected_length = pixel_count * 4;
assert!(
source.len() >= expected_length && mask.len() >= expected_length,
"apply_alpha_mask: buffer too short (expected {expected_length}, got source={} mask={})",
source.len(),
mask.len()
);
let mut output = vec![0u8; expected_length];
for pixel_index in 0..pixel_count {
let byte_offset = pixel_index * 4;
let source_alpha = source[byte_offset + 3] as f32 / 255.0;
let mask_alpha = mask[byte_offset + 3] as f32 / 255.0;
output[byte_offset] = source[byte_offset];
output[byte_offset + 1] = source[byte_offset + 1];
output[byte_offset + 2] = source[byte_offset + 2];
output[byte_offset + 3] = (source_alpha * mask_alpha * 255.0) as u8;
}
output
}
pub fn apply_luminance_mask(source: &[u8], mask: &[u8], width: u32, height: u32) -> Vec<u8> {
let pixel_count = (width * height) as usize;
let expected_length = pixel_count * 4;
assert!(
source.len() >= expected_length && mask.len() >= expected_length,
"apply_luminance_mask: buffer too short (expected {expected_length}, got source={} mask={})",
source.len(),
mask.len()
);
let mut output = vec![0u8; expected_length];
for pixel_index in 0..pixel_count {
let byte_offset = pixel_index * 4;
let source_alpha = source[byte_offset + 3] as f32 / 255.0;
let luminance = mask[byte_offset] as f32 * 0.299 / 255.0
+ mask[byte_offset + 1] as f32 * 0.587 / 255.0
+ mask[byte_offset + 2] as f32 * 0.114 / 255.0;
output[byte_offset] = source[byte_offset];
output[byte_offset + 1] = source[byte_offset + 1];
output[byte_offset + 2] = source[byte_offset + 2];
output[byte_offset + 3] = (source_alpha * luminance * 255.0) as u8;
}
output
}
pub struct CompositeLayer {
pub texture: Vec<u8>,
pub width: u32,
pub height: u32,
pub offset_x: i32,
pub offset_y: i32,
pub opacity: f32,
}
pub fn composite_layers(
layers: &[CompositeLayer],
output_width: u32,
output_height: u32,
) -> Vec<u8> {
let mut output = vec![0u8; (output_width * output_height * 4) as usize];
for layer in layers {
let opacity = layer.opacity.clamp(0.0, 1.0);
if opacity <= 0.0 {
continue;
}
for source_y in 0..layer.height {
let destination_y = source_y as i32 + layer.offset_y;
if destination_y < 0 || destination_y >= output_height as i32 {
continue;
}
for source_x in 0..layer.width {
let destination_x = source_x as i32 + layer.offset_x;
if destination_x < 0 || destination_x >= output_width as i32 {
continue;
}
let source_offset = ((source_y * layer.width + source_x) * 4) as usize;
let destination_offset =
((destination_y as u32 * output_width + destination_x as u32) * 4) as usize;
let source_alpha = layer.texture[source_offset + 3] as f32 / 255.0 * opacity;
if source_alpha <= 0.0 {
continue;
}
let destination_alpha = output[destination_offset + 3] as f32 / 255.0;
let out_alpha = source_alpha + destination_alpha * (1.0 - source_alpha);
if out_alpha > 0.0 {
let source_weight = source_alpha / out_alpha;
let destination_weight = destination_alpha * (1.0 - source_alpha) / out_alpha;
output[destination_offset] = (layer.texture[source_offset] as f32
* source_weight
+ output[destination_offset] as f32 * destination_weight)
as u8;
output[destination_offset + 1] = (layer.texture[source_offset + 1] as f32
* source_weight
+ output[destination_offset + 1] as f32 * destination_weight)
as u8;
output[destination_offset + 2] = (layer.texture[source_offset + 2] as f32
* source_weight
+ output[destination_offset + 2] as f32 * destination_weight)
as u8;
}
output[destination_offset + 3] = (out_alpha * 255.0) as u8;
}
}
}
output
}
pub fn generate_blurred_texture(source: &[u8], width: u32, height: u32, radius: u32) -> Vec<u8> {
if radius == 0 {
return source.to_vec();
}
let kernel_size = radius * 2 + 1;
let sigma = (radius as f32 + 0.5) / 2.0;
let mut weights = Vec::with_capacity(kernel_size as usize);
let mut weight_sum = 0.0f32;
for index in 0..kernel_size {
let offset = index as f32 - radius as f32;
let weight = (-offset * offset / (2.0 * sigma * sigma)).exp();
weights.push(weight);
weight_sum += weight;
}
for weight in &mut weights {
*weight /= weight_sum;
}
let pixel_count = (width * height) as usize;
let mut intermediate = vec![0.0f32; pixel_count * 4];
for pixel_y in 0..height {
for pixel_x in 0..width {
let mut red = 0.0f32;
let mut green = 0.0f32;
let mut blue = 0.0f32;
let mut alpha = 0.0f32;
for kernel_index in 0..kernel_size {
let sample_x = (pixel_x as i32 + kernel_index as i32 - radius as i32)
.clamp(0, width as i32 - 1) as u32;
let source_offset = ((pixel_y * width + sample_x) * 4) as usize;
let weight = weights[kernel_index as usize];
red += source[source_offset] as f32 * weight;
green += source[source_offset + 1] as f32 * weight;
blue += source[source_offset + 2] as f32 * weight;
alpha += source[source_offset + 3] as f32 * weight;
}
let destination_offset = ((pixel_y * width + pixel_x) * 4) as usize;
intermediate[destination_offset] = red;
intermediate[destination_offset + 1] = green;
intermediate[destination_offset + 2] = blue;
intermediate[destination_offset + 3] = alpha;
}
}
let mut output = vec![0u8; pixel_count * 4];
for pixel_y in 0..height {
for pixel_x in 0..width {
let mut red = 0.0f32;
let mut green = 0.0f32;
let mut blue = 0.0f32;
let mut alpha = 0.0f32;
for kernel_index in 0..kernel_size {
let sample_y = (pixel_y as i32 + kernel_index as i32 - radius as i32)
.clamp(0, height as i32 - 1) as u32;
let source_offset = ((sample_y * width + pixel_x) * 4) as usize;
let weight = weights[kernel_index as usize];
red += intermediate[source_offset] * weight;
green += intermediate[source_offset + 1] * weight;
blue += intermediate[source_offset + 2] * weight;
alpha += intermediate[source_offset + 3] * weight;
}
let destination_offset = ((pixel_y * width + pixel_x) * 4) as usize;
output[destination_offset] = red.clamp(0.0, 255.0) as u8;
output[destination_offset + 1] = green.clamp(0.0, 255.0) as u8;
output[destination_offset + 2] = blue.clamp(0.0, 255.0) as u8;
output[destination_offset + 3] = alpha.clamp(0.0, 255.0) as u8;
}
}
output
}
#[cfg(feature = "assets")]
pub fn load_image_rgba(bytes: &[u8]) -> Result<(Vec<u8>, u32, u32), String> {
let dynamic_image = image::load_from_memory(bytes).map_err(|error| error.to_string())?;
let slot_max = crate::render::wgpu::sprite_texture_atlas::SPRITE_ATLAS_SLOT_SIZE;
let max_width = slot_max.0;
let max_height = slot_max.1;
let resized = if dynamic_image.width() > max_width || dynamic_image.height() > max_height {
dynamic_image.resize(max_width, max_height, image::imageops::FilterType::Lanczos3)
} else {
dynamic_image
};
let rgba = resized.to_rgba8();
let width = rgba.width();
let height = rgba.height();
Ok((rgba.into_raw(), width, height))
}
fn generate_outlined_rect_texture(size: u32) -> Vec<u8> {
let mut data = vec![0u8; (size * size * 4) as usize];
let margin = 2.0;
let border_width = size as f32 * 0.08;
for pixel_y in 0..size {
for pixel_x in 0..size {
let px = pixel_x as f32;
let py = pixel_y as f32;
let distance_to_left = px - margin;
let distance_to_right = (size as f32 - margin) - px;
let distance_to_top = py - margin;
let distance_to_bottom = (size as f32 - margin) - py;
let distance_to_nearest_edge = distance_to_left
.min(distance_to_right)
.min(distance_to_top)
.min(distance_to_bottom);
let outside = distance_to_nearest_edge < 0.0;
let inside_border = distance_to_nearest_edge < border_width;
let index = ((pixel_y * size + pixel_x) * 4) as usize;
if outside {
let alpha = (1.0 + distance_to_nearest_edge).clamp(0.0, 1.0);
if alpha > 0.0 {
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (alpha * 255.0) as u8;
}
} else if inside_border {
let inner_alpha = (border_width - distance_to_nearest_edge).clamp(0.0, 1.0);
data[index] = 255;
data[index + 1] = 255;
data[index + 2] = 255;
data[index + 3] = (inner_alpha * 255.0) as u8;
}
}
}
data
}