use crate::ecs::sprite::commands::spawn_sprite;
use crate::ecs::sprite::slot_allocator::{allocate_sprite_slot, sized_slot_uv};
use crate::ecs::world::commands::WorldCommand;
use crate::ecs::world::{Entity, World};
use crate::render::wgpu::sprite_shapes;
use nalgebra_glm::Vec2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum SpriteShape {
Rect,
Circle,
SoftCircle,
Ring,
Triangle,
Capsule,
OutlinedRect,
}
pub fn spawn_shape(
world: &mut World,
shape: SpriteShape,
position: Vec2,
size: Vec2,
color: [f32; 4],
) -> Entity {
let entity = spawn_sprite(world, position, size);
let (texture_index, uv_min, uv_max) = shape_texture_info(shape);
if let Some(sprite) = world.sprite2d.get_sprite_mut(entity) {
sprite.texture_index = texture_index;
sprite.texture_index2 = texture_index;
sprite.uv_min = uv_min;
sprite.uv_max = uv_max;
sprite.color = color;
}
entity
}
pub fn shape_texture_info(shape: SpriteShape) -> (u32, Vec2, Vec2) {
match shape {
SpriteShape::Rect => (0, Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)),
SpriteShape::Circle => {
let (uv_min, uv_max) = sprite_shapes::shape_uv();
(sprite_shapes::SHAPE_SLOT_CIRCLE, uv_min, uv_max)
}
SpriteShape::SoftCircle => {
let (uv_min, uv_max) = sprite_shapes::shape_uv();
(sprite_shapes::SHAPE_SLOT_SOFT_CIRCLE, uv_min, uv_max)
}
SpriteShape::Ring => {
let (uv_min, uv_max) = sprite_shapes::shape_uv();
(sprite_shapes::SHAPE_SLOT_RING, uv_min, uv_max)
}
SpriteShape::Triangle => {
let (uv_min, uv_max) = sprite_shapes::shape_uv();
(sprite_shapes::SHAPE_SLOT_TRIANGLE, uv_min, uv_max)
}
SpriteShape::Capsule => {
let (uv_min, uv_max) = sprite_shapes::shape_uv();
(sprite_shapes::SHAPE_SLOT_CAPSULE, uv_min, uv_max)
}
SpriteShape::OutlinedRect => {
let (uv_min, uv_max) = sprite_shapes::shape_uv();
(sprite_shapes::SHAPE_SLOT_OUTLINED_RECT, uv_min, uv_max)
}
}
}
pub fn spawn_rect(world: &mut World, position: Vec2, size: Vec2, color: [f32; 4]) -> Entity {
spawn_shape(world, SpriteShape::Rect, position, size, color)
}
pub fn spawn_circle(world: &mut World, position: Vec2, radius: f32, color: [f32; 4]) -> Entity {
let size = Vec2::new(radius * 2.0, radius * 2.0);
spawn_shape(world, SpriteShape::Circle, position, size, color)
}
pub fn spawn_ring(world: &mut World, position: Vec2, radius: f32, color: [f32; 4]) -> Entity {
let size = Vec2::new(radius * 2.0, radius * 2.0);
spawn_shape(world, SpriteShape::Ring, position, size, color)
}
pub fn spawn_triangle(world: &mut World, position: Vec2, size: Vec2, color: [f32; 4]) -> Entity {
spawn_shape(world, SpriteShape::Triangle, position, size, color)
}
pub fn spawn_capsule(world: &mut World, position: Vec2, size: Vec2, color: [f32; 4]) -> Entity {
spawn_shape(world, SpriteShape::Capsule, position, size, color)
}
pub fn spawn_outlined_rect(
world: &mut World,
position: Vec2,
size: Vec2,
color: [f32; 4],
) -> Entity {
spawn_shape(world, SpriteShape::OutlinedRect, position, size, color)
}
pub fn spawn_line(
world: &mut World,
start: Vec2,
end: Vec2,
thickness: f32,
color: [f32; 4],
) -> Entity {
let delta = end - start;
let length = nalgebra_glm::length(&delta);
let angle = delta.y.atan2(delta.x);
let midpoint = (start + end) * 0.5;
let aa_padding = (thickness * 0.3).max(1.0);
let (uv_min, uv_max) = sprite_shapes::shape_uv();
let entity = spawn_sprite(
world,
midpoint,
Vec2::new(length + aa_padding, thickness + aa_padding),
);
if let Some(sprite) = world.sprite2d.get_sprite_mut(entity) {
sprite.rotation = angle;
sprite.color = color;
sprite.texture_index = sprite_shapes::SHAPE_SLOT_ANTIALIASED_RECT;
sprite.texture_index2 = sprite_shapes::SHAPE_SLOT_ANTIALIASED_RECT;
sprite.uv_min = uv_min;
sprite.uv_max = uv_max;
}
entity
}
pub fn spawn_arrow(
world: &mut World,
start: Vec2,
end: Vec2,
thickness: f32,
head_size: f32,
color: [f32; 4],
) -> (Entity, Entity) {
let delta = end - start;
let angle = delta.y.atan2(delta.x);
let line_entity = spawn_line(world, start, end, thickness, color);
let head_entity = spawn_triangle(world, end, Vec2::new(head_size, head_size), color);
if let Some(sprite) = world.sprite2d.get_sprite_mut(head_entity) {
sprite.rotation = angle - std::f32::consts::FRAC_PI_2;
}
(line_entity, head_entity)
}
pub fn spawn_path(
world: &mut World,
points: &[Vec2],
thickness: f32,
color: [f32; 4],
closed: bool,
) -> Vec<Entity> {
let mut entities = Vec::new();
if points.len() < 2 {
return entities;
}
for index in 0..points.len() - 1 {
entities.push(spawn_line(
world,
points[index],
points[index + 1],
thickness,
color,
));
}
if closed && points.len() > 2 {
entities.push(spawn_line(
world,
points[points.len() - 1],
points[0],
thickness,
color,
));
}
entities
}
fn evaluate_quadratic_bezier(start: Vec2, control: Vec2, end: Vec2, parameter: f32) -> Vec2 {
let inverse = 1.0 - parameter;
let inverse_squared = inverse * inverse;
let parameter_squared = parameter * parameter;
Vec2::new(
inverse_squared * start.x
+ 2.0 * inverse * parameter * control.x
+ parameter_squared * end.x,
inverse_squared * start.y
+ 2.0 * inverse * parameter * control.y
+ parameter_squared * end.y,
)
}
fn evaluate_cubic_bezier(
start: Vec2,
control_a: Vec2,
control_b: Vec2,
end: Vec2,
parameter: f32,
) -> Vec2 {
let inverse = 1.0 - parameter;
let inverse_squared = inverse * inverse;
let inverse_cubed = inverse_squared * inverse;
let parameter_squared = parameter * parameter;
let parameter_cubed = parameter_squared * parameter;
Vec2::new(
inverse_cubed * start.x
+ 3.0 * inverse_squared * parameter * control_a.x
+ 3.0 * inverse * parameter_squared * control_b.x
+ parameter_cubed * end.x,
inverse_cubed * start.y
+ 3.0 * inverse_squared * parameter * control_a.y
+ 3.0 * inverse * parameter_squared * control_b.y
+ parameter_cubed * end.y,
)
}
pub fn spawn_quadratic_bezier(
world: &mut World,
start: Vec2,
control: Vec2,
end: Vec2,
segments: u32,
thickness: f32,
color: [f32; 4],
) -> Vec<Entity> {
let mut entities = Vec::with_capacity(segments as usize);
let mut previous = start;
for index in 1..=segments {
let parameter = index as f32 / segments as f32;
let current = evaluate_quadratic_bezier(start, control, end, parameter);
entities.push(spawn_line(world, previous, current, thickness, color));
previous = current;
}
entities
}
pub struct CubicBezier {
pub start: Vec2,
pub control_a: Vec2,
pub control_b: Vec2,
pub end: Vec2,
}
pub fn spawn_cubic_bezier(
world: &mut World,
curve: &CubicBezier,
segments: u32,
thickness: f32,
color: [f32; 4],
) -> Vec<Entity> {
let mut entities = Vec::with_capacity(segments as usize);
let mut previous = curve.start;
for index in 1..=segments {
let parameter = index as f32 / segments as f32;
let current = evaluate_cubic_bezier(
curve.start,
curve.control_a,
curve.control_b,
curve.end,
parameter,
);
entities.push(spawn_line(world, previous, current, thickness, color));
previous = current;
}
entities
}
pub fn spawn_ellipse(
world: &mut World,
position: Vec2,
radius_x: f32,
radius_y: f32,
color: [f32; 4],
) -> Entity {
let safe_radius_x = radius_x.max(0.001);
let diameter = safe_radius_x * 2.0;
let entity = spawn_circle(world, position, safe_radius_x, color);
if let Some(sprite) = world.sprite2d.get_sprite_mut(entity) {
sprite.size = Vec2::new(diameter, diameter);
sprite.scale = Vec2::new(1.0, radius_y / safe_radius_x);
}
entity
}
pub fn spawn_rounded_rect(
world: &mut World,
position: Vec2,
size: Vec2,
texture_slot: u32,
color: [f32; 4],
) -> Entity {
let (uv_min, uv_max) = sprite_shapes::shape_uv();
let entity = spawn_sprite(world, position, size);
if let Some(sprite) = world.sprite2d.get_sprite_mut(entity) {
sprite.texture_index = texture_slot;
sprite.texture_index2 = texture_slot;
sprite.uv_min = uv_min;
sprite.uv_max = uv_max;
sprite.color = color;
}
entity
}
pub fn spawn_filled_polygon(
world: &mut World,
position: Vec2,
size: Vec2,
texture_slot: u32,
color: [f32; 4],
) -> Entity {
let (uv_min, uv_max) = sprite_shapes::shape_uv();
let entity = spawn_sprite(world, position, size);
if let Some(sprite) = world.sprite2d.get_sprite_mut(entity) {
sprite.texture_index = texture_slot;
sprite.texture_index2 = texture_slot;
sprite.uv_min = uv_min;
sprite.uv_max = uv_max;
sprite.color = color;
}
entity
}
pub fn spawn_dashed_line(
world: &mut World,
start: Vec2,
end: Vec2,
thickness: f32,
dash_length: f32,
gap_length: f32,
color: [f32; 4],
) -> Vec<Entity> {
let mut entities = Vec::new();
let delta = end - start;
let total_length = nalgebra_glm::length(&delta);
if total_length < 0.001 {
return entities;
}
let direction = delta / total_length;
let stride = dash_length + gap_length;
let mut offset = 0.0;
while offset < total_length {
let dash_end_offset = (offset + dash_length).min(total_length);
let dash_start = start + direction * offset;
let dash_end = start + direction * dash_end_offset;
entities.push(spawn_line(world, dash_start, dash_end, thickness, color));
offset += stride;
}
entities
}
pub fn spawn_variable_width_path(
world: &mut World,
points: &[Vec2],
pressures: &[f32],
base_thickness: f32,
color: [f32; 4],
) -> Vec<Entity> {
let mut entities = Vec::new();
if points.len() < 2 || pressures.len() < points.len() {
return entities;
}
for index in 0..points.len() - 1 {
let pressure = (pressures[index] + pressures[index + 1]) * 0.5;
let thickness = base_thickness * pressure;
entities.push(spawn_line(
world,
points[index],
points[index + 1],
thickness,
color,
));
}
entities
}
pub fn spawn_filled_and_stroked_circle(
world: &mut World,
position: Vec2,
radius: f32,
fill_color: [f32; 4],
stroke_color: [f32; 4],
) -> (Entity, Entity) {
let fill_entity = spawn_circle(world, position, radius, fill_color);
if let Some(sprite) = world.sprite2d.get_sprite_mut(fill_entity) {
sprite.depth = 0.01;
}
let stroke_entity = spawn_ring(world, position, radius, stroke_color);
(fill_entity, stroke_entity)
}
pub fn spawn_filled_and_stroked_rect(
world: &mut World,
position: Vec2,
size: Vec2,
fill_color: [f32; 4],
stroke_color: [f32; 4],
) -> (Entity, Entity) {
let fill_entity = spawn_rect(world, position, size, fill_color);
if let Some(sprite) = world.sprite2d.get_sprite_mut(fill_entity) {
sprite.depth = 0.01;
}
let stroke_entity = spawn_outlined_rect(world, position, size, stroke_color);
(fill_entity, stroke_entity)
}
pub fn spawn_filled_and_stroked_ellipse(
world: &mut World,
position: Vec2,
radius_x: f32,
radius_y: f32,
fill_color: [f32; 4],
stroke_color: [f32; 4],
ring_texture_slot: u32,
) -> (Entity, Entity) {
let fill_entity = spawn_ellipse(world, position, radius_x, radius_y, fill_color);
if let Some(sprite) = world.sprite2d.get_sprite_mut(fill_entity) {
sprite.depth = 0.01;
}
let (uv_min, uv_max) = sprite_shapes::shape_uv();
let diameter_x = radius_x * 2.0;
let diameter_y = radius_y * 2.0;
let entity = spawn_sprite(world, position, Vec2::new(diameter_x, diameter_y));
if let Some(sprite) = world.sprite2d.get_sprite_mut(entity) {
sprite.texture_index = ring_texture_slot;
sprite.texture_index2 = ring_texture_slot;
sprite.uv_min = uv_min;
sprite.uv_max = uv_max;
sprite.color = stroke_color;
}
(fill_entity, entity)
}
pub fn screen_pixels_to_world_size(world: &World, pixels: f32) -> f32 {
let viewport_width = world
.resources
.window
.cached_viewport_size
.map(|(width, _)| width as f32)
.unwrap_or(1920.0);
let ortho_x_mag = world
.resources
.active_camera
.and_then(|entity| world.core.get_camera(entity))
.and_then(|camera| {
if let crate::ecs::camera::components::Projection::Orthographic(ortho) =
&camera.projection
{
Some(ortho.x_mag)
} else {
None
}
})
.unwrap_or(480.0);
pixels * (ortho_x_mag * 2.0) / viewport_width
}
pub fn tessellate_quadratic_bezier(
start: Vec2,
control: Vec2,
end: Vec2,
segments: u32,
) -> Vec<Vec2> {
let mut points = Vec::with_capacity(segments as usize + 1);
for index in 0..=segments {
let parameter = index as f32 / segments as f32;
points.push(evaluate_quadratic_bezier(start, control, end, parameter));
}
points
}
pub fn tessellate_cubic_bezier(
start: Vec2,
control_a: Vec2,
control_b: Vec2,
end: Vec2,
segments: u32,
) -> Vec<Vec2> {
let mut points = Vec::with_capacity(segments as usize + 1);
for index in 0..=segments {
let parameter = index as f32 / segments as f32;
points.push(evaluate_cubic_bezier(
start, control_a, control_b, end, parameter,
));
}
points
}
pub fn spawn_filled_path(
world: &mut World,
points: &[Vec2],
color: [f32; 4],
position: Vec2,
size: Vec2,
depth: f32,
) -> Entity {
let normalized_points: Vec<[f32; 2]> = points.iter().map(|point| [point.x, point.y]).collect();
let texture_size = 256u32;
let texture_data = sprite_shapes::generate_filled_polygon_texture(
texture_size,
texture_size,
&normalized_points,
);
let slot = allocate_sprite_slot(world);
let (uv_min, uv_max) = sized_slot_uv(texture_size, texture_size);
world
.resources
.command_queue
.push(WorldCommand::UploadSpriteTexture {
slot,
rgba_data: texture_data,
width: texture_size,
height: texture_size,
});
let entity = spawn_sprite(world, position, size);
if let Some(sprite) = world.sprite2d.get_sprite_mut(entity) {
sprite.texture_index = slot;
sprite.texture_index2 = slot;
sprite.uv_min = uv_min;
sprite.uv_max = uv_max;
sprite.color = color;
sprite.depth = depth;
}
entity
}
pub struct BlurSource<'a> {
pub texture: &'a [u8],
pub width: u32,
pub height: u32,
pub blur_radius: u32,
}
fn pad_texture(source: &[u8], width: u32, height: u32, padding: u32) -> Vec<u8> {
let padded_width = width + padding * 2;
let padded_height = height + padding * 2;
let mut padded = vec![0u8; (padded_width * padded_height * 4) as usize];
for row in 0..height {
let source_row_start = (row * width * 4) as usize;
let destination_row_start = ((row + padding) * padded_width + padding) as usize * 4;
let row_bytes = (width * 4) as usize;
padded[destination_row_start..destination_row_start + row_bytes]
.copy_from_slice(&source[source_row_start..source_row_start + row_bytes]);
}
padded
}
pub fn spawn_shadow(
world: &mut World,
source: &BlurSource,
shadow_color: [f32; 4],
offset: Vec2,
position: Vec2,
size: Vec2,
depth: f32,
) -> Entity {
let padding = source.blur_radius;
let padded_width = source.width + padding * 2;
let padded_height = source.height + padding * 2;
let padded = pad_texture(source.texture, source.width, source.height, padding);
let mut blurred = sprite_shapes::generate_blurred_texture(
&padded,
padded_width,
padded_height,
source.blur_radius,
);
let pixel_count = (padded_width * padded_height) as usize;
for pixel_index in 0..pixel_count {
let byte_offset = pixel_index * 4;
blurred[byte_offset] = (shadow_color[0] * 255.0) as u8;
blurred[byte_offset + 1] = (shadow_color[1] * 255.0) as u8;
blurred[byte_offset + 2] = (shadow_color[2] * 255.0) as u8;
blurred[byte_offset + 3] =
(blurred[byte_offset + 3] as f32 / 255.0 * shadow_color[3] * 255.0) as u8;
}
let slot = allocate_sprite_slot(world);
let (uv_min, uv_max) = sized_slot_uv(padded_width, padded_height);
world
.resources
.command_queue
.push(WorldCommand::UploadSpriteTexture {
slot,
rgba_data: blurred,
width: padded_width,
height: padded_height,
});
let entity = spawn_sprite(world, position + offset, size);
if let Some(sprite) = world.sprite2d.get_sprite_mut(entity) {
sprite.texture_index = slot;
sprite.texture_index2 = slot;
sprite.uv_min = uv_min;
sprite.uv_max = uv_max;
sprite.color = [1.0, 1.0, 1.0, 1.0];
sprite.depth = depth;
}
entity
}
pub fn spawn_glow(
world: &mut World,
source: &BlurSource,
glow_color: [f32; 4],
position: Vec2,
size: Vec2,
depth: f32,
) -> Entity {
let padding = source.blur_radius;
let padded_width = source.width + padding * 2;
let padded_height = source.height + padding * 2;
let padded = pad_texture(source.texture, source.width, source.height, padding);
let mut blurred = sprite_shapes::generate_blurred_texture(
&padded,
padded_width,
padded_height,
source.blur_radius,
);
let pixel_count = (padded_width * padded_height) as usize;
for pixel_index in 0..pixel_count {
let byte_offset = pixel_index * 4;
blurred[byte_offset] = (glow_color[0] * 255.0) as u8;
blurred[byte_offset + 1] = (glow_color[1] * 255.0) as u8;
blurred[byte_offset + 2] = (glow_color[2] * 255.0) as u8;
blurred[byte_offset + 3] =
(blurred[byte_offset + 3] as f32 / 255.0 * glow_color[3] * 255.0) as u8;
}
let slot = allocate_sprite_slot(world);
let (uv_min, uv_max) = sized_slot_uv(padded_width, padded_height);
world
.resources
.command_queue
.push(WorldCommand::UploadSpriteTexture {
slot,
rgba_data: blurred,
width: padded_width,
height: padded_height,
});
let entity = spawn_sprite(world, position, size);
if let Some(sprite) = world.sprite2d.get_sprite_mut(entity) {
sprite.texture_index = slot;
sprite.texture_index2 = slot;
sprite.uv_min = uv_min;
sprite.uv_max = uv_max;
sprite.color = [1.0, 1.0, 1.0, 1.0];
sprite.depth = depth;
sprite.blend_mode = crate::ecs::sprite::components::SpriteBlendMode::Additive;
}
entity
}
pub fn spawn_shape_with_aa(
world: &mut World,
shape_texture: Vec<u8>,
texture_size: u32,
color: [f32; 4],
position: Vec2,
size: Vec2,
depth: f32,
) -> Entity {
let slot = allocate_sprite_slot(world);
let (uv_min, uv_max) = sized_slot_uv(texture_size, texture_size);
world
.resources
.command_queue
.push(WorldCommand::UploadSpriteTexture {
slot,
rgba_data: shape_texture,
width: texture_size,
height: texture_size,
});
let entity = spawn_sprite(world, position, size);
if let Some(sprite) = world.sprite2d.get_sprite_mut(entity) {
sprite.texture_index = slot;
sprite.texture_index2 = slot;
sprite.uv_min = uv_min;
sprite.uv_max = uv_max;
sprite.color = color;
sprite.depth = depth;
}
entity
}
#[cfg(feature = "assets")]
pub fn spawn_sprite_from_image_bytes(
world: &mut World,
bytes: &[u8],
position: Vec2,
size: Vec2,
depth: f32,
) -> Entity {
let (rgba_data, width, height) =
sprite_shapes::load_image_rgba(bytes).expect("Failed to decode image");
let slot = allocate_sprite_slot(world);
let (uv_min, uv_max) = sized_slot_uv(width, height);
world
.resources
.command_queue
.push(WorldCommand::UploadSpriteTexture {
slot,
rgba_data,
width,
height,
});
let entity = spawn_sprite(world, position, size);
if let Some(sprite) = world.sprite2d.get_sprite_mut(entity) {
sprite.texture_index = slot;
sprite.texture_index2 = slot;
sprite.uv_min = uv_min;
sprite.uv_max = uv_max;
sprite.depth = depth;
}
entity
}