1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![no_std]

//! A safe and ergonomic image API for the playdate
//!
//! ## Feature flags
//!
//! * `playdate-sys-v02`: implementations of the input source traits for the types `&ffi::playdate_graphics` of the crate [`playdate-sys`](https://docs.rs/playdate-sys/0.2) (version `0.2`)
//! * `anyhow`: implementations of `From` error type for `anyhow::Error`

extern crate alloc;

/// Implementations of the the API traits
#[allow(missing_docs)]
pub mod impls {

    /// Implementations fpr [playdate-sys](https://docs.rs/playdate-sys) version `0.2`
    #[cfg(feature = "playdate-sys-v02")]
    pub mod playdate_sys_v02;
}

/// Ability to load an image from path
pub trait LoadImage {
    /// Type of image being loaded
    type Image;
    /// Error type representing failure to load an image
    type Error;

    /// Load an image from its path
    ///
    /// # Errors
    ///
    /// Returns [`Self::Error`] if the image cannot be loaded (i.e image not found)
    fn load_from_path(&self, path: impl AsRef<str>) -> Result<Self::Image, Self::Error>;
}

/// Split the images into `n` columns of the same size
pub trait ToColumns: Sized {
    fn to_columns(&self, n: usize) -> impl Iterator<Item = Self>;
}

/// Split the images into `n` rows of the same size
pub trait ToRows: Sized {
    fn to_rows(&self, n: usize) -> impl Iterator<Item = Self>;
}

/// Ability to draw an image on screen
pub trait DrawImage<I> {
    /// Draw the image on screen with the top-left corner at the given screen coordinates
    fn draw(&self, image: &I, top_left: impl Into<[i32; 2]>) {
        self.draw_with_flip(image, top_left, [false, false]);
    }

    /// Draw the image on screen with the top-left corner at the given screen coordinates
    fn draw_with_flip(&self, image: &I, top_left: impl Into<[i32; 2]>, flip: impl Into<[bool; 2]>);
}

/// Ability to draw an image from its origin point (instead of from the top-left)
///
/// This trait is automatically implemented for implementations of `DrawImage<I>` where `I: HasSize`
pub trait DrawFromOrigin<I> {
    /// Draw the image so that the `origin` is at `position`
    ///
    /// The origin is expressed in ratio of the size. So `[0., 0.]` is the top-left and `[1.,1.]` is the bottom right.
    fn draw_from_origin(
        &self,
        image: &I,
        position: impl Into<[i32; 2]>,
        origin: impl Into<[f32; 2]>,
    ) {
        self.draw_from_origin_with_flip(image, position, origin, [false, false]);
    }

    /// Draw the image so that the `origin` is at `position` with given `flip` argument.
    ///
    /// The origin is expressed in ratio of the size. So `[0., 0.]` is the top-left and `[1.,1.]` is the bottom right.
    ///
    /// If flipped, the image is fliped around its origin.
    fn draw_from_origin_with_flip(
        &self,
        image: &I,
        position: impl Into<[i32; 2]>,
        origin: impl Into<[f32; 2]>,
        flip: impl Into<[bool; 2]>,
    );
}

impl<T, I> DrawFromOrigin<I> for T
where
    T: DrawImage<I>,
    I: HasSize,
{
    fn draw_from_origin_with_flip(
        &self,
        image: &I,
        position: impl Into<[i32; 2]>,
        origin: impl Into<[f32; 2]>,
        flip: impl Into<[bool; 2]>,
    ) {
        let flip = flip.into();
        let position = position_from_origin(position.into(), origin.into(), image.size(), flip);
        self.draw_with_flip(image, position, flip);
    }
}

#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
fn position_from_origin(
    [x, y]: [i32; 2],
    [origin_x, origin_y]: [f32; 2],
    [w, h]: [i32; 2],
    [flip_x, flip_y]: [bool; 2],
) -> [i32; 2] {
    let x = if flip_x {
        x - (w as f32 * (1.0 - origin_x)) as i32
    } else {
        x - (w as f32 * origin_x) as i32
    };
    let y = if flip_y {
        y - (h as f32 * (1.0 - origin_y)) as i32
    } else {
        y - (h as f32 * origin_y) as i32
    };
    [x, y]
}

pub trait HasSize {
    fn size(&self) -> [i32; 2];
}

#[non_exhaustive]
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
pub enum DrawMode {
    /// Images are drawn exactly as they are (black pixels are drawn black and white pixels are drawn white)
    #[default]
    Copy,
    /// Any white portions of an image are drawn transparent (black pixels are drawn black and white pixels are drawn transparent)
    WhiteTransparent,
    /// Any black portions of an image are drawn transparent (black pixels are drawn transparent and white pixels are drawn white)
    BlackTransparent,
    /// All non-transparent pixels are drawn white (black pixels are drawn white and white pixels are drawn white)
    FillWhite,
    /// All non-transparent pixels are drawn black (black pixels are drawn black and white pixels are drawn black)
    FillBlack,
    /// Pixels are drawn inverted on white backgrounds, creating an effect where any white pixels in the original image will always be visible,
    /// regardless of the background color, and any black pixels will appear transparent (on a white background, black pixels are drawn white and white pixels are drawn black)
    XOR,
    /// Pixels are drawn inverted on black backgrounds, creating an effect where any black pixels in the original image will always be visible,
    /// regardless of the background color, and any white pixels will appear transparent (on a black background, black pixels are drawn white and white pixels are drawn black)
    NXOR,
    /// Pixels are drawn inverted (black pixels are drawn white and white pixels are drawn black)
    Inverted,
}

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

    #[rstest]
    #[case([0, 0], [0., 0.], [2, 3], [false, false], [0, 0])]
    #[case([0, 0], [1., 0.], [2, 3], [false, false], [-2, 0])]
    #[case([0, 0], [0., 1.], [2, 3], [false, false], [0, -3])]
    #[case([0, 0], [1., 1.], [2, 3], [false, false], [-2, -3])]
    #[case([0, 0], [2., 2.], [2, 3], [false, false], [-4, -6])]
    #[case([0, 0], [0., 0.], [2, 3], [true, false], [-2, 0])]
    #[case([0, 0], [1., 0.], [2, 3], [true, false], [0, 0])]
    #[case([0, 0], [0., 1.], [2, 3], [false, true], [0, 0])]
    #[case([0, 0], [1., 1.], [2, 3], [true, true], [0, 0])]
    #[case([0, 0], [2., 0.], [2, 3], [true, false], [2, 0])]
    #[case([0, 0], [0., 2.], [2, 3], [false, true], [0, 3])]
    fn test_position_from_origin(
        #[case] position: [i32; 2],
        #[case] origin: [f32; 2],
        #[case] size: [i32; 2],
        #[case] flip: [bool; 2],
        #[case] expected: [i32; 2],
    ) {
        let actual = position_from_origin(position, origin, size, flip);
        assert_eq!(actual, expected);
    }
}