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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
use nalgebra as na;

use crate::geometry::Rect;

use super::OrthoCamera;

impl OrthoCamera {
    /// Build a new camera with a given viewport height and aspect ratio.
    ///
    /// # Params
    ///
    /// - `viewport_height` defines the height of the view rectangle in world
    ///   space.
    /// - `aspect_ratio` is the ratio of the desired viewport's `width/height`.
    pub fn with_viewport(viewport_height: f32, aspect_ratio: f32) -> Self {
        let viewport_width = viewport_height * aspect_ratio;
        Self {
            projection: Self::centered_ortho(viewport_width, viewport_height),
            view: na::Translation2::identity(),
            viewport_height,
            viewport_width,
        }
    }

    /// Get the camera's full transformation matrix. This can be passed to a
    /// shader for transformations.
    pub fn as_matrix(&self) -> na::Matrix4<f32> {
        let view_3d = na::Translation3::new(self.view.x, self.view.y, 0.0);
        self.projection.as_matrix() * view_3d.to_homogeneous()
    }

    /// The camera's bounds in world-space.
    ///
    /// # Example
    ///
    /// ```rust
    /// # use draw2d::camera::*;
    /// # use approx::assert_relative_eq;
    /// # use nalgebra as na;
    /// #
    /// let ortho = OrthoCamera::with_viewport(1.0, 2.0);
    /// let bounds = ortho.bounds();
    ///
    /// assert_relative_eq!(bounds.left, -1.0);
    /// assert_relative_eq!(bounds.right, 1.0);
    /// assert_relative_eq!(bounds.top, 0.5);
    /// assert_relative_eq!(bounds.bottom, -0.5);
    /// ```
    pub fn bounds(&self) -> Rect<f32> {
        let viewport_top_left = na::Point2::new(
            -self.viewport_width / 2.0,
            self.viewport_height / 2.0,
        );
        let viewport_bottom_right = na::Point2::new(
            self.viewport_width / 2.0,
            -self.viewport_height / 2.0,
        );
        let inverse = self.view.inverse();
        let world_top_left = inverse.transform_point(&viewport_top_left);
        let world_bottom_right =
            inverse.transform_point(&viewport_bottom_right);
        Rect {
            left: world_top_left.x,
            right: world_bottom_right.x,
            top: world_top_left.y,
            bottom: world_bottom_right.y,
        }
    }

    /// Set the camera's position in world-space.
    ///
    /// # Example
    ///
    /// ```rust
    /// # use draw2d::camera::*;
    /// # use approx::assert_relative_eq;
    /// # use nalgebra as na;
    /// #
    /// let mut ortho = OrthoCamera::with_viewport(2.0, 1.0);
    /// ortho.set_world_position(&na::Point2::new(30.0, -0.5));
    ///
    /// assert_relative_eq!(
    ///   ortho.world_position(),
    ///   na::Point2::new(30.0, -0.5)
    /// );
    ///
    /// let bounds = ortho.bounds();
    /// assert_relative_eq!(bounds.left, -1.0 + 30.0);
    /// assert_relative_eq!(bounds.right, 1.0 + 30.0);
    /// assert_relative_eq!(bounds.top, 1.0 - 0.5);
    /// assert_relative_eq!(bounds.bottom, -1.0 - 0.5);
    /// ```
    pub fn set_world_position(&mut self, world_pos: &na::Point2<f32>) {
        self.view.x = -world_pos.x;
        self.view.y = -world_pos.y;
    }

    /// Get the camera's position in world space.
    ///
    ///
    /// # Example
    ///
    /// ```rust
    /// # use draw2d::camera::*;
    /// # use approx::assert_relative_eq;
    /// # use nalgebra as na;
    /// #
    /// let pos = OrthoCamera::with_viewport(1.0, 1.0).world_position();
    ///
    /// assert_relative_eq!(pos, na::Point2::new(0.0, 0.0));
    /// ```
    pub fn world_position(&self) -> na::Point2<f32> {
        na::Point2::new(-self.view.x, -self.view.y)
    }

    /// Resize the viewport's width such that the viewing rectangle has the
    /// desired aspect ratio.
    ///
    /// # Example
    ///
    /// ```rust
    /// # use draw2d::camera::*;
    /// # use approx::assert_relative_eq;
    /// # use nalgebra as na;
    /// #
    /// let mut ortho = OrthoCamera::with_viewport(1.0, 1.0);
    /// ortho.set_aspect_ratio(2.0);
    ///
    /// let bounds = ortho.bounds();
    /// assert_relative_eq!(bounds.left, -1.0);
    /// assert_relative_eq!(bounds.right, 1.0);
    /// assert_relative_eq!(bounds.top, 0.5);
    /// assert_relative_eq!(bounds.bottom, -0.5);
    /// ```
    pub fn set_aspect_ratio(&mut self, desired_aspect_ratio: f32) {
        self.viewport_width = self.viewport_height * desired_aspect_ratio;
        self.projection =
            Self::centered_ortho(self.viewport_width, self.viewport_height);
    }

    /// The camera viewport's aspect ratio.
    pub fn aspect_ratio(&self) -> f32 {
        self.viewport_width / self.viewport_height
    }

    /// Get the height of the viewport.
    pub fn viewport_height(&self) -> f32 {
        self.viewport_height
    }

    /// Get the width of the viewport.
    pub fn viewport_width(&self) -> f32 {
        self.viewport_width
    }

    /// Set the viewport's height to a new value.
    ///
    /// Automatically resizes the viewport's width to maintain the current
    /// aspect ratio.
    ///
    /// # Example
    ///
    /// ```rust
    /// # use draw2d::camera::*;
    /// # use approx::assert_relative_eq;
    /// # use nalgebra as na;
    /// #
    /// // a camera which is 3x as wide as it is tall
    /// let mut ortho = OrthoCamera::with_viewport(2.0, 3.0);
    ///
    /// assert_relative_eq!(ortho.aspect_ratio(), 3.0);
    /// assert_relative_eq!(ortho.viewport_height(), 2.0);
    /// assert_relative_eq!(ortho.viewport_width(), 6.0);
    ///
    /// ortho.set_viewport_height(3.3);
    ///
    /// assert_relative_eq!(ortho.aspect_ratio(), 3.0);
    /// assert_relative_eq!(ortho.viewport_height(), 3.3);
    /// assert_relative_eq!(ortho.viewport_width(), 9.9);
    /// ```
    pub fn set_viewport_height(&mut self, desired_height: f32) {
        let current_aspect_ratio = self.aspect_ratio();
        self.viewport_height = desired_height;
        self.set_aspect_ratio(current_aspect_ratio);
    }

    /// Unproject a vector from normalized device coordinates (NDC) to view
    /// space.
    ///
    /// Vectors are just a direction and a magnitude, so this transformation
    /// does not apply the camera's translation in world space.
    ///
    /// # Example
    ///
    /// ```rust
    /// # use draw2d::camera::*;
    /// # use approx::assert_relative_eq;
    /// # use nalgebra as na;
    /// #
    /// // a camera which is 3x as wide as it is tall
    /// let mut ortho = OrthoCamera::with_viewport(2.0, 3.0);
    ///
    /// // the camera's world position is ignored when unprojecting a vector
    /// ortho.set_world_position(&na::Point2::new(100.0, -34523.0));
    ///
    /// // Vulkan ndc coords have Y ranging from -1 at the top of the screen,
    /// // to 1 at the bottom of the screen.
    /// let top_right_ndc = na::Vector2::new(1.0, -1.0);
    ///
    /// // The unprojected vector should point to the top right of the viewport
    /// // rectangle, but is not influenced by the camera's world position.
    /// let unprojected = ortho.unproject_vec(&top_right_ndc);
    /// assert_relative_eq!(unprojected, na::Vector2::new(3.0, 1.0));
    /// ```
    pub fn unproject_vec(&self, ndc: &na::Vector2<f32>) -> na::Vector2<f32> {
        self.projection
            .inverse()
            .transform_vector(&na::Vector3::new(ndc.x, ndc.y, 0.0))
            .xy()
    }

    /// Unproject a point from normalized device coordinates (NDC) to world
    /// space.
    ///
    /// Points are logically a specific location in space. As such, the point's
    /// coordinates will be transformed b ythe camera's location in world
    /// space.
    ///
    /// e.g. this method returns where the ndc point would *actually* be
    /// located in world coordinates.
    ///
    /// # Example
    ///
    /// ```rust
    /// # use draw2d::camera::*;
    /// # use approx::assert_relative_eq;
    /// # use nalgebra as na;
    /// #
    /// // a camera which is 3x as wide as it is tall
    /// let mut ortho = OrthoCamera::with_viewport(2.0, 3.0);
    ///
    /// // the camera's world position is ignored when unprojecting a vector
    /// ortho.set_world_position(&na::Point2::new(100.0, -34523.0));
    ///
    /// // Vulkan ndc coords have Y ranging from -1 at the top of the screen,
    /// // to 1 at the bottom of the screen.
    /// let bottom_left_ndc = na::Point2::new(-1.0, 1.0);
    ///
    /// // The unprojected point should account for both the camera's viewing
    /// // rectangle, and the camera's world position.
    /// let unprojected = ortho.unproject_point(&bottom_left_ndc);
    /// assert_relative_eq!(
    ///     unprojected,
    ///     na::Point2::new(-3.0, -1.0) + ortho.world_position().coords
    /// );
    /// ```
    pub fn unproject_point(&self, ndc: &na::Point2<f32>) -> na::Point2<f32> {
        let unprojected = self.unproject_vec(&ndc.coords);
        self.view
            .inverse_transform_point(&na::Point2::from(unprojected))
    }

    /// Construct an orthographic projection centered around the origin with
    /// the provided width and height.
    fn centered_ortho(width: f32, height: f32) -> na::Orthographic3<f32> {
        let half_width = width / 2.0;
        let half_height = height / 2.0;
        na::Orthographic3::new(
            -half_width,
            half_width,
            half_height,
            -half_height,
            1.0,
            -1.0,
        )
    }
}