Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add runty8-graphics crate #23

Draft
wants to merge 21 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -5,7 +5,11 @@ edition = "2021"
default-run = "runty8"
readme = "README.md"

[workspace]
members = ["runty8-graphics"]

[dependencies]
glium = "*"
itertools = "*"
rand = "*"
runty8-graphics = { path = "./runty8-graphics" }
1 change: 0 additions & 1 deletion examples/celeste/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::f32::consts::{FRAC_1_SQRT_2, PI};
use std::path::Path;

use rand::Rng;
use runty8::{App, Button, Pico8};
36 changes: 36 additions & 0 deletions examples/circles/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use runty8::{self, App, Button, Pico8};

fn main() {
runty8::run_app::<Circles>("examples/circles".to_owned()).unwrap();
}

struct Circles {}
impl Circles {
fn do_draw(&self, pico8: &mut Pico8) {
pico8.cls(0);

pico8.rect(0, 0, 127, 127, 7);
for i in 0..=7 {
pico8.circ(5 + i * 15, 64, i, 7);
pico8.circfill(5 + i * 15, 84, i, 7);
pico8.pset(5 + i * 15, 64, 9);
}
}
}

impl App for Circles {
fn init(pico8: &mut Pico8) -> Self {
let this = Self {};

this.do_draw(pico8);
this
}

fn update(&mut self, pico8: &mut Pico8) {
if pico8.btnp(Button::C) {
self.do_draw(pico8);
}
}

fn draw(&mut self, _: &mut Pico8) {}
}
53 changes: 53 additions & 0 deletions examples/shapes/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use rand::Rng;
use runty8::{self, App, Button, Pico8};

fn main() {
runty8::run_app::<Circles>("examples/circles".to_owned()).unwrap();
}

struct Circles {}
impl Circles {
fn do_draw(&self, pico8: &mut Pico8) {
pico8.cls(0);

pico8.rect(0, 0, 127, 127, 7);

fn rand(n: i32) -> i32 {
rand::thread_rng().gen_range(0..n)
}

for _ in 0..=7 {
pico8.rectfill(
rand(128),
rand(128),
rand(128),
rand(128),
rand::thread_rng().gen_range(9..16),
);
pico8.rect(
rand(128),
rand(128),
rand(128),
rand(128),
rand::thread_rng().gen_range(1..8),
);
}
}
}

impl App for Circles {
fn init(pico8: &mut Pico8) -> Self {
let this = Self {};

this.do_draw(pico8);
this
}

fn update(&mut self, pico8: &mut Pico8) {
if pico8.btnp(Button::C) {
self.do_draw(pico8);
}
}

fn draw(&mut self, _: &mut Pico8) {}
}
56 changes: 56 additions & 0 deletions examples/sprites/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use runty8::{self, App, Button, Pico8};

fn main() {
runty8::run_app::<Sprites>("examples/sprites".to_owned()).unwrap();
}

struct Sprites {}
impl Sprites {
fn do_draw(&self, pico8: &mut Pico8) {
pico8.cls(0);

for i in 0..(128 / 8) {
pico8.line(0, i * 8, 127, i * 8, if i % 2 == 0 { 7 } else { 5 });
pico8.line(i * 8, 0, i * 8, 127, if i % 2 == 0 { 6 } else { 13 });
}

pico8.spr_(32, 32, 32, 0.5, 0.5, false, false);
pico8.spr_(32, 40, 40, 1.0, 1.0, false, false);

// TEST FLIPS
const RIGHT_ARROW: usize = 1;
const UP_ARROW: usize = 2;
const UP_RIGHT_ARROW: usize = 3;

pico8.spr_(RIGHT_ARROW, 48, 48, 1.0, 1.0, false, false);
pico8.spr_(RIGHT_ARROW, 48, 64, 1.0, 1.0, true, false);
pico8.spr_(UP_ARROW, 48, 80, 1.0, 1.0, false, false);
pico8.spr_(UP_ARROW, 48, 96, 1.0, 1.0, false, true);

pico8.spr_(UP_RIGHT_ARROW, 80, 48, 1.0, 1.0, false, false);
pico8.spr_(UP_RIGHT_ARROW, 80, 64, 1.0, 1.0, true, false);
pico8.spr_(UP_RIGHT_ARROW, 80, 80, 1.0, 1.0, false, true);
pico8.spr_(UP_RIGHT_ARROW, 80, 96, 1.0, 1.0, true, true);

// TEST BIG SPRITES
pico8.print("BIG SPRITE", 16, 8, 7);
pico8.spr_(7, 16, 16, 2.0, 2.0, false, false);
}
}

impl App for Sprites {
fn init(pico8: &mut Pico8) -> Self {
let this = Self {};

this.do_draw(pico8);
this
}

fn update(&mut self, pico8: &mut Pico8) {
if pico8.btnp(Button::C) {
self.do_draw(pico8);
}
}

fn draw(&mut self, _: &mut Pico8) {}
}
4 changes: 4 additions & 0 deletions examples/sprites/map.ppm

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions examples/sprites/map.txt

Large diffs are not rendered by default.

256 changes: 256 additions & 0 deletions examples/sprites/sprite_flags.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
4 changes: 4 additions & 0 deletions examples/sprites/sprite_sheet.ppm

Large diffs are not rendered by default.

128 changes: 128 additions & 0 deletions examples/sprites/sprite_sheet.txt

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions runty8-graphics/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "runty8-graphics"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
117 changes: 117 additions & 0 deletions runty8-graphics/src/circle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use crate::line::horizontal_line;
use crate::Graphics;

/// Iterator over points in the circumference of a circle.
pub fn circle(cx: i32, cy: i32, r: u32) -> impl Graphics {
northwest_octant(r)
.flat_map(|(x, y)| {
[
(x, y),
(x, -y),
(-x, y),
(-x, -y),
(y, x),
(y, -x),
(-y, x),
(-y, -x),
]
.into_iter()
})
.map(move |(x, y)| (cx + x, cy + y))
}

/// Iterator over points of a circle (both circumference and interior).
pub fn filled_circle(cx: i32, cy: i32, r: u32) -> impl Graphics {
northwest_octant(r)
.flat_map(|(x, y)| [(x, y), (y, x)]) // Generate a quadrant, instead of an octant
.flat_map(|(x, y)| horizontal_line(-x, x, y).chain(horizontal_line(-x, x, -y)))
.map(move |(x, y)| (cx + x, cy + y))
}

struct NorthwestOctant {
x: i32,
y: i32,
d: i32,
}

impl NorthwestOctant {
fn new(r: i32) -> Self {
Self {
x: 0,
y: r,
d: 1 - r,
}
}
}

// Taken from Pemsa, a C++ implementation of pico8.
// This looks similar to Bresenham's circle drawing algorithm.
//
// See: https://github.com/egordorichev/pemsa/blob/master/src/pemsa/graphics/pemsa_graphics_api.cpp#L393
impl Iterator for NorthwestOctant {
type Item = (i32, i32);

fn next(&mut self) -> Option<Self::Item> {
if self.y < self.x {
return None;
}
let ret_value = (self.x, self.y);

self.x += 1;
if self.d < 0 {
self.d += 2 * self.x + 1;
} else {
self.y -= 1;
self.d += 2 * (self.x - self.y) + 1;
}

Some(ret_value)
}
}

fn northwest_octant(r: u32) -> impl Graphics {
NorthwestOctant::new(
r.try_into()
.expect(&format!("Couldn't convert radius {} to i32", r)),
)
}

#[cfg(test)]
mod tests {
use super::*;

// Examples compiled from looking at Pico8's circ output.
#[test]
fn radius_0_northwest_octant() {
assert_eq!(northwest_octant(0).collect::<Vec<_>>(), vec![(0, 0)]);
}

#[test]
fn radius_1_northwest_octant() {
assert_eq!(northwest_octant(1).collect::<Vec<_>>(), vec![(0, 1)]);
}

#[test]
fn radius_2_northwest_octant() {
assert_eq!(
northwest_octant(2).collect::<Vec<_>>(),
vec![(0, 2), (1, 2)]
);
}

#[test]
fn radius_3_northwest_octant() {
assert_eq!(
northwest_octant(3).collect::<Vec<_>>(),
vec![(0, 3), (1, 3), (2, 2)]
);
}

#[test]
fn radius_4_northwest_octant() {
assert_eq!(
northwest_octant(4).collect::<Vec<_>>(),
vec![(0, 4), (1, 4), (2, 3), (3, 3)]
);
}
}
48 changes: 48 additions & 0 deletions runty8-graphics/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
mod circle;
mod line;
mod rectangle;

pub use circle::{circle, filled_circle};
pub use line::line;
pub use rectangle::Rectangle;

/// An alias for `Iterator<Item = (i32, i32)>`.
pub trait Graphics: Iterator<Item = (i32, i32)> {}

impl<Type: Iterator<Item = (i32, i32)>> Graphics for Type {}

/// Conditionally reversed iterator.
pub struct MaybeReverse<I> {
iter: I,
reverse: bool,
}

/// Extension trait to conditionally reverse iterators.
pub trait ReverseIf
where
Self: Sized,
{
fn reverse_if(self, reverse: bool) -> MaybeReverse<Self>;
}

impl<T: DoubleEndedIterator> ReverseIf for T {
/// Conditionally reverse an iterator.
fn reverse_if(self, reverse: bool) -> MaybeReverse<Self> {
MaybeReverse {
iter: self,
reverse,
}
}
}

impl<I: DoubleEndedIterator> Iterator for MaybeReverse<I> {
type Item = I::Item;

fn next(&mut self) -> Option<Self::Item> {
if self.reverse {
self.iter.next_back()
} else {
self.iter.next()
}
}
}
45 changes: 43 additions & 2 deletions src/draw.rs → runty8-graphics/src/line.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
pub fn line(x0: i32, y0: i32, x1: i32, y1: i32) -> LineIter {
use crate::Graphics;

/// Iterator over the points of a line (includes endpoints).
pub fn line(x0: i32, y0: i32, x1: i32, y1: i32) -> impl Graphics {
LineIter::new(x0, y0, x1, y1)
}

pub struct LineIter {
struct LineIter {
x0: i32,
y0: i32,
x1: i32,
@@ -68,3 +71,41 @@ impl Iterator for LineIter {
Some(ret)
}
}

pub fn horizontal_line(x0: i32, x1: i32, y: i32) -> impl DoubleEndedIterator<Item = (i32, i32)> {
(x0..=x1).map(move |x| (x, y))
}

pub fn vertical_line(x: i32, y0: i32, y1: i32) -> impl Graphics {
(y0..=y1).map(move |y| (x, y))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn line_is_horizontal() {
assert_eq!(
line(20, 0, 30, 0).collect::<Vec<_>>(),
horizontal_line(20, 30, 0).collect::<Vec<_>>()
);
}

#[test]
fn line_is_vertical() {
assert_eq!(
line(0, 20, 0, 30).collect::<Vec<_>>(),
vertical_line(0, 20, 30).collect::<Vec<_>>()
);
}

#[test]
fn line_is_diagonal() {
assert_eq!(
line(2, 2, 5, 5).collect::<Vec<_>>(),
vec![(2, 2), (3, 3), (4, 4), (5, 5)]
)
}
// TODO: Test cases with inverted params (x1 > x0, etc)
}
136 changes: 136 additions & 0 deletions runty8-graphics/src/rectangle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use crate::line::{horizontal_line, vertical_line};
use crate::Graphics;

/// Iterate over rectangle surface/interior.
#[derive(Clone, Copy, Debug)]
pub struct Rectangle {
x: i32,
y: i32,
width: u32,
height: u32,
}

impl Rectangle {
pub fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
Self {
x,
y,
width,
height,
}
}

/// Iterator over the points of the surface of a rectangle.
/// Iteration order is unspecified.
pub fn surface(self) -> impl Graphics {
let width: i32 = self
.width
.try_into()
.expect(&format!("Couldn't convert width {} to i32", self.width));

let height: i32 = self
.height
.try_into()
.expect(&format!("Couldn't convert height {} to i32", self.height));

let top_bottom = [
horizontal_line(self.x, self.x + width - 1, self.y),
horizontal_line(self.x, self.x + width - 1, self.y + height - 1),
]
.into_iter()
.flatten();

let left_right = [
vertical_line(self.x, self.y + 1, self.y + height - 2),
vertical_line(self.x + width - 1, self.y + 1, self.y + height - 2),
]
.into_iter()
.flatten();

top_bottom.chain(left_right)
}

/// Iterator over the horizontal lines of the rectangle.
/// Includes surface and interior.
pub fn horizontal_lines(
self,
) -> impl DoubleEndedIterator<Item = impl DoubleEndedIterator<Item = (i32, i32)>> {
let x0 = self.x;
let x1 = self.x + self.width as i32 - 1;

(self.y..self.y + self.height as i32).map(move |y| horizontal_line(x0, x1, y))
}
}

/// Iterator over the points of a rectangle (interior and border).
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn empty_rectangle() {
assert_eq!(Rectangle::new(34, 22, 0, 0).surface().count(), 0);
}

#[test]
fn small_rectangle_count() {
assert_eq!(Rectangle::new(8, 4, 5, 3).surface().count(), 12);
}

#[test]
fn small_rectangle() {
assert_eq!(
Rectangle::new(8, 4, 5, 3).surface().collect::<Vec<_>>(),
vec![
(8, 4), // Top
(9, 4),
(10, 4),
(11, 4),
(12, 4),
(8, 6), // Bottom
(9, 6),
(10, 6),
(11, 6),
(12, 6),
(8, 5), // Left
(12, 5), // Rigth
]
);
}

#[test]
fn empty_filled_rectangle() {
assert_eq!(
Rectangle::new(34, 22, 0, 0)
.horizontal_lines()
.flatten()
.count(),
0
);
}

#[test]
fn small_filled_rectangle_count() {
assert_eq!(
Rectangle::new(8, 4, 2, 3)
.horizontal_lines()
.flatten()
.count(),
6
);
}

#[test]
fn small_filled_rectangle() {
let mut filled_rectangle = Rectangle::new(8, 4, 2, 3).horizontal_lines().flatten();

assert_eq!(filled_rectangle.next(), Some((8, 4)));
assert_eq!(filled_rectangle.next(), Some((9, 4)));
assert_eq!(filled_rectangle.next(), Some((8, 5)));
assert_eq!(filled_rectangle.next(), Some((9, 5)));
assert_eq!(filled_rectangle.next(), Some((8, 6)));
assert_eq!(filled_rectangle.next(), Some((9, 6)));
assert_eq!(filled_rectangle.next(), None);
}
}
3 changes: 1 addition & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -8,13 +8,12 @@ pub mod ui;

pub use app::App;
pub use app::ElmApp;
pub use pico8::{rnd, sin, Pico8};
pub use pico8::{mid, rnd, sin, Pico8};
pub use runtime::draw_data::colors;
pub use runtime::sprite_sheet::Color;
pub use runtime::state::Button;

mod controller;
mod draw;
mod editor;
mod font;
mod graphics;
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use runty8::{App, Pico8};

fn main() {
runty8::run_app::<EmptyApp>("src/editor_assets".to_owned()).unwrap()
runty8::run_app::<EmptyApp>("src/editor_assets".to_owned()).unwrap();
}

struct EmptyApp;
76 changes: 65 additions & 11 deletions src/pico8.rs
Original file line number Diff line number Diff line change
@@ -116,15 +116,21 @@ impl Pico8 {
}

pub fn spr(&mut self, spr: usize, x: i32, y: i32) {
let spr = self.resources.sprite_sheet.get_sprite(spr);

self.draw_data.spr(spr, x, y);
self.spr_(spr, x, y, 1.0, 1.0, false, false);
}

pub fn spr_(&mut self, spr: usize, x: i32, y: i32, w: f32, h: f32, flip_x: bool, flip_y: bool) {
let spr = self.resources.sprite_sheet.get_sprite(spr);

self.draw_data.spr_(spr, x, y, w, h, flip_x, flip_y);
spr_from(
&mut self.draw_data,
&self.resources.sprite_sheet,
spr,
x,
y,
w,
h,
flip_x,
flip_y,
);
}

// TODO: Test
@@ -183,14 +189,46 @@ impl Pico8 {
}
}

fn spr_from(
draw_data: &mut DrawData,
sprite_sheet: &SpriteSheet,
spr: usize,
x: i32,
y: i32,
w: f32,
h: f32,
flip_x: bool,
flip_y: bool,
) {
draw_data.spr_(spr, sprite_sheet, x, y, w, h, flip_x, flip_y);
}

// Utility pub(crate) methods
impl Pico8 {
// TODO: Remove this `allow` when we use it in the editor.
#[allow(dead_code)]
pub(crate) fn spr_from(&mut self, sprite_sheet: &SpriteSheet, spr: usize, x: i32, y: i32) {
let spr = sprite_sheet.get_sprite(spr);

self.draw_data.spr(spr, x, y);
pub(crate) fn spr_from(
&mut self,
sprite_sheet: &SpriteSheet,
spr: usize,
x: i32,
y: i32,
w: f32,
h: f32,
flip_x: bool,
flip_y: bool,
) {
spr_from(
&mut self.draw_data,
sprite_sheet,
spr,
x,
y,
w,
h,
flip_x,
flip_y,
);
}

pub(crate) fn raw_spr(&mut self, sprite: &Sprite, x: i32, y: i32) {
@@ -215,9 +253,17 @@ pub fn rnd(limit: f32) -> f32 {
rand::thread_rng().gen_range(0.0..limit)
}

/// <https://pico-8.fandom.com/wiki/Mid>
pub fn mid(min: f32, val: f32, max: f32) -> f32 {
let mut arr = [min, val, max];
arr.sort_by(|a, b| a.partial_cmp(b).unwrap());

arr[1]
}

#[cfg(test)]
mod tests {
use super::{rnd, sin};
use super::{mid, rnd, sin};

macro_rules! assert_delta {
($x:expr, $y:expr, $d:expr) => {
@@ -248,4 +294,12 @@ mod tests {
assert!(0.0 < random_value && random_value < 50.0);
}
}

#[test]
fn mid_works() {
assert_delta!(mid(8.0, 2.0, 4.0), 4.0, 0.00001);
assert_delta!(mid(-3.5, -3.4, -3.6), -3.5, 0.00001);
assert_delta!(mid(6.0, 6.0, 8.0), 6.0, 0.00001);
assert_delta!(mid(0.0, 2.0, 1.0), 1.0, 0.00001);
}
}
184 changes: 88 additions & 96 deletions src/runtime/draw_data.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::font;
use crate::runtime::flags::Flags;
use crate::runtime::map::Map;
use crate::runtime::sprite_sheet::SpriteSheet;
use crate::{draw, font};
use runty8_graphics::ReverseIf;

use super::sprite_sheet::{Color, Sprite};

@@ -112,36 +113,6 @@ impl DrawData {
}
}
}

pub(crate) fn quarter_bresenham(
&mut self,
cx: i32,
cy: i32,
radius: i32,
color: Color,
plot: fn(&mut Self, i32, i32, i32, i32, Color),
) {
let mut x = radius;
let mut y = 0;
let mut error = 1 - radius;

while y <= x {
plot(self, cx, cy, x, y, color);

if error < 0 {
error += 2 * y + 3;
} else {
if x != y {
plot(self, cx, cy, y, x, color);
}

x -= 1;
error += 2 * (y - x) + 3;
}

y += 1;
}
}
}

// Functions which more directly implement pico8 functionality
@@ -163,22 +134,30 @@ impl DrawData {
}

pub(crate) fn rectfill(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
for y in y0..=y1 {
self.line(x0, y, x1, y, color);
}
let (x0, x1) = min_max(x0, x1);
let (y0, y1) = min_max(y0, y1);
let width = (x1 - x0 + 1) as u32;
let height = (y1 - y0 + 1) as u32;

runty8_graphics::Rectangle::new(x0, y0, width, height)
.horizontal_lines()
.flatten()
.for_each(|(x, y)| self.pset(x, y, color))
}

pub(crate) fn rect(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
self.line(x0, y0, x1, y0, color);
self.line(x0, y0, x0, y1, color);
self.line(x0, y1, x1, y1, color);
self.line(x1, y0, x1, y1, color);
let (x0, x1) = min_max(x0, x1);
let (y0, y1) = min_max(y0, y1);
let width = (x1 - x0 + 1) as u32;
let height = (y1 - y0 + 1) as u32;

runty8_graphics::Rectangle::new(x0, y0, width, height)
.surface()
.for_each(|(x, y)| self.pset(x, y, color))
}

pub(crate) fn line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
for (x, y) in draw::line(x0, y0, x1, y1) {
self.pset(x, y, color);
}
runty8_graphics::line(x0, y0, x1, y1).for_each(|(x, y)| self.pset(x, y, color));
}

pub(crate) fn reset_pal(&mut self) {
@@ -191,45 +170,14 @@ impl DrawData {
self.transparent_color = transparent_color
}

// Taken from Pemsa, a C++ implementation of pico8.
// This looks similar to Bresenham's circle drawing algorithm.
//
// See: https://github.com/egordorichev/pemsa/blob/master/src/pemsa/graphics/pemsa_graphics_api.cpp#L393
pub(crate) fn circ(&mut self, cx: i32, cy: i32, radius: i32, color: Color) {
fn plot(this: &mut DrawData, cx: i32, cy: i32, x: i32, y: i32, c: u8) {
let points = [
(x, y),
(-x, y),
(x, -y),
(-x, -y),
(y, x),
(-y, x),
(y, -x),
(-y, -x),
];

for (x, y) in points {
this.pset(cx + x, cy + y, c);
}
}

self.quarter_bresenham(cx, cy, radius, color, plot);
runty8_graphics::circle(cx, cy, radius as u32)
.for_each(move |(x, y)| self.pset(x, y, color))
}

// Taken from Pemsa, a C++ implementation of pico8.
// This looks similar to Bresenham's circle drawing algorithm.
//
// See: https://github.com/egordorichev/pemsa/blob/master/src/pemsa/graphics/pemsa_graphics_api.cpp#L393
pub(crate) fn circfill(&mut self, cx: i32, cy: i32, radius: i32, color: Color) {
fn plot(this: &mut DrawData, cx: i32, cy: i32, x: i32, y: i32, c: u8) {
this.line(cx - x, cy + y, cx + x, cy + y, c);

if y != 0 {
this.line(cx - x, cy - y, cx + x, cy - y, c);
}
}

self.quarter_bresenham(cx, cy, radius, color, plot);
runty8_graphics::filled_circle(cx, cy, radius as u32)
.for_each(move |(x, y)| self.pset(x, y, color));
}

pub(crate) fn print(&mut self, str: &str, x: i32, y: i32, color: Color) {
@@ -240,37 +188,78 @@ impl DrawData {
}
}

fn draw_partial_spr(
&mut self,
spr: &Sprite,
x: i32,
y: i32,
w: f32,
h: f32,
flip_x: bool,
flip_y: bool,
) {
let w = crate::mid(0.0, w, 1.0);
let h = crate::mid(0.0, h, 1.0);
let sprite_buffer = &spr.sprite;

let iter = runty8_graphics::Rectangle::new(
0,
0,
(8.0 * w).round() as u32,
(8.0 * h).round() as u32,
)
.horizontal_lines()
.reverse_if(flip_y);

iter.enumerate().for_each(|(j, line)| {
let line = line.reverse_if(flip_x);

line.enumerate().for_each(|(i, local_coords)| {
let (local_x, local_y) = local_coords;
let (world_x, world_y) = (x + local_x, y + local_y);
let (screen_x, screen_y) = self.apply_camera(world_x, world_y);

if let Some(index) = self.index(screen_x, screen_y) {
self.set_pixel_with_transparency(index, sprite_buffer[(i + j * 8) as usize])
}
});
});
}

#[allow(clippy::too_many_arguments)]
// TODO: Implement w and h params functionality
pub fn spr_(
&mut self,
sprite: &Sprite,
sprite: usize,
sprite_sheet: &SpriteSheet,
x: i32,
y: i32,
_w: f32,
_h: f32,
w: f32,
h: f32,
flip_x: bool,
flip_y: bool,
) {
let buffer = &sprite.sprite;

for i in 0..8 {
for j in 0..8 {
let world_x = if flip_x { x + 7 - i } else { x + i };
let world_y = if flip_y { y + 7 - j } else { y + j };

let (x, y) = self.apply_camera(world_x, world_y);
if let Some(index) = self.index(x, y) {
self.set_pixel_with_transparency(index, buffer[(i + j * 8) as usize])
}
let w_spr = w.ceil() as usize;
let h_spr = h.ceil() as usize;
let (spr_x, spr_y) = SpriteSheet::coords_from_sprite_index(sprite);

for (w_off, h_off) in itertools::iproduct!((0..w_spr), (0..h_spr)) {
if let Some(sprite_index) =
SpriteSheet::sprite_index_from_coords(spr_x + w_off, spr_y + h_off)
{
let spr = sprite_sheet.get_sprite(sprite_index);
self.draw_partial_spr(
spr,
x + 8 * w_off as i32,
y + 8 * h_off as i32,
w,
h,
flip_x,
flip_y,
);
}
}
}

pub(crate) fn spr(&mut self, sprite: &Sprite, x: i32, y: i32) {
self.spr_(sprite, x, y, 1.0, 1.0, false, false)
}

pub(crate) fn cls_color(&mut self, color: Color) {
self.rectfill(0, 0, 127, 127, color);
}
@@ -303,7 +292,7 @@ impl DrawData {
let y = screen_y + 8 * i_y as i32;

let spr = sprite_sheet.get_sprite(spr as usize);
self.spr(spr, x, y);
self.draw_partial_spr(spr, x, y, 1.0, 1.0, false, false);
}
}
}
@@ -316,6 +305,9 @@ impl Default for DrawData {
}
}

fn min_max(a: i32, b: i32) -> (i32, i32) {
(a.min(b), a.max(b))
}
// Pico8 api

fn get_color(index: Color) -> u32 {
49 changes: 49 additions & 0 deletions src/runtime/sprite_sheet.rs
Original file line number Diff line number Diff line change
@@ -50,6 +50,22 @@ impl SpriteSheet {
self.sprite_sheet[Self::to_linear_index(x, y)] = c;
}

/// Converts (x, y) sprite coordinates into the sprite index if within bounds.
/// The sprite index is what's needed for functions like `Pico8::spr`.
pub(crate) fn coords_from_sprite_index(index: usize) -> (usize, usize) {
(index % Self::SPRITES_PER_ROW, index / Self::SPRITES_PER_ROW)
}

/// Converts (x, y) sprite coordinates into the sprite index if within bounds.
/// The sprite index is what's needed for functions like `Pico8::spr`.
pub(crate) fn sprite_index_from_coords(x: usize, y: usize) -> Option<usize> {
if x >= 16 || y >= 16 {
None
} else {
Some(x + y * 16)
}
}

pub fn to_linear_index(x: usize, y: usize) -> usize {
let x_part = 64 * (x / 8) + x % 8;
let y_part = 16 * 64 * (y / 8) + 8 * (y % 8);
@@ -197,6 +213,39 @@ impl Sprite {
mod tests {
use super::*;

#[test]
fn sprite_index_from_coords_works() {
assert_eq!(SpriteSheet::sprite_index_from_coords(0, 0), Some(0));
assert_eq!(SpriteSheet::sprite_index_from_coords(0, 1), Some(16));
assert_eq!(SpriteSheet::sprite_index_from_coords(8, 0), Some(8));
assert_eq!(SpriteSheet::sprite_index_from_coords(8, 1), Some(24));
assert_eq!(SpriteSheet::sprite_index_from_coords(8, 7), Some(120));
assert_eq!(SpriteSheet::sprite_index_from_coords(16, 9), None);
assert_eq!(SpriteSheet::sprite_index_from_coords(1, 16), None);
}

#[test]
fn coords_from_sprite_index_works() {
assert_eq!(SpriteSheet::coords_from_sprite_index(0), (0, 0));
assert_eq!(SpriteSheet::coords_from_sprite_index(1), (1, 0));
assert_eq!(SpriteSheet::coords_from_sprite_index(15), (15, 0));
assert_eq!(SpriteSheet::coords_from_sprite_index(16), (0, 1));
assert_eq!(SpriteSheet::coords_from_sprite_index(17), (1, 1));
assert_eq!(SpriteSheet::coords_from_sprite_index(109), (13, 6));
assert_eq!(SpriteSheet::coords_from_sprite_index(117), (5, 7));
assert_eq!(SpriteSheet::coords_from_sprite_index(255), (15, 15));
}

#[test]
fn sprite_index_coords_roundtrip() {
for spr in 0..=255 {
let (x, y) = SpriteSheet::coords_from_sprite_index(spr);
let roundtrip_spr = SpriteSheet::sprite_index_from_coords(x, y);

assert_eq!(Some(spr), roundtrip_spr);
}
}

#[test]
fn indexing_works() {
assert_eq!(SpriteSheet::to_linear_index(7, 0), 7);