crankit_image/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![no_std]
3
4//! A safe and ergonomic image API for the playdate
5//!
6//! ## Feature flags
7//!
8//! * `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`)
9//! * `anyhow`: implementations of `From` error type for `anyhow::Error`
10
11extern crate alloc;
12
13/// Implementations of the the API traits
14#[allow(missing_docs)]
15pub mod impls {
16
17    /// Implementations fpr [playdate-sys](https://docs.rs/playdate-sys) version `0.2`
18    #[cfg(feature = "playdate-sys-v02")]
19    pub mod playdate_sys_v02;
20}
21
22/// Ability to load an image from path
23pub trait LoadImage {
24    /// Type of image being loaded
25    type Image;
26    /// Error type representing failure to load an image
27    type Error;
28
29    /// Load an image from its path
30    ///
31    /// # Errors
32    ///
33    /// Returns [`Self::Error`] if the image cannot be loaded (i.e image not found)
34    fn load_from_path(&self, path: impl AsRef<str>) -> Result<Self::Image, Self::Error>;
35}
36
37/// Split the images into `n` columns of the same size
38pub trait ToColumns: Sized {
39    fn to_columns(&self, n: usize) -> impl Iterator<Item = Self>;
40}
41
42/// Split the images into `n` rows of the same size
43pub trait ToRows: Sized {
44    fn to_rows(&self, n: usize) -> impl Iterator<Item = Self>;
45}
46
47/// Ability to draw an image on screen
48pub trait DrawImage<I> {
49    /// Draw the image on screen with the top-left corner at the given screen coordinates
50    fn draw(&self, image: &I, top_left: impl Into<[i32; 2]>) {
51        self.draw_with_flip(image, top_left, [false, false]);
52    }
53
54    /// Draw the image on screen with the top-left corner at the given screen coordinates
55    fn draw_with_flip(&self, image: &I, top_left: impl Into<[i32; 2]>, flip: impl Into<[bool; 2]>);
56}
57
58/// Ability to draw an image from its origin point (instead of from the top-left)
59///
60/// This trait is automatically implemented for implementations of `DrawImage<I>` where `I: HasSize`
61pub trait DrawFromOrigin<I> {
62    /// Draw the image so that the `origin` is at `position`
63    ///
64    /// The origin is expressed in ratio of the size. So `[0., 0.]` is the top-left and `[1.,1.]` is the bottom right.
65    fn draw_from_origin(
66        &self,
67        image: &I,
68        position: impl Into<[i32; 2]>,
69        origin: impl Into<[f32; 2]>,
70    ) {
71        self.draw_from_origin_with_flip(image, position, origin, [false, false]);
72    }
73
74    /// Draw the image so that the `origin` is at `position` with given `flip` argument.
75    ///
76    /// The origin is expressed in ratio of the size. So `[0., 0.]` is the top-left and `[1.,1.]` is the bottom right.
77    ///
78    /// If flipped, the image is fliped around its origin.
79    fn draw_from_origin_with_flip(
80        &self,
81        image: &I,
82        position: impl Into<[i32; 2]>,
83        origin: impl Into<[f32; 2]>,
84        flip: impl Into<[bool; 2]>,
85    );
86}
87
88impl<T, I> DrawFromOrigin<I> for T
89where
90    T: DrawImage<I>,
91    I: HasSize,
92{
93    fn draw_from_origin_with_flip(
94        &self,
95        image: &I,
96        position: impl Into<[i32; 2]>,
97        origin: impl Into<[f32; 2]>,
98        flip: impl Into<[bool; 2]>,
99    ) {
100        let flip = flip.into();
101        let position = position_from_origin(position.into(), origin.into(), image.size(), flip);
102        self.draw_with_flip(image, position, flip);
103    }
104}
105
106#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
107fn position_from_origin(
108    [x, y]: [i32; 2],
109    [origin_x, origin_y]: [f32; 2],
110    [w, h]: [i32; 2],
111    [flip_x, flip_y]: [bool; 2],
112) -> [i32; 2] {
113    let x = if flip_x {
114        x - (w as f32 * (1.0 - origin_x)) as i32
115    } else {
116        x - (w as f32 * origin_x) as i32
117    };
118    let y = if flip_y {
119        y - (h as f32 * (1.0 - origin_y)) as i32
120    } else {
121        y - (h as f32 * origin_y) as i32
122    };
123    [x, y]
124}
125
126pub trait HasSize {
127    fn size(&self) -> [i32; 2];
128}
129
130#[non_exhaustive]
131#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
132pub enum DrawMode {
133    /// Images are drawn exactly as they are (black pixels are drawn black and white pixels are drawn white)
134    #[default]
135    Copy,
136    /// Any white portions of an image are drawn transparent (black pixels are drawn black and white pixels are drawn transparent)
137    WhiteTransparent,
138    /// Any black portions of an image are drawn transparent (black pixels are drawn transparent and white pixels are drawn white)
139    BlackTransparent,
140    /// All non-transparent pixels are drawn white (black pixels are drawn white and white pixels are drawn white)
141    FillWhite,
142    /// All non-transparent pixels are drawn black (black pixels are drawn black and white pixels are drawn black)
143    FillBlack,
144    /// Pixels are drawn inverted on white backgrounds, creating an effect where any white pixels in the original image will always be visible,
145    /// 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)
146    XOR,
147    /// Pixels are drawn inverted on black backgrounds, creating an effect where any black pixels in the original image will always be visible,
148    /// 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)
149    NXOR,
150    /// Pixels are drawn inverted (black pixels are drawn white and white pixels are drawn black)
151    Inverted,
152}
153
154#[cfg(test)]
155mod test {
156    use super::*;
157    use rstest::rstest;
158
159    #[rstest]
160    #[case([0, 0], [0., 0.], [2, 3], [false, false], [0, 0])]
161    #[case([0, 0], [1., 0.], [2, 3], [false, false], [-2, 0])]
162    #[case([0, 0], [0., 1.], [2, 3], [false, false], [0, -3])]
163    #[case([0, 0], [1., 1.], [2, 3], [false, false], [-2, -3])]
164    #[case([0, 0], [2., 2.], [2, 3], [false, false], [-4, -6])]
165    #[case([0, 0], [0., 0.], [2, 3], [true, false], [-2, 0])]
166    #[case([0, 0], [1., 0.], [2, 3], [true, false], [0, 0])]
167    #[case([0, 0], [0., 1.], [2, 3], [false, true], [0, 0])]
168    #[case([0, 0], [1., 1.], [2, 3], [true, true], [0, 0])]
169    #[case([0, 0], [2., 0.], [2, 3], [true, false], [2, 0])]
170    #[case([0, 0], [0., 2.], [2, 3], [false, true], [0, 3])]
171    fn test_position_from_origin(
172        #[case] position: [i32; 2],
173        #[case] origin: [f32; 2],
174        #[case] size: [i32; 2],
175        #[case] flip: [bool; 2],
176        #[case] expected: [i32; 2],
177    ) {
178        let actual = position_from_origin(position, origin, size, flip);
179        assert_eq!(actual, expected);
180    }
181}