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
264 #[test]
265 fn thin_lens_focus_is_exact_and_out_of_focus_radius_is_capped() {
266 let lens = super::DepthOfField {
267 focus_distance: 1000.0,
268 f_stop: 2.0,
269 focal_length: 50.0,
270 max_radius: 10.0,
271 perspective: true,
272 };
273 assert_eq!(lens.radius(1000.0).unwrap(), 0.0);
274 assert!(lens.radius(100.0).unwrap() > 0.0);
275 assert!(lens.radius(50.0).is_err());
276 assert!(
277 super::DepthOfField {
278 f_stop: 0.0,
279 ..lens
280 }
281 .validate()
282 .is_err()
283 );
284 }
285
286 #[test]
287 fn camera_invalid_parameters_fail_without_rendering_and_do_not_mutate_views() {
288 let cache = crate::SceneCache::new(0);
289 let context = crate::SceneContext::new(
290 crate::Time::ZERO,
291 crate::FrameRate::from(30),
292 crate::Size::new(8.0, 8.0),
293 &cache,
294 );
295 for view in [
296 Camera {
297 zoom: 0.0,
298 ..Camera::default()
299 },
300 Camera {
301 zoom: -1.0,
302 ..Camera::default()
303 },
304 Camera {
305 rotation: f64::NAN,
306 ..Camera::default()
307 },
308 Camera {
309 center: crate::Point::new(f64::INFINITY, 0.0),
310 ..Camera::default()
311 },
312 ] {
313 assert!(try_camera(&context, crate::Node::empty(), view).is_err());
314 assert!(
315 view.screen_to_world(&context, crate::Point::ORIGIN)
316 .is_err()
317 );
318 }
319 let view = Camera::default();
320 assert!(
321 view.world_to_screen(&context, crate::Point::new(f64::NAN, 0.0))
322 .is_err()
323 );
324 assert_eq!(view, Camera::default());
325 }
326
327 #[test]
328 fn projection_keeps_fixed_clip_parent_mapping_and_overflow_guards() -> crate::Result {
329 use crate::{DrawCommand, Node, NodeModifiers, Pivot, Point, Rect, Size, Transform};
330 let cache = crate::SceneCache::new(0);
331 let context = crate::SceneContext::new(
332 crate::Time::ZERO,
333 crate::FrameRate::NTSC,
334 Size::new(120.0, 80.0),
335 &cache,
336 );
337 let view = Camera {
338 center: Point::new(30.0, -20.0),
339 zoom: 2.5,
340 rotation: 0.7,
341 };
342 let viewport = Rect::new(20.0, 40.0, 140.0, 120.0);
343 for parent in [
344 Transform::IDENTITY,
345 Transform::scale_non_uniform(-1.3, 1.1)
346 .then(Transform::rotate(0.3))
347 .then(Transform::translate(80.0, 100.0)),
348 ] {
349 let artwork = try_camera(
350 &context,
351 Rect::new(0.0, 0.0, 12.0, 8.0).fill(crate::Color::WHITE),
352 view,
353 )?;
354 let world = Node::group([artwork.translate(20.0, 40.0)])
355 .clip_with(viewport, crate::ClipMode::Geometric);
356 let components = parent.decompose().unwrap();
357 let node = world
358 .pivot(Pivot::Point(Point::ORIGIN))
359 .scale_xy(components.scale.x, components.scale.y)
360 .rotate(components.rotation)
361 .translate(components.translation.x, components.translation.y);
362 let list = node.compile(
363 Size::new(320.0, 180.0),
364 crate::PixelSize::new(320, 180),
365 crate::ValidationLimits::default(),
366 )?;
367 let transform = list
368 .commands
369 .iter()
370 .find_map(|command| {
371 if let DrawCommand::FillRect { transform, .. } = command {
372 Some(*transform)
373 } else {
374 None
375 }
376 })
377 .unwrap();
378 let clip = list
379 .commands
380 .iter()
381 .find_map(|command| {
382 if let DrawCommand::PushRectLayer { rect_clip, .. } = command {
383 Some(*rect_clip)
384 } else {
385 None
386 }
387 })
388 .unwrap();
389 assert_eq!(clip.bounds, viewport);
390 for point in [
391 Point::new(30.0, -20.0),
392 Point::ORIGIN,
393 Point::new(-10.0, 25.0),
394 ] {
395 let expected = parent.apply(
396 view.world_to_screen(&context, point)? + crate::Vector2::new(20.0, 40.0),
397 );
398 let actual = transform.apply(point);
399 assert!(
400 (actual.x - expected.x).abs() < 1e-10 && (actual.y - expected.y).abs() < 1e-10
401 );
402 let recovered =
403 view.screen_to_world(&context, view.world_to_screen(&context, point)?)?;
404 assert!(
405 (recovered.x - point.x).abs() < 1e-12 && (recovered.y - point.y).abs() < 1e-12
406 );
407 }
408 assert!(
409 clip.transform
410 .coefficients()
411 .iter()
412 .zip(parent.coefficients())
413 .all(|(actual, expected)| (actual - expected).abs() < 1e-12)
414 );
415 }
416 for zoom in [0.0, -1.0, f64::NAN, f64::INFINITY, f64::from_bits(1)] {
417 let invalid = Camera { zoom, ..view };
418 assert!(invalid.world_to_screen(&context, Point::ORIGIN).is_err());
419 assert!(invalid.screen_to_world(&context, Point::ORIGIN).is_err());
420 assert!(try_camera(&context, Node::empty(), invalid).is_err());
421 }
422 let zoomed = Camera {
423 zoom: 2.0,
424 ..Camera::default()
425 };
426 assert!(
427 zoomed
428 .world_to_screen(&context, Point::new(f64::MAX, 0.0))
429 .is_err()
430 );
431 let zoomed = Camera {
432 zoom: 0.5,
433 ..Camera::default()
434 };
435 assert!(
436 zoomed
437 .screen_to_world(&context, Point::new(f64::MAX, 0.0))
438 .is_err()
439 );
440 Ok(())
441 }
442}