1#[derive(Clone, Copy, Debug, PartialEq)]
5pub struct Camera {
6 pub center: crate::Point,
8 pub zoom: f64,
10 pub rotation: f64,
12}
13
14impl Default for Camera {
15 fn default() -> Self {
16 Self {
17 center: crate::Point::ORIGIN,
18 zoom: 1.0,
19 rotation: 0.0,
20 }
21 }
22}
23
24impl Camera {
25 fn transform(
26 self,
27 context: &crate::SceneContext<'_>,
28 ) -> Result<crate::Transform, crate::ValidationError> {
29 if ![self.center.x, self.center.y, self.zoom, self.rotation]
30 .iter()
31 .all(|value| value.is_finite())
32 || self.zoom <= 0.0
33 {
34 return Err(invalid(
35 "Camera requires finite coordinates, rotation and positive zoom",
36 ));
37 }
38 let center = context.center();
39 let transform = crate::Transform::translate(-self.center.x, -self.center.y)
40 .then(crate::Transform::scale(self.zoom))
41 .then(crate::Transform::rotate(-self.rotation))
42 .then(crate::Transform::translate(center.x, center.y));
43 if transform
44 .coefficients()
45 .iter()
46 .any(|value| !value.is_finite())
47 || transform.inverse().is_none()
48 {
49 return Err(invalid("Camera mapping must be finite and invertible"));
50 }
51 Ok(transform)
52 }
53
54 pub fn world_to_screen(
56 self,
57 context: &crate::SceneContext<'_>,
58 point: crate::Point,
59 ) -> Result<crate::Point, crate::ValidationError> {
60 map(self.transform(context)?, point)
61 }
62
63 pub fn screen_to_world(
65 self,
66 context: &crate::SceneContext<'_>,
67 point: crate::Point,
68 ) -> Result<crate::Point, crate::ValidationError> {
69 map(
70 self.transform(context)?
71 .inverse()
72 .ok_or_else(|| invalid("Camera inverse is not representable"))?,
73 point,
74 )
75 }
76}
77
78pub fn camera(
80 context: &crate::SceneContext<'_>,
81 content: impl Into<crate::Node>,
82 view: Camera,
83) -> crate::Node {
84 try_camera(context, content, view).unwrap_or_else(super::scene::failure)
85}
86
87pub fn try_camera(
120 context: &crate::SceneContext<'_>,
121 content: impl Into<crate::Node>,
122 view: Camera,
123) -> Result<crate::Node, crate::ValidationError> {
124 use crate::NodeModifiers;
125 view.transform(context)?;
126 Ok(
127 crate::Node::group([content.into().translate(-view.center.x, -view.center.y)])
128 .pivot(crate::Pivot::Point(crate::Point::ORIGIN))
129 .scale(view.zoom)
130 .rotate(-view.rotation)
131 .translate(context.center().x, context.center().y),
132 )
133}
134
135fn invalid(message: &str) -> crate::ValidationError {
136 crate::ValidationError::invalid("camera", "view", message)
137}
138
139#[derive(Clone, Debug)]
141pub struct DepthPlane {
142 pub node: crate::Node,
144 pub depth: f64,
146}
147
148#[derive(Clone, Copy, Debug, PartialEq)]
150pub struct DepthOfField {
151 pub focus_distance: f64,
153 pub f_stop: f64,
155 pub focal_length: f64,
157 pub max_radius: f64,
159 pub perspective: bool,
161}
162
163impl DepthOfField {
164 pub fn validate(self) -> Result<(), crate::ValidationError> {
166 if ![
167 self.focus_distance,
168 self.f_stop,
169 self.focal_length,
170 self.max_radius,
171 ]
172 .iter()
173 .all(|value| value.is_finite())
174 || self.f_stop <= 0.0
175 || self.focal_length <= 0.0
176 || self.focus_distance <= self.focal_length
177 || self.max_radius < 0.0
178 {
179 return Err(invalid(
180 "Require a positive aperture/focal length, focus beyond the lens and a finite blur cap",
181 ));
182 }
183 Ok(())
184 }
185
186 pub fn radius(self, depth: f64) -> Result<f64, crate::ValidationError> {
188 self.validate()?;
189 if !depth.is_finite() || depth <= self.focal_length {
190 return Err(invalid(
191 "Plane depth must be finite and beyond the focal length",
192 ));
193 }
194 let radius = (self.focal_length / self.f_stop)
195 * (self.focal_length / (self.focus_distance - self.focal_length))
196 * ((depth - self.focus_distance).abs() / depth)
197 * 0.5;
198 Ok(radius.min(self.max_radius))
199 }
200
201 pub fn capture(
203 self,
204 context: &crate::SceneContext<'_>,
205 view: Camera,
206 planes: impl IntoIterator<Item = DepthPlane>,
207 ) -> Result<crate::Node, crate::ValidationError> {
208 use crate::NodeModifiers;
209 self.validate()?;
210 view.transform(context)?;
211 let mut planes = planes.into_iter().take(257).collect::<Vec<_>>();
212 if planes.len() > 256 {
213 return Err(invalid("Depth capture supports at most 256 planes"));
214 }
215 for plane in &planes {
216 self.radius(plane.depth)?;
217 }
218 planes.sort_by(|first, last| last.depth.total_cmp(&first.depth));
219 let mut nodes = Vec::with_capacity(planes.len());
220 for plane in planes {
221 let radius = self.radius(plane.depth)?;
222 let factor = if self.perspective {
223 (self.focus_distance - self.focal_length) / (plane.depth - self.focal_length)
224 } else {
225 1.0
226 };
227 let transform = view.transform(context)?;
228 let node = plane
229 .node
230 .transformed(crate::Transform::scale(factor).then(transform));
231 nodes.push(if radius == 0.0 {
232 node
233 } else {
234 node.filter(crate::Filter::Optics(crate::Optics::Bokeh {
235 radius,
236 shape: crate::ApertureShape::Disc,
237 highlight_boost: 0.0,
238 }))
239 });
240 }
241 Ok(crate::Node::group(nodes))
242 }
243}
244
245fn map(
246 transform: crate::Transform,
247 point: crate::Point,
248) -> Result<crate::Point, crate::ValidationError> {
249 let result = transform.apply(point);
250 if [point.x, point.y, result.x, result.y]
251 .iter()
252 .all(|value| value.is_finite())
253 {
254 Ok(result)
255 } else {
256 Err(invalid("Camera points must map to finite coordinates"))
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use crate::Shape;
264
265 #[test]
266 fn thin_lens_focus_is_exact_and_out_of_focus_radius_is_capped() {
267 let lens = super::DepthOfField {
268 focus_distance: 1000.0,
269 f_stop: 2.0,
270 focal_length: 50.0,
271 max_radius: 10.0,
272 perspective: true,
273 };
274 assert_eq!(lens.radius(1000.0).unwrap(), 0.0);
275 assert!(lens.radius(100.0).unwrap() > 0.0);
276 assert!(lens.radius(50.0).is_err());
277 assert!(
278 super::DepthOfField {
279 f_stop: 0.0,
280 ..lens
281 }
282 .validate()
283 .is_err()
284 );
285 }
286
287 #[test]
288 fn camera_invalid_parameters_fail_without_rendering_and_do_not_mutate_views() {
289 let cache = crate::SceneCache::new(0);
290 let context = crate::SceneContext::new(
291 crate::Time::ZERO,
292 crate::fps!(30),
293 crate::Size::new(8.0, 8.0),
294 &cache,
295 );
296 for view in [
297 Camera {
298 zoom: 0.0,
299 ..Camera::default()
300 },
301 Camera {
302 zoom: -1.0,
303 ..Camera::default()
304 },
305 Camera {
306 rotation: f64::NAN,
307 ..Camera::default()
308 },
309 Camera {
310 center: crate::Point::new(f64::INFINITY, 0.0),
311 ..Camera::default()
312 },
313 ] {
314 assert!(try_camera(&context, crate::Node::empty(), view).is_err());
315 assert!(
316 view.screen_to_world(&context, crate::Point::ORIGIN)
317 .is_err()
318 );
319 }
320 let view = Camera::default();
321 assert!(
322 view.world_to_screen(&context, crate::Point::new(f64::NAN, 0.0))
323 .is_err()
324 );
325 assert_eq!(view, Camera::default());
326 }
327
328 #[test]
329 fn projection_keeps_fixed_clip_parent_mapping_and_overflow_guards() -> crate::Result {
330 use crate::{DrawCommand, Node, NodeModifiers, Pivot, Point, Rect, Size, Transform};
331 let cache = crate::SceneCache::new(0);
332 let context = crate::SceneContext::new(
333 crate::Time::ZERO,
334 crate::FrameRate::NTSC,
335 Size::new(120.0, 80.0),
336 &cache,
337 );
338 let view = Camera {
339 center: Point::new(30.0, -20.0),
340 zoom: 2.5,
341 rotation: 0.7,
342 };
343 let viewport = Rect::new(20.0, 40.0, 140.0, 120.0);
344 for parent in [
345 Transform::IDENTITY,
346 Transform::scale_non_uniform(-1.3, 1.1)
347 .then(Transform::rotate(0.3))
348 .then(Transform::translate(80.0, 100.0)),
349 ] {
350 let artwork = try_camera(
351 &context,
352 Rect::new(0.0, 0.0, 12.0, 8.0).fill(crate::Color::WHITE),
353 view,
354 )?;
355 let world = Node::group([artwork.translate(20.0, 40.0)])
356 .clip_with(viewport, crate::ClipMode::Geometric);
357 let components = parent.decompose().unwrap();
358 let node = world
359 .pivot(Pivot::Point(Point::ORIGIN))
360 .scale_xy(components.scale.x, components.scale.y)
361 .rotate(components.rotation)
362 .translate(components.translation.x, components.translation.y);
363 let list = node.compile(
364 Size::new(320.0, 180.0),
365 crate::PixelSize::new(320, 180),
366 crate::ValidationLimits::default(),
367 )?;
368 let transform = list
369 .commands
370 .iter()
371 .find_map(|command| {
372 if let DrawCommand::FillRect { transform, .. } = command {
373 Some(*transform)
374 } else {
375 None
376 }
377 })
378 .unwrap();
379 let clip = list
380 .commands
381 .iter()
382 .find_map(|command| {
383 if let DrawCommand::PushRectLayer { rect_clip, .. } = command {
384 Some(*rect_clip)
385 } else {
386 None
387 }
388 })
389 .unwrap();
390 assert_eq!(clip.bounds, viewport);
391 for point in [
392 Point::new(30.0, -20.0),
393 Point::ORIGIN,
394 Point::new(-10.0, 25.0),
395 ] {
396 let expected = parent.apply(
397 view.world_to_screen(&context, point)? + crate::Vector2::new(20.0, 40.0),
398 );
399 let actual = transform.apply(point);
400 assert!(
401 (actual.x - expected.x).abs() < 1e-10 && (actual.y - expected.y).abs() < 1e-10
402 );
403 let recovered =
404 view.screen_to_world(&context, view.world_to_screen(&context, point)?)?;
405 assert!(
406 (recovered.x - point.x).abs() < 1e-12 && (recovered.y - point.y).abs() < 1e-12
407 );
408 }
409 assert!(
410 clip.transform
411 .coefficients()
412 .iter()
413 .zip(parent.coefficients())
414 .all(|(actual, expected)| (actual - expected).abs() < 1e-12)
415 );
416 }
417 for zoom in [0.0, -1.0, f64::NAN, f64::INFINITY, f64::from_bits(1)] {
418 let invalid = Camera { zoom, ..view };
419 assert!(invalid.world_to_screen(&context, Point::ORIGIN).is_err());
420 assert!(invalid.screen_to_world(&context, Point::ORIGIN).is_err());
421 assert!(try_camera(&context, Node::empty(), invalid).is_err());
422 }
423 let zoomed = Camera {
424 zoom: 2.0,
425 ..Camera::default()
426 };
427 assert!(
428 zoomed
429 .world_to_screen(&context, Point::new(f64::MAX, 0.0))
430 .is_err()
431 );
432 let zoomed = Camera {
433 zoom: 0.5,
434 ..Camera::default()
435 };
436 assert!(
437 zoomed
438 .screen_to_world(&context, Point::new(f64::MAX, 0.0))
439 .is_err()
440 );
441 Ok(())
442 }
443}