[orx-camera] Introduce customizable camera defaults and control functions

This commit is contained in:
Edwin Jakobs
2025-08-18 17:38:25 +02:00
parent 8b4d31786c
commit fd2d035511

View File

@@ -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<Unit>()
@@ -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
}