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}