diff --git a/orx-camera/src/commonMain/kotlin/Camera2D.kt b/orx-camera/src/commonMain/kotlin/Camera2D.kt index 365c22e5..ffb82158 100644 --- a/orx-camera/src/commonMain/kotlin/Camera2D.kt +++ b/orx-camera/src/commonMain/kotlin/Camera2D.kt @@ -1,6 +1,7 @@ package org.openrndr.extra.camera import org.openrndr.Extension +import org.openrndr.KeyEvents import org.openrndr.MouseButton import org.openrndr.MouseEvents import org.openrndr.Program @@ -26,8 +27,35 @@ class Camera2D : Extension, ChangeEvents { private lateinit var program: Program private var controlInitialized = false + + /** + * Represents the 4x4 transformation matrix of the camera view in a 2D drawing environment. + * This matrix is used to apply custom transformations such as translation, rotation, + * or scaling to the viewport. By default, it is set to the identity matrix. + * + * When modified, the `dirty` flag is automatically set to `true` to indicate + * that the view matrix has been updated and subsequent transformations might + * need recalculation or application. + */ var view = Matrix44.IDENTITY + set(value) { + field = value + dirty = true + } + + /** + * Represents the center of rotation for the camera in 2D space. + * + * Changes to this property will mark the camera's state as dirty, necessitating + * a re-calculation of the view transformation. + * + * Default value is [Vector2.ZERO]. + */ var rotationCenter = Vector2.ZERO + set(value) { + field = value + dirty = true + } override val changed = Event() @@ -61,13 +89,90 @@ class Camera2D : Extension, ChangeEvents { /** * Reinitialize the camera to its default state, where no transformations - * (such as rotation, translation, or scaling) are applied. It also sets the `dirty` flag to `true`, - * indicating that the camera's state has been modified and needs to be updated or rendered accordingly. + * (such as rotation, translation, or scaling) are applied. */ - fun defaults() { + var defaults = { view = Matrix44.IDENTITY rotationCenter = Vector2.ZERO - dirty = true + } + + /** + * Applies a panning transformation to the camera view. The method modifies the current view + * by translating it based on the provided displacement vector, effectively shifting the + * camera's view in the scene. + * + * @param displacement the vector by which the camera view is translated. + */ + fun pan(displacement: Vector2) { + view = buildTransform { + translate(displacement) + } * view + } + + /** + * Rotates the camera view by a specified angle around its rotation center. + * + * @param angle the angle in degrees by which the view is rotated. + */ + fun rotate(angle: Double) { + view = buildTransform { + translate(rotationCenter) + rotate(angle) + translate(-rotationCenter) + } + } + + /** + * Applies a zoom transformation to the camera view. The transformation is centered + * around the specified point while adjusting the zoom level by the given factor. + * + * @param center The point in space around which the zoom transformation is centered. + * @param factor The zoom factor, where values greater than 1.0 zoom in and values less than 1.0 zoom out. + */ + fun zoom(center: Vector2, factor: Double) { + view = buildTransform { + translate(center) + scale(factor) + translate(-center) + } * view + } + + /** + * Sets up and applies mouse and keyboard controls for interacting with the camera. + * This variable provides event-driven logic to handle user input for panning, rotation, and zooming. + * + * - Mouse button interactions are used to configure the center of rotation and reset the view. + * - Mouse drag events control panning and rotation with the left and right mouse buttons respectively. + * - Mouse scrolling adjusts the zoom level based on the scroll direction and position. + * + * @param mouse an instance of `MouseEvents` providing data for mouse interactions, + * such as button presses, movement, and scrolling. + * @param keyboard an instance of `KeyEvents` providing the framework for handling keyboard inputs, + * though currently unused in this implementation. + */ + var controls = { mouse: MouseEvents, keyboard: KeyEvents -> + mouse.buttonDown.listen { + rotationCenter = it.position + if (it.button == MouseButton.CENTER) { + defaults() + } + } + mouse.dragged.listen { + if (!it.propagationCancelled) { + when (it.button) { + MouseButton.LEFT -> pan(it.dragDisplacement) + MouseButton.RIGHT -> rotate(it.dragDisplacement.x + it.dragDisplacement.y) + else -> Unit + } + } + } + mouse.scrolled.listen { + if (!it.propagationCancelled) { + val scaleFactor = 1.0 - it.rotation.y * 0.03 + zoom(it.position, scaleFactor) + } + } + Unit } /** @@ -77,42 +182,9 @@ class Camera2D : Extension, ChangeEvents { * @param mouse the MouseEvents instance that provides mouse interaction data, including * button presses, dragging, and scrolling events. */ - fun setupMouseEvents(mouse: MouseEvents) { - mouse.buttonDown.listen { - rotationCenter = it.position - - if (it.button == MouseButton.CENTER) { - defaults() - } - } - mouse.dragged.listen { - if (!it.propagationCancelled) { - when (it.button) { - MouseButton.LEFT -> view = buildTransform { - translate(it.dragDisplacement) - } * view - - MouseButton.RIGHT -> view = buildTransform { - translate(rotationCenter) - rotate(it.dragDisplacement.x + it.dragDisplacement.y) - translate(-rotationCenter) - } * view - - else -> Unit - } - dirty = true - } - } - mouse.scrolled.listen { - if (!it.propagationCancelled) { - val scaleFactor = 1.0 - it.rotation.y * 0.03 - view = buildTransform { - translate(it.position) - scale(scaleFactor) - translate(-it.position) - } * view - dirty = true - } + fun setupControls(mouse: MouseEvents, keyboard: KeyEvents) { + if (!controlInitialized) { + controls(mouse, keyboard) } controlInitialized = true } @@ -120,8 +192,9 @@ class Camera2D : Extension, ChangeEvents { override fun setup(program: Program) { this.program = program if (!controlInitialized) { - setupMouseEvents(program.mouse) + setupControls(program.mouse, program.keyboard) } + defaults() } override fun beforeDraw(drawer: Drawer, program: Program) { @@ -137,12 +210,19 @@ class Camera2D : Extension, ChangeEvents { } /** - * Creates a new instance of the Camera2D extension suitable for manual application. + * Creates and sets up a custom-configured Camera2D instance within a Program. * - * @return a configured Camera2D instance ready to be used with the calling Program. + * This function initializes a new Camera2D, applies the provided configuration block, + * and sets it up with the current Program context for interactive 2D transformations + * such as panning, rotating, and zooming. + * + * @param configure an optional configuration block where you can set up the Camera2D + * instance (e.g., setting view or rotation center). The default is an empty block. + * @return the configured Camera2D instance. */ -fun Program.Camera2DManual(): Camera2D { +fun Program.Camera2DManual(configure: Camera2D.() -> Unit = { }): Camera2D { val camera = Camera2D() + camera.configure() camera.setup(this) return camera }