diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index 087317692..168c02b92 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -491,11 +491,11 @@ var cel_button_scene: PackedScene = load("res://src/UI/Timeline/CelButton.tscn") "PreviewViewportContainer" ) ## Camera of the main canvas. It has the [param CameraMovement.gd] script attached. -@onready var camera: Camera2D = main_viewport.find_child("Camera2D") +@onready var camera: CanvasCamera = main_viewport.find_child("Camera2D") ## Camera of the second canvas preview. It has the [param CameraMovement.gd] script attached. -@onready var camera2: Camera2D = second_viewport.find_child("Camera2D2") +@onready var camera2: CanvasCamera = second_viewport.find_child("Camera2D2") ## Camera of the canvas preview. It has the [param CameraMovement.gd] script attached. -@onready var camera_preview: Camera2D = control.find_child("CameraPreview") +@onready var camera_preview: CanvasCamera = control.find_child("CameraPreview") ## Array of cameras used in Pixelorama. @onready var cameras := [camera, camera2, camera_preview] ## Horizontal ruler of the main canvas. It has the [param HorizontalRuler.gd] script attached. diff --git a/src/UI/Canvas/CameraMovement.gd b/src/UI/Canvas/CanvasCamera.gd similarity index 69% rename from src/UI/Canvas/CameraMovement.gd rename to src/UI/Canvas/CanvasCamera.gd index 38f6e5aa3..c74c2f174 100644 --- a/src/UI/Canvas/CameraMovement.gd +++ b/src/UI/Canvas/CanvasCamera.gd @@ -1,7 +1,9 @@ -extends Camera2D +class_name CanvasCamera +extends Node2D signal zoom_changed signal rotation_changed +signal offset_changed enum Cameras { MAIN, SECOND, SMALL } @@ -9,6 +11,24 @@ const CAMERA_SPEED_RATE := 15.0 @export var index := 0 +var zoom := Vector2.ONE: + set(value): + zoom = value + zoom_changed.emit() + _update_viewport_transform() +var camera_angle := 0.0: + set(value): + camera_angle = wrapf(value, -PI, PI) + camera_angle_degrees = rad_to_deg(camera_angle) + rotation_changed.emit() + _update_viewport_transform() +var camera_angle_degrees := 0.0 +var offset := Vector2.ZERO: + set(value): + offset = value + offset_changed.emit() + _update_viewport_transform() +var camera_screen_center := Vector2.ZERO var zoom_in_max := Vector2(500, 500) var zoom_out_max := Vector2(0.01, 0.01) var viewport_container: SubViewportContainer @@ -19,55 +39,24 @@ var rotation_slider: ValueSlider var zoom_slider: ValueSlider var should_tween := true +@onready var viewport := get_viewport() + func _ready() -> void: if not DisplayServer.is_touchscreen_available(): set_process_input(false) if index == Cameras.MAIN: rotation_slider = Global.top_menu_container.get_node("%RotationSlider") - rotation_slider.value_changed.connect(_rotation_value_changed) + rotation_slider.value_changed.connect(_rotation_slider_value_changed) zoom_slider = Global.top_menu_container.get_node("%ZoomSlider") - zoom_slider.value_changed.connect(_zoom_value_changed) + zoom_slider.value_changed.connect(_zoom_slider_value_changed) zoom_changed.connect(_zoom_changed) rotation_changed.connect(_rotation_changed) - ignore_rotation = false viewport_container = get_parent().get_parent() transparent_checker = get_parent().get_node("TransparentChecker") update_transparent_checker_offset() -func _rotation_value_changed(value: float) -> void: - # Negative makes going up rotate clockwise - var degrees := -value - var difference := degrees - rotation_degrees - var canvas_center: Vector2 = Global.current_project.size / 2 - offset = (offset - canvas_center).rotated(deg_to_rad(difference)) + canvas_center - rotation_degrees = wrapf(degrees, -180, 180) - rotation_changed.emit() - - -func _zoom_value_changed(value: float) -> void: - if value <= 0: - value = 1 - var new_zoom := Vector2(value, value) / 100.0 - if zoom == new_zoom: - return - if Global.smooth_zoom and should_tween: - var tween := create_tween().set_trans(Tween.TRANS_LINEAR).set_ease(Tween.EASE_IN) - tween.step_finished.connect(_on_tween_step) - tween.tween_property(self, "zoom", new_zoom, 0.05) - else: - zoom = new_zoom - zoom_changed.emit() - - -func update_transparent_checker_offset() -> void: - var o := get_global_transform_with_canvas().get_origin() - var s := get_global_transform_with_canvas().get_scale() - o.y = get_viewport_rect().size.y - o.y - transparent_checker.update_offset(o, s) - - func _input(event: InputEvent) -> void: if !Global.can_draw: drag = false @@ -89,43 +78,19 @@ func _input(event: InputEvent) -> void: zoom_camera(-1) elif event is InputEventPanGesture: # Pan gesture on touchscreens - offset = offset + event.delta.rotated(rotation) * 7.0 / zoom + offset = offset + event.delta.rotated(camera_angle) * 7.0 / zoom elif event is InputEventMouseMotion: if drag: - offset = offset - event.relative.rotated(rotation) / zoom + offset = offset - event.relative.rotated(camera_angle) / zoom update_transparent_checker_offset() - _update_rulers() else: var dir := Input.get_vector(&"camera_left", &"camera_right", &"camera_up", &"camera_down") if dir != Vector2.ZERO and !_has_selection_tool(): - offset += (dir.rotated(rotation) / zoom) * CAMERA_SPEED_RATE - _update_rulers() + offset += (dir.rotated(camera_angle) / zoom) * CAMERA_SPEED_RATE save_values_to_project() -func _has_selection_tool() -> bool: - for slot in Tools._slots.values(): - if slot.tool_node is BaseSelectionTool: - return true - return false - - -# Rotate Camera -func _rotate_camera_around_point(degrees: float, point: Vector2) -> void: - offset = (offset - point).rotated(deg_to_rad(degrees)) + point - rotation_degrees = wrapf(rotation_degrees + degrees, -180, 180) - rotation_changed.emit() - - -func _rotation_changed() -> void: - if index == Cameras.MAIN: - # Negative to make going up in value clockwise, and match the spinbox which does the same - var degrees := wrapf(-rotation_degrees, -180, 180) - rotation_slider.value = degrees - _update_rulers() - - func zoom_camera(dir: int) -> void: var viewport_size := viewport_container.size if Global.smooth_zoom: @@ -137,13 +102,12 @@ func zoom_camera(dir: int) -> void: var new_offset := ( offset + ( - (-0.5 * viewport_size + mouse_pos).rotated(rotation) + (-0.5 * viewport_size + mouse_pos).rotated(camera_angle) * (Vector2.ONE / zoom - Vector2.ONE / new_zoom) ) ) var tween := create_tween().set_parallel() tween.set_trans(Tween.TRANS_LINEAR).set_ease(Tween.EASE_IN) - tween.step_finished.connect(_on_tween_step) tween.tween_property(self, "zoom", new_zoom, 0.05) tween.tween_property(self, "offset", new_offset, 0.05) else: @@ -158,37 +122,15 @@ func zoom_camera(dir: int) -> void: offset = ( offset + ( - (-0.5 * viewport_size + mouse_pos).rotated(rotation) + (-0.5 * viewport_size + mouse_pos).rotated(camera_angle) * (Vector2.ONE / prev_zoom - Vector2.ONE / zoom) ) ) - zoom_changed.emit() - - -func _zoom_changed() -> void: - update_transparent_checker_offset() - if index == Cameras.MAIN: - zoom_slider.value = zoom.x * 100.0 - _update_rulers() - for guide in Global.current_project.guides: - guide.width = 1.0 / zoom.x * 2 - - -func _update_rulers() -> void: - Global.horizontal_ruler.queue_redraw() - Global.vertical_ruler.queue_redraw() - - -func _on_tween_step(_idx: int) -> void: - should_tween = false - zoom_changed.emit() - should_tween = true func zoom_100() -> void: zoom = Vector2.ONE offset = Global.current_project.size / 2 - zoom_changed.emit() func fit_to_frame(size: Vector2) -> void: @@ -199,12 +141,12 @@ func fit_to_frame(size: Vector2) -> void: offset = size / 2 # Adjust to the rotated size: - if rotation != 0.0: + if camera_angle != 0.0: # Calculating the rotated corners of the frame to find its rotated size var a := Vector2.ZERO # Top left - var b := Vector2(size.x, 0).rotated(rotation) # Top right - var c := Vector2(0, size.y).rotated(rotation) # Bottom left - var d := Vector2(size.x, size.y).rotated(rotation) # Bottom right + var b := Vector2(size.x, 0).rotated(camera_angle) # Top right + var c := Vector2(0, size.y).rotated(camera_angle) # Bottom left + var d := Vector2(size.x, size.y).rotated(camera_angle) # Bottom right # Find how far apart each opposite point is on each axis, and take the longer one size.x = maxf(absf(a.x - d.x), absf(b.x - c.x)) @@ -229,12 +171,85 @@ func fit_to_frame(size: Vector2) -> void: ratio = clampf(ratio, 0.1, ratio) zoom = Vector2(ratio, ratio) - zoom_changed.emit() if reset_integer_zoom: Global.integer_zoom = !Global.integer_zoom func save_values_to_project() -> void: - Global.current_project.cameras_rotation[index] = rotation + Global.current_project.cameras_rotation[index] = camera_angle Global.current_project.cameras_zoom[index] = zoom Global.current_project.cameras_offset[index] = offset + + +func update_transparent_checker_offset() -> void: + var o := get_global_transform_with_canvas().get_origin() + var s := get_global_transform_with_canvas().get_scale() + o.y = get_viewport_rect().size.y - o.y + transparent_checker.update_offset(o, s) + + +## Updates the viewport's canvas transform, which is the area of the canvas that is +## currently visible. Called every time the camera's zoom, rotation or origin changes. +func _update_viewport_transform() -> void: + if not is_instance_valid(viewport): + return + var zoom_scale := Vector2.ONE / zoom + var viewport_size := get_viewport_rect().size + var screen_offset := viewport_size * 0.5 * zoom_scale + screen_offset = screen_offset.rotated(camera_angle) + var screen_rect := Rect2(-screen_offset, viewport_size * zoom_scale) + screen_rect.position += offset + var xform := Transform2D(camera_angle, zoom_scale, 0, screen_rect.position) + camera_screen_center = xform * (viewport_size * 0.5) + viewport.canvas_transform = xform.affine_inverse() + + +func _zoom_changed() -> void: + update_transparent_checker_offset() + if index == Cameras.MAIN: + should_tween = false + zoom_slider.value = zoom.x * 100.0 + should_tween = true + for guide in Global.current_project.guides: + guide.width = 1.0 / zoom.x * 2 + + +func _rotation_changed() -> void: + if index == Cameras.MAIN: + # Negative to make going up in value clockwise, and match the spinbox which does the same + rotation_slider.value = -camera_angle_degrees + + +func _zoom_slider_value_changed(value: float) -> void: + if value <= 0: + value = 1 + var new_zoom := Vector2(value, value) / 100.0 + if zoom.is_equal_approx(new_zoom): + return + if Global.smooth_zoom and should_tween: + var tween := create_tween().set_trans(Tween.TRANS_LINEAR).set_ease(Tween.EASE_IN) + tween.tween_property(self, "zoom", new_zoom, 0.05) + else: + zoom = new_zoom + + +func _rotation_slider_value_changed(value: float) -> void: + # Negative makes going up rotate clockwise + var angle := deg_to_rad(-value) + var difference := angle - camera_angle + var canvas_center: Vector2 = Global.current_project.size / 2 + offset = (offset - canvas_center).rotated(difference) + canvas_center + camera_angle = angle + + +func _has_selection_tool() -> bool: + for slot in Tools._slots.values(): + if slot.tool_node is BaseSelectionTool: + return true + return false + + +func _rotate_camera_around_point(degrees: float, point: Vector2) -> void: + var angle := deg_to_rad(degrees) + offset = (offset - point).rotated(angle) + point + camera_angle = camera_angle + angle diff --git a/src/UI/Canvas/Rulers/HorizontalRuler.gd b/src/UI/Canvas/Rulers/HorizontalRuler.gd index fcba83191..35f1a991c 100644 --- a/src/UI/Canvas/Rulers/HorizontalRuler.gd +++ b/src/UI/Canvas/Rulers/HorizontalRuler.gd @@ -11,6 +11,9 @@ var last: Vector2 func _ready() -> void: Global.project_switched.connect(queue_redraw) + Global.camera.zoom_changed.connect(queue_redraw) + Global.camera.rotation_changed.connect(queue_redraw) + Global.camera.offset_changed.connect(queue_redraw) func _gui_input(event: InputEvent) -> void: diff --git a/src/UI/Canvas/Rulers/VerticalRuler.gd b/src/UI/Canvas/Rulers/VerticalRuler.gd index d42cbf739..e21589832 100644 --- a/src/UI/Canvas/Rulers/VerticalRuler.gd +++ b/src/UI/Canvas/Rulers/VerticalRuler.gd @@ -11,6 +11,9 @@ var last: Vector2 func _ready() -> void: Global.project_switched.connect(queue_redraw) + Global.camera.zoom_changed.connect(queue_redraw) + Global.camera.rotation_changed.connect(queue_redraw) + Global.camera.offset_changed.connect(queue_redraw) func _gui_input(event: InputEvent) -> void: diff --git a/src/UI/Canvas/Selection.gd b/src/UI/Canvas/Selection.gd index b63dd9400..4e86d3a77 100644 --- a/src/UI/Canvas/Selection.gd +++ b/src/UI/Canvas/Selection.gd @@ -742,7 +742,7 @@ func paste(in_place := false) -> void: project.selection_offset = clipboard.selection_offset big_bounding_rectangle = clipboard.big_bounding_rectangle if not in_place: # If "Paste" is selected, and not "Paste in Place" - var camera_center := Global.camera.get_screen_center_position() + var camera_center := Global.camera.camera_screen_center camera_center -= Vector2(big_bounding_rectangle.size) / 2.0 var max_pos := project.size - big_bounding_rectangle.size if max_pos.x >= 0: diff --git a/src/UI/CanvasPreviewContainer/CanvasPreviewContainer.gd b/src/UI/CanvasPreviewContainer/CanvasPreviewContainer.gd index 26c355a49..cf8152770 100644 --- a/src/UI/CanvasPreviewContainer/CanvasPreviewContainer.gd +++ b/src/UI/CanvasPreviewContainer/CanvasPreviewContainer.gd @@ -2,7 +2,7 @@ extends PanelContainer @onready var preview_zoom_slider := $VBox/HBox/VBoxContainer/PreviewZoomSlider as VSlider @onready var canvas_preview := $"%CanvasPreview" as Node2D -@onready var camera := $"%CameraPreview" as Camera2D +@onready var camera := $"%CameraPreview" as CanvasCamera @onready var play_button := $"%PlayButton" as Button @onready var start_frame := $"%StartFrame" as ValueSlider @onready var end_frame := $"%EndFrame" as ValueSlider diff --git a/src/UI/CanvasPreviewContainer/CanvasPreviewContainer.tscn b/src/UI/CanvasPreviewContainer/CanvasPreviewContainer.tscn index 4a16a0818..715a8e54e 100644 --- a/src/UI/CanvasPreviewContainer/CanvasPreviewContainer.tscn +++ b/src/UI/CanvasPreviewContainer/CanvasPreviewContainer.tscn @@ -2,9 +2,9 @@ [ext_resource type="PackedScene" uid="uid://3pmb60gpst7b" path="res://src/UI/Nodes/TransparentChecker.tscn" id="1"] [ext_resource type="Shader" path="res://src/Shaders/TransparentChecker.gdshader" id="2"] -[ext_resource type="Script" path="res://src/UI/Canvas/CameraMovement.gd" id="3"] [ext_resource type="Script" path="res://src/UI/CanvasPreviewContainer/CanvasPreviewContainer.gd" id="4"] [ext_resource type="PackedScene" uid="uid://c546tskdu53j1" path="res://src/UI/Canvas/CanvasPreview.tscn" id="5"] +[ext_resource type="Script" path="res://src/UI/Canvas/CanvasCamera.gd" id="5_ge2km"] [ext_resource type="Texture2D" uid="uid://c7smxwfa8826j" path="res://assets/graphics/timeline/play.png" id="6"] [ext_resource type="Script" path="res://src/UI/Nodes/CollapsibleContainer.gd" id="7_o7sn3"] [ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="8"] @@ -77,11 +77,9 @@ anchors_preset = 0 [node name="CanvasPreview" parent="VBox/HBox/PreviewViewportContainer/SubViewport" instance=ExtResource("5")] unique_name_in_owner = true -[node name="CameraPreview" type="Camera2D" parent="VBox/HBox/PreviewViewportContainer/SubViewport"] +[node name="CameraPreview" type="Node2D" parent="VBox/HBox/PreviewViewportContainer/SubViewport"] unique_name_in_owner = true -offset = Vector2(32, 32) -zoom = Vector2(0.15, 0.15) -script = ExtResource("3") +script = ExtResource("5_ge2km") index = 2 [node name="Animation" type="HBoxContainer" parent="VBox"] diff --git a/src/UI/UI.tscn b/src/UI/UI.tscn index 2b4f7b70b..372fb3d2e 100644 --- a/src/UI/UI.tscn +++ b/src/UI/UI.tscn @@ -6,7 +6,7 @@ [ext_resource type="Script" path="res://src/UI/Canvas/Rulers/VerticalRuler.gd" id="4"] [ext_resource type="PackedScene" uid="uid://3pmb60gpst7b" path="res://src/UI/Nodes/TransparentChecker.tscn" id="5"] [ext_resource type="Script" path="res://src/UI/Canvas/Rulers/HorizontalRuler.gd" id="6"] -[ext_resource type="Script" path="res://src/UI/Canvas/CameraMovement.gd" id="7"] +[ext_resource type="Script" path="res://src/UI/Canvas/CanvasCamera.gd" id="7"] [ext_resource type="Shader" path="res://src/Shaders/Greyscale.gdshader" id="8"] [ext_resource type="Shader" path="res://src/Shaders/TransparentChecker.gdshader" id="9"] [ext_resource type="PackedScene" uid="uid://wo0hqxkst808" path="res://src/UI/GlobalToolOptions/GlobalToolOptions.tscn" id="10"] @@ -322,8 +322,7 @@ anchors_preset = 0 [node name="Canvas" parent="DockableContainer/Main Canvas/ViewportandVerticalRuler/SubViewportContainer/SubViewport" instance=ExtResource("19")] -[node name="Camera2D" type="Camera2D" parent="DockableContainer/Main Canvas/ViewportandVerticalRuler/SubViewportContainer/SubViewport"] -zoom = Vector2(0.15, 0.15) +[node name="Camera2D" type="Node2D" parent="DockableContainer/Main Canvas/ViewportandVerticalRuler/SubViewportContainer/SubViewport"] script = ExtResource("7") [node name="CanvasLayer" type="CanvasLayer" parent="DockableContainer/Main Canvas/ViewportandVerticalRuler/SubViewportContainer/SubViewport"] @@ -359,8 +358,7 @@ anchors_preset = 0 [node name="CanvasPreview" parent="DockableContainer/Second Canvas/SubViewport" instance=ExtResource("2")] -[node name="Camera2D2" type="Camera2D" parent="DockableContainer/Second Canvas/SubViewport"] -zoom = Vector2(0.15, 0.15) +[node name="Camera2D2" type="Node2D" parent="DockableContainer/Second Canvas/SubViewport"] script = ExtResource("7") index = 1 diff --git a/src/UI/ViewportContainer.gd b/src/UI/ViewportContainer.gd index 8fa787917..dde0bb6e8 100644 --- a/src/UI/ViewportContainer.gd +++ b/src/UI/ViewportContainer.gd @@ -2,7 +2,7 @@ extends SubViewportContainer @export var camera_path: NodePath -@onready var camera := get_node(camera_path) as Camera2D +@onready var camera := get_node(camera_path) as CanvasCamera func _ready() -> void: