baba/gfx/
texture.rs

1use std::path::Path;
2use std::rc::Rc;
3
4use glam::{vec2, Vec2};
5use image::io::Reader;
6use sdl2::pixels::PixelFormatEnum;
7use thiserror::Error;
8
9use crate::math::Rect;
10use crate::SdlError;
11
12use super::{with_canvas, Canvas, Drawable, Transform, Vertex};
13
14/// Texture load error.
15#[derive(Debug, Error)]
16pub enum LoadError {
17    /// This texture couldn't be opened.
18    #[error(transparent)]
19    Io(#[from] std::io::Error),
20    /// The image data couldn't be decoded.
21    #[error(transparent)]
22    Decode(#[from] image::ImageError),
23    /// The renderer failed to create a texture, for unknown reasons.
24    #[error(transparent)]
25    Renderer(#[from] SdlError),
26}
27
28/// Texture origin.
29///
30/// This is usually on the top-left, as are the coordinates onscreen.
31#[derive(Debug, Default, Clone, Copy)]
32pub struct Origin(pub Vec2);
33
34impl Origin {
35    /// Top left, [0, 0]
36    pub const TOP_LEFT: Self = Self(Vec2::ZERO);
37    /// Top right, [1, 0]
38    pub const TOP_RIGHT: Self = Self(Vec2::X);
39    /// Bottom left, [0, 1]
40    pub const BOTTOM_LEFT: Self = Self(Vec2::Y);
41    /// Bottom right, [1, 1]
42    pub const BOTTOM_RIGHT: Self = Self(Vec2::ONE);
43    /// Center, [0.5, 0.5]
44    pub const CENTER: Self = Self(Vec2::splat(0.5));
45}
46
47/// Texture scaling.
48#[derive(Default, Clone, Copy)]
49#[repr(u32)]
50pub enum ScaleMode {
51    /// Nearest-neighbor scaling. This is best for pixel art.
52    #[default]
53    Nearest = 0,
54    /// Linear, "fuzzy" scaling.
55    Linear = 1,
56    // No SDL2 backend uses this, seemingly
57    // Anisotropic = 2,
58}
59
60/// Texture load options.
61#[derive(Default)]
62pub struct Options {
63    // blend: BlendMode,
64    /// How this texture is scaled. The default depends on engine [settings][crate::Settings].
65    pub scaling: Option<ScaleMode>,
66    /// The origin point for this texture. Defaults to top left.
67    pub origin: Origin,
68}
69
70impl From<ScaleMode> for Options {
71    fn from(scaling: ScaleMode) -> Self {
72        Self {
73            scaling: Some(scaling),
74            ..Default::default()
75        }
76    }
77}
78
79impl From<Origin> for Options {
80    fn from(origin: Origin) -> Self {
81        Self {
82            origin,
83            ..Default::default()
84        }
85    }
86}
87
88pub struct TextureData {
89    ptr: *mut sdl2_sys::SDL_Texture,
90    w: u32,
91    h: u32,
92}
93
94impl TextureData {
95    const fn empty() -> Self {
96        Self {
97            ptr: std::ptr::null_mut(),
98            w: 0,
99            h: 0,
100        }
101    }
102
103    fn from_image(img: image::DynamicImage, opts: &Options) -> Result<Self, LoadError> {
104        let w = img.width();
105        let h = img.height();
106        let (format, mut data) = if img.color().has_alpha() {
107            (PixelFormatEnum::RGBA32, img.into_rgba8().into_raw())
108        } else {
109            (PixelFormatEnum::RGB24, img.into_rgb8().into_raw())
110        };
111        let pitch = w * format.byte_size_per_pixel() as u32;
112
113        with_canvas(|canvas| unsafe {
114            let surface = sdl2_sys::SDL_CreateRGBSurfaceWithFormatFrom(
115                data.as_mut_ptr().cast(),
116                w as i32,
117                h as i32,
118                /* unused */ 0,
119                pitch as i32,
120                format as u32,
121            );
122            if surface.is_null() {
123                return Err(SdlError::from_sdl())?;
124            }
125
126            let ptr = sdl2_sys::SDL_CreateTextureFromSurface(canvas.renderer(), surface);
127            if ptr.is_null() {
128                log::warn!("Failed to create a texture: {}", SdlError::from_sdl());
129            }
130
131            if let Some(scale) = opts.scaling {
132                let scale = std::mem::transmute::<ScaleMode, sdl2_sys::SDL_ScaleMode>(scale);
133                sdl2_sys::SDL_SetTextureScaleMode(ptr, scale);
134            }
135
136            Ok(Self { ptr, w, h })
137        })
138    }
139
140    pub const fn raw(&self) -> *mut sdl2_sys::SDL_Texture {
141        self.ptr
142    }
143}
144
145impl Drop for TextureData {
146    fn drop(&mut self) {
147        unsafe { sdl2_sys::SDL_DestroyTexture(self.ptr) }
148    }
149}
150
151/// An image which lives on the GPU.
152#[must_use]
153#[derive(Clone)]
154pub struct Texture {
155    data: Rc<TextureData>,
156    origin: Vec2,
157}
158
159impl Texture {
160    /// Creates an empty texture. This is a placeholder value.
161    pub fn empty() -> Self {
162        let data = Rc::new(TextureData::empty());
163        let origin = Vec2::ZERO;
164        Self { data, origin }
165    }
166
167    /// Loads a texture at a given path.
168    pub fn load(path: impl AsRef<Path>) -> Self {
169        Self::load_with(path, Options::default())
170    }
171
172    /// Loads a texture at a given path, with custom options.
173    ///
174    /// You may specify a [`ScaleMode`] or an [`Origin`] for the texture, or both using [`TextureOptions`][Options].
175    /// 
176    /// ```no_run
177    /// # use baba::prelude::*;
178    /// // Create a texture which is positioned around its center.
179    /// let my_texture = Texture::load_with("resources/image.png", Origin::CENTER);
180    /// // Create a texture with linear scaling
181    /// let my_texture = Texture::load_with("resources/image.png", ScaleMode::Linear);
182    /// ```
183    pub fn load_with(path: impl AsRef<Path>, options: impl Into<Options>) -> Self {
184        Self::try_load(path.as_ref(), options)
185            .inspect_err(|e| log::error!("Failed to load {}: {e}", path.as_ref().display()))
186            .unwrap_or_else(|_| Self::empty())
187    }
188
189    /// Like [`load`][Texture::load], but returns an error instead of outputting a warning.
190    pub fn try_load(
191        path: impl AsRef<Path>,
192        options: impl Into<Options>,
193    ) -> Result<Self, LoadError> {
194        Self::from_image(Reader::open(path.as_ref())?.decode()?, options.into())
195    }
196
197    /// Creates a texture from an image in memory.
198    pub fn from_image(
199        img: image::DynamicImage,
200        options: impl Into<Options>,
201    ) -> Result<Self, LoadError> {
202        let options = options.into();
203        let origin = options.origin.0;
204        let data = Rc::new(TextureData::from_image(img, &options)?);
205        Ok(Self { data, origin })
206    }
207
208    /// Creates a slice which points to part of this texture. Useful for spritesheets.
209    pub fn slice(&self, rect: Rect) -> TextureSlice {
210        let texture = self.clone();
211        TextureSlice { texture, rect }
212    }
213
214    /// Sets the origin on this texture.
215    pub const fn with_origin(mut self, origin: Origin) -> Self {
216        self.origin = origin.0;
217        self
218    }
219
220    /// The width of this texture.
221    #[must_use]
222    pub fn width(&self) -> u32 {
223        self.data.w
224    }
225
226    /// The height of this texture.
227    #[must_use]
228    pub fn height(&self) -> u32 {
229        self.data.h
230    }
231
232    pub(crate) fn raw(&self) -> *mut sdl2_sys::SDL_Texture {
233        self.data.raw()
234    }
235}
236
237/// A [`Texture`] which only draws a small rectangle of it.
238#[must_use]
239#[derive(Clone)]
240pub struct TextureSlice {
241    texture: Texture,
242    rect: Rect,
243}
244
245const QUAD_VERTS: [Vec2; 4] = [vec2(0., 0.), vec2(1., 0.), vec2(0., 1.), vec2(1., 1.)];
246const QUAD_IDX: [i32; 6] = [0, 1, 2, 2, 1, 3];
247
248impl Drawable for Texture {
249    fn draw(&self, canvas: &mut Canvas, transform: Transform) {
250        let size = vec2(self.data.w as f32, self.data.h as f32);
251        let transform = transform.scale(size);
252        let verts =
253            QUAD_VERTS.map(|p| Vertex::from_xy_uv(transform.transform_point(p - self.origin), p));
254
255        canvas.draw_geometry(self, &verts, Some(&QUAD_IDX));
256    }
257}
258
259impl Drawable for TextureSlice {
260    fn draw(&self, canvas: &mut Canvas, transform: Transform) {
261        let data = &self.texture.data;
262        let origin = self.texture.origin;
263
264        let size = vec2(self.rect.w as f32, self.rect.h as f32);
265        let uv = vec2(
266            self.rect.x as f32 / data.w as f32,
267            self.rect.y as f32 / data.h as f32,
268        );
269        let uv_size = vec2(
270            self.rect.w as f32 / data.w as f32,
271            self.rect.h as f32 / data.h as f32,
272        );
273        let transform = transform.scale(size);
274
275        let verts = QUAD_VERTS
276            .map(|p| Vertex::from_xy_uv(transform.transform_point(p - origin), p * uv_size + uv));
277
278        canvas.draw_geometry(&self.texture, &verts, Some(&QUAD_IDX));
279    }
280}