initial commit

This commit is contained in:
Gered 2022-05-15 12:11:38 -04:00
commit 38e6826440
41 changed files with 8332 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
Cargo.lock
.DS_Store

5
Cargo.toml Normal file
View file

@ -0,0 +1,5 @@
[workspace]
members = [
"libretrogd",
"examples/*",
]

0
examples/.keep Normal file
View file

32
libretrogd/Cargo.toml Normal file
View file

@ -0,0 +1,32 @@
[package]
name = "libretrogd"
description = "A 'retro'-like game development library, for funsies."
version = "0.1.0"
authors = ["Gered King <gered@blarg.ca>"]
edition = "2021"
[features]
low_res = []
wide = []
[dependencies]
sdl2 = { version = "0.34.5", features = ["static-link", "bundled", "unsafe_textures" ] }
byte-slice-cast = "1.2.1"
byteorder = "1.4.3"
thiserror = "1.0.30"
rand = "0.8.5"
num-traits = "0.2.14"
[dev-dependencies]
claim = "0.5.0"
criterion = "0.3.5"
anyhow = "1.0.55"
tempfile = "3.3.0"
[[bench]]
name = "bitmap"
harness = false
[[bench]]
name = "blit"
harness = false

BIN
libretrogd/assets/vga.fnt Normal file

Binary file not shown.

BIN
libretrogd/assets/vga.pal Normal file

Binary file not shown.

View file

@ -0,0 +1,28 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use libretrogd::Bitmap;
use libretrogd::Palette;
use libretrogd::{SCREEN_HEIGHT, SCREEN_WIDTH};
pub fn criterion_benchmark(c: &mut Criterion) {
let mut source = Bitmap::new(SCREEN_WIDTH, SCREEN_HEIGHT).unwrap();
let mut dest = vec![0u32; (SCREEN_WIDTH * SCREEN_HEIGHT * 4) as usize].into_boxed_slice();
let palette = Palette::new_vga_palette().unwrap();
c.bench_function("deindex_bitmap_pixels", |b| {
b.iter(|| source.copy_as_argb_to(&mut dest, &palette))
});
c.bench_function("set_pixel", |b| {
b.iter(|| source.set_pixel(black_box(100), black_box(100), black_box(42)))
});
c.bench_function("set_pixel_unchecked", |b| {
b.iter(|| unsafe {
source.set_pixel_unchecked(black_box(100), black_box(100), black_box(42))
})
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View file

@ -0,0 +1,62 @@
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use libretrogd::{Bitmap, BlitMethod, Rect};
pub fn criterion_benchmark(c: &mut Criterion) {
let mut framebuffer = Bitmap::new(320, 240).unwrap();
let (bmp, _) = Bitmap::load_iff_file(Path::new("./test-assets/test-tiles.lbm")).unwrap();
let mut solid_bmp = Bitmap::new(16, 16).unwrap();
solid_bmp.blit_region(BlitMethod::Solid, &bmp, &Rect::new(16, 16, 16, 16), 0, 0);
let mut trans_bmp = Bitmap::new(16, 16).unwrap();
trans_bmp.blit_region(BlitMethod::Solid, &bmp, &Rect::new(160, 0, 16, 16), 0, 0);
c.bench_function("blit_single_checked_solid", |b| {
b.iter(|| {
framebuffer.blit(
black_box(BlitMethod::Solid),
black_box(&solid_bmp),
black_box(100),
black_box(100),
)
})
});
c.bench_function("blit_single_unchecked_solid", |b| {
b.iter(|| unsafe {
framebuffer.blit_unchecked(
black_box(BlitMethod::Solid),
black_box(&solid_bmp),
black_box(100),
black_box(100),
)
})
});
c.bench_function("blit_single_checked_transparent", |b| {
b.iter(|| {
framebuffer.blit(
black_box(BlitMethod::Transparent(0)),
black_box(&trans_bmp),
black_box(100),
black_box(100),
)
})
});
c.bench_function("blit_single_unchecked_transparent", |b| {
b.iter(|| unsafe {
framebuffer.blit_unchecked(
black_box(BlitMethod::Transparent(0)),
black_box(&trans_bmp),
black_box(100),
black_box(100),
)
})
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View file

@ -0,0 +1,703 @@
use std::any::{TypeId};
use std::cell::{Ref, RefCell, RefMut};
use std::collections::{HashMap, HashSet};
use crate::utils::AsAny;
pub type EntityId = usize;
///////////////////////////////////////////////////////////////////////////////////////////////////
// alias `Component` to always be `'static` ...
pub trait Component: 'static {}
impl<T: 'static> Component for T {}
pub type ComponentStore<T> = RefCell<HashMap<EntityId, T>>;
pub type RefComponents<'a, T> = Ref<'a, HashMap<EntityId, T>>;
pub type RefMutComponents<'a, T> = RefMut<'a, HashMap<EntityId, T>>;
pub trait GenericComponentStore: AsAny {
fn has(&self, entity: EntityId) -> bool;
fn remove(&mut self, entity: EntityId) -> bool;
fn clear(&mut self);
}
impl<T: Component> GenericComponentStore for ComponentStore<T> {
#[inline]
fn has(&self, entity: EntityId) -> bool {
self.borrow().contains_key(&entity)
}
#[inline]
fn remove(&mut self, entity: EntityId) -> bool {
self.get_mut().remove(&entity).is_some()
}
#[inline]
fn clear(&mut self) {
self.get_mut().clear();
}
}
#[inline]
pub fn as_component_store<T: Component>(
collection: &dyn GenericComponentStore,
) -> &ComponentStore<T> {
collection.as_any().downcast_ref().unwrap()
}
#[inline]
pub fn as_component_store_mut<T: Component>(
collection: &mut dyn GenericComponentStore,
) -> &mut ComponentStore<T> {
collection.as_any_mut().downcast_mut().unwrap()
}
///////////////////////////////////////////////////////////////////////////////////////////////////
pub struct Entities {
entities: HashSet<EntityId>,
component_stores: HashMap<TypeId, Box<dyn GenericComponentStore>>,
next_id: EntityId,
}
impl Entities {
pub fn new() -> Self {
Entities {
entities: HashSet::new(),
component_stores: HashMap::new(),
next_id: 0,
}
}
#[inline]
fn has_component_store<T: Component>(&self) -> bool {
let type_id = TypeId::of::<T>();
self.component_stores.contains_key(&type_id)
}
fn get_component_store<T: Component>(&self) -> Option<&ComponentStore<T>> {
if !self.has_component_store::<T>() {
None
} else {
let type_id = TypeId::of::<T>();
Some(as_component_store(
self.component_stores.get(&type_id).unwrap().as_ref(),
))
}
}
fn add_component_store<T: Component>(&mut self) -> &ComponentStore<T> {
if self.has_component_store::<T>() {
self.get_component_store().unwrap()
} else {
let component_store = ComponentStore::<T>::new(HashMap::new());
let type_id = TypeId::of::<T>();
self.component_stores
.insert(type_id, Box::new(component_store));
as_component_store(self.component_stores.get(&type_id).unwrap().as_ref())
}
}
#[inline]
pub fn has_entity(&self, entity: EntityId) -> bool {
self.entities.contains(&entity)
}
pub fn new_entity(&mut self) -> EntityId {
let new_entity_id = self.next_id;
self.next_id = self.next_id.wrapping_add(1);
self.entities.insert(new_entity_id);
new_entity_id
}
pub fn remove_entity(&mut self, entity: EntityId) -> bool {
if !self.has_entity(entity) {
return false;
}
self.entities.remove(&entity);
for (_, component_store) in self.component_stores.iter_mut() {
component_store.remove(entity);
}
true
}
pub fn remove_all_entities(&mut self) {
self.entities.clear();
for (_, component_store) in self.component_stores.iter_mut() {
component_store.clear();
}
}
pub fn has_component<T: Component>(&self, entity: EntityId) -> bool {
if !self.has_entity(entity) {
false
} else {
let type_id = TypeId::of::<T>();
if let Some(component_store) = self.component_stores.get(&type_id) {
component_store.has(entity)
} else {
false
}
}
}
pub fn add_component<T: Component>(&mut self, entity: EntityId, component: T) -> bool {
if !self.has_entity(entity) {
false
} else {
if let Some(component_store) = self.get_component_store::<T>() {
component_store.borrow_mut().insert(entity, component);
} else {
self.add_component_store::<T>()
.borrow_mut()
.insert(entity, component);
}
true
}
}
pub fn remove_component<T: Component>(&mut self, entity: EntityId) -> bool {
if !self.has_entity(entity) {
false
} else {
let type_id = TypeId::of::<T>();
if let Some(component_store) = self.component_stores.get_mut(&type_id) {
component_store.remove(entity)
} else {
false
}
}
}
#[inline]
pub fn components<T: Component>(&self) -> Option<RefComponents<T>> {
if let Some(component_store) = self.get_component_store() {
Some(component_store.borrow())
} else {
None
}
}
#[inline]
pub fn components_mut<T: Component>(&self) -> Option<RefMutComponents<T>> {
if let Some(component_store) = self.get_component_store() {
Some(component_store.borrow_mut())
} else {
None
}
}
pub fn init_components<T: Component>(&mut self) {
if self.get_component_store::<T>().is_none() {
self.add_component_store::<T>();
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// TODO: is there some fancy way to get rid of the impl duplication here ... ?
pub trait ComponentStoreConvenience<T: Component> {
fn single(&self) -> Option<(&EntityId, &T)>;
fn get(&self, k: &EntityId) -> Option<&T>;
}
pub trait ComponentStoreConvenienceMut<T: Component> {
fn single_mut(&mut self) -> Option<(&EntityId, &T)>;
fn get_mut(&mut self, k: &EntityId) -> Option<&mut T>;
}
impl<'a, T: Component> ComponentStoreConvenience<T> for Option<RefComponents<'a, T>> {
fn single(&self) -> Option<(&EntityId, &T)> {
if let Some(components) = self {
if let Some((entity_id, component)) = components.iter().next() {
return Some((entity_id, component));
}
}
None
}
fn get(&self, k: &EntityId) -> Option<&T> {
if let Some(components) = self {
components.get(k)
} else {
None
}
}
}
impl<'a, T: Component> ComponentStoreConvenience<T> for Option<RefMutComponents<'a, T>> {
fn single(&self) -> Option<(&EntityId, &T)> {
if let Some(components) = self {
if let Some((entity_id, component)) = components.iter().next() {
return Some((entity_id, component));
}
}
None
}
fn get(&self, k: &EntityId) -> Option<&T> {
if let Some(components) = self {
components.get(k)
} else {
None
}
}
}
impl<'a, T: Component> ComponentStoreConvenienceMut<T> for Option<RefMutComponents<'a, T>> {
fn single_mut(&mut self) -> Option<(&EntityId, &T)> {
if let Some(components) = self {
if let Some((entity_id, component)) = components.iter_mut().next() {
return Some((entity_id, component));
}
}
None
}
fn get_mut(&mut self, k: &EntityId) -> Option<&mut T> {
if let Some(components) = self {
components.get_mut(k)
} else {
None
}
}
}
pub trait OptionComponentStore<T: Component> {
fn len(&self) -> usize;
fn is_empty(&self) -> bool;
}
impl<'a, T: Component> OptionComponentStore<T> for Option<RefComponents<'a, T>> {
fn len(&self) -> usize {
if let Some(components) = self {
components.len()
} else {
0
}
}
fn is_empty(&self) -> bool {
if let Some(components) = self {
components.is_empty()
} else {
true
}
}
}
impl<'a, T: Component> OptionComponentStore<T> for Option<RefMutComponents<'a, T>> {
fn len(&self) -> usize {
if let Some(components) = self {
components.len()
} else {
0
}
}
fn is_empty(&self) -> bool {
if let Some(components) = self {
components.is_empty()
} else {
true
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
pub type UpdateFn<T> = fn(&mut Entities, &mut T);
pub type RenderFn<T> = fn(&mut Entities, &mut T);
pub struct ComponentSystems<U, R> {
update_systems: Vec<UpdateFn<U>>,
render_systems: Vec<RenderFn<R>>,
}
impl<U, R> ComponentSystems<U, R> {
pub fn new() -> Self {
ComponentSystems {
update_systems: Vec::new(),
render_systems: Vec::new(),
}
}
pub fn add_update_system(&mut self, f: UpdateFn<U>) {
self.update_systems.push(f);
}
pub fn add_render_system(&mut self, f: RenderFn<R>) {
self.render_systems.push(f);
}
pub fn reset(&mut self) {
self.update_systems.clear();
self.render_systems.clear();
}
pub fn update(&mut self, entities: &mut Entities, context: &mut U) {
for f in self.update_systems.iter_mut() {
f(entities, context);
}
}
pub fn render(&mut self, entities: &mut Entities, context: &mut R) {
for f in self.render_systems.iter_mut() {
f(entities, context);
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
#[cfg(test)]
mod tests {
use claim::*;
use super::*;
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
struct Name(&'static str);
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
struct Position(i32, i32);
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
struct Velocity(i32, i32);
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
struct Health(u32);
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
struct Counter(u32);
#[test]
fn add_and_remove_entities() {
let mut em = Entities::new();
// add first entity
assert!(!em.has_entity(1));
let a = em.new_entity();
assert!(em.has_entity(a));
// add second entity with totally different id from first entity
let b = em.new_entity();
assert!(em.has_entity(a));
assert!(em.has_entity(b));
assert_ne!(a, b);
// remove first entity
assert!(em.remove_entity(a));
assert!(!em.has_entity(a));
assert!(em.has_entity(b));
// remove second entity
assert!(em.remove_entity(b));
assert!(!em.has_entity(b));
// removing entities which don't exist shouldn't blow up
assert!(!em.remove_entity(a));
assert!(!em.remove_entity(b));
// add third entity, will not re-use previous entity ids
let c = em.new_entity();
assert!(em.has_entity(c));
assert_ne!(a, c);
assert_ne!(b, c);
}
#[test]
fn add_and_remove_entity_components() {
let mut em = Entities::new();
// create first entity
let a = em.new_entity();
assert!(em.has_entity(a));
// add component
assert!(!em.has_component::<Name>(a));
assert!(em.add_component(a, Name("Someone")));
assert!(em.has_component::<Name>(a));
// verify the added component
{
let names = em.components::<Name>().unwrap();
let name_a = names.get(&a).unwrap();
assert_eq!("Someone", name_a.0);
}
// create second entity
let b = em.new_entity();
assert!(em.has_entity(b));
// add component to second entity
assert!(!em.has_component::<Position>(b));
assert_none!(em.components::<Position>());
assert!(em.add_component(b, Position(1, 2)));
assert!(em.has_component::<Position>(b));
// verify the added component
{
let positions = em.components::<Position>().unwrap();
let position_b = positions.get(&b).unwrap();
assert!(1 == position_b.0 && 2 == position_b.1);
}
// verify current components for both entities are what we expect
assert!(em.has_component::<Name>(a));
assert!(!em.has_component::<Name>(b));
assert!(!em.has_component::<Position>(a));
assert!(em.has_component::<Position>(b));
// add new component to first entity
assert!(em.add_component(a, Position(5, 3)));
assert!(em.has_component::<Position>(a));
// verify both position components for both entities
{
let positions = em.components::<Position>().unwrap();
let position_a = positions.get(&a).unwrap();
assert!(5 == position_a.0 && 3 == position_a.1);
let position_b = positions.get(&b).unwrap();
assert!(1 == position_b.0 && 2 == position_b.1);
}
// verify current components for both entities are what we expect
assert!(em.has_component::<Name>(a));
assert!(!em.has_component::<Name>(b));
assert!(em.has_component::<Position>(a));
assert!(em.has_component::<Position>(b));
// remove position component from first entity
assert!(em.remove_component::<Position>(a));
assert!(!em.has_component::<Position>(a));
// verify current components for both entities are what we expect
assert!(em.has_component::<Name>(a));
assert!(!em.has_component::<Name>(b));
assert!(!em.has_component::<Position>(a));
assert!(em.has_component::<Position>(b));
{
let positions = em.components::<Position>().unwrap();
let position_b = positions.get(&b).unwrap();
assert!(1 == position_b.0 && 2 == position_b.1);
}
}
#[test]
fn modify_components() {
let mut em = Entities::new();
// create entities
let a = em.new_entity();
em.add_component(a, Position(10, 20));
let b = em.new_entity();
em.add_component(b, Position(17, 5));
// change entity positions
{
let mut positions = em.components_mut::<Position>().unwrap();
let position_a = positions.get_mut(&a).unwrap();
assert_eq!(Position(10, 20), *position_a);
position_a.0 = 13;
position_a.1 = 23;
}
{
let mut positions = em.components_mut::<Position>().unwrap();
let position_b = positions.get_mut(&b).unwrap();
assert_eq!(Position(17, 5), *position_b);
position_b.0 = 15;
position_b.1 = 8;
}
// verify both entity position components
{
let positions = em.components::<Position>().unwrap();
let position_a = positions.get(&a).unwrap();
assert_eq!(Position(13, 23), *position_a);
let position_b = positions.get(&b).unwrap();
assert_eq!(Position(15, 8), *position_b);
}
}
#[test]
fn get_all_components_of_type() {
let mut em = Entities::new();
// create entities
let a = em.new_entity();
em.add_component(a, Health(20));
em.add_component(a, Position(10, 20));
let b = em.new_entity();
em.add_component(b, Health(30));
em.add_component(b, Position(17, 5));
// verify initial entity positions
{
let positions = em.components::<Position>().unwrap();
assert_eq!(2, positions.len());
let positions: HashSet<&Position> = positions.values().collect();
assert!(positions.contains(&Position(10, 20)));
assert!(positions.contains(&Position(17, 5)));
}
// modify position components
{
let mut positions = em.components_mut::<Position>().unwrap();
for mut component in positions.values_mut() {
component.0 += 5;
}
assert_eq!(Position(15, 20), *positions.get(&a).unwrap());
assert_eq!(Position(22, 5), *positions.get(&b).unwrap());
}
// modify health components
{
let mut healths = em.components_mut::<Health>().unwrap();
for mut component in healths.values_mut() {
component.0 += 5;
}
assert_eq!(Health(25), *healths.get(&a).unwrap());
assert_eq!(Health(35), *healths.get(&b).unwrap());
}
}
#[test]
fn get_all_entities_with_component() {
let mut em = Entities::new();
// create entities
let a = em.new_entity();
em.add_component(a, Name("A"));
em.add_component(a, Health(20));
em.add_component(a, Position(10, 20));
let b = em.new_entity();
em.add_component(b, Name("B"));
em.add_component(b, Position(17, 5));
// get entities with position components
{
let positions = em.components::<Position>().unwrap();
let entities = positions.keys();
assert_eq!(2, entities.len());
let entities: HashSet<&EntityId> = entities.collect();
assert!(entities.contains(&a));
assert!(entities.contains(&b));
}
//
let names = em.components::<Name>().unwrap();
for (entity, name) in names.iter() {
// just written this way to verify can grab two mutable components at once
// (since this wouldn't be an uncommon way to want to work with an entity)
let mut healths = em.components_mut::<Health>().unwrap();
let mut positions = em.components_mut::<Position>().unwrap();
let health = healths.get_mut(&entity);
let position = positions.get_mut(&entity);
println!(
"entity {}, health: {:?}, position: {:?}",
name.0, health, position
);
if let Some(mut health) = health {
health.0 += 5;
}
if let Some(mut position) = position {
position.0 += 5;
}
}
let positions = em.components::<Position>().unwrap();
assert_eq!(Position(15, 20), *positions.get(&a).unwrap());
assert_eq!(Position(22, 5), *positions.get(&b).unwrap());
let healths = em.components::<Health>().unwrap();
assert_eq!(Health(25), *healths.get(&a).unwrap());
assert!(healths.get(&b).is_none());
}
struct UpdateContext(f32);
struct RenderContext(f32);
fn system_print_entity_positions(entities: &mut Entities, _context: &mut RenderContext) {
let positions = entities.components::<Position>().unwrap();
for (entity, position) in positions.iter() {
println!("entity {} at x:{}, y:{}", entity, position.0, position.1)
}
}
fn system_move_entities_forward(entities: &mut Entities, _context: &mut UpdateContext) {
let mut positions = entities.components_mut::<Position>().unwrap();
let velocities = entities.components::<Velocity>().unwrap();
for (entity, position) in positions.iter_mut() {
if let Some(velocity) = velocities.get(&entity) {
position.0 += velocity.0;
position.1 += velocity.1;
}
}
}
fn system_increment_counter(entities: &mut Entities, _context: &mut UpdateContext) {
let mut counters = entities.components_mut::<Counter>().unwrap();
for (_entity, counter) in counters.iter_mut() {
counter.0 += 1;
}
}
fn system_print_counter(entities: &mut Entities, _context: &mut RenderContext) {
let counters = entities.components::<Counter>().unwrap();
for (entity, counter) in counters.iter() {
println!("entity {} has counter {}", entity, counter.0);
}
}
#[test]
pub fn component_systems() {
let mut em = Entities::new();
// create entities
let a = em.new_entity();
em.add_component(a, Position(5, 6));
em.add_component(a, Velocity(1, 1));
em.add_component(a, Counter(0));
let b = em.new_entity();
em.add_component(b, Position(-3, 0));
em.add_component(b, Velocity(1, 0));
em.add_component(b, Counter(0));
let c = em.new_entity();
em.add_component(c, Position(2, 9));
em.add_component(c, Counter(0));
// setup component systems
let mut cs = ComponentSystems::new();
cs.add_update_system(system_move_entities_forward);
cs.add_update_system(system_increment_counter);
cs.add_render_system(system_print_entity_positions);
cs.add_render_system(system_print_counter);
// run some update+render iterations
for _ in 0..5 {
let mut update_context = UpdateContext(0.0);
let mut render_context = RenderContext(0.0);
cs.update(&mut em, &mut update_context);
cs.render(&mut em, &mut render_context);
}
// verify expected entity positions
let positions = em.components::<Position>().unwrap();
let velocities = em.components::<Velocity>().unwrap();
let counters = em.components::<Counter>().unwrap();
assert_eq!(Position(10, 11), *positions.get(&a).unwrap());
assert_eq!(Velocity(1, 1), *velocities.get(&a).unwrap());
assert_eq!(Counter(5), *counters.get(&a).unwrap());
assert_eq!(Position(2, 0), *positions.get(&b).unwrap());
assert_eq!(Velocity(1, 0), *velocities.get(&b).unwrap());
assert_eq!(Counter(5), *counters.get(&b).unwrap());
assert_eq!(Position(2, 9), *positions.get(&c).unwrap());
assert_eq!(None, velocities.get(&c));
assert_eq!(Counter(5), *counters.get(&c).unwrap());
}
}

View file

@ -0,0 +1,291 @@
use std::collections::VecDeque;
pub type ListenerFn<EventType, ContextType> = fn(event: &EventType, &mut ContextType) -> bool;
pub struct EventPublisher<EventType> {
queue: VecDeque<EventType>,
}
impl<EventType> EventPublisher<EventType> {
pub fn new() -> Self {
EventPublisher {
queue: VecDeque::new(),
}
}
#[inline]
pub fn len(&self) -> usize {
self.queue.len()
}
#[inline]
pub fn clear(&mut self) {
self.queue.clear();
}
#[inline]
pub fn queue(&mut self, event: EventType) {
self.queue.push_back(event);
}
pub fn take_queue(&mut self, destination: &mut VecDeque<EventType>) {
destination.clear();
destination.append(&mut self.queue);
self.clear();
}
}
pub struct EventListeners<EventType, ContextType> {
listeners: Vec<ListenerFn<EventType, ContextType>>,
dispatch_queue: VecDeque<EventType>,
}
impl<EventType, ContextType> EventListeners<EventType, ContextType> {
pub fn new() -> Self {
EventListeners {
listeners: Vec::new(),
dispatch_queue: VecDeque::new(),
}
}
pub fn len(&self) -> usize {
self.listeners.len()
}
pub fn clear(&mut self) {
self.listeners.clear();
}
pub fn add(&mut self, listener: ListenerFn<EventType, ContextType>) -> bool {
// HACK?: most advice i've seen right now for comparing function pointers suggests doing
// this, but i've also come across comments suggesting there are times where this
// might not be foolproof? (e.g. where generics or lifetimes come into play ... )
if self.listeners.iter().any(|&l| l as usize == listener as usize) {
false // don't add a duplicate listener
} else {
self.listeners.push(listener);
true
}
}
pub fn remove(&mut self, listener: ListenerFn<EventType, ContextType>) -> bool {
let before_size = self.listeners.len();
// HACK?: comparing function pointers -- see above "HACK?" comment. same concern here.
self.listeners.retain(|&l| l as usize != listener as usize);
// return true if the listener was removed
return before_size != self.listeners.len()
}
pub fn take_queue_from(&mut self, publisher: &mut EventPublisher<EventType>) -> usize {
publisher.take_queue(&mut self.dispatch_queue);
self.dispatch_queue.len()
}
pub fn dispatch_queue(&mut self, context: &mut ContextType) {
while let Some(event) = self.dispatch_queue.pop_front() {
for listener in &self.listeners {
if listener(&event, context) {
break;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
enum TestEvent {
Dummy,
Foobar(i32),
Message(&'static str),
}
struct DummyContext;
struct TestContext {
pub count: i32,
pub events: Vec<TestEvent>,
}
impl TestContext {
pub fn new() -> Self {
TestContext { count: 0, events: Vec::new() }
}
}
fn dummy_listener(_event: &TestEvent, _context: &mut DummyContext) -> bool {
false
}
fn other_dummy_listener(_event: &TestEvent, _context: &mut DummyContext) -> bool {
false
}
fn event_logger(event: &TestEvent, context: &mut TestContext) -> bool {
context.events.push(*event);
false
}
fn event_counter(_event: &TestEvent, context: &mut TestContext) -> bool {
context.count += 1;
false
}
fn message_filter(event: &TestEvent, _context: &mut TestContext) -> bool {
match event {
TestEvent::Message(s) => {
if *s == "filter" {
true // means event was handled, and no subsequent listeners should be called
} else {
false
}
},
_ => false
}
}
#[test]
pub fn adding_and_removing_listeners() {
let mut listeners = EventListeners::<TestEvent, DummyContext>::new();
// add and remove
assert_eq!(0, listeners.len());
assert!(listeners.add(dummy_listener));
assert_eq!(1, listeners.len());
assert!(!listeners.add(dummy_listener));
assert_eq!(1, listeners.len());
assert!(listeners.remove(dummy_listener));
assert_eq!(0, listeners.len());
assert!(!listeners.remove(dummy_listener));
assert_eq!(0, listeners.len());
// add and remove multiple
assert!(listeners.add(dummy_listener));
assert_eq!(1, listeners.len());
assert!(listeners.add(other_dummy_listener));
assert_eq!(2, listeners.len());
assert!(listeners.remove(dummy_listener));
assert_eq!(1, listeners.len());
assert!(!listeners.remove(dummy_listener));
assert_eq!(1, listeners.len());
assert!(listeners.remove(other_dummy_listener));
assert_eq!(0, listeners.len());
// clear all
assert!(listeners.add(dummy_listener));
assert!(listeners.add(other_dummy_listener));
assert_eq!(2, listeners.len());
listeners.clear();
assert_eq!(0, listeners.len());
}
#[test]
pub fn queueing_events() {
use TestEvent::*;
let mut publisher = EventPublisher::<TestEvent>::new();
assert_eq!(0, publisher.len());
publisher.queue(Dummy);
assert_eq!(1, publisher.len());
publisher.queue(Foobar(1));
assert_eq!(2, publisher.len());
publisher.queue(Foobar(2));
assert_eq!(3, publisher.len());
let mut queue = VecDeque::<TestEvent>::new();
publisher.take_queue(&mut queue);
assert_eq!(0, publisher.len());
assert_eq!(Dummy, queue.pop_front().unwrap());
assert_eq!(Foobar(1), queue.pop_front().unwrap());
assert_eq!(Foobar(2), queue.pop_front().unwrap());
assert!(queue.pop_front().is_none());
publisher.queue(Dummy);
assert_eq!(1, publisher.len());
publisher.clear();
assert_eq!(0, publisher.len());
let mut queue = VecDeque::<TestEvent>::new();
publisher.take_queue(&mut queue);
assert_eq!(0, publisher.len());
assert_eq!(0, queue.len());
}
#[test]
pub fn listeners_receive_events() {
use TestEvent::*;
let mut listeners = EventListeners::<TestEvent, TestContext>::new();
assert!(listeners.add(event_logger));
let mut publisher = EventPublisher::<TestEvent>::new();
publisher.queue(Dummy);
publisher.queue(Foobar(1));
publisher.queue(Dummy);
publisher.queue(Foobar(42));
assert_eq!(4, listeners.take_queue_from(&mut publisher));
let mut context = TestContext::new();
assert!(context.events.is_empty());
assert_eq!(0, context.count);
listeners.dispatch_queue(&mut context);
assert!(!context.events.is_empty());
assert_eq!(0, context.count);
assert_eq!(
vec![Dummy, Foobar(1), Dummy, Foobar(42)],
context.events
);
let mut context = TestContext::new();
assert!(context.events.is_empty());
assert_eq!(0, context.count);
listeners.dispatch_queue(&mut context);
assert!(context.events.is_empty());
assert!(listeners.add(event_counter));
publisher.queue(Foobar(10));
publisher.queue(Foobar(20));
publisher.queue(Dummy);
listeners.take_queue_from(&mut publisher);
let mut context = TestContext::new();
listeners.dispatch_queue(&mut context);
assert!(!context.events.is_empty());
assert_eq!(3, context.count);
assert_eq!(
vec![Foobar(10), Foobar(20), Dummy],
context.events
);
}
#[test]
pub fn listener_filtering() {
use TestEvent::*;
let mut listeners = EventListeners::<TestEvent, TestContext>::new();
assert!(listeners.add(message_filter));
assert!(listeners.add(event_logger));
assert!(listeners.add(event_counter));
let mut publisher = EventPublisher::<TestEvent>::new();
publisher.queue(Message("hello"));
publisher.queue(Dummy);
publisher.queue(Message("filter"));
publisher.queue(Foobar(3));
assert_eq!(4, listeners.take_queue_from(&mut publisher));
let mut context = TestContext::new();
assert!(context.events.is_empty());
assert_eq!(0, context.count);
listeners.dispatch_queue(&mut context);
assert!(!context.events.is_empty());
assert_eq!(3, context.count);
assert_eq!(
vec![Message("hello"), Dummy, Foobar(3)],
context.events
);
}
}

View file

@ -0,0 +1,342 @@
use crate::{Bitmap, Rect};
pub enum BlitMethod {
Solid,
Transparent(u8),
}
/// Clips the region for a source bitmap to be used in a subsequent blit operation. The source
/// region will be clipped against the clipping region given for the destination bitmap. The
/// top-left coordinates of the location to blit to on the destination bitmap are also adjusted
/// only if necessary based on the clipping performed.
///
/// # Arguments
///
/// * `dest_clip_region`: the clipping region for the destination bitmap
/// * `src_blit_region`: the region on the source bitmap that is to be blitted, which may be
/// clipped if necessary to at least partially fit into the destination clipping region given
/// * `dest_x`: the x (left) coordinate of the location on the destination bitmap to blit the
/// source to, which may be adjusted as necessary during clipping
/// * `dest_y`: the y (top) coordinate of the location on the destination bitmap to blit the source
/// to, which may be adjusted as necessary during clipping
///
/// returns: true if the results of the clip is partially or entirely visible on the destination
/// bitmap, or false if the blit is entirely outside of the destination bitmap (and so no blit
/// needs to occur)
pub fn clip_blit(
dest_clip_region: &Rect,
src_blit_region: &mut Rect,
dest_x: &mut i32,
dest_y: &mut i32,
) -> bool {
// off the left edge?
if *dest_x < dest_clip_region.x {
// completely off the left edge?
if (*dest_x + src_blit_region.width as i32 - 1) < dest_clip_region.x {
return false;
}
let offset = dest_clip_region.x - *dest_x;
src_blit_region.x += offset;
src_blit_region.width = (src_blit_region.width as i32 - offset) as u32;
*dest_x = dest_clip_region.x;
}
// off the right edge?
if *dest_x > dest_clip_region.width as i32 - src_blit_region.width as i32 {
// completely off the right edge?
if *dest_x > dest_clip_region.right() {
return false;
}
let offset = *dest_x + src_blit_region.width as i32 - dest_clip_region.width as i32;
src_blit_region.width = (src_blit_region.width as i32 - offset) as u32;
}
// off the top edge?
if *dest_y < dest_clip_region.y {
// completely off the top edge?
if (*dest_y + src_blit_region.height as i32 - 1) < dest_clip_region.y {
return false;
}
let offset = dest_clip_region.y - *dest_y;
src_blit_region.y += offset;
src_blit_region.height = (src_blit_region.height as i32 - offset) as u32;
*dest_y = dest_clip_region.y;
}
// off the bottom edge?
if *dest_y > dest_clip_region.height as i32 - src_blit_region.height as i32 {
// completely off the bottom edge?
if *dest_y > dest_clip_region.bottom() {
return false;
}
let offset = *dest_y + src_blit_region.height as i32 - dest_clip_region.height as i32;
src_blit_region.height = (src_blit_region.height as i32 - offset) as u32;
}
true
}
impl Bitmap {
pub unsafe fn solid_blit(&mut self, src: &Bitmap, src_region: &Rect, dest_x: i32, dest_y: i32) {
let src_row_length = src_region.width as usize;
let src_pitch = src.width as usize;
let dest_pitch = self.width as usize;
let mut src_pixels = src.pixels_at_ptr_unchecked(src_region.x, src_region.y);
let mut dest_pixels = self.pixels_at_mut_ptr_unchecked(dest_x, dest_y);
for _ in 0..src_region.height {
dest_pixels.copy_from(src_pixels, src_row_length);
src_pixels = src_pixels.add(src_pitch);
dest_pixels = dest_pixels.add(dest_pitch);
}
}
pub unsafe fn transparent_blit(
&mut self,
src: &Bitmap,
src_region: &Rect,
dest_x: i32,
dest_y: i32,
transparent_color: u8,
) {
let src_next_row_inc = (src.width - src_region.width) as usize;
let dest_next_row_inc = (self.width - src_region.width) as usize;
let mut src_pixels = src.pixels_at_ptr_unchecked(src_region.x, src_region.y);
let mut dest_pixels = self.pixels_at_mut_ptr_unchecked(dest_x, dest_y);
for _ in 0..src_region.height {
for _ in 0..src_region.width {
let pixel = *src_pixels;
if pixel != transparent_color {
*dest_pixels = pixel;
}
src_pixels = src_pixels.add(1);
dest_pixels = dest_pixels.add(1);
}
src_pixels = src_pixels.add(src_next_row_inc);
dest_pixels = dest_pixels.add(dest_next_row_inc);
}
}
pub fn blit_region(
&mut self,
method: BlitMethod,
src: &Bitmap,
src_region: &Rect,
mut dest_x: i32,
mut dest_y: i32,
) {
let mut src_region = *src_region;
if !clip_blit(
self.clip_region(),
&mut src_region,
&mut dest_x,
&mut dest_y,
) {
return;
}
unsafe {
self.blit_region_unchecked(method, src, &src_region, dest_x, dest_y);
};
}
#[inline]
pub unsafe fn blit_region_unchecked(
&mut self,
method: BlitMethod,
src: &Bitmap,
src_region: &Rect,
dest_x: i32,
dest_y: i32,
) {
use BlitMethod::*;
match method {
Solid => self.solid_blit(src, src_region, dest_x, dest_y),
Transparent(transparent_color) => {
self.transparent_blit(src, src_region, dest_x, dest_y, transparent_color)
}
}
}
#[inline]
pub fn blit(&mut self, method: BlitMethod, src: &Bitmap, x: i32, y: i32) {
let src_region = Rect::new(0, 0, src.width, src.height);
self.blit_region(method, src, &src_region, x, y);
}
#[inline]
pub unsafe fn blit_unchecked(&mut self, method: BlitMethod, src: &Bitmap, x: i32, y: i32) {
let src_region = Rect::new(0, 0, src.width, src.height);
self.blit_region_unchecked(method, src, &src_region, x, y);
}
}
#[cfg(test)]
pub mod tests {
use super::*;
#[test]
pub fn clip_blit_regions() {
let dest = Rect::new(0, 0, 320, 240);
let mut src: Rect;
let mut x: i32;
let mut y: i32;
src = Rect::new(0, 0, 16, 16);
x = 10;
y = 10;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(0, 0, 16, 16));
assert_eq!(10, x);
assert_eq!(10, y);
// left edge
src = Rect::new(0, 0, 16, 16);
x = 0;
y = 10;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(0, 0, 16, 16));
assert_eq!(0, x);
assert_eq!(10, y);
src = Rect::new(0, 0, 16, 16);
x = -5;
y = 10;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(5, 0, 11, 16));
assert_eq!(0, x);
assert_eq!(10, y);
src = Rect::new(0, 0, 16, 16);
x = -16;
y = 10;
assert!(!clip_blit(&dest, &mut src, &mut x, &mut y));
// right edge
src = Rect::new(0, 0, 16, 16);
x = 304;
y = 10;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(0, 0, 16, 16));
assert_eq!(304, x);
assert_eq!(10, y);
src = Rect::new(0, 0, 16, 16);
x = 310;
y = 10;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(0, 0, 10, 16));
assert_eq!(310, x);
assert_eq!(10, y);
src = Rect::new(0, 0, 16, 16);
x = 320;
y = 10;
assert!(!clip_blit(&dest, &mut src, &mut x, &mut y));
// top edge
src = Rect::new(0, 0, 16, 16);
x = 10;
y = 0;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(0, 0, 16, 16));
assert_eq!(10, x);
assert_eq!(0, y);
src = Rect::new(0, 0, 16, 16);
x = 10;
y = -5;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(0, 5, 16, 11));
assert_eq!(10, x);
assert_eq!(0, y);
src = Rect::new(0, 0, 16, 16);
x = 10;
y = -16;
assert!(!clip_blit(&dest, &mut src, &mut x, &mut y));
// bottom edge
src = Rect::new(0, 0, 16, 16);
x = 10;
y = 224;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(0, 0, 16, 16));
assert_eq!(10, x);
assert_eq!(224, y);
src = Rect::new(0, 0, 16, 16);
x = 10;
y = 229;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(0, 0, 16, 11));
assert_eq!(10, x);
assert_eq!(229, y);
src = Rect::new(0, 0, 16, 16);
x = 10;
y = 240;
assert!(!clip_blit(&dest, &mut src, &mut x, &mut y));
src = Rect::new(16, 16, 16, 16);
x = -1;
y = 112;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(17, 16, 15, 16));
assert_eq!(0, x);
assert_eq!(112, y);
}
#[test]
pub fn clip_blit_regions_large_source() {
let dest = Rect::new(0, 0, 64, 64);
let mut src: Rect;
let mut x: i32;
let mut y: i32;
src = Rect::new(0, 0, 128, 128);
x = 0;
y = 0;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(0, 0, 64, 64));
assert_eq!(0, x);
assert_eq!(0, y);
src = Rect::new(0, 0, 128, 128);
x = -16;
y = -24;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(16, 24, 64, 64));
assert_eq!(0, x);
assert_eq!(0, y);
src = Rect::new(0, 0, 32, 128);
x = 10;
y = -20;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(0, 20, 32, 64));
assert_eq!(10, x);
assert_eq!(0, y);
src = Rect::new(0, 0, 128, 32);
x = -20;
y = 10;
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
assert_eq!(src, Rect::new(20, 0, 64, 32));
assert_eq!(0, x);
assert_eq!(10, y);
}
}

View file

@ -0,0 +1,620 @@
use std::fs::File;
use std::io;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::Path;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use thiserror::Error;
use crate::utils::packbits::{pack_bits, unpack_bits, PackBitsError};
use crate::{Bitmap, Palette, PaletteError, PaletteFormat};
#[derive(Error, Debug)]
pub enum IffError {
#[error("Bad or unsupported IFF file: {0}")]
BadFile(String),
#[error("IFF palette data error")]
BadPalette(#[from] PaletteError),
#[error("PackBits error")]
PackBitsError(#[from] PackBitsError),
#[error("IFF I/O error")]
IOError(#[from] std::io::Error),
}
pub enum IffFormat {
Pbm,
PbmUncompressed,
Ilbm,
IlbmUncompressed,
}
impl IffFormat {
pub fn compressed(&self) -> bool {
use IffFormat::*;
match self {
Pbm | Ilbm => true,
PbmUncompressed | IlbmUncompressed => false,
}
}
pub fn chunky(&self) -> bool {
use IffFormat::*;
match self {
Pbm | PbmUncompressed => true,
Ilbm | IlbmUncompressed => false,
}
}
pub fn type_id(&self) -> [u8; 4] {
use IffFormat::*;
match self {
Pbm | PbmUncompressed => *b"PBM ",
Ilbm | IlbmUncompressed => *b"ILBM",
}
}
}
#[derive(Debug, Copy, Clone)]
#[repr(packed)]
struct IffId {
id: [u8; 4],
}
impl IffId {
pub fn read<T: Read>(reader: &mut T) -> Result<Self, IffError> {
let mut id = [0u8; 4];
reader.read_exact(&mut id)?;
Ok(IffId { id })
}
pub fn write<T: Write>(&self, writer: &mut T) -> Result<(), IffError> {
writer.write_all(&self.id)?;
Ok(())
}
}
#[derive(Debug, Copy, Clone)]
#[repr(packed)]
struct FormChunkHeader {
chunk_id: IffId,
size: u32,
type_id: IffId,
}
impl FormChunkHeader {
pub fn read<T: ReadBytesExt>(reader: &mut T) -> Result<Self, IffError> {
let chunk_id = IffId::read(reader)?;
let size = reader.read_u32::<BigEndian>()?;
let type_id = IffId::read(reader)?;
Ok(FormChunkHeader {
chunk_id,
size,
type_id,
})
}
pub fn write<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), IffError> {
self.chunk_id.write(writer)?;
writer.write_u32::<BigEndian>(self.size)?;
self.type_id.write(writer)?;
Ok(())
}
}
#[derive(Debug, Copy, Clone)]
#[repr(packed)]
struct SubChunkHeader {
chunk_id: IffId,
size: u32,
}
impl SubChunkHeader {
pub fn read<T: ReadBytesExt>(reader: &mut T) -> Result<Self, IffError> {
let chunk_id = IffId::read(reader)?;
let mut size = reader.read_u32::<BigEndian>()?;
if (size & 1) == 1 {
size += 1; // account for the padding byte
}
Ok(SubChunkHeader { chunk_id, size })
}
pub fn write<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), IffError> {
self.chunk_id.write(writer)?;
writer.write_u32::<BigEndian>(self.size)?;
Ok(())
}
}
#[derive(Debug, Copy, Clone)]
#[repr(packed)]
struct BMHDChunk {
width: u16,
height: u16,
left: u16,
top: u16,
bitplanes: u8,
masking: u8,
compress: u8,
padding: u8,
transparency: u16,
x_aspect_ratio: u8,
y_aspect_ratio: u8,
page_width: u16,
page_height: u16,
}
impl BMHDChunk {
pub fn read<T: ReadBytesExt>(reader: &mut T) -> Result<Self, IffError> {
Ok(BMHDChunk {
width: reader.read_u16::<BigEndian>()?,
height: reader.read_u16::<BigEndian>()?,
left: reader.read_u16::<BigEndian>()?,
top: reader.read_u16::<BigEndian>()?,
bitplanes: reader.read_u8()?,
masking: reader.read_u8()?,
compress: reader.read_u8()?,
padding: reader.read_u8()?,
transparency: reader.read_u16::<BigEndian>()?,
x_aspect_ratio: reader.read_u8()?,
y_aspect_ratio: reader.read_u8()?,
page_width: reader.read_u16::<BigEndian>()?,
page_height: reader.read_u16::<BigEndian>()?,
})
}
pub fn write<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), IffError> {
writer.write_u16::<BigEndian>(self.width)?;
writer.write_u16::<BigEndian>(self.height)?;
writer.write_u16::<BigEndian>(self.left)?;
writer.write_u16::<BigEndian>(self.top)?;
writer.write_u8(self.bitplanes)?;
writer.write_u8(self.masking)?;
writer.write_u8(self.compress)?;
writer.write_u8(self.padding)?;
writer.write_u16::<BigEndian>(self.transparency)?;
writer.write_u8(self.x_aspect_ratio)?;
writer.write_u8(self.y_aspect_ratio)?;
writer.write_u16::<BigEndian>(self.page_width)?;
writer.write_u16::<BigEndian>(self.page_height)?;
Ok(())
}
}
fn merge_bitplane(plane: u32, src: &[u8], dest: &mut [u8], row_size: usize) {
let bitmask = 1 << plane;
for x in 0..row_size {
let data = src[x];
if (data & 128) > 0 {
dest[x * 8] |= bitmask;
}
if (data & 64) > 0 {
dest[(x * 8) + 1] |= bitmask;
}
if (data & 32) > 0 {
dest[(x * 8) + 2] |= bitmask;
}
if (data & 16) > 0 {
dest[(x * 8) + 3] |= bitmask;
}
if (data & 8) > 0 {
dest[(x * 8) + 4] |= bitmask;
}
if (data & 4) > 0 {
dest[(x * 8) + 5] |= bitmask;
}
if (data & 2) > 0 {
dest[(x * 8) + 6] |= bitmask;
}
if (data & 1) > 0 {
dest[(x * 8) + 7] |= bitmask;
}
}
}
fn extract_bitplane(plane: u32, src: &[u8], dest: &mut [u8], row_size: usize) {
let bitmask = 1 << plane;
let mut src_base_index = 0;
for x in 0..row_size {
let mut data = 0;
if src[src_base_index] & bitmask != 0 {
data |= 128;
}
if src[src_base_index + 1] & bitmask != 0 {
data |= 64;
}
if src[src_base_index + 2] & bitmask != 0 {
data |= 32;
}
if src[src_base_index + 3] & bitmask != 0 {
data |= 16;
}
if src[src_base_index + 4] & bitmask != 0 {
data |= 8;
}
if src[src_base_index + 5] & bitmask != 0 {
data |= 4;
}
if src[src_base_index + 6] & bitmask != 0 {
data |= 2;
}
if src[src_base_index + 7] & bitmask != 0 {
data |= 1;
}
src_base_index += 8;
dest[x] = data;
}
}
fn load_planar_body<T: ReadBytesExt>(reader: &mut T, bmhd: &BMHDChunk) -> Result<Bitmap, IffError> {
let mut bitmap = Bitmap::new(bmhd.width as u32, bmhd.height as u32).unwrap();
let row_bytes = (((bmhd.width + 15) >> 4) << 1) as usize;
let mut buffer = vec![0u8; row_bytes];
for y in 0..bmhd.height {
// planar data is stored for each bitplane in sequence for the scanline.
// that is, ALL of bitplane1, followed by ALL of bitplane2, etc, NOT
// alternating after each pixel. if compression is enabled, it does NOT
// cross bitplane boundaries. each bitplane is compressed individually.
// bitplanes also do NOT cross the scanline boundary. basically, each
// scanline of pixel data, and within that, each of the bitplanes of
// pixel data found in each scanline can all be treated as they are all
// their own self-contained bit of data as far as this loading process
// is concerned (well, except that we merge all of the scanline's
// bitplanes together at the end of each line)
// read all the bitplane rows per scanline
for plane in 0..(bmhd.bitplanes as u32) {
if bmhd.compress == 1 {
// decompress packed line for this bitplane only
buffer.clear();
unpack_bits(reader, &mut buffer, row_bytes)?
} else {
// TODO: check this. maybe row_bytes calculation is wrong? either way, i don't
// think that DP2 or Grafx2 ever output uncompressed interleaved files ...
// just read all this bitplane's line data in as-is
reader.read_exact(&mut buffer)?;
}
// merge this bitplane data into the final destination. after all of
// the bitplanes have been loaded and merged in this way for this
// scanline, the destination pointer will contain VGA-friendly
// "chunky pixel"-format pixel data
merge_bitplane(
plane,
&buffer,
bitmap.pixels_at_mut(0, y as i32).unwrap(),
row_bytes,
);
}
}
Ok(bitmap)
}
fn load_chunky_body<T: ReadBytesExt>(reader: &mut T, bmhd: &BMHDChunk) -> Result<Bitmap, IffError> {
let mut bitmap = Bitmap::new(bmhd.width as u32, bmhd.height as u32).unwrap();
for y in 0..bmhd.height {
if bmhd.compress == 1 {
// for compression-enabled, read row of pixels using PackBits
let mut writer = bitmap.pixels_at_mut(0, y as i32).unwrap();
unpack_bits(reader, &mut writer, bmhd.width as usize)?
} else {
// for uncompressed, read row of pixels literally
let dest = &mut bitmap.pixels_at_mut(0, y as i32).unwrap()[0..bmhd.width as usize];
reader.read_exact(dest)?;
}
}
Ok(bitmap)
}
fn write_planar_body<T: WriteBytesExt>(
writer: &mut T,
bitmap: &Bitmap,
bmhd: &BMHDChunk,
) -> Result<(), IffError> {
let row_bytes = (((bitmap.width() + 15) >> 4) << 1) as usize;
let mut buffer = vec![0u8; row_bytes];
for y in 0..bitmap.height() {
for plane in 0..(bmhd.bitplanes as u32) {
extract_bitplane(
plane,
bitmap.pixels_at(0, y as i32).unwrap(),
&mut buffer,
row_bytes,
);
if bmhd.compress == 1 {
// for compression-enabled, write this plane's pixels using PackBits
pack_bits(&mut buffer.as_slice(), writer, row_bytes)?;
} else {
// TODO: check this. maybe row_bytes calculation is wrong? either way, i don't
// think that DP2 or Grafx2 ever output uncompressed interleaved files ...
// for uncompressed, write this plane's pixels literally
writer.write_all(&buffer)?;
}
}
}
Ok(())
}
fn write_chunky_body<T: WriteBytesExt>(
writer: &mut T,
bitmap: &Bitmap,
bmhd: &BMHDChunk,
) -> Result<(), IffError> {
for y in 0..bitmap.height() {
if bmhd.compress == 1 {
// for compression-enabled, write row of pixels using PackBits
let mut reader = bitmap.pixels_at(0, y as i32).unwrap();
pack_bits(&mut reader, writer, bitmap.width() as usize)?;
} else {
// for uncompressed, write out the row of pixels literally
let src = &bitmap.pixels_at(0, y as i32).unwrap()[0..bitmap.width() as usize];
writer.write_all(src)?;
}
}
Ok(())
}
impl Bitmap {
pub fn load_iff_bytes<T: ReadBytesExt + Seek>(
reader: &mut T,
) -> Result<(Bitmap, Palette), IffError> {
let form_chunk = FormChunkHeader::read(reader)?;
if form_chunk.chunk_id.id != *b"FORM" {
return Err(IffError::BadFile(String::from(
"Unexpected form chunk ID, probably not an IFF file",
)));
}
if form_chunk.type_id.id != *b"ILBM" && form_chunk.type_id.id != *b"PBM " {
return Err(IffError::BadFile(String::from(
"Only ILBM or PBM formats are supported",
)));
}
let mut bmhd: Option<BMHDChunk> = None;
let mut palette: Option<Palette> = None;
let mut bitmap: Option<Bitmap> = None;
loop {
let header = match SubChunkHeader::read(reader) {
Ok(header) => header,
Err(IffError::IOError(io_error))
if io_error.kind() == io::ErrorKind::UnexpectedEof =>
{
break
}
Err(err) => return Err(err),
};
let chunk_data_position = reader.stream_position()?;
// todo: process chunk here
if header.chunk_id.id == *b"BMHD" {
bmhd = Some(BMHDChunk::read(reader)?);
if bmhd.as_ref().unwrap().bitplanes != 8 {
return Err(IffError::BadFile(String::from(
"Only 8bpp files are supported",
)));
}
if bmhd.as_ref().unwrap().masking == 1 {
return Err(IffError::BadFile(String::from("Masking is not supported")));
}
} else if header.chunk_id.id == *b"CMAP" {
if header.size != 768 {
return Err(IffError::BadFile(String::from(
"Only 256 color files are supported",
)));
}
palette = Some(Palette::load_from_bytes(reader, PaletteFormat::Normal)?)
} else if header.chunk_id.id == *b"BODY" {
if let Some(bmhd) = &bmhd {
if form_chunk.type_id.id == *b"PBM " {
bitmap = Some(load_chunky_body(reader, bmhd)?);
} else {
bitmap = Some(load_planar_body(reader, bmhd)?);
}
} else {
// TODO: does this ever occur in practice? and if so, we can probably make some
// changes to allow for it ...
return Err(IffError::BadFile(String::from(
"BODY chunk occurs before BMHD chunk, or no BMHD chunk exists",
)));
}
}
reader.seek(SeekFrom::Start(chunk_data_position + header.size as u64))?;
}
if bitmap.is_none() {
return Err(IffError::BadFile(String::from("No BODY chunk was found")));
}
// TODO: we can probably make this optional ...
if palette.is_none() {
return Err(IffError::BadFile(String::from("No CMAP chunk was found")));
}
Ok((bitmap.unwrap(), palette.unwrap()))
}
pub fn load_iff_file(path: &Path) -> Result<(Bitmap, Palette), IffError> {
let f = File::open(path)?;
let mut reader = BufReader::new(f);
Self::load_iff_bytes(&mut reader)
}
pub fn to_iff_bytes<T: WriteBytesExt + Seek>(
&self,
writer: &mut T,
palette: &Palette,
format: IffFormat,
) -> Result<(), IffError> {
let form_chunk_position = writer.stream_position()?;
let mut form_chunk = FormChunkHeader {
chunk_id: IffId { id: *b"FORM" },
type_id: IffId {
id: format.type_id(),
},
size: 0, // filled in later once we know the size
};
// skip over the form chunk for now. will come back here and write it out later once we
// know what the final size is
writer.seek(SeekFrom::Current(
std::mem::size_of::<FormChunkHeader>() as i64
))?;
let bmhd_chunk_header = SubChunkHeader {
chunk_id: IffId { id: *b"BMHD" },
size: std::mem::size_of::<BMHDChunk>() as u32,
};
let bmhd = BMHDChunk {
width: self.width() as u16,
height: self.height() as u16,
left: 0,
top: 0,
bitplanes: 8,
masking: 0,
compress: if format.compressed() { 1 } else { 0 },
padding: 0,
transparency: 0,
// the following values are based on what DP2 writes out in 320x200 modes. good enough.
x_aspect_ratio: 5,
y_aspect_ratio: 6,
page_width: 320,
page_height: 200,
};
bmhd_chunk_header.write(writer)?;
bmhd.write(writer)?;
let cmap_chunk_header = SubChunkHeader {
chunk_id: IffId { id: *b"CMAP" },
size: 768,
};
cmap_chunk_header.write(writer)?;
palette.to_bytes(writer, PaletteFormat::Normal)?;
let body_position = writer.stream_position()?;
let mut body_chunk_header = SubChunkHeader {
chunk_id: IffId { id: *b"BODY" },
size: 0, // filled in later once we know the size
};
// skip over the body chunk header for now. we will again come back here and write it out
// later once we know what the final size again.
writer.seek(SeekFrom::Current(
std::mem::size_of::<SubChunkHeader>() as i64
))?;
if format.chunky() {
write_chunky_body(writer, self, &bmhd)?;
} else {
write_planar_body(writer, self, &bmhd)?;
}
// add a padding byte (only if necessary) to the body we just finished writing
let mut eof_pos = writer.stream_position()?;
if (eof_pos - body_position) & 1 == 1 {
writer.write_u8(0)?;
eof_pos += 1;
}
// go back and write out the form chunk header now that we know the final file size
form_chunk.size = (eof_pos - (std::mem::size_of::<IffId>() as u64 * 2)) as u32;
writer.seek(SeekFrom::Start(form_chunk_position))?;
form_chunk.write(writer)?;
// and then write out the body chunk header since we now know the size of that too
body_chunk_header.size = eof_pos as u32 - std::mem::size_of::<SubChunkHeader>() as u32;
writer.seek(SeekFrom::Start(body_position))?;
body_chunk_header.write(writer)?;
// and then go back to eof
writer.seek(SeekFrom::Start(eof_pos))?;
Ok(())
}
pub fn to_iff_file(
&self,
path: &Path,
palette: &Palette,
format: IffFormat,
) -> Result<(), IffError> {
let f = File::create(path)?;
let mut writer = BufWriter::new(f);
self.to_iff_bytes(&mut writer, palette, format)
}
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
pub static TEST_BMP_PIXELS_RAW: &[u8] =
include_bytes!("../../../test-assets/test_bmp_pixels_raw.bin");
#[test]
pub fn load_and_save() -> Result<(), IffError> {
let dp2_palette =
Palette::load_from_file(Path::new("./test-assets/dp2.pal"), PaletteFormat::Normal)
.unwrap();
let tmp_dir = TempDir::new()?;
// ILBM format
let (bmp, palette) = Bitmap::load_iff_file(Path::new("./test-assets/test_ilbm.lbm"))?;
assert_eq!(16, bmp.width());
assert_eq!(16, bmp.height());
assert_eq!(bmp.pixels(), TEST_BMP_PIXELS_RAW);
assert_eq!(palette, dp2_palette);
let save_path = tmp_dir.path().join("test_save_ilbm.lbm");
bmp.to_iff_file(&save_path, &palette, IffFormat::Ilbm)?;
let (reloaded_bmp, reloaded_palette) = Bitmap::load_iff_file(&save_path)?;
assert_eq!(16, reloaded_bmp.width());
assert_eq!(16, reloaded_bmp.height());
assert_eq!(reloaded_bmp.pixels(), TEST_BMP_PIXELS_RAW);
assert_eq!(reloaded_palette, dp2_palette);
// PBM format
let (bmp, palette) = Bitmap::load_iff_file(Path::new("./test-assets/test_pbm.lbm"))?;
assert_eq!(16, bmp.width());
assert_eq!(16, bmp.height());
assert_eq!(bmp.pixels(), TEST_BMP_PIXELS_RAW);
assert_eq!(palette, dp2_palette);
let save_path = tmp_dir.path().join("test_save_pbm.lbm");
bmp.to_iff_file(&save_path, &palette, IffFormat::Pbm)?;
let (reloaded_bmp, reloaded_palette) = Bitmap::load_iff_file(&save_path)?;
assert_eq!(16, reloaded_bmp.width());
assert_eq!(16, reloaded_bmp.height());
assert_eq!(reloaded_bmp.pixels(), TEST_BMP_PIXELS_RAW);
assert_eq!(reloaded_palette, dp2_palette);
Ok(())
}
#[test]
pub fn load_larger_image() -> Result<(), IffError> {
let (bmp, _palette) = Bitmap::load_iff_file(Path::new("./test-assets/test_image.lbm"))?;
assert_eq!(320, bmp.width());
assert_eq!(200, bmp.height());
Ok(())
}
}

View file

@ -0,0 +1,584 @@
use std::path::Path;
use std::slice;
use thiserror::Error;
pub use crate::blit::*;
pub use crate::iff::*;
pub use crate::pcx::*;
pub use crate::primitives::*;
use crate::{Palette, Rect};
pub mod blit;
pub mod iff;
pub mod pcx;
pub mod primitives;
#[derive(Error, Debug)]
pub enum BitmapError {
#[error("Invalid bitmap dimensions")]
InvalidDimensions,
#[error("Region is not fully within bitmap boundaries")]
OutOfBounds,
#[error("Unknown bitmap file type: {0}")]
UnknownFileType(String),
#[error("Bitmap IFF file error")]
IffError(#[from] iff::IffError),
#[error("Bitmap PCX file error")]
PcxError(#[from] pcx::PcxError),
}
/// Container for 256 color 2D pixel/image data that can be rendered to the screen. Pixel data
/// is stored as contiguous bytes, where each pixel is an index into a separate 256 color palette
/// stored independently of the bitmap. The pixel data is not padded in any way, so the stride from
/// one row to the next is always exactly equal to the bitmap width. Rendering operations provided
/// here are done with respect to the bitmaps clipping region, where rendering outside of the
/// clipping region is simply not performed / stops at the clipping boundary.
#[derive(Debug, Clone)]
pub struct Bitmap {
width: u32,
height: u32,
pixels: Box<[u8]>,
clip_region: Rect,
}
impl Bitmap {
/// Creates a new Bitmap with the specified dimensions.
///
/// # Arguments
///
/// * `width`: the width of the bitmap in pixels
/// * `height`: the height of the bitmap in pixels
///
/// returns: `Result<Bitmap, BitmapError>`
pub fn new(width: u32, height: u32) -> Result<Bitmap, BitmapError> {
if width == 0 || height == 0 {
return Err(BitmapError::InvalidDimensions);
}
Ok(Bitmap {
width,
height,
pixels: vec![0u8; (width * height) as usize].into_boxed_slice(),
clip_region: Rect {
x: 0,
y: 0,
width,
height,
},
})
}
/// Creates a new Bitmap, copying the pixel data from a sub-region of another source Bitmap.
/// The resulting bitmap will have dimensions equal to that of the region specified.
///
/// # Arguments
///
/// * `source`: the source bitmap to copy from
/// * `region`: the region on the source bitmap to copy from
///
/// returns: `Result<Bitmap, BitmapError>`
pub fn from(source: &Bitmap, region: &Rect) -> Result<Bitmap, BitmapError> {
if !source.full_bounds().contains_rect(region) {
return Err(BitmapError::OutOfBounds);
}
let mut bmp = Bitmap::new(region.width, region.height)?;
unsafe { bmp.solid_blit(source, region, 0, 0) };
Ok(bmp)
}
pub fn load_file(path: &Path) -> Result<(Bitmap, Palette), BitmapError> {
if let Some(extension) = path.extension() {
let extension = extension.to_ascii_lowercase();
match extension.to_str() {
Some("pcx") => Ok(Self::load_pcx_file(path)?),
Some("iff") | Some("lbm") | Some("pbm") | Some("bbm") => {
Ok(Self::load_iff_file(path)?)
}
_ => Err(BitmapError::UnknownFileType(String::from(
"Unrecognized file extension",
))),
}
} else {
Err(BitmapError::UnknownFileType(String::from(
"No file extension",
)))
}
}
/// Returns the width of the bitmap in pixels.
#[inline]
pub fn width(&self) -> u32 {
self.width
}
/// Returns the height of the bitmap in pixels.
#[inline]
pub fn height(&self) -> u32 {
self.height
}
/// Returns the right x coordinate of the bitmap.
#[inline]
pub fn right(&self) -> u32 {
self.width - 1
}
/// Returns the bottom x coordinate of the bitmap.
#[inline]
pub fn bottom(&self) -> u32 {
self.height - 1
}
/// Returns the current clipping region set on this bitmap.
#[inline]
pub fn clip_region(&self) -> &Rect {
&self.clip_region
}
/// Returns a rect representing the full bitmap boundaries, ignoring the current clipping
/// region set on this bitmap.
#[inline]
pub fn full_bounds(&self) -> Rect {
Rect {
x: 0,
y: 0,
width: self.width,
height: self.height,
}
}
/// Sets a new clipping region on this bitmap. The region will be automatically clamped to
/// the maximum bitmap boundaries if the supplied region extends beyond it.
///
/// # Arguments
///
/// * `region`: the new clipping region
pub fn set_clip_region(&mut self, region: &Rect) {
self.clip_region = *region;
self.clip_region.clamp_to(&self.full_bounds());
}
/// Resets the bitmaps clipping region back to the default (full boundaries of the bitmap).
pub fn reset_clip_region(&mut self) {
self.clip_region = self.full_bounds();
}
/// Returns a reference to the raw pixels in this bitmap.
#[inline]
pub fn pixels(&self) -> &[u8] {
&self.pixels
}
/// Returns a mutable reference to the raw pixels in this bitmap.
#[inline]
pub fn pixels_mut(&mut self) -> &mut [u8] {
&mut self.pixels
}
/// Returns a reference to the subset of the raw pixels in this bitmap beginning at the
/// given coordinates and extending to the end of the bitmap. If the coordinates given are
/// outside the bitmap's current clipping region, None is returned.
#[inline]
pub fn pixels_at(&self, x: i32, y: i32) -> Option<&[u8]> {
if self.is_xy_visible(x, y) {
let offset = self.get_offset_to_xy(x, y);
Some(&self.pixels[offset..])
} else {
None
}
}
/// Returns a mutable reference to the subset of the raw pixels in this bitmap beginning at the
/// given coordinates and extending to the end of the bitmap. If the coordinates given are
/// outside the bitmap's current clipping region, None is returned.
#[inline]
pub fn pixels_at_mut(&mut self, x: i32, y: i32) -> Option<&mut [u8]> {
if self.is_xy_visible(x, y) {
let offset = self.get_offset_to_xy(x, y);
Some(&mut self.pixels[offset..])
} else {
None
}
}
/// Returns an unsafe reference to the subset of the raw pixels in this bitmap beginning at the
/// given coordinates and extending to the end of the bitmap. The coordinates are not checked
/// for validity, so it is up to you to ensure they lie within the bounds of the bitmap.
#[inline]
pub unsafe fn pixels_at_unchecked(&self, x: i32, y: i32) -> &[u8] {
let offset = self.get_offset_to_xy(x, y);
slice::from_raw_parts(self.pixels.as_ptr().add(offset), self.pixels.len() - offset)
}
/// Returns a mutable unsafe reference to the subset of the raw pixels in this bitmap beginning
/// at the given coordinates and extending to the end of the bitmap. The coordinates are not
/// checked for validity, so it is up to you to ensure they lie within the bounds of the bitmap.
#[inline]
pub unsafe fn pixels_at_mut_unchecked(&mut self, x: i32, y: i32) -> &mut [u8] {
let offset = self.get_offset_to_xy(x, y);
slice::from_raw_parts_mut(
self.pixels.as_mut_ptr().add(offset),
self.pixels.len() - offset,
)
}
/// Returns a pointer to the subset of the raw pixels in this bitmap beginning at the given
/// coordinates. If the coordinates given are outside the bitmap's current clipping region,
/// None is returned.
#[inline]
pub unsafe fn pixels_at_ptr(&self, x: i32, y: i32) -> Option<*const u8> {
if self.is_xy_visible(x, y) {
let offset = self.get_offset_to_xy(x, y);
Some(self.pixels.as_ptr().add(offset))
} else {
None
}
}
/// Returns a mutable pointer to the subset of the raw pixels in this bitmap beginning at the
/// given coordinates. If the coordinates given are outside the bitmap's current clipping
/// region, None is returned.
#[inline]
pub unsafe fn pixels_at_mut_ptr(&mut self, x: i32, y: i32) -> Option<*mut u8> {
if self.is_xy_visible(x, y) {
let offset = self.get_offset_to_xy(x, y);
Some(self.pixels.as_mut_ptr().add(offset))
} else {
None
}
}
/// Returns an unsafe pointer to the subset of the raw pixels in this bitmap beginning at the
/// given coordinates. The coordinates are not checked for validity, so it is up to you to
/// ensure they lie within the bounds of the bitmap.
#[inline]
pub unsafe fn pixels_at_ptr_unchecked(&self, x: i32, y: i32) -> *const u8 {
let offset = self.get_offset_to_xy(x, y);
self.pixels.as_ptr().add(offset)
}
/// Returns a mutable unsafe pointer to the subset of the raw pixels in this bitmap beginning
/// at the given coordinates. The coordinates are not checked for validity, so it is up to you
/// to ensure they lie within the bounds of the bitmap.
#[inline]
pub unsafe fn pixels_at_mut_ptr_unchecked(&mut self, x: i32, y: i32) -> *mut u8 {
let offset = self.get_offset_to_xy(x, y);
self.pixels.as_mut_ptr().add(offset)
}
/// Returns an offset corresponding to the coordinates of the pixel given on this bitmap that
/// can be used with a reference to the raw pixels in this bitmap to access that pixel. The
/// coordinates given are not checked for validity.
#[inline]
pub fn get_offset_to_xy(&self, x: i32, y: i32) -> usize {
((y * self.width as i32) + x) as usize
}
/// Returns true if the coordinates given lie within the bitmaps clipping region.
#[inline]
pub fn is_xy_visible(&self, x: i32, y: i32) -> bool {
(x >= self.clip_region.x)
&& (y >= self.clip_region.y)
&& (x <= self.clip_region.right())
&& (y <= self.clip_region.bottom())
}
/// Copies and converts the entire pixel data from this bitmap to a destination expecting
/// 32-bit ARGB-format pixel data. This can be used to display the contents of the bitmap
/// on-screen by using an SDL Surface, OpenGL texture, etc as the destination.
///
/// # Arguments
///
/// * `dest`: destination 32-bit ARGB pixel buffer to copy converted pixels to
/// * `palette`: the 256 colour palette to use during pixel conversion
pub fn copy_as_argb_to(&self, dest: &mut [u32], palette: &Palette) {
for (src, dest) in self.pixels().iter().zip(dest.iter_mut()) {
*dest = palette[*src];
}
}
}
#[cfg(test)]
pub mod tests {
use claim::assert_matches;
use super::*;
#[rustfmt::skip]
static RAW_BMP_PIXELS: &[u8] = &[
0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 2,
];
#[rustfmt::skip]
static RAW_BMP_PIXELS_SUBSET: &[u8] = &[
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 2,
];
#[test]
pub fn creation_and_sizing() {
assert_matches!(Bitmap::new(0, 0), Err(BitmapError::InvalidDimensions));
assert_matches!(Bitmap::new(16, 0), Err(BitmapError::InvalidDimensions));
assert_matches!(Bitmap::new(0, 32), Err(BitmapError::InvalidDimensions));
let bmp = Bitmap::new(16, 32).unwrap();
assert_eq!(16, bmp.width());
assert_eq!(32, bmp.height());
assert_eq!(15, bmp.right());
assert_eq!(31, bmp.bottom());
assert_eq!(
Rect {
x: 0,
y: 0,
width: 16,
height: 32
},
bmp.full_bounds()
);
assert_eq!(
Rect {
x: 0,
y: 0,
width: 16,
height: 32
},
*bmp.clip_region()
);
}
#[test]
pub fn copy_from() {
let mut bmp = Bitmap::new(8, 8).unwrap();
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
assert_matches!(
Bitmap::from(&bmp, &Rect::new(0, 0, 16, 16)),
Err(BitmapError::OutOfBounds)
);
let copy = Bitmap::from(&bmp, &Rect::new(0, 0, 8, 8)).unwrap();
assert_eq!(bmp.pixels(), copy.pixels());
let copy = Bitmap::from(&bmp, &Rect::new(4, 4, 4, 4)).unwrap();
assert_eq!(RAW_BMP_PIXELS_SUBSET, copy.pixels());
}
#[test]
pub fn xy_offset_calculation() {
let bmp = Bitmap::new(20, 15).unwrap();
assert_eq!(0, bmp.get_offset_to_xy(0, 0));
assert_eq!(19, bmp.get_offset_to_xy(19, 0));
assert_eq!(20, bmp.get_offset_to_xy(0, 1));
assert_eq!(280, bmp.get_offset_to_xy(0, 14));
assert_eq!(299, bmp.get_offset_to_xy(19, 14));
assert_eq!(227, bmp.get_offset_to_xy(7, 11));
}
#[test]
pub fn bounds_testing_and_clip_region() {
let mut bmp = Bitmap::new(16, 8).unwrap();
assert!(bmp.is_xy_visible(0, 0));
assert!(bmp.is_xy_visible(15, 0));
assert!(bmp.is_xy_visible(0, 7));
assert!(bmp.is_xy_visible(15, 7));
assert!(!bmp.is_xy_visible(-1, -1));
assert!(!bmp.is_xy_visible(16, 8));
assert!(!bmp.is_xy_visible(4, -2));
assert!(!bmp.is_xy_visible(11, 8));
assert!(!bmp.is_xy_visible(-1, 3));
assert!(!bmp.is_xy_visible(16, 6));
let new_clip_region = Rect::from_coords(4, 2, 12, 6);
bmp.set_clip_region(&new_clip_region);
assert_eq!(
Rect {
x: 0,
y: 0,
width: 16,
height: 8
},
bmp.full_bounds()
);
assert_eq!(new_clip_region, *bmp.clip_region());
assert!(bmp.is_xy_visible(4, 2));
assert!(bmp.is_xy_visible(12, 2));
assert!(bmp.is_xy_visible(4, 6));
assert!(bmp.is_xy_visible(12, 6));
assert!(!bmp.is_xy_visible(3, 1));
assert!(!bmp.is_xy_visible(13, 7));
assert!(!bmp.is_xy_visible(5, 1));
assert!(!bmp.is_xy_visible(10, 7));
assert!(!bmp.is_xy_visible(3, 4));
assert!(!bmp.is_xy_visible(13, 5));
assert!(!bmp.is_xy_visible(0, 0));
assert!(!bmp.is_xy_visible(15, 0));
assert!(!bmp.is_xy_visible(0, 7));
assert!(!bmp.is_xy_visible(15, 7));
bmp.reset_clip_region();
assert!(bmp.is_xy_visible(0, 0));
assert!(bmp.is_xy_visible(15, 0));
assert!(bmp.is_xy_visible(0, 7));
assert!(bmp.is_xy_visible(15, 7));
assert!(!bmp.is_xy_visible(-1, -1));
assert!(!bmp.is_xy_visible(16, 8));
}
#[test]
pub fn pixels_at() {
let mut bmp = Bitmap::new(8, 8).unwrap();
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
assert_eq!(None, bmp.pixels_at(-1, -1));
let offset = bmp.get_offset_to_xy(1, 1);
let pixels = bmp.pixels_at(0, 0).unwrap();
assert_eq!(64, pixels.len());
assert_eq!(0, pixels[0]);
assert_eq!(1, pixels[offset]);
assert_eq!(2, pixels[63]);
let pixels = bmp.pixels_at(1, 1).unwrap();
assert_eq!(55, pixels.len());
assert_eq!(1, pixels[0]);
assert_eq!(2, pixels[54]);
}
#[test]
pub fn pixels_at_mut() {
let mut bmp = Bitmap::new(8, 8).unwrap();
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
assert_eq!(None, bmp.pixels_at_mut(-1, -1));
let offset = bmp.get_offset_to_xy(1, 1);
let pixels = bmp.pixels_at_mut(0, 0).unwrap();
assert_eq!(64, pixels.len());
assert_eq!(0, pixels[0]);
assert_eq!(1, pixels[offset]);
assert_eq!(2, pixels[63]);
let pixels = bmp.pixels_at_mut(1, 1).unwrap();
assert_eq!(55, pixels.len());
assert_eq!(1, pixels[0]);
assert_eq!(2, pixels[54]);
}
#[test]
pub fn pixels_at_unchecked() {
let mut bmp = Bitmap::new(8, 8).unwrap();
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
let offset = bmp.get_offset_to_xy(1, 1);
let pixels = unsafe { bmp.pixels_at_unchecked(0, 0) };
assert_eq!(64, pixels.len());
assert_eq!(0, pixels[0]);
assert_eq!(1, pixels[offset]);
assert_eq!(2, pixels[63]);
let pixels = unsafe { bmp.pixels_at_unchecked(1, 1) };
assert_eq!(55, pixels.len());
assert_eq!(1, pixels[0]);
assert_eq!(2, pixels[54]);
}
#[test]
pub fn pixels_at_mut_unchecked() {
let mut bmp = Bitmap::new(8, 8).unwrap();
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
let offset = bmp.get_offset_to_xy(1, 1);
let pixels = unsafe { bmp.pixels_at_mut_unchecked(0, 0) };
assert_eq!(64, pixels.len());
assert_eq!(0, pixels[0]);
assert_eq!(1, pixels[offset]);
assert_eq!(2, pixels[63]);
let pixels = unsafe { bmp.pixels_at_mut_unchecked(1, 1) };
assert_eq!(55, pixels.len());
assert_eq!(1, pixels[0]);
assert_eq!(2, pixels[54]);
}
#[test]
pub fn pixels_at_ptr() {
let mut bmp = Bitmap::new(8, 8).unwrap();
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
assert_eq!(None, unsafe { bmp.pixels_at_ptr(-1, -1) });
let offset = bmp.get_offset_to_xy(1, 1);
let pixels = unsafe { bmp.pixels_at_ptr(0, 0).unwrap() };
assert_eq!(0, unsafe { *pixels });
assert_eq!(1, unsafe { *(pixels.add(offset)) });
assert_eq!(2, unsafe { *(pixels.add(63)) });
let pixels = unsafe { bmp.pixels_at_ptr(1, 1).unwrap() };
assert_eq!(1, unsafe { *pixels });
assert_eq!(2, unsafe { *(pixels.add(54)) });
}
#[test]
pub fn pixels_at_mut_ptr() {
let mut bmp = Bitmap::new(8, 8).unwrap();
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
assert_eq!(None, unsafe { bmp.pixels_at_mut_ptr(-1, -1) });
let offset = bmp.get_offset_to_xy(1, 1);
let pixels = unsafe { bmp.pixels_at_mut_ptr(0, 0).unwrap() };
assert_eq!(0, unsafe { *pixels });
assert_eq!(1, unsafe { *(pixels.add(offset)) });
assert_eq!(2, unsafe { *(pixels.add(63)) });
let pixels = unsafe { bmp.pixels_at_mut_ptr(1, 1).unwrap() };
assert_eq!(1, unsafe { *pixels });
assert_eq!(2, unsafe { *(pixels.add(54)) });
}
#[test]
pub fn pixels_at_ptr_unchecked() {
let mut bmp = Bitmap::new(8, 8).unwrap();
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
let offset = bmp.get_offset_to_xy(1, 1);
let pixels = unsafe { bmp.pixels_at_ptr_unchecked(0, 0) };
assert_eq!(0, unsafe { *pixels });
assert_eq!(1, unsafe { *(pixels.add(offset)) });
assert_eq!(2, unsafe { *(pixels.add(63)) });
let pixels = unsafe { bmp.pixels_at_ptr_unchecked(1, 1) };
assert_eq!(1, unsafe { *pixels });
assert_eq!(2, unsafe { *(pixels.add(54)) });
}
#[test]
pub fn pixels_at_mut_ptr_unchecked() {
let mut bmp = Bitmap::new(8, 8).unwrap();
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
let offset = bmp.get_offset_to_xy(1, 1);
let pixels = unsafe { bmp.pixels_at_mut_ptr_unchecked(0, 0) };
assert_eq!(0, unsafe { *pixels });
assert_eq!(1, unsafe { *(pixels.add(offset)) });
assert_eq!(2, unsafe { *(pixels.add(63)) });
let pixels = unsafe { bmp.pixels_at_mut_ptr_unchecked(1, 1) };
assert_eq!(1, unsafe { *pixels });
assert_eq!(2, unsafe { *(pixels.add(54)) });
}
}

View file

@ -0,0 +1,322 @@
use std::fs::File;
use std::io::{BufReader, BufWriter, Cursor, Seek, SeekFrom};
use std::path::Path;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use thiserror::Error;
use crate::utils::bytes::ReadFixedLengthByteArray;
use crate::{from_rgb32, Bitmap, Palette, PaletteError, PaletteFormat};
#[derive(Error, Debug)]
pub enum PcxError {
#[error("Bad or unsupported PCX file: {0}")]
BadFile(String),
#[error("PCX palette data error")]
BadPalette(#[from] PaletteError),
#[error("PCX I/O error")]
IOError(#[from] std::io::Error),
}
#[derive(Debug, Copy, Clone)]
#[repr(packed)]
struct PcxHeader {
manufacturer: u8,
version: u8,
encoding: u8,
bpp: u8,
x1: u16,
y1: u16,
x2: u16,
y2: u16,
horizontal_dpi: u16,
vertical_dpi: u16,
ega_palette: [u8; 48],
reserved: u8,
num_color_planes: u8,
bytes_per_line: u16,
palette_type: u16,
horizontal_size: u16,
vertical_size: u16,
padding: [u8; 54],
}
impl PcxHeader {
pub fn read<T: ReadBytesExt>(reader: &mut T) -> Result<Self, PcxError> {
Ok(PcxHeader {
manufacturer: reader.read_u8()?,
version: reader.read_u8()?,
encoding: reader.read_u8()?,
bpp: reader.read_u8()?,
x1: reader.read_u16::<LittleEndian>()?,
y1: reader.read_u16::<LittleEndian>()?,
x2: reader.read_u16::<LittleEndian>()?,
y2: reader.read_u16::<LittleEndian>()?,
horizontal_dpi: reader.read_u16::<LittleEndian>()?,
vertical_dpi: reader.read_u16::<LittleEndian>()?,
ega_palette: reader.read_bytes()?,
reserved: reader.read_u8()?,
num_color_planes: reader.read_u8()?,
bytes_per_line: reader.read_u16::<LittleEndian>()?,
palette_type: reader.read_u16::<LittleEndian>()?,
horizontal_size: reader.read_u16::<LittleEndian>()?,
vertical_size: reader.read_u16::<LittleEndian>()?,
padding: reader.read_bytes()?,
})
}
pub fn write<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), PcxError> {
writer.write_u8(self.manufacturer)?;
writer.write_u8(self.version)?;
writer.write_u8(self.encoding)?;
writer.write_u8(self.bpp)?;
writer.write_u16::<LittleEndian>(self.x1)?;
writer.write_u16::<LittleEndian>(self.y1)?;
writer.write_u16::<LittleEndian>(self.x2)?;
writer.write_u16::<LittleEndian>(self.y2)?;
writer.write_u16::<LittleEndian>(self.horizontal_dpi)?;
writer.write_u16::<LittleEndian>(self.vertical_dpi)?;
writer.write_all(&self.ega_palette)?;
writer.write_u8(self.reserved)?;
writer.write_u8(self.num_color_planes)?;
writer.write_u16::<LittleEndian>(self.bytes_per_line)?;
writer.write_u16::<LittleEndian>(self.palette_type)?;
writer.write_u16::<LittleEndian>(self.horizontal_size)?;
writer.write_u16::<LittleEndian>(self.vertical_size)?;
writer.write_all(&self.padding)?;
Ok(())
}
}
fn write_pcx_data<T: WriteBytesExt>(
writer: &mut T,
run_count: u8,
pixel: u8,
) -> Result<(), PcxError> {
if (run_count > 1) || ((pixel & 0xc0) == 0xc0) {
writer.write_u8(0xc0 | run_count)?;
}
writer.write_u8(pixel)?;
Ok(())
}
impl Bitmap {
pub fn load_pcx_bytes<T: ReadBytesExt + Seek>(
reader: &mut T,
) -> Result<(Bitmap, Palette), PcxError> {
let header = PcxHeader::read(reader)?;
if header.manufacturer != 10 {
return Err(PcxError::BadFile(String::from(
"Unexpected header.manufacturer value, probably not a PCX file",
)));
}
if header.version != 5 {
return Err(PcxError::BadFile(String::from(
"Only version 5 PCX files are supported",
)));
}
if header.encoding != 1 {
return Err(PcxError::BadFile(String::from(
"Only RLE-compressed PCX files are supported",
)));
}
if header.bpp != 8 {
return Err(PcxError::BadFile(String::from(
"Only 8-bit indexed (256 color palette) PCX files are supported",
)));
}
if header.x2 == 0 || header.y2 == 0 {
return Err(PcxError::BadFile(String::from(
"Invalid PCX image dimensions",
)));
}
// read the PCX file's pixel data into a bitmap
let width = (header.x2 + 1) as u32;
let height = (header.y2 + 1) as u32;
let mut bmp = Bitmap::new(width, height).unwrap();
let mut writer = Cursor::new(bmp.pixels_mut());
for _y in 0..height {
// read the next scanline's worth of pixels from the PCX file
let mut x: u32 = 0;
while x < (header.bytes_per_line as u32) {
let mut data: u8;
let mut count: u32;
// read pixel or RLE count
data = reader.read_u8()?;
if (data & 0xc0) == 0xc0 {
// it was an RLE count, actual pixel is the next byte ...
count = (data & 0x3f) as u32;
data = reader.read_u8()?;
} else {
// it was just a single pixel
count = 1;
}
// write the current pixel value 'data' to the bitmap 'count' number of times
while count > 0 {
if x <= width {
writer.write_u8(data)?;
} else {
writer.seek(SeekFrom::Current(1))?;
}
x += 1;
count -= 1;
}
}
}
// now read the palette data located at the end of the PCX file
// palette data should be for 256 colors, 3 bytes per color = 768 bytes
// the palette is preceded by a single byte, 0x0c, which we will also validate
reader.seek(SeekFrom::End(-769))?;
let palette_marker = reader.read_u8()?;
if palette_marker != 0x0c {
return Err(PcxError::BadFile(String::from(
"Palette not found at end of file",
)));
}
let palette = Palette::load_from_bytes(reader, PaletteFormat::Normal)?;
Ok((bmp, palette))
}
pub fn load_pcx_file(path: &Path) -> Result<(Bitmap, Palette), PcxError> {
let f = File::open(path)?;
let mut reader = BufReader::new(f);
Self::load_pcx_bytes(&mut reader)
}
pub fn to_pcx_bytes<T: WriteBytesExt>(
&self,
writer: &mut T,
palette: &Palette,
) -> Result<(), PcxError> {
let header = PcxHeader {
manufacturer: 10,
version: 5,
encoding: 1,
bpp: 8,
x1: 0,
y1: 0,
x2: self.right() as u16,
y2: self.bottom() as u16,
horizontal_dpi: 320,
vertical_dpi: 200,
ega_palette: [0u8; 48],
reserved: 0,
num_color_planes: 1,
bytes_per_line: self.width() as u16,
palette_type: 1,
horizontal_size: self.width() as u16,
vertical_size: self.height() as u16,
padding: [0u8; 54],
};
header.write(writer)?;
let pixels = self.pixels();
let mut i = 0;
for _y in 0..=self.bottom() {
// write one scanline at a time. breaking runs that could have continued across
// scanlines in the process, as per the pcx standard
let mut run_count = 0;
let mut run_pixel = 0;
for _x in 0..=self.right() {
let pixel = pixels[i];
i += 1;
if run_count == 0 {
run_count = 1;
run_pixel = pixel;
} else {
if (pixel != run_pixel) || (run_count >= 63) {
write_pcx_data(writer, run_count, run_pixel)?;
run_count = 1;
run_pixel = pixel;
} else {
run_count += 1;
}
}
}
// end the scanline, writing out whatever run we might have had going
write_pcx_data(writer, run_count, run_pixel)?;
}
// marker for beginning of palette data
writer.write_u8(0xc)?;
for i in 0..=255 {
let argb = palette[i];
let (r, g, b) = from_rgb32(argb);
writer.write_u8(r)?;
writer.write_u8(g)?;
writer.write_u8(b)?;
}
Ok(())
}
pub fn to_pcx_file(&self, path: &Path, palette: &Palette) -> Result<(), PcxError> {
let f = File::create(path)?;
let mut writer = BufWriter::new(f);
self.to_pcx_bytes(&mut writer, palette)
}
}
#[cfg(test)]
pub mod tests {
use tempfile::TempDir;
use super::*;
pub static TEST_BMP_PIXELS_RAW: &[u8] =
include_bytes!("../../../test-assets/test_bmp_pixels_raw.bin");
#[test]
pub fn load_and_save() -> Result<(), PcxError> {
let dp2_palette =
Palette::load_from_file(Path::new("./test-assets/dp2.pal"), PaletteFormat::Normal)
.unwrap();
let tmp_dir = TempDir::new()?;
let (bmp, palette) = Bitmap::load_pcx_file(Path::new("./test-assets/test.pcx"))?;
assert_eq!(16, bmp.width());
assert_eq!(16, bmp.height());
assert_eq!(bmp.pixels(), TEST_BMP_PIXELS_RAW);
assert_eq!(palette, dp2_palette);
let save_path = tmp_dir.path().join("test_save.pcx");
bmp.to_pcx_file(&save_path, &palette)?;
let (reloaded_bmp, reloaded_palette) = Bitmap::load_pcx_file(&save_path)?;
assert_eq!(16, reloaded_bmp.width());
assert_eq!(16, reloaded_bmp.height());
assert_eq!(reloaded_bmp.pixels(), TEST_BMP_PIXELS_RAW);
assert_eq!(reloaded_palette, dp2_palette);
Ok(())
}
#[test]
pub fn load_larger_image() -> Result<(), PcxError> {
let (bmp, _palette) = Bitmap::load_pcx_file(Path::new("./test-assets/test_image.pcx"))?;
assert_eq!(320, bmp.width());
assert_eq!(200, bmp.height());
Ok(())
}
}

View file

@ -0,0 +1,378 @@
use std::mem::swap;
use crate::{Bitmap, Character, Font, FontRenderOpts, Rect};
impl Bitmap {
/// Fills the entire bitmap with the given color.
pub fn clear(&mut self, color: u8) {
self.pixels.fill(color);
}
/// Sets the pixel at the given coordinates to the color specified. If the coordinates lie
/// outside of the bitmaps clipping region, no pixels will be changed.
#[inline]
pub fn set_pixel(&mut self, x: i32, y: i32, color: u8) {
if let Some(pixels) = self.pixels_at_mut(x, y) {
pixels[0] = color;
}
}
/// Sets the pixel at the given coordinates to the color specified. The coordinates are not
/// checked for validity, so it is up to you to ensure they lie within the bounds of the
/// bitmap.
#[inline]
pub unsafe fn set_pixel_unchecked(&mut self, x: i32, y: i32, color: u8) {
let p = self.pixels_at_mut_ptr_unchecked(x, y);
*p = color;
}
/// Gets the pixel at the given coordinates. If the coordinates lie outside of the bitmaps
/// clipping region, None is returned.
#[inline]
pub fn get_pixel(&self, x: i32, y: i32) -> Option<u8> {
if let Some(pixels) = self.pixels_at(x, y) {
Some(pixels[0])
} else {
None
}
}
/// Gets the pixel at the given coordinates. The coordinates are not checked for validity, so
/// it is up to you to ensure they lie within the bounds of the bitmap.
#[inline]
pub unsafe fn get_pixel_unchecked(&self, x: i32, y: i32) -> u8 {
*(self.pixels_at_ptr_unchecked(x, y))
}
/// Renders a single character using the font given.
#[inline]
pub fn print_char<T: Font>(&mut self, ch: char, x: i32, y: i32, color: u8, font: &T) {
font.character(ch)
.draw(self, x, y, FontRenderOpts::Color(color));
}
/// Renders the string of text using the font given.
pub fn print_string<T: Font>(&mut self, text: &str, x: i32, y: i32, color: u8, font: &T) {
let mut current_x = x;
let mut current_y = y;
for ch in text.chars() {
match ch {
' ' => current_x += font.space_width() as i32,
'\n' => {
current_x = x;
current_y += font.line_height() as i32
}
'\r' => (),
otherwise => {
self.print_char(otherwise, current_x, current_y, color, font);
current_x += font.character(otherwise).bounds().width as i32;
}
}
}
}
/// Draws a line from x1,y1 to x2,y2.
pub fn line(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, color: u8) {
let mut dx = x1;
let mut dy = y1;
let delta_x = x2 - x1;
let delta_y = y2 - y1;
let delta_x_abs = delta_x.abs();
let delta_y_abs = delta_y.abs();
let delta_x_sign = delta_x.signum();
let delta_y_sign = delta_y.signum();
let mut x = delta_x_abs / 2;
let mut y = delta_y_abs / 2;
let offset_x_inc = delta_x_sign;
let offset_y_inc = delta_y_sign * self.width as i32;
unsafe {
// safety: while we are blindly getting a pointer to this x/y coordinate, we don't
// write to it unless we know the coordinates are in bounds.
// TODO: should be ok ... ? or am i making too many assumptions about memory layout?
let mut dest = self.pixels_at_mut_ptr_unchecked(x1, y1);
if self.is_xy_visible(dx, dy) {
*dest = color;
}
if delta_x_abs >= delta_y_abs {
for _ in 0..delta_x_abs {
y += delta_y_abs;
if y >= delta_x_abs {
y -= delta_x_abs;
dy += delta_y_sign;
dest = dest.offset(offset_y_inc as isize);
}
dx += delta_x_sign;
dest = dest.offset(offset_x_inc as isize);
if self.is_xy_visible(dx, dy) {
*dest = color;
}
}
} else {
for _ in 0..delta_y_abs {
x += delta_x_abs;
if x >= delta_y_abs {
x -= delta_y_abs;
dx += delta_x_sign;
dest = dest.offset(offset_x_inc as isize);
}
dy += delta_y_sign;
dest = dest.offset(offset_y_inc as isize);
if self.is_xy_visible(dx, dy) {
*dest = color;
}
}
}
}
}
/// Draws a horizontal line from x1,y to x2,y.
pub fn horiz_line(&mut self, x1: i32, x2: i32, y: i32, color: u8) {
let mut region = Rect::from_coords(x1, y, x2, y);
if region.clamp_to(&self.clip_region) {
unsafe {
let dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y);
dest.write_bytes(color, region.width as usize);
}
}
}
/// Draws a vertical line from x,y1 to x,y2.
pub fn vert_line(&mut self, x: i32, y1: i32, y2: i32, color: u8) {
let mut region = Rect::from_coords(x, y1, x, y2);
if region.clamp_to(&self.clip_region) {
unsafe {
let mut dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y);
for _ in 0..region.height {
*dest = color;
dest = dest.add(self.width as usize);
}
}
}
}
/// Draws an empty box (rectangle) using the points x1,y1 and x2,y2 to form the box to be
/// drawn, assuming they are specifying the top-left and bottom-right corners respectively.
pub fn rect(&mut self, mut x1: i32, mut y1: i32, mut x2: i32, mut y2: i32, color: u8) {
// note: need to manually do all this instead of just relying on Rect::from_coords (which
// could otherwise figure all this out for us) mainly just because we need the post-swap
// x1,y1,x2,y2 values for post-region-clamping comparison purposes ...
if x2 < x1 {
swap(&mut x1, &mut x2);
}
if y2 < y1 {
swap(&mut y1, &mut y2);
}
let mut region = Rect {
x: x1,
y: y1,
width: (x2 - x1 + 1) as u32,
height: (y2 - y1 + 1) as u32,
};
if !region.clamp_to(&self.clip_region) {
return;
}
// top line, only if y1 was originally within bounds
if y1 == region.y {
unsafe {
let dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y);
dest.write_bytes(color, region.width as usize);
}
}
// bottom line, only if y2 was originally within bounds
if y2 == region.bottom() {
unsafe {
let dest = self.pixels_at_mut_ptr_unchecked(region.x, region.bottom());
dest.write_bytes(color, region.width as usize);
}
}
// left line, only if x1 was originally within bounds
if x1 == region.x {
unsafe {
let mut dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y);
for _ in 0..region.height {
*dest = color;
dest = dest.add(self.width as usize);
}
}
}
// right line, only if x2 was originally within bounds
if x2 == region.right() {
unsafe {
let mut dest = self.pixels_at_mut_ptr_unchecked(region.right(), region.y);
for _ in 0..region.height {
*dest = color;
dest = dest.add(self.width as usize);
}
}
}
}
/// Draws a filled box (rectangle) using the points x1,y1 and x2,y2 to form the box to be
/// drawn, assuming they are specifying the top-left and bottom-right corners respectively.
pub fn filled_rect(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, color: u8) {
let mut region = Rect::from_coords(x1, y1, x2, y2);
if region.clamp_to(&self.clip_region) {
unsafe {
let mut dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y);
for _ in 0..region.height {
dest.write_bytes(color, region.width as usize);
dest = dest.add(self.width as usize);
}
}
}
}
/// Draws the outline of a circle formed by the center point and radius given.
pub fn circle(&mut self, center_x: i32, center_y: i32, radius: u32, color: u8) {
// TODO: optimize
let mut x = 0;
let mut y = radius as i32;
let mut m = 5 - 4 * radius as i32;
while x <= y {
self.set_pixel(center_x + x, center_y + y, color);
self.set_pixel(center_x + x, center_y - y, color);
self.set_pixel(center_x - x, center_y + y, color);
self.set_pixel(center_x - x, center_y - y, color);
self.set_pixel(center_x + y, center_y + x, color);
self.set_pixel(center_x + y, center_y - x, color);
self.set_pixel(center_x - y, center_y + x, color);
self.set_pixel(center_x - y, center_y - x, color);
if m > 0 {
y -= 1;
m -= 8 * y;
}
x += 1;
m += 8 * x + 4;
}
}
/// Draws a filled circle formed by the center point and radius given.
pub fn filled_circle(&mut self, center_x: i32, center_y: i32, radius: u32, color: u8) {
// TODO: optimize
let mut x = 0;
let mut y = radius as i32;
let mut m = 5 - 4 * radius as i32;
while x <= y {
self.horiz_line(center_x - x, center_x + x, center_y - y, color);
self.horiz_line(center_x - y, center_x + y, center_y - x, color);
self.horiz_line(center_x - y, center_x + y, center_y + x, color);
self.horiz_line(center_x - x, center_x + x, center_y + y, color);
if m > 0 {
y -= 1;
m -= 8 * y;
}
x += 1;
m += 8 * x + 4;
}
}
}
#[cfg(test)]
pub mod tests {
use super::*;
#[rustfmt::skip]
#[test]
pub fn set_and_get_pixel() {
let mut bmp = Bitmap::new(8, 8).unwrap();
assert_eq!(None, bmp.get_pixel(-1, -1));
assert_eq!(0, bmp.get_pixel(0, 0).unwrap());
bmp.set_pixel(0, 0, 7);
assert_eq!(7, bmp.get_pixel(0, 0).unwrap());
assert_eq!(
bmp.pixels(),
&[
7, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]
);
assert_eq!(0, bmp.get_pixel(2, 4).unwrap());
bmp.set_pixel(2, 4, 5);
assert_eq!(5, bmp.get_pixel(2, 4).unwrap());
assert_eq!(
bmp.pixels(),
&[
7, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 5, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]
);
}
#[rustfmt::skip]
#[test]
pub fn set_and_get_pixel_unchecked() {
let mut bmp = Bitmap::new(8, 8).unwrap();
assert_eq!(0, unsafe { bmp.get_pixel_unchecked(0, 0) });
unsafe { bmp.set_pixel_unchecked(0, 0, 7) };
assert_eq!(7, unsafe { bmp.get_pixel_unchecked(0, 0) });
assert_eq!(
bmp.pixels(),
&[
7, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]
);
assert_eq!(0, unsafe { bmp.get_pixel_unchecked(2, 4) });
unsafe { bmp.set_pixel_unchecked(2, 4, 5) };
assert_eq!(5, unsafe { bmp.get_pixel_unchecked(2, 4) });
assert_eq!(
bmp.pixels(),
&[
7, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 5, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]
);
}
}

View file

@ -0,0 +1,178 @@
use std::ops::Index;
use thiserror::Error;
use crate::{Bitmap, Rect};
#[derive(Error, Debug)]
pub enum BitmapAtlasError {
#[error("Region is out of bounds for the Bitmap used by the BitmapAtlas")]
OutOfBounds,
}
#[derive(Debug)]
pub struct BitmapAtlas {
bitmap: Bitmap,
bounds: Rect,
tiles: Vec<Rect>,
}
impl BitmapAtlas {
pub fn new(bitmap: Bitmap) -> BitmapAtlas {
let bounds = bitmap.full_bounds();
BitmapAtlas {
bitmap,
bounds,
tiles: Vec::new(),
}
}
pub fn add(&mut self, rect: Rect) -> Result<usize, BitmapAtlasError> {
if !self.bounds.contains_rect(&rect) {
return Err(BitmapAtlasError::OutOfBounds);
}
self.tiles.push(rect);
Ok(self.tiles.len() - 1)
}
pub fn add_grid(
&mut self,
tile_width: u32,
tile_height: u32,
) -> Result<usize, BitmapAtlasError> {
if self.bounds.width < tile_width || self.bounds.height < tile_height {
return Err(BitmapAtlasError::OutOfBounds);
}
for yt in 0..(self.bounds.height / tile_height) {
for xt in 0..(self.bounds.width) / tile_width {
let x = xt * tile_width;
let y = yt * tile_height;
let rect = Rect::new(x as i32, y as i32, tile_width, tile_height);
self.tiles.push(rect);
}
}
Ok(self.tiles.len() - 1)
}
pub fn add_custom_grid(
&mut self,
start_x: u32,
start_y: u32,
tile_width: u32,
tile_height: u32,
x_tiles: u32,
y_tiles: u32,
border: u32,
) -> Result<usize, BitmapAtlasError> {
// figure out of the grid properties given would result in us creating any
// rects that lie out of the bounds of this bitmap
let grid_region = Rect::new(
start_x as i32,
start_y as i32,
(tile_width + border) * x_tiles + border,
(tile_height + border) * y_tiles + border,
);
if !self.bounds.contains_rect(&grid_region) {
return Err(BitmapAtlasError::OutOfBounds);
}
// all good! now create all the tiles needed for the grid specified
for yt in 0..y_tiles {
for xt in 0..x_tiles {
let x = start_x + (tile_width + border) * xt;
let y = start_y + (tile_height + border) * yt;
let rect = Rect::new(x as i32, y as i32, tile_width, tile_height);
self.tiles.push(rect);
}
}
Ok(self.tiles.len() - 1)
}
pub fn clear(&mut self) {
self.tiles.clear()
}
pub fn len(&self) -> usize {
self.tiles.len()
}
pub fn bitmap(&self) -> &Bitmap {
&self.bitmap
}
}
impl Index<usize> for BitmapAtlas {
type Output = Rect;
fn index(&self, index: usize) -> &Self::Output {
&self.tiles[index]
}
}
#[cfg(test)]
pub mod tests {
use claim::assert_matches;
use super::*;
#[test]
pub fn adding_rects() {
let bmp = Bitmap::new(64, 64).unwrap();
let mut atlas = BitmapAtlas::new(bmp);
let rect = Rect::new(0, 0, 16, 16);
assert_eq!(0, atlas.add(rect.clone()).unwrap());
assert_eq!(rect, atlas[0]);
assert_eq!(1, atlas.len());
let rect = Rect::new(16, 0, 16, 16);
assert_eq!(1, atlas.add(rect.clone()).unwrap());
assert_eq!(rect, atlas[1]);
assert_eq!(2, atlas.len());
assert_matches!(
atlas.add(Rect::new(56, 0, 16, 16)),
Err(BitmapAtlasError::OutOfBounds)
);
assert_eq!(2, atlas.len());
assert_matches!(
atlas.add(Rect::new(-8, 4, 16, 16)),
Err(BitmapAtlasError::OutOfBounds)
);
assert_eq!(2, atlas.len());
assert_matches!(
atlas.add(Rect::new(0, 0, 128, 128)),
Err(BitmapAtlasError::OutOfBounds)
);
assert_eq!(2, atlas.len());
}
#[test]
pub fn adding_grid() {
let bmp = Bitmap::new(64, 64).unwrap();
let mut atlas = BitmapAtlas::new(bmp);
assert_eq!(3, atlas.add_custom_grid(0, 0, 8, 8, 2, 2, 0).unwrap());
assert_eq!(4, atlas.len());
assert_eq!(Rect::new(0, 0, 8, 8), atlas[0]);
assert_eq!(Rect::new(8, 0, 8, 8), atlas[1]);
assert_eq!(Rect::new(0, 8, 8, 8), atlas[2]);
assert_eq!(Rect::new(8, 8, 8, 8), atlas[3]);
atlas.clear();
assert_eq!(0, atlas.len());
assert_eq!(3, atlas.add_custom_grid(0, 0, 4, 8, 2, 2, 1).unwrap());
assert_eq!(4, atlas.len());
assert_eq!(Rect::new(0, 0, 4, 8), atlas[0]);
assert_eq!(Rect::new(5, 0, 4, 8), atlas[1]);
assert_eq!(Rect::new(0, 9, 4, 8), atlas[2]);
assert_eq!(Rect::new(5, 9, 4, 8), atlas[3]);
}
}

View file

@ -0,0 +1,202 @@
use std::fs::File;
use std::io::{BufReader, BufWriter, Cursor};
use std::path::Path;
use byteorder::{ReadBytesExt, WriteBytesExt};
use thiserror::Error;
use crate::{Bitmap, Rect};
pub static VGA_FONT_BYTES: &[u8] = include_bytes!("../../assets/vga.fnt");
pub const NUM_CHARS: usize = 256;
pub const CHAR_HEIGHT: usize = 8;
pub const CHAR_FIXED_WIDTH: usize = 8;
#[derive(Error, Debug)]
pub enum FontError {
#[error("Invalid font file: {0}")]
InvalidFile(String),
#[error("Font I/O error")]
IOError(#[from] std::io::Error),
}
#[derive(Debug)]
pub enum FontRenderOpts {
Color(u8),
None,
}
pub trait Character {
fn bounds(&self) -> &Rect;
fn draw(&self, dest: &mut Bitmap, x: i32, y: i32, opts: FontRenderOpts);
}
pub trait Font {
type CharacterType: Character;
fn character(&self, ch: char) -> &Self::CharacterType;
fn space_width(&self) -> u8;
fn line_height(&self) -> u8;
}
#[derive(Debug)]
pub struct BitmaskCharacter {
bytes: [u8; CHAR_HEIGHT],
bounds: Rect,
}
impl Character for BitmaskCharacter {
#[inline]
fn bounds(&self) -> &Rect {
&self.bounds
}
fn draw(&self, dest: &mut Bitmap, x: i32, y: i32, opts: FontRenderOpts) {
// out of bounds check
if ((x + self.bounds.width as i32) < dest.clip_region().x)
|| ((y + self.bounds.height as i32) < dest.clip_region().y)
|| (x >= dest.clip_region().right())
|| (y >= dest.clip_region().bottom())
{
return;
}
let color = match opts {
FontRenderOpts::Color(color) => color,
_ => 0,
};
// TODO: i'm sure this can be optimized, lol
for char_y in 0..self.bounds.height as usize {
let mut bit_mask = 0x80;
for char_x in 0..self.bounds.width as usize {
if self.bytes[char_y] & bit_mask > 0 {
dest.set_pixel(x + char_x as i32, y + char_y as i32, color);
}
bit_mask >>= 1;
}
}
}
}
#[derive(Debug)]
pub struct BitmaskFont {
characters: Box<[BitmaskCharacter]>,
line_height: u8,
space_width: u8,
}
impl BitmaskFont {
pub fn new_vga_font() -> Result<BitmaskFont, FontError> {
BitmaskFont::load_from_bytes(&mut Cursor::new(VGA_FONT_BYTES))
}
pub fn load_from_file(path: &Path) -> Result<BitmaskFont, FontError> {
let f = File::open(path)?;
let mut reader = BufReader::new(f);
BitmaskFont::load_from_bytes(&mut reader)
}
pub fn load_from_bytes<T: ReadBytesExt>(reader: &mut T) -> Result<BitmaskFont, FontError> {
let mut characters: Vec<BitmaskCharacter> = Vec::with_capacity(NUM_CHARS);
// read character bitmap data
for _ in 0..NUM_CHARS {
let mut buffer = [0u8; CHAR_HEIGHT];
reader.read_exact(&mut buffer)?;
let character = BitmaskCharacter {
bytes: buffer,
// bounds are filled in below. ugh.
bounds: Rect {
x: 0,
y: 0,
width: 0,
height: 0,
},
};
characters.push(character);
}
// read character widths (used for rendering)
for i in 0..NUM_CHARS {
characters[i].bounds.width = reader.read_u8()? as u32;
}
// read global font height (used for rendering)
let line_height = reader.read_u8()?;
for i in 0..NUM_CHARS {
characters[i].bounds.height = line_height as u32;
}
let space_width = characters[' ' as usize].bounds.width as u8;
Ok(BitmaskFont {
characters: characters.into_boxed_slice(),
line_height,
space_width,
})
}
pub fn to_file(&self, path: &Path) -> Result<(), FontError> {
let f = File::create(path)?;
let mut writer = BufWriter::new(f);
self.to_bytes(&mut writer)
}
pub fn to_bytes<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), FontError> {
// write character bitmap data
for i in 0..NUM_CHARS {
writer.write_all(&self.characters[i].bytes)?;
}
// write character widths
for i in 0..NUM_CHARS {
writer.write_u8(self.characters[i].bounds.width as u8)?;
}
// write global font height
writer.write_u8(self.line_height)?;
Ok(())
}
}
impl Font for BitmaskFont {
type CharacterType = BitmaskCharacter;
#[inline]
fn character(&self, ch: char) -> &Self::CharacterType {
&self.characters[ch as usize]
}
#[inline]
fn space_width(&self) -> u8 {
self.space_width
}
#[inline]
fn line_height(&self) -> u8 {
self.line_height
}
}
#[cfg(test)]
pub mod tests {
use super::*;
#[test]
pub fn load_font() -> Result<(), FontError> {
let font = BitmaskFont::load_from_file(Path::new("./assets/vga.fnt"))?;
assert_eq!(256, font.characters.len());
assert_eq!(CHAR_FIXED_WIDTH as u8, font.space_width);
for character in font.characters.iter() {
assert_eq!(CHAR_FIXED_WIDTH as u8, character.bounds.width as u8);
assert_eq!(CHAR_HEIGHT, character.bytes.len());
}
Ok(())
}
}

View file

@ -0,0 +1,4 @@
pub mod bitmap;
pub mod bitmapatlas;
pub mod font;
pub mod palette;

View file

@ -0,0 +1,563 @@
use std::cmp::min;
use std::fs::File;
use std::io::{BufReader, BufWriter, Cursor};
use std::ops::{Bound, Index, IndexMut, RangeBounds};
use std::path::Path;
use byteorder::{ReadBytesExt, WriteBytesExt};
use thiserror::Error;
use crate::utils::abs_diff;
use crate::NUM_COLORS;
// silly "hack" (???) which allows us to alias the generic constraint `RangeBounds<u8> + Iterator<Item = u8>` to `ColorRange`
pub trait ColorRange: RangeBounds<u8> + Iterator<Item = u8> {}
impl<T> ColorRange for T where T: RangeBounds<u8> + Iterator<Item = u8> {}
pub static VGA_PALETTE_BYTES: &[u8] = include_bytes!("../../assets/vga.pal");
/// Converts a set of individual ARGB components to a combined 32-bit color value, packed into
/// the format 0xAARRGGBB
///
/// # Arguments
///
/// * `a`: the alpha component (0-255)
/// * `r`: the red component (0-255)
/// * `g`: the green component (0-255)
/// * `b`: the blue component (0-255)
///
/// returns: the u32 packed color
#[inline]
pub fn to_argb32(a: u8, r: u8, g: u8, b: u8) -> u32 {
(b as u32) + ((g as u32) << 8) + ((r as u32) << 16) + ((a as u32) << 24)
}
/// Extracts the individual ARGB components out of a combined 32-bit color value which is in the
/// format 0xAARRGGBB
///
/// # Arguments
///
/// * `argb`: the 32-bit packed color
///
/// returns: the individual ARGB color components (0-255 each) in order: alpha, red, green, blue
#[inline]
pub fn from_argb32(argb: u32) -> (u8, u8, u8, u8) {
let a = ((argb & 0xff000000) >> 24) as u8;
let r = ((argb & 0x00ff0000) >> 16) as u8;
let g = ((argb & 0x0000ff00) >> 8) as u8;
let b = (argb & 0x000000ff) as u8;
(a, r, g, b)
}
/// Converts a set of individual RGB components to a combined 32-bit color value, packed into
/// the format 0xAARRGGBB. Substitutes a value of 255 for the missing alpha component.
///
/// # Arguments
///
/// * `r`: the red component (0-255)
/// * `g`: the green component (0-255)
/// * `b`: the blue component (0-255)
///
/// returns: the u32 packed color
#[inline]
pub fn to_rgb32(r: u8, g: u8, b: u8) -> u32 {
to_argb32(255, r, g, b)
}
/// Extracts the individual RGB components out of a combined 32-bit color value which is in the
/// format 0xAARRGGBB. Ignores the alpha component.
///
/// # Arguments
///
/// * `argb`: the 32-bit packed color
///
/// returns: the individual ARGB color components (0-255 each) in order: red, green, blue
#[inline]
pub fn from_rgb32(rgb: u32) -> (u8, u8, u8) {
// ignore alpha component at 0xff000000 ...
let r = ((rgb & 0x00ff0000) >> 16) as u8;
let g = ((rgb & 0x0000ff00) >> 8) as u8;
let b = (rgb & 0x000000ff) as u8;
(r, g, b)
}
/// Linearly interpolates between two 32-bit packed colors in the format 0xAARRGGBB.
///
/// # Arguments
///
/// * `a`: the first 32-bit packed color
/// * `b`: the second 32-bit packed color
/// * `t`: the amount to interpolate between the two values, specified as a fraction.
#[inline]
pub fn lerp_argb32(a: u32, b: u32, t: f32) -> u32 {
let (a1, r1, g1, b1) = from_argb32(a);
let (a2, r2, g2, b2) = from_argb32(b);
to_argb32(
((a1 as f32) + ((a2 as f32) - (a1 as f32)) * t) as u8,
((r1 as f32) + ((r2 as f32) - (r1 as f32)) * t) as u8,
((g1 as f32) + ((g2 as f32) - (g1 as f32)) * t) as u8,
((b1 as f32) + ((b2 as f32) - (b1 as f32)) * t) as u8,
)
}
/// Linearly interpolates between two 32-bit packed colors in the format 0xAARRGGBB. Ignores the
/// alpha component, which will always be set to 255 in the return value.
///
/// # Arguments
///
/// * `a`: the first 32-bit packed color
/// * `b`: the second 32-bit packed color
/// * `t`: the amount to interpolate between the two values, specified as a fraction.
#[inline]
pub fn lerp_rgb32(a: u32, b: u32, t: f32) -> u32 {
let (r1, g1, b1) = from_rgb32(a);
let (r2, g2, b2) = from_rgb32(b);
to_rgb32(
((r1 as f32) + ((r2 as f32) - (r1 as f32)) * t) as u8,
((g1 as f32) + ((g2 as f32) - (g1 as f32)) * t) as u8,
((b1 as f32) + ((b2 as f32) - (b1 as f32)) * t) as u8,
)
}
// vga bios (0-63) format
fn read_256color_6bit_palette<T: ReadBytesExt>(
reader: &mut T,
) -> Result<[u32; NUM_COLORS], PaletteError> {
let mut colors = [0u32; NUM_COLORS];
for color in colors.iter_mut() {
let r = reader.read_u8()?;
let g = reader.read_u8()?;
let b = reader.read_u8()?;
*color = to_rgb32(r * 4, g * 4, b * 4);
}
Ok(colors)
}
fn write_256color_6bit_palette<T: WriteBytesExt>(
writer: &mut T,
colors: &[u32; NUM_COLORS],
) -> Result<(), PaletteError> {
for color in colors.iter() {
let (r, g, b) = from_rgb32(*color);
writer.write_u8(r / 4)?;
writer.write_u8(g / 4)?;
writer.write_u8(b / 4)?;
}
Ok(())
}
// normal (0-255) format
fn read_256color_8bit_palette<T: ReadBytesExt>(
reader: &mut T,
) -> Result<[u32; NUM_COLORS], PaletteError> {
let mut colors = [0u32; NUM_COLORS];
for color in colors.iter_mut() {
let r = reader.read_u8()?;
let g = reader.read_u8()?;
let b = reader.read_u8()?;
*color = to_rgb32(r, g, b);
}
Ok(colors)
}
fn write_256color_8bit_palette<T: WriteBytesExt>(
writer: &mut T,
colors: &[u32; NUM_COLORS],
) -> Result<(), PaletteError> {
for color in colors.iter() {
let (r, g, b) = from_rgb32(*color);
writer.write_u8(r)?;
writer.write_u8(g)?;
writer.write_u8(b)?;
}
Ok(())
}
#[derive(Error, Debug)]
pub enum PaletteError {
#[error("Palette I/O error")]
IOError(#[from] std::io::Error),
}
pub enum PaletteFormat {
/// Individual RGB components in 6-bits (0-63) for VGA BIOS compatibility
Vga,
/// Individual RGB components in 8-bits (0-255)
Normal,
}
/// Contains a 256 color palette, and provides methods useful for working with palettes. The
/// colors are all stored individually as 32-bit packed values in the format 0xAARRGGBB.
#[derive(Debug, Clone)]
pub struct Palette {
colors: [u32; NUM_COLORS],
}
impl Palette {
/// Creates a new Palette with all black colors.
pub fn new() -> Palette {
Palette {
colors: [0; NUM_COLORS],
}
}
/// Creates a new Palette, pre-loaded with the default VGA BIOS colors.
pub fn new_vga_palette() -> Result<Palette, PaletteError> {
Palette::load_from_bytes(&mut Cursor::new(VGA_PALETTE_BYTES), PaletteFormat::Vga)
}
/// Loads and returns a Palette from a palette file on disk.
///
/// # Arguments
///
/// * `path`: the path of the palette file to be loaded
/// * `format`: the format that the palette data is expected to be in
pub fn load_from_file(path: &Path, format: PaletteFormat) -> Result<Palette, PaletteError> {
let f = File::open(path)?;
let mut reader = BufReader::new(f);
Self::load_from_bytes(&mut reader, format)
}
/// Loads and returns a Palette from a reader. The data being loaded is expected to be the same
/// as if the palette was being loaded from a file on disk.
///
/// # Arguments
///
/// * `reader`: the reader to load the palette from
/// * `format`: the format that the palette data is expected to be in
pub fn load_from_bytes<T: ReadBytesExt>(
reader: &mut T,
format: PaletteFormat,
) -> Result<Palette, PaletteError> {
let colors = match format {
PaletteFormat::Vga => read_256color_6bit_palette(reader)?,
PaletteFormat::Normal => read_256color_8bit_palette(reader)?,
};
Ok(Palette { colors })
}
/// Writes the palette to a file on disk. If the file already exists, it will be overwritten.
///
/// # Arguments
///
/// * `path`: the path of the file to save the palette to
/// * `format`: the format to write the palette data in
pub fn to_file(&self, path: &Path, format: PaletteFormat) -> Result<(), PaletteError> {
let f = File::create(path)?;
let mut writer = BufWriter::new(f);
self.to_bytes(&mut writer, format)
}
/// Writes the palette to a writer, in the same format as if it was writing to a file on disk.
///
/// # Arguments
///
/// * `writer`: the writer to write palette data to
/// * `format`: the format to write the palette data in
pub fn to_bytes<T: WriteBytesExt>(
&self,
writer: &mut T,
format: PaletteFormat,
) -> Result<(), PaletteError> {
match format {
PaletteFormat::Vga => write_256color_6bit_palette(writer, &self.colors),
PaletteFormat::Normal => write_256color_8bit_palette(writer, &self.colors),
}
}
/// Fades a single color in the palette from its current RGB values towards the given RGB
/// values by up to the step amount given. This function is intended to be run many times
/// over a number of frames where each run completes a small step towards the complete fade.
///
/// # Arguments
///
/// * `color`: the color index to fade
/// * `target_r`: the target red component (0-255) to fade towards
/// * `target_g`: the target green component (0-255) to fade towards
/// * `target_b`: the target blue component (0-255) to fade towards
/// * `step`: the amount to "step" by towards the target RGB values
///
/// returns: true if the color has reached the target RGB values, false otherwise
pub fn fade_color_toward_rgb(
&mut self,
color: u8,
target_r: u8,
target_g: u8,
target_b: u8,
step: u8,
) -> bool {
let mut modified = false;
let (mut r, mut g, mut b) = from_rgb32(self.colors[color as usize]);
if r != target_r {
modified = true;
let diff_r = r.overflowing_sub(target_r).0;
if r > target_r {
r -= min(step, diff_r);
} else {
r += min(step, diff_r);
}
}
if g != target_g {
modified = true;
let diff_g = g.overflowing_sub(target_g).0;
if g > target_g {
g -= min(step, diff_g);
} else {
g += min(step, diff_g);
}
}
if b != target_b {
modified = true;
let diff_b = b.overflowing_sub(target_b).0;
if b > target_b {
b -= min(step, diff_b);
} else {
b += min(step, diff_b);
}
}
if modified {
self.colors[color as usize] = to_rgb32(r, g, b);
}
(target_r == r) && (target_g == g) && (target_b == b)
}
/// Fades a range of colors in the palette from their current RGB values all towards the given
/// RGB values by up to the step amount given. This function is intended to be run many times
/// over a number of frames where each run completes a small step towards the complete fade.
///
/// # Arguments
///
/// * `colors`: the range of colors to be faded
/// * `target_r`: the target red component (0-255) to fade towards
/// * `target_g`: the target green component (0-255) to fade towards
/// * `target_b`: the target blue component (0-255) to fade towards
/// * `step`: the amount to "step" by towards the target RGB values
///
/// returns: true if all of the colors in the range have reached the target RGB values, false
/// otherwise
pub fn fade_colors_toward_rgb<T: ColorRange>(
&mut self,
colors: T,
target_r: u8,
target_g: u8,
target_b: u8,
step: u8,
) -> bool {
let mut all_faded = true;
for color in colors {
if !self.fade_color_toward_rgb(color, target_r, target_g, target_b, step) {
all_faded = false;
}
}
all_faded
}
/// Fades a range of colors in the palette from their current RGB values all towards the RGB
/// values in the other palette specified, by up to the step amount given. This function is
/// intended to be run many times over a number of frames where each run completes a small step
/// towards the complete fade.
///
/// # Arguments
///
/// * `colors`: the range of colors to be faded
/// * `palette`: the other palette to use as the target to fade towards
/// * `step`: the amount to "step" by towards the target RGB values
///
/// returns: true if all of the colors in the range have reached the RGB values from the other
/// target palette, false otherwise
pub fn fade_colors_toward_palette<T: ColorRange>(
&mut self,
colors: T,
palette: &Palette,
step: u8,
) -> bool {
let mut all_faded = true;
for color in colors {
let (r, g, b) = from_rgb32(palette[color]);
if !self.fade_color_toward_rgb(color, r, g, b, step) {
all_faded = false;
}
}
all_faded
}
/// Linearly interpolates between the specified colors in two palettes, storing the
/// interpolation results in this palette.
///
/// # Arguments
///
/// * `colors`: the range of colors to be interpolated
/// * `a`: the first palette
/// * `b`: the second palette
/// * `t`: the amount to interpolate between the two palettes, specified as a fraction
pub fn lerp<T: ColorRange>(&mut self, colors: T, a: &Palette, b: &Palette, t: f32) {
for color in colors {
self[color] = lerp_rgb32(a[color], b[color], t);
}
}
/// Rotates a range of colors in the palette by a given amount.
///
/// # Arguments
///
/// * `colors`: the range of colors to be rotated
/// * `step`: the number of positions (and direction) to rotate all colors by
pub fn rotate_colors<T: ColorRange>(&mut self, colors: T, step: i8) {
use Bound::*;
let start = match colors.start_bound() {
Excluded(&start) => start + 1,
Included(&start) => start,
Unbounded => 0,
} as usize;
let end = match colors.end_bound() {
Excluded(&end) => end - 1,
Included(&end) => end,
Unbounded => 255,
} as usize;
let subset = &mut self.colors[start..=end];
match step.signum() {
-1 => subset.rotate_left(step.abs() as usize),
1 => subset.rotate_right(step.abs() as usize),
_ => {}
}
}
/// Finds and returns the index of the closest color in this palette to the RGB values provided.
/// This will not always return great results. It depends largely on the palette and the RGB
/// values being searched (for example, searching for bright green 0,255,0 in a palette which
/// contains no green hues at all is not likely to return a useful result).
pub fn find_color(&self, r: u8, g: u8, b: u8) -> u8 {
let mut closest_distance = 255 * 3;
let mut closest = 0;
for (index, color) in self.colors.iter().enumerate() {
let (this_r, this_g, this_b) = from_rgb32(*color);
// this comparison method is using the sRGB Euclidean formula described here:
// https://en.wikipedia.org/wiki/Color_difference
let distance = abs_diff(this_r, r) as u32
+ abs_diff(this_g, g) as u32
+ abs_diff(this_b, b) as u32;
if distance < closest_distance {
closest = index as u8;
closest_distance = distance;
}
}
closest
}
}
impl Index<u8> for Palette {
type Output = u32;
#[inline]
fn index(&self, index: u8) -> &Self::Output {
&self.colors[index as usize]
}
}
impl IndexMut<u8> for Palette {
#[inline]
fn index_mut(&mut self, index: u8) -> &mut Self::Output {
&mut self.colors[index as usize]
}
}
impl PartialEq for Palette {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.colors == other.colors
}
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
#[test]
fn argb_conversions() {
let argb = to_argb32(0x11, 0x22, 0x33, 0x44);
assert_eq!(argb, 0x11223344);
let argb = to_rgb32(0x22, 0x33, 0x44);
assert_eq!(argb, 0xff223344);
let (a, r, g, b) = from_argb32(0x11223344);
assert_eq!(0x11, a);
assert_eq!(0x22, r);
assert_eq!(0x33, g);
assert_eq!(0x44, b);
let (r, g, b) = from_rgb32(0x11223344);
assert_eq!(0x22, r);
assert_eq!(0x33, g);
assert_eq!(0x44, b);
}
#[test]
fn get_and_set_colors() {
let mut palette = Palette::new();
assert_eq!(0, palette[0]);
assert_eq!(0, palette[1]);
palette[0] = 0x11223344;
assert_eq!(0x11223344, palette[0]);
assert_eq!(0, palette[1]);
}
fn assert_vga_palette(palette: &Palette) {
assert_eq!(0xff000000, palette[0]);
assert_eq!(0xff0000a8, palette[1]);
assert_eq!(0xff00a800, palette[2]);
assert_eq!(0xff00a8a8, palette[3]);
assert_eq!(0xffa80000, palette[4]);
assert_eq!(0xffa800a8, palette[5]);
assert_eq!(0xffa85400, palette[6]);
assert_eq!(0xffa8a8a8, palette[7]);
assert_eq!(0xff545454, palette[8]);
assert_eq!(0xff5454fc, palette[9]);
assert_eq!(0xff54fc54, palette[10]);
assert_eq!(0xff54fcfc, palette[11]);
assert_eq!(0xfffc5454, palette[12]);
assert_eq!(0xfffc54fc, palette[13]);
assert_eq!(0xfffcfc54, palette[14]);
assert_eq!(0xfffcfcfc, palette[15]);
}
#[test]
fn load_and_save() -> Result<(), PaletteError> {
let tmp_dir = TempDir::new()?;
// vga format
let palette = Palette::load_from_file(Path::new("./assets/vga.pal"), PaletteFormat::Vga)?;
assert_vga_palette(&palette);
let save_path = tmp_dir.path().join("test_save_vga_format.pal");
palette.to_file(&save_path, PaletteFormat::Vga)?;
let reloaded_palette = Palette::load_from_file(&save_path, PaletteFormat::Vga)?;
assert_eq!(palette, reloaded_palette);
// normal format
let palette =
Palette::load_from_file(Path::new("./test-assets/dp2.pal"), PaletteFormat::Normal)?;
let save_path = tmp_dir.path().join("test_save_normal_format.pal");
palette.to_file(&save_path, PaletteFormat::Normal)?;
let reloaded_palette = Palette::load_from_file(&save_path, PaletteFormat::Normal)?;
assert_eq!(palette, reloaded_palette);
Ok(())
}
}

68
libretrogd/src/lib.rs Normal file
View file

@ -0,0 +1,68 @@
extern crate core;
extern crate sdl2;
pub use crate::graphics::bitmap::*;
pub use crate::graphics::bitmapatlas::*;
pub use crate::graphics::font::*;
pub use crate::graphics::palette::*;
pub use crate::graphics::*;
pub use crate::math::circle::*;
pub use crate::math::matrix3x3::*;
pub use crate::math::rect::*;
pub use crate::math::vector2::*;
pub use crate::math::*;
pub use crate::system::input_devices::keyboard::*;
pub use crate::system::input_devices::mouse::*;
pub use crate::system::input_devices::*;
pub use crate::system::*;
pub mod entities;
pub mod events;
pub mod graphics;
pub mod math;
pub mod states;
pub mod system;
pub mod utils;
pub const LOW_RES: bool = if cfg!(feature = "low_res") {
true
} else {
false
};
pub const WIDE_SCREEN: bool = if cfg!(feature = "wide") {
true
} else {
false
};
pub const SCREEN_WIDTH: u32 = if cfg!(feature = "low_res") {
if cfg!(feature = "wide") {
214
} else {
160
}
} else {
if cfg!(feature = "wide") {
428
} else {
320
}
};
pub const SCREEN_HEIGHT: u32 = if cfg!(feature = "low_res") {
120
} else {
240
};
pub const SCREEN_TOP: u32 = 0;
pub const SCREEN_LEFT: u32 = 0;
pub const SCREEN_RIGHT: u32 = SCREEN_WIDTH - 1;
pub const SCREEN_BOTTOM: u32 = SCREEN_HEIGHT - 1;
pub const DEFAULT_SCALE_FACTOR: u32 = if cfg!(feature = "low_res") {
6
} else {
3
};
pub const NUM_COLORS: usize = 256; // i mean ... the number of colors is really defined by the size of u8 ...

View file

@ -0,0 +1,113 @@
use crate::{distance_between, distance_squared_between, Vector2};
/// Represents a 2D circle, using integer center coordinates and radius.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Circle {
pub x: i32,
pub y: i32,
pub radius: u32,
}
impl Circle {
#[inline]
pub fn new(x: i32, y: i32, radius: u32) -> Self {
Circle { x, y, radius }
}
pub fn new_encapsulating(points: &[Vector2]) -> Option<Circle> {
if points.is_empty() {
return None;
}
let mut min_x = points[0].x;
let mut min_y = points[0].y;
let mut max_x = min_x;
let mut max_y = min_y;
for i in 0..points.len() {
let point = &points[i];
min_x = point.x.min(min_x);
min_y = point.y.min(min_y);
max_x = point.x.max(max_x);
max_y = point.y.max(max_y);
}
let radius = distance_between(min_x, min_y, max_x, max_y) * 0.5;
let center_x = (max_x - min_x) / 2.0;
let center_y = (max_y - min_y) / 2.0;
Some(Circle {
x: center_x as i32,
y: center_y as i32,
radius: radius as u32,
})
}
/// Calculates the diameter of the circle.
#[inline]
pub fn diameter(&self) -> u32 {
self.radius * 2
}
/// Returns true if the given point is contained within the bounds of this circle.
pub fn contains_point(&self, x: i32, y: i32) -> bool {
let distance_squared =
distance_squared_between(self.x as f32, self.y as f32, x as f32, y as f32);
let radius_squared = (self.radius * self.radius) as f32;
distance_squared <= radius_squared
}
/// Returns true if the given circle at least partially overlaps the bounds of this circle.
pub fn overlaps(&self, other: &Circle) -> bool {
let distance_squared =
distance_squared_between(self.x as f32, self.y as f32, other.x as f32, other.y as f32);
let minimum_distance_squared =
((self.radius + other.radius) * (self.radius + other.radius)) as f32;
distance_squared <= minimum_distance_squared
}
}
#[cfg(test)]
pub mod tests {
use super::*;
#[test]
pub fn test_new() {
let c = Circle::new(1, 2, 4);
assert_eq!(1, c.x);
assert_eq!(2, c.y);
assert_eq!(4, c.radius);
}
#[test]
pub fn test_diameter() {
let c = Circle::new(4, 4, 3);
assert_eq!(6, c.diameter());
}
#[test]
pub fn test_contains_point() {
let c = Circle::new(1, 1, 6);
assert!(c.contains_point(4, 4));
assert!(!c.contains_point(8, 4));
let c = Circle::new(0, 1, 2);
assert!(!c.contains_point(3, 3));
assert!(c.contains_point(0, 0));
}
#[test]
pub fn test_overlaps() {
let a = Circle::new(3, 4, 5);
let b = Circle::new(14, 18, 8);
assert!(!a.overlaps(&b));
let a = Circle::new(2, 3, 12);
let b = Circle::new(15, 28, 10);
assert!(!a.overlaps(&b));
let a = Circle::new(-10, 8, 30);
let b = Circle::new(14, -24, 10);
assert!(a.overlaps(&b));
}
}

View file

@ -0,0 +1,420 @@
use std::ops::{Mul, MulAssign};
use crate::{nearly_equal, Vector2};
/// Represents a 3x3 column-major matrix and provides common methods for matrix math.
#[derive(Debug, Copy, Clone)]
pub struct Matrix3x3 {
pub m: [f32; 9],
}
impl Matrix3x3 {
pub const M11: usize = 0;
pub const M12: usize = 3;
pub const M13: usize = 6;
pub const M21: usize = 1;
pub const M22: usize = 4;
pub const M23: usize = 7;
pub const M31: usize = 2;
pub const M32: usize = 5;
pub const M33: usize = 8;
pub const IDENTITY: Matrix3x3 = Matrix3x3 {
m: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
};
/// Returns a new identity matrix.
#[inline]
pub fn identity() -> Matrix3x3 {
Matrix3x3::IDENTITY
}
/// Creates a new matrix with the specified elements.
#[rustfmt::skip]
#[inline]
pub fn new(
m11: f32, m12: f32, m13: f32,
m21: f32, m22: f32, m23: f32,
m31: f32, m32: f32, m33: f32,
) -> Matrix3x3 {
Matrix3x3 {
m: [
m11, m21, m31,
m12, m22, m32,
m13, m23, m33
],
}
}
/// Creates a new rotation matrix from a set of euler angles.
///
/// # Arguments
///
/// * `x`: the x angle (in radians)
/// * `y`: the y angle (in radians)
/// * `z`: the z angle (in radians)
pub fn from_euler_angles(x: f32, y: f32, z: f32) -> Matrix3x3 {
let rotate_z = Matrix3x3::new_rotation_z(z);
let rotate_y = Matrix3x3::new_rotation_y(y);
let rotate_x = Matrix3x3::new_rotation_x(x);
// "right-to-left" column-major matrix concatenation
rotate_z * rotate_y * rotate_x
}
/// Creates a new rotation matrix for rotation around the x axis.
///
/// # Arguments
///
/// * `radians`: angle to rotate the x axis around (in radians)
#[rustfmt::skip]
#[inline]
pub fn new_rotation_x(radians: f32) -> Matrix3x3 {
let (s, c) = radians.sin_cos();
Matrix3x3::new(
1.0, 0.0, 0.0,
0.0, c, -s,
0.0, s, c
)
}
/// Creates a new rotation matrix for rotation around the y axis.
///
/// # Arguments
///
/// * `radians`: angle to rotate the y axis around (in radians)
#[rustfmt::skip]
#[inline]
pub fn new_rotation_y(radians: f32) -> Matrix3x3 {
let (s, c) = radians.sin_cos();
Matrix3x3::new(
c, 0.0, s,
0.0, 1.0, 0.0,
-s, 0.0, c
)
}
/// Creates a new rotation matrix for rotation around the z axis.
///
/// # Arguments
///
/// * `radians`: angle to rotate the z axis around (in radians)
#[rustfmt::skip]
#[inline]
pub fn new_rotation_z(radians: f32) -> Matrix3x3 {
let (s, c) = radians.sin_cos();
Matrix3x3::new(
c, -s, 0.0,
s, c, 0.0,
0.0, 0.0, 1.0
)
}
/// Creates a translation matrix. For use with 2D coordinates only.
///
/// # Arguments
///
/// * `x`: the amount to translate on the x axis
/// * `y`: the amount to translate on the y axis
#[rustfmt::skip]
#[inline]
pub fn new_2d_translation(x: f32, y: f32) -> Matrix3x3 {
Matrix3x3::new(
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
x, y, 1.0
)
}
/// Creates a scaling matrix from scaling factors for each axis. For use with 2D coordinates
/// only.
///
/// # Arguments
///
/// * `x`: the scale factor for the x axis
/// * `y`: the scale factor for the y axis
#[rustfmt::skip]
#[inline]
pub fn new_2d_scaling(x: f32, y: f32) -> Matrix3x3 {
Matrix3x3::new(
x, 0.0, 0.0,
0.0, y, 0.0,
0.0, 0.0, 1.0
)
}
/// Creates a new rotation matrix. For use with 2D coordinates only.
///
/// # Arguments
///
/// * `radians`: angle to rotate by (in radians)
#[inline(always)]
pub fn new_2d_rotation(radians: f32) -> Matrix3x3 {
Matrix3x3::new_rotation_z(radians)
}
/// Calculates the determinant of this matrix.
#[rustfmt::skip]
#[inline]
pub fn determinant(&self) -> f32 {
self.m[Matrix3x3::M11] * self.m[Matrix3x3::M22] * self.m[Matrix3x3::M33] +
self.m[Matrix3x3::M12] * self.m[Matrix3x3::M23] * self.m[Matrix3x3::M31] +
self.m[Matrix3x3::M13] * self.m[Matrix3x3::M21] * self.m[Matrix3x3::M32] -
self.m[Matrix3x3::M11] * self.m[Matrix3x3::M23] * self.m[Matrix3x3::M32] -
self.m[Matrix3x3::M12] * self.m[Matrix3x3::M21] * self.m[Matrix3x3::M33] -
self.m[Matrix3x3::M13] * self.m[Matrix3x3::M22] * self.m[Matrix3x3::M31]
}
/// Calculates the inverse of this matrix.
#[rustfmt::skip]
pub fn invert(&self) -> Matrix3x3 {
let d = self.determinant();
if nearly_equal(d, 0.0, 0.000001) {
Matrix3x3::IDENTITY
} else {
let d = 1.0 / d;
Matrix3x3 {
m: [
d * (self.m[Matrix3x3::M22] * self.m[Matrix3x3::M33] - self.m[Matrix3x3::M32] * self.m[Matrix3x3::M23]),
d * (self.m[Matrix3x3::M31] * self.m[Matrix3x3::M23] - self.m[Matrix3x3::M21] * self.m[Matrix3x3::M33]),
d * (self.m[Matrix3x3::M21] * self.m[Matrix3x3::M32] - self.m[Matrix3x3::M31] * self.m[Matrix3x3::M22]),
d * (self.m[Matrix3x3::M32] * self.m[Matrix3x3::M13] - self.m[Matrix3x3::M12] * self.m[Matrix3x3::M33]),
d * (self.m[Matrix3x3::M11] * self.m[Matrix3x3::M33] - self.m[Matrix3x3::M31] * self.m[Matrix3x3::M13]),
d * (self.m[Matrix3x3::M31] * self.m[Matrix3x3::M12] - self.m[Matrix3x3::M11] * self.m[Matrix3x3::M32]),
d * (self.m[Matrix3x3::M12] * self.m[Matrix3x3::M23] - self.m[Matrix3x3::M22] * self.m[Matrix3x3::M13]),
d * (self.m[Matrix3x3::M21] * self.m[Matrix3x3::M13] - self.m[Matrix3x3::M11] * self.m[Matrix3x3::M23]),
d * (self.m[Matrix3x3::M11] * self.m[Matrix3x3::M22] - self.m[Matrix3x3::M21] * self.m[Matrix3x3::M12]),
]
}
}
}
/// Calculates the transpose of this matrix.
#[inline]
pub fn transpose(&self) -> Matrix3x3 {
Matrix3x3::new(
self.m[Matrix3x3::M11],
self.m[Matrix3x3::M21],
self.m[Matrix3x3::M31],
self.m[Matrix3x3::M12],
self.m[Matrix3x3::M22],
self.m[Matrix3x3::M32],
self.m[Matrix3x3::M13],
self.m[Matrix3x3::M23],
self.m[Matrix3x3::M33],
)
}
/// Sets all of the elements of this matrix.
#[inline]
pub fn set(
&mut self,
m11: f32,
m12: f32,
m13: f32,
m21: f32,
m22: f32,
m23: f32,
m31: f32,
m32: f32,
m33: f32,
) {
self.m[Matrix3x3::M11] = m11;
self.m[Matrix3x3::M12] = m12;
self.m[Matrix3x3::M13] = m13;
self.m[Matrix3x3::M21] = m21;
self.m[Matrix3x3::M22] = m22;
self.m[Matrix3x3::M23] = m23;
self.m[Matrix3x3::M31] = m31;
self.m[Matrix3x3::M32] = m32;
self.m[Matrix3x3::M33] = m33;
}
}
impl Mul for Matrix3x3 {
type Output = Self;
#[rustfmt::skip]
#[inline]
fn mul(self, rhs: Self) -> Self::Output {
Matrix3x3::new(
self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M31],
self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M32],
self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M33],
self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M31],
self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M32],
self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M33],
self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M31],
self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M32],
self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M33]
)
}
}
impl MulAssign for Matrix3x3 {
#[rustfmt::skip]
#[inline]
fn mul_assign(&mut self, rhs: Self) {
self.set(
self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M31],
self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M32],
self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M33],
self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M31],
self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M32],
self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M33],
self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M31],
self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M32],
self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M33]
)
}
}
impl Mul<Vector2> for Matrix3x3 {
type Output = Vector2;
#[rustfmt::skip]
#[inline]
fn mul(self, rhs: Vector2) -> Self::Output {
Vector2 {
x: rhs.x * self.m[Matrix3x3::M11] + rhs.y * self.m[Matrix3x3::M12] + self.m[Matrix3x3::M13] + self.m[Matrix3x3::M31],
y: rhs.x * self.m[Matrix3x3::M21] + rhs.y * self.m[Matrix3x3::M22] + self.m[Matrix3x3::M23] + self.m[Matrix3x3::M32]
}
}
}
#[cfg(test)]
pub mod tests {
use crate::math::{RADIANS_180, RADIANS_90};
use super::*;
#[test]
pub fn test_new() {
let m = Matrix3x3::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0);
assert_eq!(1.0, m.m[Matrix3x3::M11]);
assert_eq!(2.0, m.m[Matrix3x3::M12]);
assert_eq!(3.0, m.m[Matrix3x3::M13]);
assert_eq!(4.0, m.m[Matrix3x3::M21]);
assert_eq!(5.0, m.m[Matrix3x3::M22]);
assert_eq!(6.0, m.m[Matrix3x3::M23]);
assert_eq!(7.0, m.m[Matrix3x3::M31]);
assert_eq!(8.0, m.m[Matrix3x3::M32]);
assert_eq!(9.0, m.m[Matrix3x3::M33]);
}
#[test]
pub fn test_set() {
let mut m = Matrix3x3 { m: [0.0; 9] };
m.set(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0);
assert_eq!(1.0, m.m[Matrix3x3::M11]);
assert_eq!(2.0, m.m[Matrix3x3::M12]);
assert_eq!(3.0, m.m[Matrix3x3::M13]);
assert_eq!(4.0, m.m[Matrix3x3::M21]);
assert_eq!(5.0, m.m[Matrix3x3::M22]);
assert_eq!(6.0, m.m[Matrix3x3::M23]);
assert_eq!(7.0, m.m[Matrix3x3::M31]);
assert_eq!(8.0, m.m[Matrix3x3::M32]);
assert_eq!(9.0, m.m[Matrix3x3::M33]);
}
#[test]
pub fn test_identity() {
let m = Matrix3x3::identity();
assert_eq!(1.0, m.m[Matrix3x3::M11]);
assert_eq!(0.0, m.m[Matrix3x3::M12]);
assert_eq!(0.0, m.m[Matrix3x3::M13]);
assert_eq!(0.0, m.m[Matrix3x3::M21]);
assert_eq!(1.0, m.m[Matrix3x3::M22]);
assert_eq!(0.0, m.m[Matrix3x3::M23]);
assert_eq!(0.0, m.m[Matrix3x3::M31]);
assert_eq!(0.0, m.m[Matrix3x3::M32]);
assert_eq!(1.0, m.m[Matrix3x3::M33]);
}
#[rustfmt::skip]
#[test]
pub fn test_transpose() {
let m = Matrix3x3::new(
1.0, 2.0, 3.0,
4.0, 5.0, 6.0,
7.0, 8.0, 9.0
);
let t = m.transpose();
assert_eq!(1.0, t.m[Matrix3x3::M11]);
assert_eq!(4.0, t.m[Matrix3x3::M12]);
assert_eq!(7.0, t.m[Matrix3x3::M13]);
assert_eq!(2.0, t.m[Matrix3x3::M21]);
assert_eq!(5.0, t.m[Matrix3x3::M22]);
assert_eq!(8.0, t.m[Matrix3x3::M23]);
assert_eq!(3.0, t.m[Matrix3x3::M31]);
assert_eq!(6.0, t.m[Matrix3x3::M32]);
assert_eq!(9.0, t.m[Matrix3x3::M33]);
}
#[test]
pub fn test_mul() {
let a = Matrix3x3::new(12.0, 8.0, 4.0, 3.0, 17.0, 14.0, 9.0, 8.0, 10.0);
let b = Matrix3x3::new(5.0, 19.0, 3.0, 6.0, 15.0, 9.0, 7.0, 8.0, 16.0);
let c = a * b;
assert!(nearly_equal(136.0, c.m[Matrix3x3::M11], 0.001));
assert!(nearly_equal(380.0, c.m[Matrix3x3::M12], 0.001));
assert!(nearly_equal(172.0, c.m[Matrix3x3::M13], 0.001));
assert!(nearly_equal(215.0, c.m[Matrix3x3::M21], 0.001));
assert!(nearly_equal(424.0, c.m[Matrix3x3::M22], 0.001));
assert!(nearly_equal(386.0, c.m[Matrix3x3::M23], 0.001));
assert!(nearly_equal(163.0, c.m[Matrix3x3::M31], 0.001));
assert!(nearly_equal(371.0, c.m[Matrix3x3::M32], 0.001));
assert!(nearly_equal(259.0, c.m[Matrix3x3::M33], 0.001));
let mut a = Matrix3x3::new(12.0, 8.0, 4.0, 3.0, 17.0, 14.0, 9.0, 8.0, 10.0);
let b = Matrix3x3::new(5.0, 19.0, 3.0, 6.0, 15.0, 9.0, 7.0, 8.0, 16.0);
a *= b;
assert!(nearly_equal(136.0, a.m[Matrix3x3::M11], 0.001));
assert!(nearly_equal(380.0, a.m[Matrix3x3::M12], 0.001));
assert!(nearly_equal(172.0, a.m[Matrix3x3::M13], 0.001));
assert!(nearly_equal(215.0, a.m[Matrix3x3::M21], 0.001));
assert!(nearly_equal(424.0, a.m[Matrix3x3::M22], 0.001));
assert!(nearly_equal(386.0, a.m[Matrix3x3::M23], 0.001));
assert!(nearly_equal(163.0, a.m[Matrix3x3::M31], 0.001));
assert!(nearly_equal(371.0, a.m[Matrix3x3::M32], 0.001));
assert!(nearly_equal(259.0, a.m[Matrix3x3::M33], 0.001));
}
#[test]
pub fn test_2d_translation() {
let v = Vector2::new(10.2, 5.7);
let m = Matrix3x3::new_2d_translation(2.0, 3.0);
let t = m * v;
assert!(nearly_equal(12.2, t.x, 0.001));
assert!(nearly_equal(8.7, t.y, 0.001));
}
#[test]
pub fn test_2d_scaling() {
let v = Vector2::new(10.2, 5.7);
let m = Matrix3x3::new_2d_scaling(3.0, 4.0);
let t = m * v;
assert!(nearly_equal(30.6, t.x, 0.001));
assert!(nearly_equal(22.8, t.y, 0.001));
}
#[test]
pub fn test_2d_rotation() {
let v = Vector2::new(0.0, 5.0);
let m = Matrix3x3::new_2d_rotation(RADIANS_90);
let t = m * v;
assert!(nearly_equal(-5.0, t.x, 0.001));
assert!(nearly_equal(0.0, t.y, 0.001));
}
#[test]
pub fn test_2d_combined_transform() {
let a = Matrix3x3::new_2d_translation(-2.0, 0.0);
let b = Matrix3x3::new_2d_rotation(RADIANS_180);
let m = a * b;
let v = Vector2::new(0.0, 5.0);
let t = m * v;
assert!(nearly_equal(2.0, t.x, 0.001));
assert!(nearly_equal(-5.0, t.y, 0.001));
}
}

272
libretrogd/src/math/mod.rs Normal file
View file

@ -0,0 +1,272 @@
use std::ops::{Add, Div, Mul, Sub};
pub mod circle;
pub mod matrix3x3;
pub mod rect;
pub mod vector2;
pub const PI: f32 = std::f32::consts::PI; // 180 degrees
pub const HALF_PI: f32 = PI / 2.0; // 90 degrees
pub const QUARTER_PI: f32 = PI / 4.0; // 45 degrees
pub const TWO_PI: f32 = PI * 2.0; // 360 degrees
pub const RADIANS_0: f32 = 0.0;
pub const RADIANS_45: f32 = PI / 4.0;
pub const RADIANS_90: f32 = PI / 2.0;
pub const RADIANS_135: f32 = (3.0 * PI) / 4.0;
pub const RADIANS_180: f32 = PI;
pub const RADIANS_225: f32 = (5.0 * PI) / 4.0;
pub const RADIANS_270: f32 = (3.0 * PI) / 2.0;
pub const RADIANS_315: f32 = (7.0 * PI) / 4.0;
pub const RADIANS_360: f32 = PI * 2.0;
pub const PI_OVER_180: f32 = PI / 180.0;
pub const INVERSE_PI_OVER_180: f32 = 180.0 / PI;
// 2d directions. intended to be usable in a 2d screen-space where the origin 0,0 is at the
// top-left of the screen, where moving up is accomplished by decrementing Y.
// TODO: this is not a perfect solution and does pose some problems in various math calculations ...
pub const UP: f32 = -RADIANS_90;
pub const DOWN: f32 = RADIANS_90;
pub const LEFT: f32 = RADIANS_180;
pub const RIGHT: f32 = RADIANS_0;
/// Returns true if the two f32 values are "close enough" to be considered equal using the
/// precision of the provided epsilon value.
#[inline]
pub fn nearly_equal(a: f32, b: f32, epsilon: f32) -> bool {
// HACK/TODO: this is a shitty way to do this. but it's "good enough" for me ...
(a - b).abs() <= epsilon
}
/// Linearly interpolates between two values.
///
/// # Arguments
///
/// * `a`: first value (low end of range)
/// * `b`: second value (high end of range)
/// * `t`: the amount to interpolate between the two values, specified as a fraction
#[inline]
pub fn lerp<N>(a: N, b: N, t: f32) -> N
where
N: Copy + Add<Output = N> + Sub<Output = N> + Mul<f32, Output = N>,
{
a + (b - a) * t
}
/// Given a linearly interpolated value and the original range (high and low) of the linear
/// interpolation, this will return the original interpolation factor (as a fraction)
///
/// # Arguments
///
/// * `a`: first value (low end of range)
/// * `b`: second value (high end of range)
/// * `lerp_result`: the interpolated value between the range given
#[inline]
pub fn inverse_lerp<N>(a: N, b: N, lerp_result: N) -> f32
where
N: Copy + Sub<Output = N> + Div<N, Output = f32>,
{
(lerp_result - a) / (b - a)
}
/// Interpolates between two values using a cubic equation.
///
/// # Arguments
///
/// * `a`: first value (low end of range)
/// * `b`: second value (high end of range)
/// * `t`: the amount to interpolate between the two values, specified as a fraction
#[inline]
pub fn smooth_lerp<N>(a: N, b: N, t: f32) -> N
where
N: Copy + Add<Output = N> + Sub<Output = N> + Mul<f32, Output = N>,
{
let t = t.clamp(0.0, 1.0);
lerp(a, b, (t * t) * (3.0 - (2.0 * t)))
}
/// Re-scales a given value from an old min/max range to a new and different min/max range such
/// that the returned value is approximately at the same relative position within the new min/max
/// range.
///
/// # Arguments
///
/// * `value`: the value to be re-scaled which is currently between `old_min` and `old_max`
/// * `old_min`: original min value (low end of range)
/// * `old_max`: original max value (high end of range)
/// * `new_min`: new min value (low end of range)
/// * `new_max`: new max value (high end of range)
#[inline]
pub fn scale_range<N>(value: N, old_min: N, old_max: N, new_min: N, new_max: N) -> N
where
N: Copy + Add<Output = N> + Sub<Output = N> + Mul<Output = N> + Div<Output = N>,
{
(new_max - new_min) * (value - old_min) / (old_max - old_min) + new_min
}
/// Calculates the angle (in radians) between the two points.
#[inline]
pub fn angle_between(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
let delta_x = x2 - x1;
let delta_y = y2 - y1;
delta_y.atan2(delta_x)
}
/// Returns the X and Y point of a normalized 2D vector that points in the same direction as
/// the given angle.
#[inline]
pub fn angle_to_direction(radians: f32) -> (f32, f32) {
let x = radians.cos();
let y = radians.sin();
(x, y)
}
/// Calculates the squared distance between two 2D points.
#[inline]
pub fn distance_squared_between(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
(x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)
}
/// Calculates the distance between two 2D points.
#[inline]
pub fn distance_between(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
distance_squared_between(x1, y1, x2, y2).sqrt()
}
pub trait NearlyEqual {
type Output;
/// Returns true if the two f32 values are "close enough" to be considered equal using the
/// precision of the provided epsilon value.
fn nearly_equal(self, other: Self::Output, epsilon: f32) -> bool;
}
impl NearlyEqual for f32 {
type Output = f32;
#[inline(always)]
fn nearly_equal(self, other: Self::Output, epsilon: f32) -> bool {
nearly_equal(self, other, epsilon)
}
}
pub trait WrappingRadians {
type Type;
/// Adds two angle values in radians together returning the result. The addition will wrap
/// around so that the returned result always lies within 0 -> 2π radians (0 -> 360 degrees).
fn wrapping_radians_add(self, other: Self::Type) -> Self::Type;
/// Subtracts two angle values in radians returning the result. The subtraction will wrap
/// around so that the returned result always lies within 0 -> 2π radians (0 -> 360 degrees).
fn wrapping_radians_sub(self, other: Self::Type) -> Self::Type;
}
impl WrappingRadians for f32 {
type Type = f32;
fn wrapping_radians_add(self, other: Self::Type) -> Self::Type {
let result = self + other;
if result < RADIANS_0 {
result + RADIANS_360
} else if result >= RADIANS_360 {
result - RADIANS_360
} else {
result
}
}
fn wrapping_radians_sub(self, other: Self::Type) -> Self::Type {
let result = self - other;
if result < RADIANS_0 {
result + RADIANS_360
} else if result >= RADIANS_360 {
result - RADIANS_360
} else {
result
}
}
}
#[cfg(test)]
pub mod tests {
use super::*;
#[test]
pub fn test_nearly_equal() {
assert!(nearly_equal(4.0, 4.0, 0.1));
assert!(4.0f32.nearly_equal(4.0, 0.1));
assert!(nearly_equal(0.1 + 0.2, 0.3, 0.01));
assert!(!nearly_equal(1.0001, 1.0005, 0.0001));
}
#[test]
pub fn test_lerp() {
assert!(nearly_equal(15.0, lerp(10.0, 20.0, 0.5), 0.0001));
assert!(nearly_equal(10.0, lerp(10.0, 20.0, 0.0), 0.0001));
assert!(nearly_equal(20.0, lerp(10.0, 20.0, 1.0), 0.0001));
}
#[test]
pub fn test_inverse_lerp() {
assert_eq!(0.5, inverse_lerp(10.0, 20.0, 15.0f32))
}
#[test]
pub fn test_angle_between() {
let angle = angle_between(20.0, 20.0, 10.0, 10.0);
assert!(nearly_equal(-RADIANS_135, angle, 0.0001));
let angle = angle_between(0.0, 0.0, 10.0, 10.0);
assert!(nearly_equal(RADIANS_45, angle, 0.0001));
let angle = angle_between(5.0, 5.0, 5.0, 5.0);
assert!(nearly_equal(0.0, angle, 0.0001));
}
#[test]
pub fn test_angle_to_direction() {
let (x, y) = angle_to_direction(RADIANS_0);
assert!(nearly_equal(x, 1.0, 0.000001));
assert!(nearly_equal(y, 0.0, 0.000001));
let (x, y) = angle_to_direction(RADIANS_45);
assert!(nearly_equal(x, 0.707106, 0.000001));
assert!(nearly_equal(y, 0.707106, 0.000001));
let (x, y) = angle_to_direction(RADIANS_225);
assert!(nearly_equal(x, -0.707106, 0.000001));
assert!(nearly_equal(y, -0.707106, 0.000001));
let (x, y) = angle_to_direction(UP);
assert!(nearly_equal(x, 0.0, 0.000001));
assert!(nearly_equal(y, -1.0, 0.000001));
let (x, y) = angle_to_direction(DOWN);
assert!(nearly_equal(x, 0.0, 0.000001));
assert!(nearly_equal(y, 1.0, 0.000001));
let (x, y) = angle_to_direction(LEFT);
assert!(nearly_equal(x, -1.0, 0.000001));
assert!(nearly_equal(y, 0.0, 0.000001));
let (x, y) = angle_to_direction(RIGHT);
assert!(nearly_equal(x, 1.0, 0.000001));
assert!(nearly_equal(y, 0.0, 0.000001));
}
#[test]
pub fn test_distance_between() {
let x1 = -2.0;
let y1 = 1.0;
let x2 = 4.0;
let y2 = 3.0;
let distance_squared = distance_squared_between(x1, y1, x2, y2);
let distance = distance_between(x1, y1, x2, y2);
assert!(nearly_equal(distance_squared, 40.0000, 0.0001));
assert!(nearly_equal(distance, 6.3245, 0.0001));
}
#[test]
pub fn test_wrapping_radians() {
assert!(nearly_equal(RADIANS_90, RADIANS_45.wrapping_radians_add(RADIANS_45), 0.0001));
assert!(nearly_equal(RADIANS_90, RADIANS_180.wrapping_radians_sub(RADIANS_90), 0.0001));
assert!(nearly_equal(RADIANS_45, RADIANS_315.wrapping_radians_add(RADIANS_90), 0.0001));
assert!(nearly_equal(RADIANS_315, RADIANS_90.wrapping_radians_sub(RADIANS_135), 0.0001));
}
}

284
libretrogd/src/math/rect.rs Normal file
View file

@ -0,0 +1,284 @@
/// Represents a 2D rectangle, using integer coordinates and dimensions.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
}
impl Rect {
#[inline]
pub fn new(x: i32, y: i32, width: u32, height: u32) -> Rect {
Rect {
x,
y,
width,
height,
}
}
/// Creates a new rect from the specified coordinates. Automatically determines if the
/// coordinates provided are swapped (where the right/bottom coordinate is provided before the
/// left/top). All of the coordinates are inclusive.
///
/// # Arguments
///
/// * `left`: the left x coordinate
/// * `top`: the top y coordinate
/// * `right`: the right x coordinate
/// * `bottom`: the bottom y coordinate
pub fn from_coords(left: i32, top: i32, right: i32, bottom: i32) -> Rect {
let x;
let y;
let width;
let height;
if left <= right {
x = left;
width = (right - left).abs() + 1;
} else {
x = right;
width = (left - right).abs() + 1;
}
if top <= bottom {
y = top;
height = (bottom - top).abs() + 1;
} else {
y = bottom;
height = (top - bottom).abs() + 1;
}
Rect {
x,
y,
width: width as u32,
height: height as u32,
}
}
pub fn set_from_coords(&mut self, left: i32, top: i32, right: i32, bottom: i32) {
if left <= right {
self.x = left;
self.width = ((right - left).abs() + 1) as u32;
} else {
self.x = right;
self.width = ((left - right).abs() + 1) as u32;
}
if top <= bottom {
self.y = top;
self.height = ((bottom - top).abs() + 1) as u32;
} else {
self.y = bottom;
self.height = ((top - bottom).abs() + 1) as u32;
}
}
/// Calculates the right-most x coordinate contained by this rect.
#[inline]
pub fn right(&self) -> i32 {
if self.width > 0 {
self.x + self.width as i32 - 1
} else {
self.x
}
}
/// Calculates the bottom-most y coordinate contained by this rect.
#[inline]
pub fn bottom(&self) -> i32 {
if self.height > 0 {
self.y + self.height as i32 - 1
} else {
self.y
}
}
/// Returns true if the given point is contained within the bounds of this rect.
pub fn contains_point(&self, x: i32, y: i32) -> bool {
(self.x <= x) && (self.right() >= x) && (self.y <= y) && (self.bottom() >= y)
}
/// Returns true if the given rect is contained completely within the bounds of this rect.
pub fn contains_rect(&self, other: &Rect) -> bool {
(other.x >= self.x && other.x < self.right())
&& (other.right() > self.x && other.right() <= self.right())
&& (other.y >= self.y && other.y < self.bottom())
&& (other.bottom() > self.y && other.bottom() <= self.bottom())
}
/// Returns true if the given rect at least partially overlaps the bounds of this rect.
pub fn overlaps(&self, other: &Rect) -> bool {
(self.x <= other.right())
&& (self.right() >= other.x)
&& (self.y <= other.bottom())
&& (self.bottom() >= other.y)
}
pub fn clamp_to(&mut self, other: &Rect) -> bool {
if !self.overlaps(other) {
// not possible to clamp this rect to the other rect as they do not overlap at all
false
} else {
// the rects at least partially overlap, so we will clamp this rect to the overlapping
// region of the other rect
let mut x1 = self.x;
let mut y1 = self.y;
let mut x2 = self.right();
let mut y2 = self.bottom();
if y1 < other.y {
y1 = other.y;
}
if y1 > other.bottom() {
y1 = other.bottom();
}
if y2 < other.y {
y2 = other.y;
}
if y2 > other.bottom() {
y2 = other.bottom();
}
if x1 < other.x {
x1 = other.x;
}
if x1 > other.right() {
x1 = other.right();
}
if x2 < other.x {
x2 = other.x;
}
if x2 > other.right() {
x2 = other.right();
}
self.set_from_coords(x1, y1, x2, y2);
true
}
}
}
#[cfg(test)]
pub mod tests {
use super::*;
#[test]
pub fn right_and_left() {
let rect = Rect {
x: 5,
y: 6,
width: 16,
height: 12,
};
assert_eq!(20, rect.right());
assert_eq!(17, rect.bottom());
let rect = Rect {
x: -11,
y: -25,
width: 16,
height: 12,
};
assert_eq!(4, rect.right());
assert_eq!(-14, rect.bottom());
}
#[test]
pub fn create_from_coords() {
let rect = Rect::from_coords(10, 15, 20, 30);
assert_eq!(10, rect.x);
assert_eq!(15, rect.y);
assert_eq!(11, rect.width);
assert_eq!(16, rect.height);
assert_eq!(20, rect.right());
assert_eq!(30, rect.bottom());
let rect = Rect::from_coords(-5, -13, 6, -2);
assert_eq!(-5, rect.x);
assert_eq!(-13, rect.y);
assert_eq!(12, rect.width);
assert_eq!(12, rect.height);
assert_eq!(6, rect.right());
assert_eq!(-2, rect.bottom());
}
#[test]
pub fn create_from_coords_with_swapped_order() {
let rect = Rect::from_coords(20, 30, 10, 15);
assert_eq!(10, rect.x);
assert_eq!(15, rect.y);
assert_eq!(11, rect.width);
assert_eq!(16, rect.height);
assert_eq!(20, rect.right());
assert_eq!(30, rect.bottom());
let rect = Rect::from_coords(6, -2, -5, -13);
assert_eq!(-5, rect.x);
assert_eq!(-13, rect.y);
assert_eq!(12, rect.width);
assert_eq!(12, rect.height);
assert_eq!(6, rect.right());
assert_eq!(-2, rect.bottom());
}
#[test]
pub fn test_contains_point() {
let r = Rect::from_coords(10, 10, 20, 20);
assert!(r.contains_point(10, 10));
assert!(r.contains_point(15, 15));
assert!(r.contains_point(20, 20));
assert!(!r.contains_point(12, 30));
assert!(!r.contains_point(8, 12));
assert!(!r.contains_point(25, 16));
assert!(!r.contains_point(17, 4));
}
#[test]
pub fn test_contains_rect() {
let r = Rect::from_coords(10, 10, 20, 20);
assert!(r.contains_rect(&Rect::from_coords(12, 12, 15, 15)));
assert!(r.contains_rect(&Rect::from_coords(10, 10, 15, 15)));
assert!(r.contains_rect(&Rect::from_coords(15, 15, 20, 20)));
assert!(r.contains_rect(&Rect::from_coords(10, 12, 20, 15)));
assert!(r.contains_rect(&Rect::from_coords(12, 10, 15, 20)));
assert!(!r.contains_rect(&Rect::from_coords(5, 5, 15, 15)));
assert!(!r.contains_rect(&Rect::from_coords(15, 15, 25, 25)));
assert!(!r.contains_rect(&Rect::from_coords(2, 2, 8, 4)));
assert!(!r.contains_rect(&Rect::from_coords(12, 21, 18, 25)));
assert!(!r.contains_rect(&Rect::from_coords(22, 12, 32, 17)));
}
#[test]
pub fn test_overlaps() {
let r = Rect::from_coords(10, 10, 20, 20);
assert!(r.overlaps(&Rect::from_coords(12, 12, 15, 15)));
assert!(r.overlaps(&Rect::from_coords(10, 10, 15, 15)));
assert!(r.overlaps(&Rect::from_coords(15, 15, 20, 20)));
assert!(r.overlaps(&Rect::from_coords(10, 12, 20, 15)));
assert!(r.overlaps(&Rect::from_coords(12, 10, 15, 20)));
assert!(r.overlaps(&Rect::from_coords(12, 5, 18, 10)));
assert!(r.overlaps(&Rect::from_coords(13, 20, 16, 25)));
assert!(r.overlaps(&Rect::from_coords(5, 12, 10, 18)));
assert!(r.overlaps(&Rect::from_coords(20, 13, 25, 16)));
assert!(r.overlaps(&Rect::from_coords(5, 5, 15, 15)));
assert!(r.overlaps(&Rect::from_coords(15, 15, 25, 25)));
assert!(!r.overlaps(&Rect::from_coords(2, 2, 8, 4)));
assert!(!r.overlaps(&Rect::from_coords(12, 21, 18, 25)));
assert!(!r.overlaps(&Rect::from_coords(22, 12, 32, 17)));
assert!(!r.overlaps(&Rect::from_coords(12, 5, 18, 9)));
assert!(!r.overlaps(&Rect::from_coords(13, 21, 16, 25)));
assert!(!r.overlaps(&Rect::from_coords(5, 12, 9, 18)));
assert!(!r.overlaps(&Rect::from_coords(21, 13, 25, 16)));
}
}

View file

@ -0,0 +1,479 @@
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
use crate::Matrix3x3;
use crate::{angle_between, angle_to_direction, nearly_equal, NearlyEqual};
/// Represents a 2D vector and provides common methods for vector math.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Vector2 {
pub x: f32,
pub y: f32,
}
impl Vector2 {
pub const ZERO: Vector2 = Vector2 { x: 0.0, y: 0.0 };
pub const UP: Vector2 = Vector2 { x: 0.0, y: -1.0 };
pub const DOWN: Vector2 = Vector2 { x: 0.0, y: 1.0 };
pub const LEFT: Vector2 = Vector2 { x: -1.0, y: 0.0 };
pub const RIGHT: Vector2 = Vector2 { x: 1.0, y: 0.0 };
pub const X_AXIS: Vector2 = Vector2 { x: 1.0, y: 0.0 };
pub const Y_AXIS: Vector2 = Vector2 { x: 0.0, y: 1.0 };
/// Creates a vector with the specified X and Y components.
#[inline]
pub fn new(x: f32, y: f32) -> Vector2 {
Vector2 { x, y }
}
/// Creates a normalized vector that points in the same direction as the given angle.
#[inline]
pub fn from_angle(radians: f32) -> Vector2 {
let (x, y) = angle_to_direction(radians);
Vector2 { x, y }
}
/// Calculates the distance between this and another vector.
#[inline]
pub fn distance(&self, other: &Vector2) -> f32 {
self.distance_squared(other).sqrt()
}
/// Calculates the squared distance between this and another vector.
#[inline]
pub fn distance_squared(&self, other: &Vector2) -> f32 {
(other.x - self.x) * (other.x - self.x) + (other.y - self.y) * (other.y - self.y)
}
/// Calculates the dot product of this and another vector.
#[inline]
pub fn dot(&self, other: &Vector2) -> f32 {
(self.x * other.x) + (self.y * other.y)
}
/// Calculates the length (a.k.a. magnitude) of this vector.
#[inline]
pub fn length(&self) -> f32 {
self.length_squared().sqrt()
}
/// Calculates the squared length of this vector.
#[inline]
pub fn length_squared(&self) -> f32 {
(self.x * self.x) + (self.y * self.y)
}
/// Returns a normalized vector from this vector.
pub fn normalize(&self) -> Vector2 {
let inverse_length = 1.0 / self.length();
Vector2 {
x: self.x * inverse_length,
y: self.y * inverse_length,
}
}
/// Returns an extended (or shrunk) vector from this vector, where the returned vector will
/// have a length exactly matching the specified length, but will retain the same direction.
pub fn extend(&self, length: f32) -> Vector2 {
*self * (length / self.length())
}
/// Returns the angle (in radians) equivalent to the direction of this vector.
#[inline]
pub fn angle(&self) -> f32 {
self.y.atan2(self.x)
}
/// Calculates the angle (in radians) between the this and another vector.
#[inline]
pub fn angle_between(&self, other: &Vector2) -> f32 {
angle_between(self.x, self.y, other.x, other.y)
}
}
impl Neg for Vector2 {
type Output = Self;
#[inline]
fn neg(self) -> Self::Output {
Vector2 {
x: -self.x,
y: -self.y,
}
}
}
impl Add for Vector2 {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self::Output {
Vector2 {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
impl AddAssign for Vector2 {
#[inline]
fn add_assign(&mut self, rhs: Self) {
self.x += rhs.x;
self.y += rhs.y;
}
}
impl Sub for Vector2 {
type Output = Self;
#[inline]
fn sub(self, rhs: Self) -> Self::Output {
Vector2 {
x: self.x - rhs.x,
y: self.y - rhs.y,
}
}
}
impl SubAssign for Vector2 {
#[inline]
fn sub_assign(&mut self, rhs: Self) {
self.x -= rhs.x;
self.y -= rhs.y;
}
}
impl Mul for Vector2 {
type Output = Self;
#[inline]
fn mul(self, rhs: Self) -> Self::Output {
Vector2 {
x: self.x * rhs.x,
y: self.y * rhs.y,
}
}
}
impl MulAssign for Vector2 {
#[inline]
fn mul_assign(&mut self, rhs: Self) {
self.x *= rhs.x;
self.y *= rhs.y;
}
}
impl Div for Vector2 {
type Output = Self;
#[inline]
fn div(self, rhs: Self) -> Self::Output {
Vector2 {
x: self.x / rhs.x,
y: self.y / rhs.y,
}
}
}
impl DivAssign for Vector2 {
#[inline]
fn div_assign(&mut self, rhs: Self) {
self.x /= rhs.x;
self.y /= rhs.y;
}
}
impl Mul<f32> for Vector2 {
type Output = Self;
#[inline]
fn mul(self, rhs: f32) -> Self::Output {
Vector2 {
x: self.x * rhs,
y: self.y * rhs,
}
}
}
impl MulAssign<f32> for Vector2 {
#[inline]
fn mul_assign(&mut self, rhs: f32) {
self.x *= rhs;
self.y *= rhs;
}
}
impl Div<f32> for Vector2 {
type Output = Self;
#[inline]
fn div(self, rhs: f32) -> Self::Output {
Vector2 {
x: self.x / rhs,
y: self.y / rhs,
}
}
}
impl DivAssign<f32> for Vector2 {
#[inline]
fn div_assign(&mut self, rhs: f32) {
self.x /= rhs;
self.y /= rhs;
}
}
impl NearlyEqual for Vector2 {
type Output = Self;
#[inline(always)]
fn nearly_equal(self, other: Self::Output, epsilon: f32) -> bool {
nearly_equal(self.x, other.x, epsilon) && nearly_equal(self.y, other.y, epsilon)
}
}
impl MulAssign<Matrix3x3> for Vector2 {
#[rustfmt::skip]
#[inline]
fn mul_assign(&mut self, rhs: Matrix3x3) {
let x = self.x * rhs.m[Matrix3x3::M11] + self.y * rhs.m[Matrix3x3::M12] + rhs.m[Matrix3x3::M13] + rhs.m[Matrix3x3::M31];
let y = self.x * rhs.m[Matrix3x3::M21] + self.y * rhs.m[Matrix3x3::M22] + rhs.m[Matrix3x3::M23] + rhs.m[Matrix3x3::M32];
self.x = x;
self.y = y;
}
}
#[cfg(test)]
pub mod tests {
use crate::math::*;
use super::*;
#[test]
pub fn test_new() {
let v = Vector2::new(3.0, 7.0);
assert!(nearly_equal(v.x, 3.0, 0.0001));
assert!(nearly_equal(v.y, 7.0, 0.0001));
}
#[test]
pub fn test_neg() {
let v = Vector2 { x: 1.0, y: 2.0 };
let neg = -v;
assert!(nearly_equal(neg.x, -1.0, 0.0001));
assert!(nearly_equal(neg.y, -2.0, 0.0001));
}
#[test]
pub fn test_add() {
let a = Vector2 { x: 3.0, y: 4.0 };
let b = Vector2 { x: 1.0, y: 2.0 };
let c = a + b;
assert!(nearly_equal(c.x, 4.0, 0.0001));
assert!(nearly_equal(c.y, 6.0, 0.0001));
let mut a = Vector2 { x: 3.0, y: 4.0 };
let b = Vector2 { x: 1.0, y: 2.0 };
a += b;
assert!(nearly_equal(a.x, 4.0, 0.0001));
assert!(nearly_equal(a.y, 6.0, 0.0001));
}
#[test]
pub fn test_sub() {
let a = Vector2 { x: 3.0, y: 4.0 };
let b = Vector2 { x: 1.0, y: 2.0 };
let c = a - b;
assert!(nearly_equal(c.x, 2.0, 0.0001));
assert!(nearly_equal(c.y, 2.0, 0.0001));
let mut a = Vector2 { x: 3.0, y: 4.0 };
let b = Vector2 { x: 1.0, y: 2.0 };
a -= b;
assert!(nearly_equal(a.x, 2.0, 0.0001));
assert!(nearly_equal(a.y, 2.0, 0.0001));
}
#[test]
pub fn test_mul() {
let a = Vector2 { x: 2.5, y: 6.0 };
let b = Vector2 { x: 1.25, y: 2.0 };
let c = a * b;
assert!(nearly_equal(c.x, 3.125, 0.0001));
assert!(nearly_equal(c.y, 12.0, 0.0001));
let mut a = Vector2 { x: 2.5, y: 6.0 };
let b = Vector2 { x: 1.25, y: 2.0 };
a *= b;
assert!(nearly_equal(a.x, 3.125, 0.0001));
assert!(nearly_equal(a.y, 12.0, 0.0001));
}
#[test]
pub fn test_div() {
let a = Vector2 { x: 2.5, y: 6.0 };
let b = Vector2 { x: 1.25, y: 2.0 };
let c = a / b;
assert!(nearly_equal(c.x, 2.0, 0.0001));
assert!(nearly_equal(c.y, 3.0, 0.0001));
let mut a = Vector2 { x: 2.5, y: 6.0 };
let b = Vector2 { x: 1.25, y: 2.0 };
a /= b;
assert!(nearly_equal(a.x, 2.0, 0.0001));
assert!(nearly_equal(a.y, 3.0, 0.0001));
}
#[test]
pub fn test_scalar_mul() {
let a = Vector2 { x: 1.0, y: 2.0 };
let b = a * 2.0;
assert!(nearly_equal(b.x, 2.0, 0.0001));
assert!(nearly_equal(b.y, 4.0, 0.0001));
let mut a = Vector2 { x: 1.0, y: 2.0 };
a *= 2.0;
assert!(nearly_equal(b.x, 2.0, 0.0001));
assert!(nearly_equal(b.y, 4.0, 0.0001));
}
#[test]
pub fn test_scalar_div() {
let a = Vector2 { x: 1.0, y: 2.0 };
let b = a / 2.0;
assert!(nearly_equal(b.x, 0.5, 0.0001));
assert!(nearly_equal(b.y, 1.0, 0.0001));
let mut a = Vector2 { x: 1.0, y: 2.0 };
a /= 2.0;
assert!(nearly_equal(b.x, 0.5, 0.0001));
assert!(nearly_equal(b.y, 1.0, 0.0001));
}
#[test]
pub fn test_nearly_equal() {
let a = Vector2 { x: 3.4, y: -7.1 };
let b = Vector2 { x: 3.5, y: -7.1 };
assert!(!a.nearly_equal(b, 0.0001));
let a = Vector2 { x: 2.0, y: 4.0 };
let b = Vector2 { x: 2.0, y: 4.0 };
assert!(a.nearly_equal(b, 0.0001));
}
#[test]
pub fn test_length() {
let v = Vector2 { x: 6.0, y: 8.0 };
let length_squared = v.length_squared();
let length = v.length();
assert!(nearly_equal(length_squared, 100.0, 0.0001));
assert!(nearly_equal(length, 10.0, 0.0001));
}
#[test]
pub fn test_dot() {
let a = Vector2 { x: -6.0, y: 8.0 };
let b = Vector2 { x: 5.0, y: 12.0 };
let dot = a.dot(&b);
assert!(nearly_equal(dot, 66.0, 0.0001));
let a = Vector2 { x: -12.0, y: 16.0 };
let b = Vector2 { x: 12.0, y: 9.0 };
let dot = a.dot(&b);
assert!(nearly_equal(dot, 0.0, 0.0001));
}
#[test]
pub fn test_distance() {
let a = Vector2 { x: 1.0, y: 1.0 };
let b = Vector2 { x: 1.0, y: 3.0 };
let distance_squared = a.distance_squared(&b);
let distance = a.distance(&b);
assert!(nearly_equal(distance_squared, 4.0, 0.0001));
assert!(nearly_equal(distance, 2.0, 0.0001));
}
#[test]
pub fn test_normalize() {
let v = Vector2 { x: 3.0, y: 4.0 };
let normalized = v.normalize();
assert!(nearly_equal(normalized.x, 0.6, 0.0001));
assert!(nearly_equal(normalized.y, 0.8, 0.0001));
}
#[test]
pub fn test_extend() {
let v = Vector2 { x: 10.0, y: 1.0 };
let extended = v.extend(2.0);
assert!(nearly_equal(extended.x, 1.990, 0.0001));
assert!(nearly_equal(extended.y, 0.199, 0.0001));
}
#[test]
#[rustfmt::skip]
pub fn test_angle() {
assert!(nearly_equal(RADIANS_0, Vector2::new(5.0, 0.0).angle(), 0.0001));
assert!(nearly_equal(RADIANS_45, Vector2::new(5.0, 5.0).angle(), 0.0001));
assert!(nearly_equal(RADIANS_90, Vector2::new(0.0, 5.0).angle(), 0.0001));
assert!(nearly_equal(RADIANS_135, Vector2::new(-5.0, 5.0).angle(), 0.0001));
assert!(nearly_equal(RADIANS_180, Vector2::new(-5.0, 0.0).angle(), 0.0001));
assert!(nearly_equal(-RADIANS_135, Vector2::new(-5.0, -5.0).angle(), 0.0001));
assert!(nearly_equal(-RADIANS_90, Vector2::new(0.0, -5.0).angle(), 0.0001));
assert!(nearly_equal(-RADIANS_45, Vector2::new(5.0, -5.0).angle(), 0.0001));
assert!(nearly_equal(crate::math::UP, Vector2::UP.angle(), 0.0001));
assert!(nearly_equal(crate::math::DOWN, Vector2::DOWN.angle(), 0.0001));
assert!(nearly_equal(crate::math::LEFT, Vector2::LEFT.angle(), 0.0001));
assert!(nearly_equal(crate::math::RIGHT, Vector2::RIGHT.angle(), 0.0001));
}
#[test]
pub fn test_from_angle() {
let v = Vector2::from_angle(RADIANS_0);
assert!(nearly_equal(v.x, 1.0, 0.000001));
assert!(nearly_equal(v.y, 0.0, 0.000001));
let v = Vector2::from_angle(RADIANS_45);
assert!(nearly_equal(v.x, 0.707106, 0.000001));
assert!(nearly_equal(v.y, 0.707106, 0.000001));
let v = Vector2::from_angle(RADIANS_225);
assert!(nearly_equal(v.x, -0.707106, 0.000001));
assert!(nearly_equal(v.y, -0.707106, 0.000001));
let v = Vector2::from_angle(UP);
assert!(v.nearly_equal(Vector2::UP, 0.000001));
let v = Vector2::from_angle(DOWN);
assert!(v.nearly_equal(Vector2::DOWN, 0.000001));
let v = Vector2::from_angle(LEFT);
assert!(v.nearly_equal(Vector2::LEFT, 0.000001));
let v = Vector2::from_angle(RIGHT);
assert!(v.nearly_equal(Vector2::RIGHT, 0.000001));
}
#[test]
pub fn test_angle_between() {
let a = Vector2::new(20.0, 20.0);
let b = Vector2::new(10.0, 10.0);
let angle = a.angle_between(&b);
assert!(nearly_equal(-RADIANS_135, angle, 0.0001));
let a = Vector2::new(0.0, 0.0);
let b = Vector2::new(10.0, 10.0);
let angle = a.angle_between(&b);
assert!(nearly_equal(RADIANS_45, angle, 0.0001));
let a = Vector2::new(5.0, 5.0);
let b = Vector2::new(5.0, 5.0);
let angle = a.angle_between(&b);
assert!(nearly_equal(0.0, angle, 0.0001));
}
#[test]
pub fn test_lerp() {
let a = Vector2 { x: 5.0, y: 1.0 };
let b = Vector2 { x: 10.0, y: 2.0 };
let c = lerp(a, b, 0.5);
assert!(nearly_equal(c.x, 7.5, 0.0001));
assert!(nearly_equal(c.y, 1.5, 0.0001));
}
}

1264
libretrogd/src/states/mod.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,93 @@
use sdl2::event::Event;
use sdl2::keyboard::Scancode;
use super::{ButtonState, InputDevice};
const MAX_KEYS: usize = 256;
/// Holds the current state of the keyboard.
///
/// Must be explicitly updated each frame by calling `handle_event` each frame for all SDL2 events
/// received, as well as calling `do_events` once each frame. Usually, you would accomplish all
/// this house-keeping by simply calling [`System`]'s `do_events` method once per frame.
///
/// [`System`]: crate::System
pub struct Keyboard {
keyboard: [ButtonState; MAX_KEYS], // Box<[ButtonState]>,
}
impl Keyboard {
pub fn new() -> Keyboard {
Keyboard {
keyboard: [ButtonState::Idle; MAX_KEYS],
}
/*
Keyboard {
keyboard: vec![ButtonState::Idle; 256].into_boxed_slice(),
}
*/
}
/// Returns true if the given key was just pressed or is being held down.
#[inline]
pub fn is_key_down(&self, scancode: Scancode) -> bool {
matches!(
self.keyboard[scancode as usize],
ButtonState::Pressed | ButtonState::Held
)
}
/// Returns true if the given key was not just pressed and is not being held down.
#[inline]
pub fn is_key_up(&self, scancode: Scancode) -> bool {
matches!(
self.keyboard[scancode as usize],
ButtonState::Released | ButtonState::Idle
)
}
/// Returns true if the given key was just pressed (not being held down, yet).
#[inline]
pub fn is_key_pressed(&self, scancode: Scancode) -> bool {
self.keyboard[scancode as usize] == ButtonState::Pressed
}
/// Returns true if the given key was just released.
#[inline]
pub fn is_key_released(&self, scancode: Scancode) -> bool {
self.keyboard[scancode as usize] == ButtonState::Released
}
}
impl InputDevice for Keyboard {
fn update(&mut self) {
for state in self.keyboard.iter_mut() {
*state = match *state {
ButtonState::Pressed => ButtonState::Held,
ButtonState::Released => ButtonState::Idle,
otherwise => otherwise,
};
}
}
fn handle_event(&mut self, event: &Event) {
match event {
Event::KeyDown { scancode, .. } => {
if let Some(scancode) = scancode {
let state = &mut self.keyboard[*scancode as usize];
*state = match *state {
ButtonState::Pressed => ButtonState::Held,
ButtonState::Held => ButtonState::Held,
_ => ButtonState::Pressed,
};
}
}
Event::KeyUp { scancode, .. } => {
if let Some(scancode) = scancode {
self.keyboard[*scancode as usize] = ButtonState::Released;
}
}
_ => (),
}
}
}

View file

@ -0,0 +1,27 @@
use sdl2::event::Event;
pub mod keyboard;
pub mod mouse;
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum ButtonState {
Idle,
Pressed,
Held,
Released,
}
/// Common trait for input device implementations.
pub trait InputDevice {
/// Performs internal house-keeping necessary for properly reporting the current state of this
/// input device. Normally this should be called on the device after all of this frame's
/// input events have been processed via `handle_event`.
fn update(&mut self);
/// Processes the data from the given [`Event`] if it is relevant for this input device. You
/// should pass in all events received every frame and let the input device figure out if it
/// is relevant to it or not.
///
/// [`Event`]: sdl2::event::Event
fn handle_event(&mut self, event: &Event);
}

View file

@ -0,0 +1,318 @@
use sdl2::event::Event;
use crate::{Bitmap, BlitMethod, Rect};
use super::{ButtonState, InputDevice};
const MAX_BUTTONS: usize = 32;
const DEFAULT_MOUSE_CURSOR_HOTSPOT_X: u32 = 0;
const DEFAULT_MOUSE_CURSOR_HOTSPOT_Y: u32 = 0;
const DEFAULT_MOUSE_CURSOR_WIDTH: usize = 16;
const DEFAULT_MOUSE_CURSOR_HEIGHT: usize = 16;
#[rustfmt::skip]
const DEFAULT_MOUSE_CURSOR: [u8; DEFAULT_MOUSE_CURSOR_WIDTH * DEFAULT_MOUSE_CURSOR_HEIGHT] = [
0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0x00,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0x00,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0x00,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,
0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,
0x00,0x0f,0x0f,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0x00,0x0f,0x00,0x00,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0x00,0x00,0xff,0xff,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
];
/// Holds the current state of the mouse.
///
/// Must be explicitly updated each frame by calling `handle_event` each frame for all SDL2 events
/// received, as well as calling `do_events` once each frame. Usually, you would accomplish all
/// this house-keeping by simply calling [`System`]'s `do_events` method once per frame.
///
/// [`System`]: crate::System
pub struct Mouse {
x: i32,
y: i32,
x_delta: i32,
y_delta: i32,
buttons: [ButtonState; MAX_BUTTONS],
cursor: Bitmap,
cursor_background: Bitmap,
cursor_hotspot_x: u32,
cursor_hotspot_y: u32,
cursor_enabled: bool,
}
impl Mouse {
pub fn new() -> Mouse {
let (cursor, cursor_background, cursor_hotspot_x, cursor_hotspot_y) =
Self::get_default_mouse_cursor();
Mouse {
x: 0,
y: 0,
x_delta: 0,
y_delta: 0,
buttons: [ButtonState::Idle; MAX_BUTTONS],
cursor,
cursor_background,
cursor_hotspot_x,
cursor_hotspot_y,
cursor_enabled: false,
}
}
/// Returns the current x coordinate of the mouse cursor.
#[inline]
pub fn x(&self) -> i32 {
self.x
}
/// Returns the current y coordinate of the mouse cursor.
#[inline]
pub fn y(&self) -> i32 {
self.y
}
/// Returns the amount of pixels along the x-axis that the mouse cursor moved since the last
/// time that the mouse state was updated.
#[inline]
pub fn x_delta(&self) -> i32 {
self.x_delta
}
/// Returns the amount of pixels along the y-axis that the mouse cursor moved since the last
/// time that the mouse state was updated.
#[inline]
pub fn y_delta(&self) -> i32 {
self.y_delta
}
/// Returns true if the given button was just pressed or is being held down.
#[inline]
pub fn is_button_down(&self, button: usize) -> bool {
matches!(
self.buttons[button],
ButtonState::Pressed | ButtonState::Held
)
}
/// Returns true if the given button was not just pressed and is not being held down.
#[inline]
pub fn is_button_up(&self, button: usize) -> bool {
matches!(
self.buttons[button],
ButtonState::Released | ButtonState::Idle
)
}
/// Returns true if the given button was just pressed (not being held down, yet).
#[inline]
pub fn is_button_pressed(&self, button: usize) -> bool {
self.buttons[button] == ButtonState::Pressed
}
/// Returns true if the given button was just released.
#[inline]
pub fn is_button_released(&self, button: usize) -> bool {
self.buttons[button] == ButtonState::Released
}
/// Returns a reference to the current mouse cursor bitmap.
#[inline]
pub fn cursor_bitmap(&self) -> &Bitmap {
&self.cursor
}
/// Returns the current mouse cursor's "hotspot" x coordinate.
#[inline]
pub fn cursor_hotspot_x(&self) -> u32 {
self.cursor_hotspot_x
}
/// Returns the current mouse cursor's "hotspot" y coordinate.
#[inline]
pub fn cursor_hotspot_y(&self) -> u32 {
self.cursor_hotspot_y
}
/// Returns true if mouse cursor bitmap rendering is enabled.
#[inline]
pub fn is_cursor_enabled(&self) -> bool {
self.cursor_enabled
}
/// Enables or disables mouse cursor bitmap rendering.
#[inline]
pub fn enable_cursor(&mut self, enable: bool) {
self.cursor_enabled = enable;
}
/// Sets the [`Bitmap`] used to display the mouse cursor and the "hotspot" coordinate. The
/// bitmap provided here should be set up to use color 255 as the transparent color.
///
/// # Arguments
///
/// * `cursor`: the bitmap to be used to display the mouse cursor on screen
/// * `hotspot_x`: the "hotspot" x coordinate
/// * `hotspot_y`: the "hotspot" y coordinate.
pub fn set_mouse_cursor(&mut self, cursor: Bitmap, hotspot_x: u32, hotspot_y: u32) {
self.cursor = cursor;
self.cursor_background = Bitmap::new(self.cursor.width(), self.cursor.height()).unwrap();
self.cursor_hotspot_x = hotspot_x;
self.cursor_hotspot_y = hotspot_y;
}
/// Resets the mouse cursor bitmap and "hotspot" coordinate back to the default settings.
pub fn set_default_mouse_cursor(&mut self) {
let (cursor, background, hotspot_x, hotspot_y) = Self::get_default_mouse_cursor();
self.cursor = cursor;
self.cursor_background = background;
self.cursor_hotspot_x = hotspot_x;
self.cursor_hotspot_y = hotspot_y;
}
fn get_default_mouse_cursor() -> (Bitmap, Bitmap, u32, u32) {
let mut cursor = Bitmap::new(
DEFAULT_MOUSE_CURSOR_WIDTH as u32,
DEFAULT_MOUSE_CURSOR_HEIGHT as u32,
)
.unwrap();
cursor.pixels_mut().copy_from_slice(&DEFAULT_MOUSE_CURSOR);
let cursor_background = Bitmap::new(cursor.width(), cursor.height()).unwrap();
(
cursor,
cursor_background,
DEFAULT_MOUSE_CURSOR_HOTSPOT_X,
DEFAULT_MOUSE_CURSOR_HOTSPOT_Y,
)
}
#[inline]
fn get_cursor_render_position(&self) -> (i32, i32) {
(
self.x - self.cursor_hotspot_x as i32,
self.y - self.cursor_hotspot_y as i32,
)
}
/// Renders the mouse cursor bitmap onto the destination bitmap at the mouse's current
/// position. The destination bitmap specified is assumed to be the [`System`]'s video
/// backbuffer bitmap. The background on the destination bitmap is saved internally and a
/// subsequent call to [`Mouse::hide_cursor`] will restore the background.
///
/// If mouse cursor rendering is not currently enabled, this method does nothing.
///
/// Applications will not normally need to call this method, as if mouse cursor rendering is
/// enabled, this will be automatically handled by [`System::display`].
///
/// [`System`]: crate::System
/// [`System::display`]: crate::System::display
pub fn render_cursor(&mut self, dest: &mut Bitmap) {
if !self.cursor_enabled {
return;
}
let (x, y) = self.get_cursor_render_position();
// preserve existing background first
self.cursor_background.blit_region(
BlitMethod::Solid,
&dest,
&Rect::new(x, y, self.cursor.width(), self.cursor.height()),
0,
0,
);
dest.blit(BlitMethod::Transparent(255), &self.cursor, x, y);
}
/// Restores the original destination bitmap contents where the mouse cursor bitmap was
/// rendered to during the previous call to [`Mouse::render_cursor`]. The destination bitmap
/// specified is assumed to be the [`System`]'s video backbuffer bitmap.
///
/// If mouse cursor rendering is not currently enabled, this method does nothing.
///
/// Applications will not normally need to call this method, as if mouse cursor rendering is
/// enabled, this will be automatically handled by [`System::display`].
///
/// [`System`]: crate::System
/// [`System::display`]: crate::System::display
pub fn hide_cursor(&mut self, dest: &mut Bitmap) {
if !self.cursor_enabled {
return;
}
let (x, y) = self.get_cursor_render_position();
dest.blit(BlitMethod::Solid, &self.cursor_background, x, y);
}
fn update_button_state(&mut self, button: u32, is_pressed: bool) {
let button_state = &mut self.buttons[button as usize];
*button_state = if is_pressed {
match *button_state {
ButtonState::Pressed => ButtonState::Held,
ButtonState::Held => ButtonState::Held,
_ => ButtonState::Pressed,
}
} else {
match *button_state {
ButtonState::Pressed | ButtonState::Held => ButtonState::Released,
ButtonState::Released => ButtonState::Idle,
ButtonState::Idle => ButtonState::Idle,
}
}
}
}
impl InputDevice for Mouse {
fn update(&mut self) {
self.x_delta = 0;
self.y_delta = 0;
for state in self.buttons.iter_mut() {
*state = match *state {
ButtonState::Pressed => ButtonState::Held,
ButtonState::Released => ButtonState::Idle,
otherwise => otherwise,
}
}
}
fn handle_event(&mut self, event: &Event) {
match event {
Event::MouseMotion {
mousestate,
x,
y,
xrel,
yrel,
..
} => {
self.x = *x;
self.y = *y;
self.x_delta = *xrel;
self.y_delta = *yrel;
for (button, is_pressed) in mousestate.mouse_buttons() {
self.update_button_state(button as u32, is_pressed);
}
}
Event::MouseButtonDown { mouse_btn, .. } => {
self.update_button_state(*mouse_btn as u32, true);
}
Event::MouseButtonUp { mouse_btn, .. } => {
self.update_button_state(*mouse_btn as u32, false);
}
_ => (),
}
}
}

View file

@ -0,0 +1,400 @@
use byte_slice_cast::AsByteSlice;
use sdl2::event::Event;
use sdl2::pixels::PixelFormatEnum;
use sdl2::render::{Texture, WindowCanvas};
use sdl2::{EventPump, Sdl, TimerSubsystem, VideoSubsystem};
use thiserror::Error;
use crate::{Bitmap, BitmaskFont, InputDevice, Keyboard, Mouse, Palette};
use crate::{DEFAULT_SCALE_FACTOR, SCREEN_HEIGHT, SCREEN_WIDTH};
pub mod input_devices;
#[derive(Error, Debug)]
pub enum SystemError {
#[error("System init error: {0}")]
InitError(String),
#[error("System display error: {0}")]
DisplayError(String),
}
/// Builder for configuring and constructing an instance of [`System`].
#[derive(Debug)]
pub struct SystemBuilder {
window_title: String,
vsync: bool,
target_framerate: Option<u32>,
initial_scale_factor: u32,
resizable: bool,
show_mouse: bool,
relative_mouse_scaling: bool,
integer_scaling: bool,
}
impl SystemBuilder {
/// Returns a new [`SystemBuilder`] with a default configuration.
pub fn new() -> SystemBuilder {
SystemBuilder {
window_title: String::new(),
vsync: false,
target_framerate: None,
initial_scale_factor: DEFAULT_SCALE_FACTOR,
resizable: true,
show_mouse: false,
relative_mouse_scaling: true,
integer_scaling: false,
}
}
/// Set the window title for the [`System`] to be built.
pub fn window_title(&mut self, window_title: &str) -> &mut SystemBuilder {
self.window_title = window_title.to_string();
self
}
/// Enables or disables V-Sync for the [`System`] to be built. Enabling V-sync automatically
/// disables `target_framerate`.
pub fn vsync(&mut self, enable: bool) -> &mut SystemBuilder {
self.vsync = enable;
self.target_framerate = None;
self
}
/// Sets a target framerate for the [`System`] being built to run at. This is intended to be
/// used when V-sync is not desired, so setting a target framerate automatically disables
/// `vsync`.
pub fn target_framerate(&mut self, target_framerate: u32) -> &mut SystemBuilder {
self.target_framerate = Some(target_framerate);
self.vsync = false;
self
}
/// Sets an integer scaling factor for the [`System`] being built to up-scale the virtual
/// framebuffer to when displaying it on screen.
pub fn scale_factor(&mut self, scale_factor: u32) -> &mut SystemBuilder {
self.initial_scale_factor = scale_factor;
self
}
/// Sets whether the window will be resizable by the user for the [`System`] being built.
pub fn resizable(&mut self, enable: bool) -> &mut SystemBuilder {
self.resizable = enable;
self
}
/// Enables or disables mouse cursor display by the operating system when the cursor is over
/// the window for the [`System`] being built. Disable this if you intend to render your own
/// custom mouse cursor.
pub fn show_mouse(&mut self, enable: bool) -> &mut SystemBuilder {
self.show_mouse = enable;
self
}
/// Enables or disables automatic DPI scaling of mouse relative movement values (delta values)
/// available via the [`Mouse`] input device.
pub fn relative_mouse_scaling(&mut self, enable: bool) -> &mut SystemBuilder {
self.relative_mouse_scaling = enable;
self
}
/// Enables or disables restricting the final rendered output to always be integer scaled,
/// even if that result will not fully fill the area of the window.
pub fn integer_scaling(&mut self, enable: bool) -> &mut SystemBuilder {
self.integer_scaling = enable;
self
}
/// Builds and returns a [`System`] from the current configuration.
pub fn build(&self) -> Result<System, SystemError> {
// todo: maybe let this be customized in the future, or at least halved so a 160x120 mode can be available ... ?
let screen_width = SCREEN_WIDTH;
let screen_height = SCREEN_HEIGHT;
let texture_pixel_size = 4; // 32-bit ARGB format
sdl2::hint::set(
"SDL_MOUSE_RELATIVE_SCALING",
if self.relative_mouse_scaling {
"1"
} else {
"0"
},
);
// build all the individual SDL subsystems
let sdl_context = match sdl2::init() {
Ok(sdl_context) => sdl_context,
Err(message) => return Err(SystemError::InitError(message)),
};
let sdl_timer_subsystem = match sdl_context.timer() {
Ok(timer_subsystem) => timer_subsystem,
Err(message) => return Err(SystemError::InitError(message)),
};
let sdl_video_subsystem = match sdl_context.video() {
Ok(video_subsystem) => video_subsystem,
Err(message) => return Err(SystemError::InitError(message)),
};
let sdl_event_pump = match sdl_context.event_pump() {
Ok(event_pump) => event_pump,
Err(message) => return Err(SystemError::InitError(message)),
};
// create the window
let window_width = screen_width * self.initial_scale_factor;
let window_height = screen_height * self.initial_scale_factor;
let mut window_builder = &mut (sdl_video_subsystem.window(
self.window_title.as_str(),
window_width,
window_height,
));
if self.resizable {
window_builder = window_builder.resizable();
}
let sdl_window = match window_builder.build() {
Ok(window) => window,
Err(error) => return Err(SystemError::InitError(error.to_string())),
};
sdl_context.mouse().show_cursor(self.show_mouse);
// turn the window into a canvas (under the hood, an SDL Renderer that owns the window)
let mut canvas_builder = sdl_window.into_canvas();
if self.vsync {
canvas_builder = canvas_builder.present_vsync();
}
let mut sdl_canvas = match canvas_builder.build() {
Ok(canvas) => canvas,
Err(error) => return Err(SystemError::InitError(error.to_string())),
};
if let Err(error) = sdl_canvas.set_logical_size(screen_width, screen_height) {
return Err(SystemError::InitError(error.to_string()));
};
// TODO: newer versions of rust-sdl2 support this directly off the WindowCanvas struct
unsafe {
sdl2::sys::SDL_RenderSetIntegerScale(
sdl_canvas.raw(),
if self.integer_scaling {
sdl2::sys::SDL_bool::SDL_TRUE
} else {
sdl2::sys::SDL_bool::SDL_FALSE
},
);
}
// create an SDL texture which we will be uploading to every frame to display the
// application's framebuffer
let sdl_texture = match sdl_canvas.create_texture_streaming(
Some(PixelFormatEnum::ARGB8888),
screen_width,
screen_height,
) {
Ok(texture) => texture,
Err(error) => return Err(SystemError::InitError(error.to_string())),
};
let sdl_texture_pitch = (sdl_texture.query().width * texture_pixel_size) as usize;
// create a raw 32-bit RGBA buffer that will be used as the temporary source for
// SDL texture uploads each frame. necessary as applications are dealing with 8-bit indexed
// bitmaps, not 32-bit RGBA pixels, so this temporary buffer is where we convert the final
// application framebuffer to 32-bit RGBA pixels before it is uploaded to the SDL texture
let texture_pixels_size = (screen_width * screen_height * texture_pixel_size) as usize;
let texture_pixels = vec![0u32; texture_pixels_size].into_boxed_slice();
// create the Bitmap object that will be exposed to the application acting as the system
// backbuffer
let framebuffer = match Bitmap::new(SCREEN_WIDTH, SCREEN_HEIGHT) {
Ok(bmp) => bmp,
Err(error) => return Err(SystemError::InitError(error.to_string())),
};
// create the default palette, initialized to the VGA default palette. also exposed to the
// application for manipulation
let palette = match Palette::new_vga_palette() {
Ok(palette) => palette,
Err(error) => return Err(SystemError::InitError(error.to_string())),
};
// create the default font, initialized to the VGA BIOS default font.
let font = match BitmaskFont::new_vga_font() {
Ok(font) => font,
Err(error) => return Err(SystemError::InitError(error.to_string())),
};
// create input device objects, exposed to the application
let keyboard = Keyboard::new();
let mouse = Mouse::new();
Ok(System {
sdl_context,
sdl_video_subsystem,
sdl_timer_subsystem,
sdl_canvas,
sdl_texture,
sdl_texture_pitch,
sdl_event_pump,
texture_pixels,
video: framebuffer,
palette,
font,
keyboard,
mouse,
target_framerate: self.target_framerate,
target_framerate_delta: None,
next_tick: 0,
})
}
}
/// Holds all primary structures necessary for interacting with the operating system and for
/// applications to render to the display, react to input device events, etc. through the
/// "virtual machine" exposed by this library.
#[allow(dead_code)]
pub struct System {
sdl_context: Sdl,
sdl_video_subsystem: VideoSubsystem,
sdl_timer_subsystem: TimerSubsystem,
sdl_canvas: WindowCanvas,
sdl_texture: Texture,
sdl_texture_pitch: usize,
sdl_event_pump: EventPump,
texture_pixels: Box<[u32]>,
target_framerate: Option<u32>,
target_framerate_delta: Option<i64>,
next_tick: i64,
/// The primary backbuffer [`Bitmap`] that will be rendered to the screen whenever
/// [`System::display`] is called. Regardless of the actual window size, this bitmap is always
/// [`SCREEN_WIDTH`]x[`SCREEN_HEIGHT`] pixels in size.
pub video: Bitmap,
/// The [`Palette`] that will be used in conjunction with the `video` backbuffer to
/// render the final output to the screen whenever [`System::display`] is called.
pub palette: Palette,
/// A pre-loaded [`Font`] that can be used for text rendering.
pub font: BitmaskFont,
/// The current keyboard state. To ensure it is updated each frame, you should call
/// [`System::do_events`] or [`System::do_events_with`] each frame.
pub keyboard: Keyboard,
/// The current mouse state. To ensure it is updated each frame, you should call
/// [`System::do_events`] or [`System::do_events_with`] each frame.
pub mouse: Mouse,
}
impl System {
/// Takes the `video` backbuffer bitmap and `palette` and renders it to the window, up-scaled
/// to fill the window (preserving aspect ratio of course). If V-sync is enabled, this method
/// will block to wait for V-sync. Otherwise, if a target framerate was configured a delay
/// might be used to try to meet that framerate.
pub fn display(&mut self) -> Result<(), SystemError> {
self.mouse.render_cursor(&mut self.video);
// convert application framebuffer to 32-bit RGBA pixels, and then upload it to the SDL
// texture so it will be displayed on screen
self.video
.copy_as_argb_to(&mut self.texture_pixels, &self.palette);
let texture_pixels = self.texture_pixels.as_byte_slice();
if let Err(error) = self
.sdl_texture
.update(None, texture_pixels, self.sdl_texture_pitch)
{
return Err(SystemError::DisplayError(error.to_string()));
}
self.sdl_canvas.clear();
if let Err(error) = self.sdl_canvas.copy(&self.sdl_texture, None, None) {
return Err(SystemError::DisplayError(error));
}
self.sdl_canvas.present();
self.mouse.hide_cursor(&mut self.video);
// if a specific target framerate is desired, apply some loop timing/delay to achieve it
// TODO: do this better. delaying when running faster like this is a poor way to do this..
if let Some(target_framerate) = self.target_framerate {
if self.target_framerate_delta.is_some() {
// normal path for every other loop iteration except the first
let delay = self.next_tick - self.ticks() as i64;
if delay < 0 {
// this loop iteration took too long, no need to delay
self.next_tick -= delay;
} else {
// this loop iteration completed before next_tick time, delay by the remainder
// time period so we're running at about the desired framerate
self.delay(((delay * 1000) / self.tick_frequency() as i64) as u32);
}
} else {
// this branch will occur on the first main loop iteration. we use the fact that
// target_framerate_delta was not yet set to avoid doing any delay on the first
// loop, just in case there was some other processing between the System struct
// being created and the actual beginning of the first loop ...
self.target_framerate_delta =
Some((self.tick_frequency() / target_framerate as u64) as i64);
}
// expected time for the next display() call to happen by
self.next_tick = (self.ticks() as i64) + self.target_framerate_delta.unwrap();
}
Ok(())
}
/// Checks for and responds to all SDL2 events waiting in the queue. Each event is passed to
/// all [`InputDevice`]'s automatically to ensure input device state is up to date.
pub fn do_events(&mut self) {
self.do_events_with(|_event| {});
}
/// Same as [`System::do_events`] but also takes a function which will be called for each
/// SDL2 event being processed (after everything else has already processed it), allowing
/// your application to also react to any events received.
pub fn do_events_with<F>(&mut self, mut f: F)
where
F: FnMut(&Event),
{
self.keyboard.update();
self.mouse.update();
self.sdl_event_pump.pump_events();
for event in self.sdl_event_pump.poll_iter() {
self.keyboard.handle_event(&event);
self.mouse.handle_event(&event);
f(&event);
}
}
pub fn ticks(&self) -> u64 {
self.sdl_timer_subsystem.performance_counter()
}
pub fn tick_frequency(&self) -> u64 {
self.sdl_timer_subsystem.performance_frequency()
}
/// Returns the number of milliseconds elapsed since SDL was initialized.
pub fn millis(&self) -> u32 {
self.sdl_timer_subsystem.ticks()
}
/// Delays (blocks) for about the number of milliseconds specified.
pub fn delay(&mut self, millis: u32) {
self.sdl_timer_subsystem.delay(millis);
}
}

View file

@ -0,0 +1,12 @@
pub trait ReadFixedLengthByteArray {
fn read_bytes<const N: usize>(&mut self) -> Result<[u8; N], std::io::Error>;
}
impl<T: std::io::Read> ReadFixedLengthByteArray for T {
fn read_bytes<const N: usize>(&mut self) -> Result<[u8; N], std::io::Error> {
assert_ne!(N, 0);
let mut array = [0u8; N];
self.read_exact(&mut array)?;
Ok(array)
}
}

View file

@ -0,0 +1,38 @@
use std::any::Any;
use num_traits::Unsigned;
use rand::distributions::uniform::SampleUniform;
use rand::Rng;
pub mod bytes;
pub mod packbits;
pub fn rnd_value<N: SampleUniform + PartialOrd>(low: N, high: N) -> N {
rand::thread_rng().gen_range(low..=high)
}
/// Returns the absolute difference between two unsigned values. This is just here as a temporary
/// alternative to the `abs_diff` method currently provided by Rust but that is marked unstable.
#[inline]
pub fn abs_diff<N: Unsigned + PartialOrd>(a: N, b: N) -> N {
if a < b {
b - a
} else {
a - b
}
}
pub trait AsAny {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
impl<A: Any> AsAny for A {
fn as_any(&self) -> &dyn Any {
self as &dyn Any
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self as &mut dyn Any
}
}

View file

@ -0,0 +1,226 @@
use byteorder::{ReadBytesExt, WriteBytesExt};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PackBitsError {
#[error("PackBits I/O error")]
IOError(#[from] std::io::Error),
}
enum PackMode {
Dump,
Run,
}
pub fn pack_bits<S, D>(src: &mut S, dest: &mut D, src_length: usize) -> Result<(), PackBitsError>
where
S: ReadBytesExt,
D: WriteBytesExt,
{
const MIN_RUN: usize = 3;
const MAX_RUN: usize = 128;
const MAX_BUFFER: usize = 128;
if src_length == 0 {
return Ok(());
}
let mut bytes_left = src_length;
let mut buffer = [0u8; (MAX_RUN * 2)];
// read the first byte from the source, just to start things off before we get into the loop
buffer[0] = src.read_u8()?;
bytes_left -= 1;
let mut mode = PackMode::Dump;
let mut run_end = 1; // 1 because we already read the first byte into the buffer
let mut run_start = 0;
let mut previous_byte = buffer[0];
while bytes_left > 0 {
let byte = src.read_u8()?;
buffer[run_end] = byte;
run_end += 1;
match mode {
// "dump" mode. keep collecting any bytes and write them as-is until we detect the
// start of a run of identical bytes
PackMode::Dump => {
if run_end > MAX_BUFFER {
// we need to flush the temp buffer to the destination
dest.write_u8((run_end - 2) as u8)?;
dest.write_all(&buffer[0..run_end])?;
buffer[0] = byte;
run_end = 1;
run_start = 0;
} else if byte == previous_byte {
// detected the start of a run of identical bytes
if (run_end - run_start) >= MIN_RUN {
if run_start > 0 {
// we've found a run, flush the buffer we have currently so we can
// start tracking the length of this run
dest.write_u8((run_start - 1) as u8)?;
dest.write_all(&buffer[0..run_start])?;
}
mode = PackMode::Run;
} else if run_start == 0 {
mode = PackMode::Run;
}
} else {
run_start = run_end - 1;
}
}
// "run" mode. keep counting up bytes as long as they are identical to each other. when
// we find the end of a run, write out the run info and switch back to dump mode
PackMode::Run => {
// check for the end of a run of identical bytes
if (byte != previous_byte) || ((run_end - run_start) > MAX_RUN) {
// the identical byte run has ended, write it out to the destination
// (this is just two bytes, the count and the actual byte)
dest.write_i8(-((run_end - run_start - 2) as i8))?;
dest.write_u8(previous_byte)?;
// clear the temp buffer for our switch back to "dump" mode
buffer[0] = byte;
run_end = 1;
run_start = 0;
mode = PackMode::Dump;
}
}
};
previous_byte = byte;
bytes_left -= 1;
}
// the source bytes have all been read, but we still might have to flush the temp buffer
// out to the destination, or finish writing out a run of identical bytes that was at the very
// end of the source
match mode {
PackMode::Dump => {
dest.write_u8((run_end - 1) as u8)?;
dest.write_all(&buffer[0..run_end])?;
}
PackMode::Run => {
dest.write_i8(-((run_end - run_start - 1) as i8))?;
dest.write_u8(previous_byte)?;
}
};
Ok(())
}
pub fn unpack_bits<S, D>(
src: &mut S,
dest: &mut D,
unpacked_length: usize,
) -> Result<(), PackBitsError>
where
S: ReadBytesExt,
D: WriteBytesExt,
{
let mut buffer = [0u8; 128];
let mut bytes_written = 0;
while bytes_written < unpacked_length {
// read the next "code" byte that determines how to process the subsequent byte(s)
let byte = src.read_u8()?;
if byte > 128 {
// 129-255 = repeat the next byte 257-n times
let run_length = (257 - byte as u32) as usize;
// read the next byte from the source and repeat it the specified number of times
let byte = src.read_u8()?;
buffer.fill(byte);
dest.write_all(&buffer[0..run_length])?;
bytes_written += run_length;
} else if byte < 128 {
// 0-128 = copy next n-1 bytes from src to dest as-is
let run_length = (byte + 1) as usize;
src.read_exact(&mut buffer[0..run_length])?;
dest.write_all(&buffer[0..run_length])?;
bytes_written += run_length;
}
// note that byte == 128 is a no-op (does it even appear in any files ???)
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::*;
struct TestData<'a> {
packed: &'a [u8],
unpacked: &'a [u8],
}
static TEST_DATA: &[TestData] = &[
TestData {
packed: &[
0xfe, 0xaa, 0x02, 0x80, 0x00, 0x2a, 0xfd, 0xaa, 0x03, 0x80, 0x00, 0x2a, 0x22, 0xf7,
0xaa,
],
unpacked: &[
0xaa, 0xaa, 0xaa, 0x80, 0x00, 0x2a, 0xaa, 0xaa, 0xaa, 0xaa, 0x80, 0x00, 0x2a, 0x22,
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
],
},
TestData {
packed: &[0x00, 0xaa],
unpacked: &[0xaa],
},
TestData {
packed: &[0xf9, 0xaa],
unpacked: &[0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa],
},
TestData {
packed: &[0xf9, 0xaa, 0x00, 0xbb],
unpacked: &[0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xbb],
},
TestData {
packed: &[0x07, 0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8],
unpacked: &[0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8],
},
TestData {
packed: &[0x08, 0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa8],
unpacked: &[0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa8],
},
TestData {
packed: &[0x06, 0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xfe, 0xa8],
unpacked: &[0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa8, 0xa8],
},
];
#[test]
fn packs() -> Result<(), PackBitsError> {
for TestData { packed, unpacked } in TEST_DATA {
let mut src = Cursor::new(*unpacked);
let mut dest = vec![0u8; 0];
pack_bits(&mut src, &mut dest, unpacked.len())?;
assert_eq!(dest, *packed);
}
Ok(())
}
#[test]
fn unpacks() -> Result<(), PackBitsError> {
for TestData { packed, unpacked } in TEST_DATA {
let mut src = Cursor::new(*packed);
let mut dest = vec![0u8; 0];
unpack_bits(&mut src, &mut dest, unpacked.len())?;
assert_eq!(dest, *unpacked);
}
Ok(())
}
}

Binary file not shown.

Binary file not shown.

BIN
libretrogd/test-assets/test.pcx Executable file

Binary file not shown.

View file

@ -0,0 +1 @@
~~~~~~~~~~~~~}}~}~}~}~~~}}~}~}~}}}|}}}|}|||}}||}|}|}}|{|||{|{|||{|{|{|{|||{z{z{z{z{z{zz{{z{{z{z{z{zyzzzyzzzzzzyzyzyzyzyzyyyyxyyyxyyyxyyyxyyxyxyxyxyxyxyxyxxxxxxxxxxxxxxxxx

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.