wasm4fun-graphics 0.1.0

Graphics primitives and subsystems for WASM-4 fantasy console
Documentation
// Copyright Claudio Mattera 2022.
//
// Distributed under the MIT License or the Apache 2.0 License at your option.
// See the accompanying files License-MIT.txt and License-Apache-2.0.txt, or
// online at
// https://opensource.org/licenses/MIT
// https://opensource.org/licenses/Apache-2.0

use wasm4fun_core::{blit, blit_sub, BLIT_FLIP_X, BLIT_FLIP_Y, BLIT_ROTATE};

/// Rotation
#[derive(Copy, Clone)]
pub enum Rotation {
    /// Does not rotate
    Rotate0,

    /// Rotate by 90 degrees clockwise
    Rotate90,

    /// Rotate by 180 degrees clockwise
    Rotate180,

    /// Rotate by 270 degrees clockwise (or 90 degrees counter-clockwise)
    Rotate270,
}

impl Rotation {
    /// Add a rotation on top of the current one
    pub const fn add(&self, other: Rotation) -> Self {
        match (self, other) {
            (Self::Rotate0, other) => other,
            (self2, Self::Rotate0) => *self2,
            (Self::Rotate90, Self::Rotate90) => Self::Rotate180,
            (Self::Rotate90, Self::Rotate180) => Self::Rotate270,
            (Self::Rotate90, Self::Rotate270) => Self::Rotate0,
            (Self::Rotate180, Self::Rotate90) => Self::Rotate270,
            (Self::Rotate180, Self::Rotate180) => Self::Rotate0,
            (Self::Rotate180, Self::Rotate270) => Self::Rotate90,
            (Self::Rotate270, Self::Rotate90) => Self::Rotate0,
            (Self::Rotate270, Self::Rotate180) => Self::Rotate90,
            (Self::Rotate270, Self::Rotate270) => Self::Rotate180,
        }
    }
}

/// A view of a modified sprite
///
/// A sprite view can be used to apply transformations to a sprite before
/// drawing it to screen.
///
/// ```no_run
/// use wasm4fun_graphics::{Rotation, Sprite};
///
/// let sprite: Sprite = unimplemented!();
/// let clipped = sprite.clip(30, 10, 80, 20);
/// let rotated = sprite.rotate(Rotation::Rotate90);
/// let clipped_and_rotated = clipped.rotate(Rotation::Rotate90);
/// rotated.blit(0, 0);
/// clipped_and_rotated.blit(50, 50);
/// ```
///
/// # Order of Operations
///
/// When applying both flipping and rotation, flipping is performed first.
#[derive(Clone, Copy)]
pub struct SpriteViewImpl<'a> {
    sprite: &'a Sprite<'a>,
    rotation: Rotation,
    src_x: u32,
    src_y: u32,
    width: u32,
    height: u32,
    flip_horizontal: bool,
    flip_vertical: bool,
}

impl<'a> SpriteViewImpl<'a> {
    /// Create a view over a sprite
    pub const fn new(sprite: &'a Sprite) -> Self {
        Self {
            sprite,
            rotation: Rotation::Rotate0,
            src_x: 0,
            src_y: 0,
            width: sprite.width,
            height: sprite.height,
            flip_horizontal: false,
            flip_vertical: false,
        }
    }

    /// Get the sprite width
    pub const fn width(&self) -> u32 {
        self.width
    }

    /// Get the sprite height
    pub const fn height(&self) -> u32 {
        self.height
    }

    /// Apply a rotation to the sprite view
    pub const fn rotate(&self, rotation: Rotation) -> Self {
        let mut new = *self;
        new.rotation = new.rotation.add(rotation);
        new
    }

    /// Flip the sprite view horizontally
    pub const fn flip_horizontally(&self, flip_horizontally: bool) -> Self {
        let mut new = *self;
        new.flip_horizontal = flip_horizontally;
        new
    }

    /// Flip the sprite view vertically
    pub const fn flip_vertically(&self, flip_vertically: bool) -> Self {
        let mut new = *self;
        new.flip_vertical = flip_vertically;
        new
    }

    /// Clip the sprite view to a subregion
    pub const fn clip(&self, src_x: u32, src_y: u32, width: u32, height: u32) -> Self {
        let mut new = *self;
        new.src_x += src_x;
        new.src_y += src_y;
        new.width = width;
        new.height = height;
        new
    }

    /// Draw the sprite view to the screen
    ///
    /// The sprite is drawn at the point `x`, `y` after applying all the
    /// transformations in the view.
    pub fn blit(&self, x: i32, y: i32) {
        let flags = match (&self.rotation, &self.flip_horizontal, &self.flip_vertical) {
            (Rotation::Rotate0, false, false) => 0,
            (Rotation::Rotate90, false, false) => BLIT_FLIP_X | BLIT_FLIP_Y | BLIT_ROTATE,
            (Rotation::Rotate180, false, false) => BLIT_FLIP_X | BLIT_FLIP_Y,
            (Rotation::Rotate270, false, false) => BLIT_ROTATE,

            (Rotation::Rotate0, false, true) => BLIT_FLIP_Y,
            (Rotation::Rotate90, false, true) => BLIT_FLIP_X | BLIT_ROTATE,
            (Rotation::Rotate180, false, true) => BLIT_FLIP_X,
            (Rotation::Rotate270, false, true) => BLIT_FLIP_Y | BLIT_ROTATE,

            (Rotation::Rotate0, true, false) => BLIT_FLIP_X,
            (Rotation::Rotate90, true, false) => BLIT_FLIP_Y | BLIT_ROTATE,
            (Rotation::Rotate180, true, false) => BLIT_FLIP_Y,
            (Rotation::Rotate270, true, false) => BLIT_FLIP_X | BLIT_ROTATE,

            (Rotation::Rotate0, true, true) => BLIT_FLIP_X | BLIT_FLIP_Y,
            (Rotation::Rotate90, true, true) => BLIT_ROTATE,
            (Rotation::Rotate180, true, true) => 0,
            (Rotation::Rotate270, true, true) => BLIT_FLIP_X | BLIT_FLIP_Y | BLIT_ROTATE,
        };

        self.sprite.blit_sub_with_flags(
            x,
            y,
            self.width,
            self.height,
            self.src_x,
            self.src_y,
            flags,
        );
    }
}

impl<'a> From<&'a Sprite<'a>> for SpriteViewImpl<'a> {
    fn from(sprite: &'a Sprite) -> Self {
        Self::new(sprite)
    }
}

/// A 1-bps or 2-bps bitmap drawable on screen
///
/// Sprites
///
/// WASM-4 natively supports sprites with the functions [`wasm4fun_core::blit`]
/// and [`wasm4fun_core::blit_sub`].
/// Instead of passing sprite information as separate arguments, this structure
/// conveniently wraps them in a single place.
/// It also makes it easier to apply transformations using a [`SpriteViewImpl`].
///
/// They can be generated by PNG images with the command `w4 png2src --rust`.
///
/// ```no_run
/// use wasm4fun_graphics::Sprite;
///
/// const CAR_WIDTH: u32 = 16;
/// const CAR_HEIGHT: u32 = 16;
/// const CAR_FLAGS: u32 = 1; // BLIT_2BPP
/// const CAR: [u8; 256] = [0; 256];
///
/// let car = Sprite::new(CAR_WIDTH, CAR_HEIGHT, CAR_FLAGS, &CAR);
///
/// // Draw car to screen
/// car.blit(10, 40);
///
/// // Create a clipped view of the original sprite
/// let clip = car.clip(0, 0, 8, 8);
/// clip.blit(10, 40);
/// ```
#[derive(Clone, Debug)]
pub struct Sprite<'a> {
    width: u32,
    height: u32,
    flags: u32,
    data: &'a [u8],
}

impl<'a> Sprite<'a> {
    /// Create a new sprite
    pub const fn new(width: u32, height: u32, flags: u32, data: &'a [u8]) -> Self {
        Self {
            width,
            height,
            flags,
            data,
        }
    }

    /// Get the sprite width
    pub const fn width(&self) -> u32 {
        self.width
    }

    /// Get the sprite height
    pub const fn height(&self) -> u32 {
        self.height
    }

    /// Apply a rotation to the sprite
    pub const fn rotate(&self, rotation: Rotation) -> SpriteViewImpl {
        let view = SpriteViewImpl::new(self);
        SpriteViewImpl::rotate(&view, rotation)
    }

    /// Flip the sprite horizontally
    pub fn flip_horizontally(&self, flip_horizontally: bool) -> SpriteViewImpl {
        let view = SpriteViewImpl::new(self);
        SpriteViewImpl::flip_horizontally(&view, flip_horizontally)
    }

    /// Flip the sprite vertically
    pub const fn flip_vertically(&self, flip_vertically: bool) -> SpriteViewImpl {
        let view = SpriteViewImpl::new(self);
        SpriteViewImpl::flip_vertically(&view, flip_vertically)
    }

    /// Clip the sprite to a subregion
    pub const fn clip(&self, src_x: u32, src_y: u32, width: u32, height: u32) -> SpriteViewImpl {
        let view = SpriteViewImpl::new(self);
        SpriteViewImpl::clip(&view, src_x, src_y, width, height)
    }

    /// Draw the sprite to the screen
    ///
    /// The sprite is drawn at the point `x`, `y`.
    pub fn blit(&self, x: i32, y: i32) {
        blit(self.data, x, y, self.width, self.height, self.flags);
    }

    /// Draw a region of the sprite to the screen
    ///
    /// The region is drawn at the point `x`, `y`.
    /// The region starts at the point `src_x`, `src_y` and has width `width`
    /// and height `height`.
    #[allow(clippy::too_many_arguments)]
    fn blit_sub_with_flags(
        &self,
        x: i32,
        y: i32,
        width: u32,
        height: u32,
        src_x: u32,
        src_y: u32,
        flags: u32,
    ) {
        blit_sub(
            self.data,
            x,
            y,
            width,
            height,
            src_x,
            src_y,
            self.width,
            self.flags | flags,
        );
    }
}

/// A generic view over a sprite or a sprite transformation
///
/// Further transformations can be applied to a generic sprite view, or it can
/// be drawn to the screen.
///
/// This trait allows to define functions that accept both "original" sprites
/// (i.e. of type [`Sprite`]) or their transformations (e.g. of type
/// [`SpriteViewImpl`]).
pub trait SpriteView<'a> {
    /// Get the sprite width
    fn width(&self) -> u32;

    /// Get the sprite height
    fn height(&self) -> u32;

    /// Apply a rotation to the sprite view
    fn rotate(&'a self, rotation: Rotation) -> SpriteViewImpl<'a>;

    /// Flip the sprite view horizontally
    fn flip_horizontally(&'a self, flip_horizontally: bool) -> SpriteViewImpl<'a>;

    /// Flip the sprite view vertically
    fn flip_vertically(&'a self, flip_vertically: bool) -> SpriteViewImpl<'a>;

    /// Clip the sprite view to a subregion
    fn clip(&'a self, src_x: u32, src_y: u32, width: u32, height: u32) -> SpriteViewImpl<'a>;

    /// Draw the sprite view to the screen
    ///
    /// The sprite is drawn at the point `x`, `y` after applying all the
    /// transformations in the view.
    fn blit(&self, x: i32, y: i32);
}

impl<'a> SpriteView<'a> for SpriteViewImpl<'a> {
    fn width(&self) -> u32 {
        SpriteViewImpl::width(self)
    }

    fn height(&self) -> u32 {
        SpriteViewImpl::height(self)
    }

    fn rotate(&'a self, rotation: Rotation) -> SpriteViewImpl<'a> {
        SpriteViewImpl::rotate(self, rotation)
    }

    fn flip_horizontally(&'a self, flip_horizontally: bool) -> SpriteViewImpl<'a> {
        SpriteViewImpl::flip_horizontally(self, flip_horizontally)
    }

    fn flip_vertically(&'a self, flip_vertically: bool) -> SpriteViewImpl<'a> {
        SpriteViewImpl::flip_vertically(self, flip_vertically)
    }

    fn clip(&'a self, src_x: u32, src_y: u32, width: u32, height: u32) -> SpriteViewImpl<'a> {
        SpriteViewImpl::clip(self, src_x, src_y, width, height)
    }

    fn blit(&self, x: i32, y: i32) {
        SpriteViewImpl::blit(self, x, y)
    }
}

impl<'a> SpriteView<'a> for Sprite<'a> {
    fn width(&self) -> u32 {
        Sprite::width(self)
    }

    fn height(&self) -> u32 {
        Sprite::height(self)
    }

    fn rotate(&'a self, rotation: Rotation) -> SpriteViewImpl<'a> {
        Sprite::rotate(self, rotation)
    }

    fn flip_horizontally(&'a self, flip_horizontally: bool) -> SpriteViewImpl<'a> {
        Sprite::flip_horizontally(self, flip_horizontally)
    }

    fn flip_vertically(&'a self, flip_vertically: bool) -> SpriteViewImpl<'a> {
        Sprite::flip_vertically(self, flip_vertically)
    }

    fn clip(&'a self, src_x: u32, src_y: u32, width: u32, height: u32) -> SpriteViewImpl<'a> {
        Sprite::clip(self, src_x, src_y, width, height)
    }

    fn blit(&self, x: i32, y: i32) {
        Sprite::blit(self, x, y)
    }
}