mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-23 12:13:04 -05:00
These crates have no production panic sites: add the deny attribute so they stay clean (geom, color, model, automation, host, export, plugin, and the desktop, CLI and web apps). The plugin's wasm-only test module gets a separate cfg(test) attribute so clippy recognises it as test code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
507 lines
19 KiB
Rust
507 lines
19 KiB
Rust
//! Geometry for EffectCraft: 2D/3D vectors, 3×3 (2D projective) and 4×4 matrices, rectangles and
|
||
//! the layer-transform composition used by the compositor.
|
||
//!
|
||
//! Conventions follow After Effects' public documentation: y grows downwards, angles are degrees
|
||
//! clockwise on screen, 3D uses a left-handed system with +z pointing into the screen, and a 3D
|
||
//! layer's rotation is Orientation (X, Y, Z) followed by X Rotation, Y Rotation, Z Rotation.
|
||
|
||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||
|
||
use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Neg, Sub, SubAssign};
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||
pub struct Vec2 {
|
||
pub x: f64,
|
||
pub y: f64,
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||
pub struct Vec3 {
|
||
pub x: f64,
|
||
pub y: f64,
|
||
pub z: f64,
|
||
}
|
||
|
||
pub const fn vec2(x: f64, y: f64) -> Vec2 {
|
||
Vec2 { x, y }
|
||
}
|
||
pub const fn vec3(x: f64, y: f64, z: f64) -> Vec3 {
|
||
Vec3 { x, y, z }
|
||
}
|
||
|
||
impl Vec2 {
|
||
pub const ZERO: Vec2 = vec2(0.0, 0.0);
|
||
pub const ONE: Vec2 = vec2(1.0, 1.0);
|
||
pub fn dot(self, o: Vec2) -> f64 {
|
||
self.x * o.x + self.y * o.y
|
||
}
|
||
pub fn cross(self, o: Vec2) -> f64 {
|
||
self.x * o.y - self.y * o.x
|
||
}
|
||
pub fn length(self) -> f64 {
|
||
self.dot(self).sqrt()
|
||
}
|
||
pub fn normalize(self) -> Vec2 {
|
||
let l = self.length();
|
||
if l > 0.0 { self / l } else { self }
|
||
}
|
||
pub fn lerp(self, o: Vec2, t: f64) -> Vec2 {
|
||
self + (o - self) * t
|
||
}
|
||
pub fn extend(self, z: f64) -> Vec3 {
|
||
vec3(self.x, self.y, z)
|
||
}
|
||
pub fn rotate_deg(self, deg: f64) -> Vec2 {
|
||
let (s, c) = deg.to_radians().sin_cos();
|
||
vec2(self.x * c - self.y * s, self.x * s + self.y * c)
|
||
}
|
||
pub fn mul_elem(self, o: Vec2) -> Vec2 {
|
||
vec2(self.x * o.x, self.y * o.y)
|
||
}
|
||
}
|
||
|
||
impl Vec3 {
|
||
pub const ZERO: Vec3 = vec3(0.0, 0.0, 0.0);
|
||
pub const ONE: Vec3 = vec3(1.0, 1.0, 1.0);
|
||
pub fn dot(self, o: Vec3) -> f64 {
|
||
self.x * o.x + self.y * o.y + self.z * o.z
|
||
}
|
||
pub fn cross(self, o: Vec3) -> Vec3 {
|
||
vec3(self.y * o.z - self.z * o.y, self.z * o.x - self.x * o.z, self.x * o.y - self.y * o.x)
|
||
}
|
||
pub fn length(self) -> f64 {
|
||
self.dot(self).sqrt()
|
||
}
|
||
pub fn normalize(self) -> Vec3 {
|
||
let l = self.length();
|
||
if l > 0.0 { self / l } else { self }
|
||
}
|
||
pub fn lerp(self, o: Vec3, t: f64) -> Vec3 {
|
||
self + (o - self) * t
|
||
}
|
||
pub fn xy(self) -> Vec2 {
|
||
vec2(self.x, self.y)
|
||
}
|
||
pub fn mul_elem(self, o: Vec3) -> Vec3 {
|
||
vec3(self.x * o.x, self.y * o.y, self.z * o.z)
|
||
}
|
||
}
|
||
|
||
impl From<[f64; 3]> for Vec3 {
|
||
fn from(a: [f64; 3]) -> Vec3 {
|
||
vec3(a[0], a[1], a[2])
|
||
}
|
||
}
|
||
impl From<[f64; 2]> for Vec2 {
|
||
fn from(a: [f64; 2]) -> Vec2 {
|
||
vec2(a[0], a[1])
|
||
}
|
||
}
|
||
|
||
macro_rules! vec_ops {
|
||
($t:ident, $($f:ident),+) => {
|
||
impl Add for $t { type Output = $t; fn add(self, o: $t) -> $t { $t { $($f: self.$f + o.$f),+ } } }
|
||
impl Sub for $t { type Output = $t; fn sub(self, o: $t) -> $t { $t { $($f: self.$f - o.$f),+ } } }
|
||
impl Mul<f64> for $t { type Output = $t; fn mul(self, k: f64) -> $t { $t { $($f: self.$f * k),+ } } }
|
||
impl Div<f64> for $t { type Output = $t; fn div(self, k: f64) -> $t { $t { $($f: self.$f / k),+ } } }
|
||
impl Neg for $t { type Output = $t; fn neg(self) -> $t { $t { $($f: -self.$f),+ } } }
|
||
impl AddAssign for $t { fn add_assign(&mut self, o: $t) { $(self.$f += o.$f;)+ } }
|
||
impl SubAssign for $t { fn sub_assign(&mut self, o: $t) { $(self.$f -= o.$f;)+ } }
|
||
impl MulAssign<f64> for $t { fn mul_assign(&mut self, k: f64) { $(self.$f *= k;)+ } }
|
||
};
|
||
}
|
||
vec_ops!(Vec2, x, y);
|
||
vec_ops!(Vec3, x, y, z);
|
||
|
||
/// Axis-aligned rectangle (min inclusive, max exclusive for pixel rects).
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||
pub struct Rect {
|
||
pub x0: f64,
|
||
pub y0: f64,
|
||
pub x1: f64,
|
||
pub y1: f64,
|
||
}
|
||
|
||
impl Rect {
|
||
pub const EMPTY: Rect = Rect { x0: f64::INFINITY, y0: f64::INFINITY, x1: f64::NEG_INFINITY, y1: f64::NEG_INFINITY };
|
||
pub fn new(x0: f64, y0: f64, x1: f64, y1: f64) -> Rect {
|
||
Rect { x0, y0, x1, y1 }
|
||
}
|
||
pub fn from_size(w: f64, h: f64) -> Rect {
|
||
Rect { x0: 0.0, y0: 0.0, x1: w, y1: h }
|
||
}
|
||
pub fn width(&self) -> f64 {
|
||
(self.x1 - self.x0).max(0.0)
|
||
}
|
||
pub fn height(&self) -> f64 {
|
||
(self.y1 - self.y0).max(0.0)
|
||
}
|
||
pub fn is_empty(&self) -> bool {
|
||
!(self.x1 > self.x0 && self.y1 > self.y0)
|
||
}
|
||
pub fn center(&self) -> Vec2 {
|
||
vec2((self.x0 + self.x1) * 0.5, (self.y0 + self.y1) * 0.5)
|
||
}
|
||
pub fn union(&self, o: &Rect) -> Rect {
|
||
Rect { x0: self.x0.min(o.x0), y0: self.y0.min(o.y0), x1: self.x1.max(o.x1), y1: self.y1.max(o.y1) }
|
||
}
|
||
pub fn intersect(&self, o: &Rect) -> Rect {
|
||
Rect { x0: self.x0.max(o.x0), y0: self.y0.max(o.y0), x1: self.x1.min(o.x1), y1: self.y1.min(o.y1) }
|
||
}
|
||
pub fn include(&mut self, p: Vec2) {
|
||
self.x0 = self.x0.min(p.x);
|
||
self.y0 = self.y0.min(p.y);
|
||
self.x1 = self.x1.max(p.x);
|
||
self.y1 = self.y1.max(p.y);
|
||
}
|
||
pub fn inflate(&self, d: f64) -> Rect {
|
||
Rect { x0: self.x0 - d, y0: self.y0 - d, x1: self.x1 + d, y1: self.y1 + d }
|
||
}
|
||
pub fn contains(&self, p: Vec2) -> bool {
|
||
p.x >= self.x0 && p.x < self.x1 && p.y >= self.y0 && p.y < self.y1
|
||
}
|
||
pub fn corners(&self) -> [Vec2; 4] {
|
||
[vec2(self.x0, self.y0), vec2(self.x1, self.y0), vec2(self.x1, self.y1), vec2(self.x0, self.y1)]
|
||
}
|
||
/// Smallest integer rect covering this one.
|
||
pub fn round_out(&self) -> Rect {
|
||
Rect { x0: self.x0.floor(), y0: self.y0.floor(), x1: self.x1.ceil(), y1: self.y1.ceil() }
|
||
}
|
||
}
|
||
|
||
/// Row-major 3×3 matrix acting on column vectors `(x, y, 1)`; a 2D projective transform.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct Mat3(pub [[f64; 3]; 3]);
|
||
|
||
impl Default for Mat3 {
|
||
fn default() -> Self {
|
||
Mat3::IDENTITY
|
||
}
|
||
}
|
||
|
||
impl Mat3 {
|
||
pub const IDENTITY: Mat3 = Mat3([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
|
||
pub fn translate(t: Vec2) -> Mat3 {
|
||
Mat3([[1.0, 0.0, t.x], [0.0, 1.0, t.y], [0.0, 0.0, 1.0]])
|
||
}
|
||
pub fn scale(s: Vec2) -> Mat3 {
|
||
Mat3([[s.x, 0.0, 0.0], [0.0, s.y, 0.0], [0.0, 0.0, 1.0]])
|
||
}
|
||
/// Clockwise on screen (y down) for positive degrees.
|
||
pub fn rotate_deg(deg: f64) -> Mat3 {
|
||
let (s, c) = deg.to_radians().sin_cos();
|
||
Mat3([[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]])
|
||
}
|
||
pub fn skew_deg(skew: f64, axis_deg: f64) -> Mat3 {
|
||
let k = skew.to_radians().tan();
|
||
Mat3::rotate_deg(axis_deg) * Mat3([[1.0, k, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]) * Mat3::rotate_deg(-axis_deg)
|
||
}
|
||
/// The After Effects 2D layer transform: layer space → parent space.
|
||
/// `p' = position + R(rotation) · S(scale/100) · (p - anchor)`.
|
||
pub fn layer_2d(anchor: Vec2, position: Vec2, scale_pct: Vec2, rotation_deg: f64) -> Mat3 {
|
||
Mat3::translate(position) * Mat3::rotate_deg(rotation_deg) * Mat3::scale(scale_pct / 100.0) * Mat3::translate(-anchor)
|
||
}
|
||
pub fn apply(&self, p: Vec2) -> Vec2 {
|
||
let m = &self.0;
|
||
let x = m[0][0] * p.x + m[0][1] * p.y + m[0][2];
|
||
let y = m[1][0] * p.x + m[1][1] * p.y + m[1][2];
|
||
let w = m[2][0] * p.x + m[2][1] * p.y + m[2][2];
|
||
if w != 1.0 && w != 0.0 { vec2(x / w, y / w) } else { vec2(x, y) }
|
||
}
|
||
/// Apply to a direction (ignores translation; affine only).
|
||
pub fn apply_vec(&self, v: Vec2) -> Vec2 {
|
||
let m = &self.0;
|
||
vec2(m[0][0] * v.x + m[0][1] * v.y, m[1][0] * v.x + m[1][1] * v.y)
|
||
}
|
||
pub fn is_affine(&self) -> bool {
|
||
self.0[2][0] == 0.0 && self.0[2][1] == 0.0 && (self.0[2][2] - 1.0).abs() < 1e-12
|
||
}
|
||
pub fn determinant(&self) -> f64 {
|
||
let m = &self.0;
|
||
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
|
||
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
|
||
}
|
||
pub fn inverse(&self) -> Option<Mat3> {
|
||
let d = self.determinant();
|
||
if d.abs() < 1e-300 || !d.is_finite() {
|
||
return None;
|
||
}
|
||
let m = &self.0;
|
||
let inv = [
|
||
[(m[1][1] * m[2][2] - m[1][2] * m[2][1]) / d, (m[0][2] * m[2][1] - m[0][1] * m[2][2]) / d, (m[0][1] * m[1][2] - m[0][2] * m[1][1]) / d],
|
||
[(m[1][2] * m[2][0] - m[1][0] * m[2][2]) / d, (m[0][0] * m[2][2] - m[0][2] * m[2][0]) / d, (m[0][2] * m[1][0] - m[0][0] * m[1][2]) / d],
|
||
[(m[1][0] * m[2][1] - m[1][1] * m[2][0]) / d, (m[0][1] * m[2][0] - m[0][0] * m[2][1]) / d, (m[0][0] * m[1][1] - m[0][1] * m[1][0]) / d],
|
||
];
|
||
Some(Mat3(inv))
|
||
}
|
||
/// Bounding box of a rect after transformation.
|
||
pub fn map_rect(&self, r: &Rect) -> Rect {
|
||
let mut out = Rect::EMPTY;
|
||
for c in r.corners() {
|
||
out.include(self.apply(c));
|
||
}
|
||
out
|
||
}
|
||
/// Average linear scale factor (for stroke widths, blur radii in transformed space).
|
||
pub fn mean_scale(&self) -> f64 {
|
||
self.determinant().abs().sqrt()
|
||
}
|
||
/// Projective transform mapping the unit square corners to `q` (in order (0,0),(1,0),(1,1),(0,1)).
|
||
pub fn square_to_quad(q: [Vec2; 4]) -> Mat3 {
|
||
let (x0, y0, x1, y1, x2, y2, x3, y3) = (q[0].x, q[0].y, q[1].x, q[1].y, q[2].x, q[2].y, q[3].x, q[3].y);
|
||
let dx1 = x1 - x2;
|
||
let dx2 = x3 - x2;
|
||
let dy1 = y1 - y2;
|
||
let dy2 = y3 - y2;
|
||
let sx = x0 - x1 + x2 - x3;
|
||
let sy = y0 - y1 + y2 - y3;
|
||
if sx.abs() < 1e-12 && sy.abs() < 1e-12 {
|
||
return Mat3([[x1 - x0, x3 - x0, x0], [y1 - y0, y3 - y0, y0], [0.0, 0.0, 1.0]]);
|
||
}
|
||
let den = dx1 * dy2 - dx2 * dy1;
|
||
let g = (sx * dy2 - dx2 * sy) / den;
|
||
let h = (dx1 * sy - sx * dy1) / den;
|
||
Mat3([[x1 - x0 + g * x1, x3 - x0 + h * x3, x0], [y1 - y0 + g * y1, y3 - y0 + h * y3, y0], [g, h, 1.0]])
|
||
}
|
||
}
|
||
|
||
impl Mul for Mat3 {
|
||
type Output = Mat3;
|
||
fn mul(self, o: Mat3) -> Mat3 {
|
||
let mut r = [[0.0; 3]; 3];
|
||
for i in 0..3 {
|
||
for j in 0..3 {
|
||
r[i][j] = (0..3).map(|k| self.0[i][k] * o.0[k][j]).sum();
|
||
}
|
||
}
|
||
Mat3(r)
|
||
}
|
||
}
|
||
|
||
/// Row-major 4×4 matrix on column vectors `(x, y, z, 1)`.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct Mat4(pub [[f64; 4]; 4]);
|
||
|
||
impl Default for Mat4 {
|
||
fn default() -> Self {
|
||
Mat4::IDENTITY
|
||
}
|
||
}
|
||
|
||
impl Mat4 {
|
||
pub const IDENTITY: Mat4 = Mat4([[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]);
|
||
pub fn translate(t: Vec3) -> Mat4 {
|
||
let mut m = Mat4::IDENTITY;
|
||
m.0[0][3] = t.x;
|
||
m.0[1][3] = t.y;
|
||
m.0[2][3] = t.z;
|
||
m
|
||
}
|
||
pub fn scale(s: Vec3) -> Mat4 {
|
||
let mut m = Mat4::IDENTITY;
|
||
m.0[0][0] = s.x;
|
||
m.0[1][1] = s.y;
|
||
m.0[2][2] = s.z;
|
||
m
|
||
}
|
||
pub fn rotate_x(deg: f64) -> Mat4 {
|
||
let (s, c) = deg.to_radians().sin_cos();
|
||
Mat4([[1.0, 0.0, 0.0, 0.0], [0.0, c, -s, 0.0], [0.0, s, c, 0.0], [0.0, 0.0, 0.0, 1.0]])
|
||
}
|
||
pub fn rotate_y(deg: f64) -> Mat4 {
|
||
let (s, c) = deg.to_radians().sin_cos();
|
||
Mat4([[c, 0.0, s, 0.0], [0.0, 1.0, 0.0, 0.0], [-s, 0.0, c, 0.0], [0.0, 0.0, 0.0, 1.0]])
|
||
}
|
||
pub fn rotate_z(deg: f64) -> Mat4 {
|
||
let (s, c) = deg.to_radians().sin_cos();
|
||
Mat4([[c, -s, 0.0, 0.0], [s, c, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]])
|
||
}
|
||
/// Orientation (degrees, X then Y then Z applied to the vector in that order).
|
||
pub fn orientation(o: Vec3) -> Mat4 {
|
||
Mat4::rotate_z(o.z) * Mat4::rotate_y(o.y) * Mat4::rotate_x(o.x)
|
||
}
|
||
/// The 3D layer transform: layer space → parent space.
|
||
pub fn layer_3d(anchor: Vec3, position: Vec3, scale_pct: Vec3, orientation: Vec3, rotation: Vec3) -> Mat4 {
|
||
Mat4::translate(position)
|
||
* Mat4::orientation(orientation)
|
||
* Mat4::rotate_z(rotation.z)
|
||
* Mat4::rotate_y(rotation.y)
|
||
* Mat4::rotate_x(rotation.x)
|
||
* Mat4::scale(scale_pct / 100.0)
|
||
* Mat4::translate(-anchor)
|
||
}
|
||
/// Embed a 2D affine transform (z untouched).
|
||
pub fn from_mat3_affine(m: &Mat3) -> Mat4 {
|
||
let a = &m.0;
|
||
Mat4([[a[0][0], a[0][1], 0.0, a[0][2]], [a[1][0], a[1][1], 0.0, a[1][2]], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]])
|
||
}
|
||
pub fn apply(&self, p: Vec3) -> Vec3 {
|
||
let m = &self.0;
|
||
let x = m[0][0] * p.x + m[0][1] * p.y + m[0][2] * p.z + m[0][3];
|
||
let y = m[1][0] * p.x + m[1][1] * p.y + m[1][2] * p.z + m[1][3];
|
||
let z = m[2][0] * p.x + m[2][1] * p.y + m[2][2] * p.z + m[2][3];
|
||
let w = m[3][0] * p.x + m[3][1] * p.y + m[3][2] * p.z + m[3][3];
|
||
if w != 1.0 && w != 0.0 { vec3(x / w, y / w, z / w) } else { vec3(x, y, z) }
|
||
}
|
||
pub fn apply_vec(&self, v: Vec3) -> Vec3 {
|
||
let m = &self.0;
|
||
vec3(m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z, m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z, m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z)
|
||
}
|
||
pub fn transpose(&self) -> Mat4 {
|
||
let mut r = [[0.0; 4]; 4];
|
||
for i in 0..4 {
|
||
for j in 0..4 {
|
||
r[i][j] = self.0[j][i];
|
||
}
|
||
}
|
||
Mat4(r)
|
||
}
|
||
/// General inverse (Gauss-Jordan with partial pivoting).
|
||
pub fn inverse(&self) -> Option<Mat4> {
|
||
let mut a = self.0;
|
||
let mut inv = Mat4::IDENTITY.0;
|
||
for col in 0..4 {
|
||
let piv = (col..4).max_by(|&i, &j| a[i][col].abs().total_cmp(&a[j][col].abs()))?;
|
||
if a[piv][col].abs() < 1e-300 {
|
||
return None;
|
||
}
|
||
a.swap(col, piv);
|
||
inv.swap(col, piv);
|
||
let d = a[col][col];
|
||
for j in 0..4 {
|
||
a[col][j] /= d;
|
||
inv[col][j] /= d;
|
||
}
|
||
for i in 0..4 {
|
||
if i != col {
|
||
let f = a[i][col];
|
||
if f != 0.0 {
|
||
for j in 0..4 {
|
||
a[i][j] -= f * a[col][j];
|
||
inv[i][j] -= f * inv[col][j];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Some(Mat4(inv))
|
||
}
|
||
/// The projective 3×3 mapping a z=0 plane (x, y) through this matrix and a perspective divide
|
||
/// by `w`: used to draw planar 3D layers. `self` must already include the projection.
|
||
pub fn plane_to_mat3(&self) -> Mat3 {
|
||
let m = &self.0;
|
||
Mat3([[m[0][0], m[0][1], m[0][3]], [m[1][0], m[1][1], m[1][3]], [m[3][0], m[3][1], m[3][3]]])
|
||
}
|
||
}
|
||
|
||
impl Mul for Mat4 {
|
||
type Output = Mat4;
|
||
fn mul(self, o: Mat4) -> Mat4 {
|
||
let mut r = [[0.0; 4]; 4];
|
||
for i in 0..4 {
|
||
for j in 0..4 {
|
||
r[i][j] = (0..4).map(|k| self.0[i][k] * o.0[k][j]).sum();
|
||
}
|
||
}
|
||
Mat4(r)
|
||
}
|
||
}
|
||
|
||
/// After Effects-style camera: the comp is viewed from `position` towards `poi` with `zoom` = the
|
||
/// distance (in pixels) from the eye to the image plane. Returns view×projection that maps world
|
||
/// points to comp pixels (x, y) with depth in z and the perspective divide in w.
|
||
pub fn camera_matrix(comp_w: f64, comp_h: f64, position: Vec3, view: Mat4, zoom: f64) -> Mat4 {
|
||
let _ = position;
|
||
// Projection: x' = zoom * x / z + cx, y' = zoom * y / z + cy (z in camera space, +z forward).
|
||
let proj = Mat4([[zoom, 0.0, comp_w * 0.5, 0.0], [0.0, zoom, comp_h * 0.5, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 1.0, 0.0]]);
|
||
proj * view
|
||
}
|
||
|
||
/// The default camera AE uses when a comp has 3D layers but no camera: 50 mm equivalent, placed
|
||
/// at (w/2, h/2, -zoom) looking at the comp centre.
|
||
pub fn default_camera_zoom(comp_w: f64) -> f64 {
|
||
// 50mm preset on a 36mm film width: zoom = w * 50 / 36.
|
||
comp_w * 50.0 / 36.0
|
||
}
|
||
|
||
/// View matrix for a camera at `pos` looking at `poi` (AE: one-node cameras look along +z of
|
||
/// their own orientation; two-node cameras auto-orient to the point of interest).
|
||
pub fn look_at(pos: Vec3, poi: Vec3, extra: Mat4) -> Mat4 {
|
||
let f = (poi - pos).normalize();
|
||
let f = if f.length() == 0.0 { vec3(0.0, 0.0, 1.0) } else { f };
|
||
// y down world; choose up = -y unless parallel.
|
||
let up = if f.cross(vec3(0.0, -1.0, 0.0)).length() < 1e-9 { vec3(0.0, 0.0, 1.0) } else { vec3(0.0, -1.0, 0.0) };
|
||
let r = up.cross(f).normalize();
|
||
let u = f.cross(r);
|
||
// Camera basis: x = right (screen +x), y = down (screen +y), z = forward.
|
||
let x = -r;
|
||
let y = -u;
|
||
let rot = Mat4([[x.x, x.y, x.z, 0.0], [y.x, y.y, y.z, 0.0], [f.x, f.y, f.z, 0.0], [0.0, 0.0, 0.0, 1.0]]);
|
||
extra * rot * Mat4::translate(-pos)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn close(a: Vec2, b: Vec2) -> bool {
|
||
(a - b).length() < 1e-9
|
||
}
|
||
|
||
#[test]
|
||
fn layer_2d_maps_anchor_to_position() {
|
||
let m = Mat3::layer_2d(vec2(50.0, 25.0), vec2(960.0, 540.0), vec2(200.0, 50.0), 30.0);
|
||
assert!(close(m.apply(vec2(50.0, 25.0)), vec2(960.0, 540.0)));
|
||
let inv = m.inverse().unwrap();
|
||
let p = vec2(12.0, -7.0);
|
||
assert!(close(inv.apply(m.apply(p)), p));
|
||
}
|
||
|
||
#[test]
|
||
fn rotation_is_clockwise_on_screen() {
|
||
let p = Mat3::rotate_deg(90.0).apply(vec2(1.0, 0.0));
|
||
assert!(close(p, vec2(0.0, 1.0)));
|
||
}
|
||
|
||
#[test]
|
||
fn quad_mapping_hits_corners() {
|
||
let q = [vec2(10.0, 10.0), vec2(110.0, 20.0), vec2(120.0, 140.0), vec2(5.0, 100.0)];
|
||
let m = Mat3::square_to_quad(q);
|
||
assert!(close(m.apply(vec2(0.0, 0.0)), q[0]));
|
||
assert!(close(m.apply(vec2(1.0, 0.0)), q[1]));
|
||
assert!(close(m.apply(vec2(1.0, 1.0)), q[2]));
|
||
assert!(close(m.apply(vec2(0.0, 1.0)), q[3]));
|
||
}
|
||
|
||
#[test]
|
||
fn mat4_inverse_roundtrip() {
|
||
let m = Mat4::layer_3d(vec3(1.0, 2.0, 3.0), vec3(100.0, 50.0, -20.0), vec3(150.0, 80.0, 100.0), vec3(10.0, 20.0, 30.0), vec3(5.0, -40.0, 12.0));
|
||
let i = m.inverse().unwrap();
|
||
let p = vec3(3.0, -4.0, 5.0);
|
||
assert!((i.apply(m.apply(p)) - p).length() < 1e-9);
|
||
}
|
||
|
||
#[test]
|
||
fn default_camera_sees_z0_plane_unscaled() {
|
||
let (w, h) = (1920.0, 1080.0);
|
||
let zoom = default_camera_zoom(w);
|
||
let pos = vec3(w / 2.0, h / 2.0, -zoom);
|
||
let view = look_at(pos, vec3(w / 2.0, h / 2.0, 0.0), Mat4::IDENTITY);
|
||
let cam = camera_matrix(w, h, pos, view, zoom);
|
||
let p = cam.apply(vec3(100.0, 200.0, 0.0));
|
||
assert!((p.x - 100.0).abs() < 1e-6 && (p.y - 200.0).abs() < 1e-6, "{p:?}");
|
||
}
|
||
|
||
proptest::proptest! {
|
||
#[test]
|
||
fn mat3_inverse(ax in -100.0f64..100.0, ay in -100.0f64..100.0, s in 10.0f64..400.0, r in -720.0f64..720.0) {
|
||
let m = Mat3::layer_2d(vec2(ax, ay), vec2(3.0, 4.0), vec2(s, s * 0.7), r);
|
||
let i = m.inverse().unwrap();
|
||
let p = vec2(17.0, 23.0);
|
||
proptest::prop_assert!((i.apply(m.apply(p)) - p).length() < 1e-6);
|
||
}
|
||
}
|
||
}
|