from __future__ import annotations import math import numpy as np from scipy.spatial.transform import Rotation from manimlib.constants import DEGREES, RADIANS from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP from manimlib.mobject.mobject import Mobject from manimlib.utils.space_ops import normalize from typing import TYPE_CHECKING if TYPE_CHECKING: from manimlib.typing import Vect3 class CameraFrame(Mobject): def __init__( self, frame_shape: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT), center_point: Vect3 = ORIGIN, focal_dist_to_height: float = 2.0, **kwargs, ): self.frame_shape = frame_shape self.center_point = center_point self.focal_dist_to_height = focal_dist_to_height self.view_matrix = np.identity(4) super().__init__(**kwargs) def init_uniforms(self) -> None: super().init_uniforms() # As a quaternion self.uniforms["orientation"] = Rotation.identity().as_quat() self.uniforms["focal_dist_to_height"] = self.focal_dist_to_height def init_points(self) -> None: self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP]) self.set_width(self.frame_shape[0], stretch=True) self.set_height(self.frame_shape[1], stretch=True) self.move_to(self.center_point) def set_orientation(self, rotation: Rotation): self.uniforms["orientation"][:] = rotation.as_quat() return self def get_orientation(self): return Rotation.from_quat(self.uniforms["orientation"]) def to_default_state(self): self.center() self.set_height(FRAME_HEIGHT) self.set_width(FRAME_WIDTH) self.set_orientation(Rotation.identity()) return self def get_euler_angles(self): return self.get_orientation().as_euler("zxz")[::-1] def get_theta(self): return self.get_euler_angles()[0] def get_phi(self): return self.get_euler_angles()[1] def get_gamma(self): return self.get_euler_angles()[2] def get_inverse_camera_rotation_matrix(self): return self.get_orientation().as_matrix().T def get_view_matrix(self): """ Returns a 4x4 for the affine transformation mapping a point into the camera's internal coordinate system """ result = self.view_matrix result[:] = np.identity(4) result[:3, 3] = -self.get_center() rotation = np.identity(4) rotation[:3, :3] = self.get_inverse_camera_rotation_matrix() result[:] = np.dot(rotation, result) return result def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs): rot = Rotation.from_rotvec(angle * normalize(axis)) self.set_orientation(rot * self.get_orientation()) return self def set_euler_angles( self, theta: float | None = None, phi: float | None = None, gamma: float | None = None, units: float = RADIANS ): eulers = self.get_euler_angles() # theta, phi, gamma for i, var in enumerate([theta, phi, gamma]): if var is not None: eulers[i] = var * units self.set_orientation(Rotation.from_euler("zxz", eulers[::-1])) return self def reorient( self, theta_degrees: float | None = None, phi_degrees: float | None = None, gamma_degrees: float | None = None, ): """ Shortcut for set_euler_angles, defaulting to taking in angles in degrees """ self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES) return self def set_theta(self, theta: float): return self.set_euler_angles(theta=theta) def set_phi(self, phi: float): return self.set_euler_angles(phi=phi) def set_gamma(self, gamma: float): return self.set_euler_angles(gamma=gamma) def increment_theta(self, dtheta: float): self.rotate(dtheta, OUT) return self def increment_phi(self, dphi: float): self.rotate(dphi, self.get_inverse_camera_rotation_matrix()[0]) return self def increment_gamma(self, dgamma: float): self.rotate(dgamma, self.get_inverse_camera_rotation_matrix()[2]) return self def set_focal_distance(self, focal_distance: float): self.uniforms["focal_dist_to_height"] = focal_distance / self.get_height() return self def set_field_of_view(self, field_of_view: float): self.uniforms["focal_dist_to_height"] = 2 * math.tan(field_of_view / 2) return self def get_shape(self): return (self.get_width(), self.get_height()) def get_center(self) -> np.ndarray: # Assumes first point is at the center return self.get_points()[0] def get_width(self) -> float: points = self.get_points() return points[2, 0] - points[1, 0] def get_height(self) -> float: points = self.get_points() return points[4, 1] - points[3, 1] def get_focal_distance(self) -> float: return self.uniforms["focal_dist_to_height"] * self.get_height() def get_field_of_view(self) -> float: return 2 * math.atan(self.uniforms["focal_dist_to_height"] / 2) def get_implied_camera_location(self) -> np.ndarray: to_camera = self.get_inverse_camera_rotation_matrix()[2] dist = self.get_focal_distance() return self.get_center() + dist * to_camera