229 lines
7.7 KiB
Kotlin
229 lines
7.7 KiB
Kotlin
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
|
|
import org.openrndr.draw.Drawer
|
|
import org.openrndr.draw.RenderTarget
|
|
import org.openrndr.draw.isolated
|
|
import org.openrndr.events.Event
|
|
import org.openrndr.math.Matrix44
|
|
import org.openrndr.math.Vector2
|
|
import org.openrndr.math.transforms.buildTransform
|
|
|
|
/**
|
|
* The [Camera2D] extension enables panning, rotating, and zooming the view
|
|
* with the mouse:
|
|
* - left click and drag to **pan**
|
|
* - right click and drag to **rotate**
|
|
* - use the mouse wheel to **zoom** in and out
|
|
*
|
|
* Usage: `extend(Camera2D())`
|
|
*/
|
|
class Camera2D : Extension, ChangeEvents {
|
|
override var enabled = true
|
|
|
|
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>()
|
|
|
|
private var dirty = true
|
|
set(value) {
|
|
if (value && !field) {
|
|
changed.trigger(Unit)
|
|
program.window.requestDraw()
|
|
}
|
|
field = value
|
|
}
|
|
override val hasChanged: Boolean
|
|
get() = dirty
|
|
|
|
|
|
/**
|
|
* Executes the provided drawing function in an isolated scope, preserving the current
|
|
* drawing state and then restoring it after the function is executed. The `ortho` projection
|
|
* and custom view transformation are applied during the isolated drawing session.
|
|
*
|
|
* @param function the drawing function to be applied within the isolated scope of the `Drawer`.
|
|
*/
|
|
fun isolated(function: Drawer.() -> Unit) {
|
|
program.drawer.isolated {
|
|
program.drawer.ortho(RenderTarget.active)
|
|
|
|
program.drawer.view = this@Camera2D.view
|
|
program.drawer.function()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reinitialize the camera to its default state, where no transformations
|
|
* (such as rotation, translation, or scaling) are applied.
|
|
*/
|
|
var defaults = {
|
|
view = Matrix44.IDENTITY
|
|
rotationCenter = Vector2.ZERO
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
|
|
/**
|
|
* Configures the mouse interaction events for controlling the camera view and handling
|
|
* transformations such as translation, rotation, and scaling via mouse inputs.
|
|
*
|
|
* @param mouse the MouseEvents instance that provides mouse interaction data, including
|
|
* button presses, dragging, and scrolling events.
|
|
*/
|
|
fun setupControls(mouse: MouseEvents, keyboard: KeyEvents) {
|
|
if (!controlInitialized) {
|
|
controls(mouse, keyboard)
|
|
}
|
|
controlInitialized = true
|
|
}
|
|
|
|
override fun setup(program: Program) {
|
|
this.program = program
|
|
if (!controlInitialized) {
|
|
setupControls(program.mouse, program.keyboard)
|
|
}
|
|
defaults()
|
|
}
|
|
|
|
override fun beforeDraw(drawer: Drawer, program: Program) {
|
|
drawer.pushTransforms()
|
|
drawer.ortho(RenderTarget.active)
|
|
drawer.view = view
|
|
}
|
|
|
|
override fun afterDraw(drawer: Drawer, program: Program) {
|
|
dirty = false
|
|
drawer.popTransforms()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates and sets up a custom-configured Camera2D instance within a 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(configure: Camera2D.() -> Unit = { }): Camera2D {
|
|
val camera = Camera2D()
|
|
camera.configure()
|
|
camera.setup(this)
|
|
return camera
|
|
}
|