# Source code for moderngl_window.scene.camera

    import time
    from typing import Any, Optional, Union

    import glm
    from glm import cos, radians, sin

    from moderngl_window.context.base import BaseKeys
    from moderngl_window.opengl.projection import Projection3D
    from moderngl_window.utils.keymaps import QWERTY, KeyMapFactory

    # Direction Definitions
    RIGHT = 1
    LEFT = 2
    FORWARD = 3
    BACKWARD = 4
    UP = 5
    DOWN = 6

    # Movement Definitions
    STILL = 0
    POSITIVE = 1
    NEGATIVE = 2

    class Camera:
        """Simple camera class containing projection.

        .. code:: python

            # create a camera
            camera = Camera(fov=60.0, aspect_ratio=1.0, near=1.0, far=100.0)

            # Get the current camera matrix as numpy array
            print(camera.matrix)

            # Get projection matrix as numpy array
            print(camera.projection.matrix)
        """

        def __init__(
            self, fov: float = 60.0, aspect_ratio: float = 1.0, near: float = 1.0, far: float = 100.0
        ):
            """Initialize camera using a specific projection

            Keyword Args:
                fov (float): Field of view
                aspect_ratio (float): Aspect ratio
                near (float): Near plane
                far (float): Far plane
            """
            self.position = glm.vec3(0.0, 0.0, 0.0)
            # Default camera placement
            self.up = glm.vec3(0.0, 1.0, 0.0)
            self.right = glm.vec3(1.0, 0.0, 0.0)
            self.dir = glm.vec3(0.0, 0.0, -1.0)
            # Yaw and Pitch
            self._yaw = -90.0
            self._pitch = 0.0

            # World up vector
            self._up = glm.vec3(0.0, 1.0, 0.0)

            # Projection
            self._projection = Projection3D(aspect_ratio, fov, near, far)

        @property
        def projection(self) -> Projection3D:
            """:py:class:`~moderngl_window.opengl.projection.Projection3D`: The 3D projection"""
            return self._projection

        def set_position(self, x: float, y: float, z: float) -> None:
            """Set the 3D position of the camera.

            Args:
                x (float): x position
                y (float): y position
                z (float): z position
            """
            self.position = glm.vec3(x, y, z)

        def set_rotation(self, yaw: float, pitch: float) -> None:
            """Set the rotation of the camera.

            Args:
                yaw (float): yaw rotation
                pitch (float): pitch rotation
            """
            self._pitch = pitch
            self._yaw = yaw
            self._update_yaw_and_pitch()

        @property
        def yaw(self) -> float:
            """float: The current yaw angle."""
            return self._yaw

        @yaw.setter
        def yaw(self, value: float) -> None:
            self._yaw = value
            self._update_yaw_and_pitch()

        @property
        def pitch(self) -> float:
            """float: The current pitch angle."""
            return self._pitch

        @pitch.setter
        def pitch(self, value: float) -> None:
            self._pitch = value
            self._update_yaw_and_pitch()

        @property
        def matrix(self) -> glm.mat4:
            """glm.mat4: The current view matrix for the camera"""
            self._update_yaw_and_pitch()
            return self._gl_look_at(self.position, self.position + self.dir, self._up)

        def _update_yaw_and_pitch(self) -> None:
            """Updates the camera vectors based on the current yaw and pitch"""
            front = glm.vec3(0.0, 0.0, 0.0)
            front.x = cos(radians(self.yaw)) * cos(radians(self.pitch))
            front.y = sin(radians(self.pitch))
            front.z = sin(radians(self.yaw)) * cos(radians(self.pitch))

            self.dir = glm.normalize(front)
            self.right = glm.normalize(glm.cross(self.dir, self._up))
            self.up = glm.normalize(glm.cross(self.right, self.dir))

        def look_at(
            self, vec: Optional[glm.vec3] = None, pos: Optional[tuple[float, float, float]] = None
        ) -> glm.mat4:
            """Look at a specific point

            Either ``vec`` or ``pos`` needs to be supplied.

            Keyword Args:
                vec (glm.vec3): position
                pos (tuple/list): list of tuple ``[x, y, x]`` / ``(x, y, x)``
            Returns:
                glm.mat4x4: Camera matrix
            """
            if pos is not None:
                vec = glm.vec3(pos)

            if vec is None:
                raise ValueError("vector or pos must be set")

            return self._gl_look_at(self.position, vec, self._up)

        def _gl_look_at(self, pos: glm.vec3, target: glm.vec3, up: glm.vec3) -> glm.mat4:
            """The standard lookAt method.

            Args:
                pos: current position
                target: target position to look at
                up: direction up
            Returns:
                glm.mat4: The matrix
            """
            z = glm.normalize(pos - target)
            x = glm.normalize(glm.cross(glm.normalize(up), z))
            y = glm.cross(z, x)

            translate = glm.mat4()
            translate[3][0] = -pos.x
            translate[3][1] = -pos.y
            translate[3][2] = -pos.z

            rotate = glm.mat4()
            rotate[0][0] = x[0]  # -- X
            rotate[1][0] = x[1]
            rotate[2][0] = x[2]
            rotate[0][1] = y[0]  # -- Y
            rotate[1][1] = y[1]
            rotate[2][1] = y[2]
            rotate[0][2] = z[0]  # -- Z
            rotate[1][2] = z[1]
            rotate[2][2] = z[2]

            return rotate * translate

    class KeyboardCamera(Camera):
        """Camera controlled by mouse and keyboard.
        The class interacts with the key constants in the
        built in window types.

        Creating a keyboard camera:

        .. code:: python

            camera = KeyboardCamera(
                self.wnd.keys,
                fov=75.0,
                aspect_ratio=self.wnd.aspect_ratio,
                near=0.1,
                far=1000.0,
            )

        We can also interact with the belonging
        :py:class:`~moderngl_window.opengl.projection.Projection3D` instance.

        .. code:: python

            # Update aspect ratio
            camera.projection.update(aspect_ratio=1.0)

            # Get projection matrix in bytes (f4)
            camera.projection.tobytes()
        """

        def __init__(
            self,
            keys: BaseKeys,
            keymap: KeyMapFactory = QWERTY,
            fov: float = 60.0,
            aspect_ratio: float = 1.0,
            near: float = 1.0,
            far: float = 100.0,
        ):
            """Initialize the camera

            Args:
                keys (BaseKeys): The key constants for the current window type
            Keyword Args:
                keymap (KeyMapFactory) : The keymap to use. By default QWERTY.
                fov (float): Field of view
                aspect_ratio (float): Aspect ratio
                near (float): near plane
                far (float): far plane
            """
            # Position movement states
            self.keys = keys
            self.keymap = keymap(keys)
            self._xdir = STILL
            self._zdir = STILL
            self._ydir = STILL
            self._last_time = 0.0
            self._last_rot_time = 0.0

            # Velocity in axis units per second
            self._velocity = 10.0
            self._mouse_sensitivity = 0.5

            super().__init__(fov=fov, aspect_ratio=aspect_ratio, near=near, far=far)

        @property
        def mouse_sensitivity(self) -> float:
            """float: Mouse sensitivity (rotation speed).

            This property can also be set::

                camera.mouse_sensitivity = 2.5
            """
            return self._mouse_sensitivity

        @mouse_sensitivity.setter
        def mouse_sensitivity(self, value: float) -> None:
            self._mouse_sensitivity = value

        @property
        def velocity(self) -> float:
            """float: The speed this camera move based on key inputs

            The property can also be modified::

                camera.velocity = 5.0
            """
            return self._velocity

        @velocity.setter
        def velocity(self, value: float) -> None:
            self._velocity = value

        def key_input(self, key: str, action: str, modifiers: Any) -> None:
            """Process key inputs and move camera

            Args:
                key: The key
                action: key action release/press
                modifiers: key modifier states such as ctrl or shit
            """
            # Right
            if key == self.keymap.RIGHT:
                if action == self.keys.ACTION_PRESS:
                    self.move_right(True)
                elif action == self.keys.ACTION_RELEASE:
                    self.move_right(False)
            # Left
            elif key == self.keymap.LEFT:
                if action == self.keys.ACTION_PRESS:
                    self.move_left(True)
                elif action == self.keys.ACTION_RELEASE:
                    self.move_left(False)
            # Forward
            elif key == self.keymap.FORWARD:
                if action == self.keys.ACTION_PRESS:
                    self.move_forward(True)
                if action == self.keys.ACTION_RELEASE:
                    self.move_forward(False)
            # Backwards
            elif key == self.keymap.BACKWARD:
                if action == self.keys.ACTION_PRESS:
                    self.move_backward(True)
                if action == self.keys.ACTION_RELEASE:
                    self.move_backward(False)

            # DOWN
            elif key == self.keymap.DOWN:
                if action == self.keys.ACTION_PRESS:
                    self.move_down(True)
                if action == self.keys.ACTION_RELEASE:
                    self.move_down(False)

            # UP
            elif key == self.keymap.UP:
                if action == self.keys.ACTION_PRESS:
                    self.move_up(True)
                if action == self.keys.ACTION_RELEASE:
                    self.move_up(False)

        def move_left(self, activate: bool) -> None:
            """The camera should be continiously moving to the left.

            Args:
                activate (bool): Activate or deactivate this state
            """
            self.move_state(LEFT, activate)

        def move_right(self, activate: bool) -> None:
            """The camera should be continiously moving to the right.

            Args:
                activate (bool): Activate or deactivate this state
            """
            self.move_state(RIGHT, activate)

        def move_forward(self, activate: bool) -> None:
            """The camera should be continiously moving forward.

            Args:
                activate (bool): Activate or deactivate this state
            """
            self.move_state(FORWARD, activate)

        def move_backward(self, activate: bool) -> None:
            """The camera should be continiously moving backwards.

            Args:
                activate (bool): Activate or deactivate this state
            """
            self.move_state(BACKWARD, activate)

        def move_up(self, activate: bool) -> None:
            """The camera should be continiously moving up.

            Args:
                activate (bool): Activate or deactivate this state
            """
            self.move_state(UP, activate)

        def move_down(self, activate: bool) -> None:
            """The camera should be continiously moving down.

            Args:
                activate (bool): Activate or deactivate this state
            """
            self.move_state(DOWN, activate)

        def move_state(self, direction: int, activate: bool) -> None:
            """Set the camera position move state.

            Args:
                direction: What direction to update
                activate: Start or stop moving in the direction
            """
            if direction == RIGHT:
                self._xdir = POSITIVE if activate else STILL
            elif direction == LEFT:
                self._xdir = NEGATIVE if activate else STILL
            elif direction == FORWARD:
                self._zdir = NEGATIVE if activate else STILL
            elif direction == BACKWARD:
                self._zdir = POSITIVE if activate else STILL
            elif direction == UP:
                self._ydir = POSITIVE if activate else STILL
            elif direction == DOWN:
                self._ydir = NEGATIVE if activate else STILL

        def rot_state(self, dx: float, dy: float) -> None:
            """Update the rotation of the camera.

            This is done by passing in the relative
            mouse movement change on x and y (delta x, delta y).

            In the past this method took the viewport position
            of the mouse. This does not work well when
            mouse exclusivity mode is enabled.

            Args:
                dx: Relative mouse position change on x
                dy: Relative mouse position change on y
            """
            now = time.time()
            delta = now - self._last_rot_time
            self._last_rot_time = now

            # Greatly decrease the chance of camera popping.
            # This can happen when the mouse enters and leaves the window
            # or when getting focus again.
            if delta > 0.1 and max(abs(dx), abs(dy)) > 2:
                return

            dx *= self._mouse_sensitivity
            dy *= self._mouse_sensitivity

            self._yaw -= dx
            self._pitch += dy

            if self.pitch > 85.0:
                self.pitch = 85.0
            if self.pitch < -85.0:
                self.pitch = -85.0

            self._update_yaw_and_pitch()

        @property
        def matrix(self) -> glm.mat4:
            """glm.mat4x4: The current view matrix for the camera"""
            # Use separate time in camera so we can move it when the demo is paused
            now = time.time()
            # If the camera has been inactive for a while, a large time delta
            # can suddenly move the camera far away from the scene
            t = max(now - self._last_time, 0)
            self._last_time = now

            # X Movement
            if self._xdir == POSITIVE:
                self.position += self.right * self._velocity * t
            elif self._xdir == NEGATIVE:
                self.position -= self.right * self._velocity * t

            # Z Movement
            if self._zdir == NEGATIVE:
                self.position += self.dir * self._velocity * t
            elif self._zdir == POSITIVE:
                self.position -= self.dir * self._velocity * t

            # Y Movement
            if self._ydir == POSITIVE:
                self.position += self.up * self._velocity * t
            elif self._ydir == NEGATIVE:
                self.position -= self.up * self._velocity * t

            return self._gl_look_at(self.position, self.position + self.dir, self._up)

    class OrbitCamera(Camera):
        """Camera controlled by the mouse to pan around the target.

        The functions :py:function:`~camera.OrbitCamera.rot_state` and
        :py:function:`~camera.OrbitCamera.rot_state` are used to update the rotation and zoom.

        Creating a orbit camera:

        .. code:: python

            camera = OrbitCamera(
                target=(0., 0., 0.),
                radius=2.0
                fov=75.0,
                aspect_ratio=self.wnd.aspect_ratio,
                near=0.1,
                far=1000.0,
            )

        We can also interact with the belonging
        :py:class:`~moderngl_window.opengl.projection.Projection3D` instance.

        .. code:: python

            # Update aspect ratio
            camera.projection.update(aspect_ratio=1.0)

            # Get projection matrix in bytes (f4)
            camera.projection.tobytes()
        """

        def __init__(
            self,
            target: Union[glm.vec3, tuple[float, float, float]] = (0.0, 0.0, 0.0),
            radius: float = 2.0,
            angles: tuple[float, float] = (45.0, -45.0),
            **kwargs: Any,
        ):
            """Initialize the camera

            Keyword Args:
                target (float, float, float): Target point
                radius (float): Radius
                angles (float, float): angle_x and angle_y in degrees
                fov (float): Field of view
                aspect_ratio (float): Aspect ratio
                near (float): near plane
                far (float): far plane
            """
            # values for orbit camera
            self.radius = radius  # radius in base units
            self.angle_x, self.angle_y = angles  # angles in degrees
            self.target = glm.vec3(target)  # camera target in base units
            self.up = glm.vec3(0.0, 1.0, 0.0)  # camera up vector

            self._mouse_sensitivity = 1.0
            self._zoom_sensitivity = 1.0

            super().__init__(**kwargs)

        @property
        def matrix(self) -> glm.mat4:
            """glm.mat4: The current view matrix for the camera"""
            # Compute camera (eye) position, calculated from angles and radius.
            position = (
                cos(radians(self.angle_x)) * sin(radians(self.angle_y)) * self.radius + self.target[0],
                cos(radians(self.angle_y)) * self.radius + self.target[1],
                sin(radians(self.angle_x)) * sin(radians(self.angle_y)) * self.radius + self.target[2],
            )
            self.set_position(*position)
            return glm.lookAt(
                position,
                self.target,  # what to look at
                self.up,  # camera up direction (change for rolling the camera)
            )

        @property
        def angle_x(self) -> float:
            """float: camera angle x in degrees.

            This property can also be set::
                camera.angle_x = 45.
            """
            return self._angle_x

        @angle_x.setter
        def angle_x(self, value: float) -> None:
            """Set camera rotation_x in degrees."""
            self._angle_x = value

        @property
        def angle_y(self) -> float:
            """float: camera angle y in degrees.

            This property can also be set::
                camera.angle_y = 45.
            """
            return self._angle_y

        @angle_y.setter
        def angle_y(self, value: float) -> None:
            """Set camera rotation_y in degrees."""
            self._angle_y = value

        @property
        def mouse_sensitivity(self) -> float:
            """float: Mouse sensitivity (rotation speed).

            This property can also be set::
                camera.mouse_sensitivity = 2.5
            """
            return self._mouse_sensitivity

        @mouse_sensitivity.setter
        def mouse_sensitivity(self, value: float) -> None:
            self._mouse_sensitivity = value

        @property
        def zoom_sensitivity(self) -> float:
            """float: Mousewheel zooming sensitivity (zoom speed).

            This property can also be set::
                camera.zoom_sensitivity = 2.5
            """
            return self._zoom_sensitivity

        @zoom_sensitivity.setter
        def zoom_sensitivity(self, value: float) -> None:
            self._zoom_sensitivity = value

        def rot_state(self, dx: float, dy: float) -> None:
            """Update the rotation of the camera around the target point.

            This is done by passing relative mouse change in the x and y axis (delta x, delta y)

            Args:
                dx: Relative mouse position change on x axis
                dy: Relative mouse position change on y axis
            """
            self.angle_x += dx * self.mouse_sensitivity / 10.0
            self.angle_y += dy * self.mouse_sensitivity / 10.0

            # clamp the y angle to avoid weird rotations
            self.angle_y = max(min(self.angle_y, -5.0), -175.0)

        def zoom_state(self, y_offset: float) -> None:
            # allow zooming in/out
            self.radius -= y_offset * self._zoom_sensitivity
            self.radius = max(1.0, self.radius)