Skip to content

Commit

Permalink
Canvas Rotation (#558)
Browse files Browse the repository at this point in the history
* Implemented canvas rotation

Co-authored-by: MrTriPie <MrTriPie>
  • Loading branch information
mrtripie committed Nov 13, 2021
1 parent 429b37f commit a9039cc
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 52 deletions.
12 changes: 8 additions & 4 deletions src/Autoload/Global.gd
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,11 @@ var horizontal_ruler : BaseButton
var vertical_ruler : BaseButton
var transparent_checker : ColorRect

var cursor_position_label : Label
var zoom_level_label : Label
var rotation_level_button : Button
var rotation_level_spinbox : SpinBox
var zoom_level_button : Button
var zoom_level_spinbox : SpinBox
var cursor_position_label : Label

var tool_panel : Panel
var right_panel : Panel
Expand Down Expand Up @@ -234,9 +236,11 @@ func _ready() -> void:
vertical_ruler = control.find_node("VerticalRuler")
transparent_checker = control.find_node("TransparentChecker")

cursor_position_label = control.find_node("CursorPosition")
zoom_level_label = control.find_node("ZoomLevel")
rotation_level_button = control.find_node("RotationLevel")
rotation_level_spinbox = control.find_node("RotationSpinbox")
zoom_level_button = control.find_node("ZoomLevel")
zoom_level_spinbox = control.find_node("ZoomSpinbox")
cursor_position_label = control.find_node("CursorPosition")

tool_panel = control.find_node("ToolPanel")
right_panel = control.find_node("RightPanel")
Expand Down
3 changes: 3 additions & 0 deletions src/Classes/Project.gd
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var selection_offset := Vector2.ZERO setget _selection_offset_changed
var has_selection := false

# For every camera (currently there are 3)
var cameras_rotation := [0.0, 0.0, 0.0] # Array of float
var cameras_zoom := [Vector2(0.15, 0.15), Vector2(0.15, 0.15), Vector2(0.15, 0.15)] # Array of Vector2
var cameras_offset := [Vector2.ZERO, Vector2.ZERO, Vector2.ZERO] # Array of Vector2
var cameras_zoom_max := [Vector2.ONE, Vector2.ONE, Vector2.ONE] # Array of Vector2
Expand Down Expand Up @@ -239,8 +240,10 @@ func change_project() -> void:

if camera == Global.camera:
Global.zoom_level_spinbox.min_value = 100.0/camera.zoom_max.x
camera.rotation = cameras_rotation[i]
camera.zoom = cameras_zoom[i]
camera.offset = cameras_offset[i]
camera.rotation_changed()
camera.zoom_changed()
i += 1

Expand Down
97 changes: 80 additions & 17 deletions src/UI/Canvas/CameraMovement.gd
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,54 @@ var drag := false


func _ready() -> void:
rotating = true
viewport_container = get_parent().get_parent()
transparent_checker = get_parent().get_node("TransparentChecker")
tween = Tween.new()
add_child(tween)
tween.connect("tween_step", self, "_on_tween_step")
update_transparent_checker_offset()

# signals regarding rotation stats
Global.rotation_level_button.connect("pressed",self,"rotation_button_pressed")
Global.rotation_level_spinbox.connect("value_changed", self, "rotation_value_changed")
Global.rotation_level_spinbox.get_child(0).connect("focus_exited", self, "rotation_focus_exited")

# signals regarding zoom stats
Global.zoom_level_label.connect("gui_input",self,"zoom_label_clicked")
Global.zoom_level_button.connect("pressed",self,"zoom_button_pressed")
Global.zoom_level_spinbox.connect("value_changed", self, "zoom_value_changed")
Global.zoom_level_spinbox.max_value = 100.0/zoom_min.x
Global.zoom_level_spinbox.get_child(0).connect("focus_exited", self, "zoom_focus_exited")


func zoom_label_clicked(event :InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
if event.doubleclick:
Global.zoom_level_label.visible = false
Global.zoom_level_spinbox.visible = true
Global.zoom_level_spinbox.editable = true
Global.zoom_level_spinbox.value = str2var(Global.zoom_level_label.text.replace("%",""))
Global.zoom_level_spinbox.get_child(0).grab_focus() #since the actual lineedit is the first child of spinbox
func rotation_button_pressed() -> void:
Global.rotation_level_button.visible = false
Global.rotation_level_spinbox.visible = true
Global.rotation_level_spinbox.editable = true
Global.rotation_level_spinbox.value = str2var(Global.rotation_level_button.text.replace("°",""))
Global.rotation_level_spinbox.get_child(0).grab_focus() #since the actual lineedit is the first child of spinbox


func rotation_value_changed(value) -> void:
if name == "Camera2D":
set_camera_rotation_degrees(-value) # Negative makes going up rotate clockwise


func rotation_focus_exited() -> void:
if Global.rotation_level_spinbox.value != rotation: #If user pressed enter while editing
if name == "Camera2D":
set_camera_rotation_degrees(-Global.rotation_level_spinbox.value) # Negative makes going up rotate clockwise
Global.rotation_level_button.visible = true
Global.rotation_level_spinbox.visible = false
Global.rotation_level_spinbox.editable = false


func zoom_button_pressed() -> void:
Global.zoom_level_button.visible = false
Global.zoom_level_spinbox.visible = true
Global.zoom_level_spinbox.editable = true
Global.zoom_level_spinbox.value = str2var(Global.zoom_level_button.text.replace("%",""))
Global.zoom_level_spinbox.get_child(0).grab_focus() #since the actual lineedit is the first child of spinbox


func zoom_value_changed(value) -> void:
Expand All @@ -44,7 +70,7 @@ func zoom_focus_exited() -> void:
if Global.zoom_level_spinbox.value != round(100 / zoom.x): #If user pressed enter while editing
if name == "Camera2D":
zoom_camera_percent(Global.zoom_level_spinbox.value)
Global.zoom_level_label.visible = true
Global.zoom_level_button.visible = true
Global.zoom_level_spinbox.visible = false
Global.zoom_level_spinbox.editable = false

Expand Down Expand Up @@ -136,7 +162,7 @@ func process_direction_action_pressed(event: InputEvent) -> void:
Global.key_move_press_time[dir] += increment
var this_direction_press_time : float = Global.key_move_press_time[dir]
var move_speed := dir_move_zoom_multiplier(this_direction_press_time)
offset = offset + move_speed * increment * directional_sign_multipliers[dir] * zoom
offset = offset + move_speed * increment * directional_sign_multipliers[dir].rotated(rotation) * zoom
update_rulers()
update_transparent_checker_offset()

Expand Down Expand Up @@ -168,11 +194,12 @@ func _input(event : InputEvent) -> void:
else:
zoom_camera(-1)
elif event is InputEventPanGesture: # Pan Gesture on a Latop touchpad
offset = offset + event.delta.rotated(rotation) * zoom * 7 # for moving the canvas
if OS.get_name() == "Android":
return
offset = offset + event.delta * zoom * 7 # for moving the canvas
elif event is InputEventMouseMotion && drag:
offset = offset - event.relative * zoom
offset = offset - event.relative.rotated(rotation) * zoom
update_transparent_checker_offset()
update_rulers()
elif is_action_direction_pressed(event):
Expand All @@ -182,6 +209,25 @@ func _input(event : InputEvent) -> void:

save_values_to_project()

# Rotate Camera
func rotate_camera_around_point(degrees: float, point: Vector2) -> void:
offset = (offset - point).rotated(deg2rad(degrees)) + point
rotation_degrees = wrapf(rotation_degrees + degrees, -180, 180)
rotation_changed()

func set_camera_rotation_degrees(degrees: float) -> void:
var difference := degrees - rotation_degrees
var canvas_center := Global.current_project.size / 2
offset = (offset - canvas_center).rotated(deg2rad(difference)) + canvas_center
rotation_degrees = wrapf(degrees, -180, 180)
rotation_changed()

func rotation_changed() -> void:
if name == "Camera2D":
# Negative to make going up in value clockwise, and match the spinbox which does the same
Global.rotation_level_button.text = str(wrapi(round(-rotation_degrees), -180, 180)) + " °"
update_rulers()


# Zoom Camera
func zoom_camera(dir : int) -> void:
Expand All @@ -190,7 +236,7 @@ func zoom_camera(dir : int) -> void:
var zoom_margin = zoom * dir / 5
var new_zoom = zoom + zoom_margin
if new_zoom > zoom_min && new_zoom < zoom_max:
var new_offset = offset + (-0.5 * viewport_size + mouse_pos) * (zoom - new_zoom)
var new_offset = offset + (-0.5 * viewport_size + mouse_pos).rotated(rotation) * (zoom - new_zoom)
tween.interpolate_property(self, "zoom", zoom, new_zoom, 0.05, Tween.TRANS_LINEAR, Tween.EASE_IN)
tween.interpolate_property(self, "offset", offset, new_offset, 0.05, Tween.TRANS_LINEAR, Tween.EASE_IN)
tween.start()
Expand All @@ -204,7 +250,7 @@ func zoom_camera(dir : int) -> void:
if zoom > zoom_max:
zoom = zoom_max

offset = offset + (-0.5 * viewport_size + mouse_pos) * (prev_zoom - zoom)
offset = offset + (-0.5 * viewport_size + mouse_pos).rotated(rotation) * (prev_zoom - zoom)
zoom_changed()


Expand All @@ -216,13 +262,13 @@ func zoom_camera_percent(value : float) -> void:
tween.start()
else:
zoom = new_zoom
zoom_changed()
zoom_changed()


func zoom_changed() -> void:
update_transparent_checker_offset()
if name == "Camera2D":
Global.zoom_level_label.text = str(round(100 / zoom.x)) + " %"
Global.zoom_level_button.text = str(round(100 / zoom.x)) + " %"
Global.canvas.pixel_grid.update()
update_rulers()
for guide in Global.current_project.guides:
Expand All @@ -243,13 +289,28 @@ func _on_tween_step(_object: Object, _key: NodePath, _elapsed: float, _value: Ob
zoom_changed()



func zoom_100() -> void:
zoom = Vector2.ONE
offset = Global.current_project.size / 2
zoom_changed()


func fit_to_frame(size : Vector2) -> void:
offset = size / 2

# Adjust to the rotated size:
if rotation != 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

# Find how far apart each opposite point is on each axis, and take the longer one
size.x = max(abs(a.x - d.x), abs(b.x - c.x))
size.y = max(abs(a.y - d.y), abs(b.y - c.y))

viewport_container = get_parent().get_parent()
var h_ratio := viewport_container.rect_size.x / size.x
var v_ratio := viewport_container.rect_size.y / size.y
Expand All @@ -269,20 +330,22 @@ func fit_to_frame(size : Vector2) -> void:

ratio = clamp(ratio, 0.1, ratio)
zoom = Vector2(1 / ratio, 1 / ratio)
offset = size / 2
zoom_changed()


func save_values_to_project() -> void:
if name == "Camera2D":
Global.current_project.cameras_rotation[0] = rotation
Global.current_project.cameras_zoom[0] = zoom
Global.current_project.cameras_offset[0] = offset
Global.current_project.cameras_zoom_max[0] = zoom_max
elif name == "Camera2D2":
Global.current_project.cameras_rotation[1] = rotation
Global.current_project.cameras_zoom[1] = zoom
Global.current_project.cameras_offset[1] = offset
Global.current_project.cameras_zoom_max[1] = zoom_max
elif name == "CameraPreview":
Global.current_project.cameras_rotation[2] = rotation
Global.current_project.cameras_zoom[2] = zoom
Global.current_project.cameras_offset[2] = offset
Global.current_project.cameras_zoom_max[2] = zoom_max
48 changes: 42 additions & 6 deletions src/UI/Canvas/Rulers/Guide.gd
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,48 @@ func _draw() -> void:
if has_focus:
var viewport_size: Vector2 = Global.main_viewport.rect_size
var zoom: Vector2 = Global.camera.zoom
if type == Types.HORIZONTAL:
draw_set_transform(Vector2(Global.camera.offset.x - (viewport_size.x / 2) * zoom.x, points[0].y + font.get_height() * zoom.x * 2), rotation, zoom * 2)
draw_string(font, Vector2.ZERO, "%spx" % str(stepify(mouse_pos.y, 0.5)))
else:
draw_set_transform(Vector2(points[0].x + font.get_height() * zoom.y, Global.camera.offset.y - (viewport_size.y / 2.25) * zoom.y), rotation, zoom * 2)
draw_string(font, Vector2.ZERO, "%spx" % str(stepify(mouse_pos.x, 0.5)))

# viewport_poly is an array of the points that make up the corners of the viewport
var viewport_poly:= [Vector2.ZERO, Vector2(viewport_size.x, 0), viewport_size, Vector2(0, viewport_size.y)]
# Adjusting viewport_poly to take into account the camera offset, zoom, and rotation
for p in range(viewport_poly.size()):
viewport_poly[p] = viewport_poly[p].rotated(Global.camera.rotation) * zoom + Vector2(Global.camera.offset.x - (viewport_size.rotated(Global.camera.rotation).x / 2) * zoom.x, Global.camera.offset.y - (viewport_size.rotated(Global.camera.rotation).y / 2) * zoom.y)

var string = "%spx" % str(stepify(mouse_pos.y if type == Types.HORIZONTAL else mouse_pos.x, 0.5))
# X and Y offsets for nicer looking spacing
var x_offset := 5
var y_offset := -7 # Only used where the string is above the guide

# Draw the string where the guide intersects with the viewport poly
# Priority is top edge, then left, then right
var intersection = Geometry.segment_intersects_segment_2d(points[0], points[1], viewport_poly[0], viewport_poly[1])
if intersection:
draw_set_transform(intersection, Global.camera.rotation, zoom * 2)
if intersection.distance_squared_to(viewport_poly[0]) < intersection.distance_squared_to(viewport_poly[1]):
draw_string(font, Vector2(x_offset, font.get_height()), string)
else:
draw_string(font, Vector2(-font.get_string_size(string).x - x_offset, font.get_height()), string)
return
intersection = Geometry.segment_intersects_segment_2d(points[0], points[1], viewport_poly[3], viewport_poly[0])
if intersection:
draw_set_transform(intersection, Global.camera.rotation, zoom * 2)
if intersection.distance_squared_to(viewport_poly[3]) < intersection.distance_squared_to(viewport_poly[0]):
draw_string(font, Vector2(x_offset, y_offset), string)
else:
draw_string(font, Vector2(x_offset, font.get_height()), string)
return
intersection = Geometry.segment_intersects_segment_2d(points[0], points[1], viewport_poly[1], viewport_poly[2])
if intersection:
draw_set_transform(intersection, Global.camera.rotation, zoom * 2)
if intersection.distance_squared_to(viewport_poly[1]) < intersection.distance_squared_to(viewport_poly[2]):
draw_string(font, Vector2(-font.get_string_size(string).x - x_offset, font.get_height()), string)
else:
draw_string(font, Vector2(-font.get_string_size(string).x - x_offset, y_offset), string)
return

# If there's no intersection with a viewport edge, show string in top left corner
draw_set_transform(viewport_poly[0], Global.camera.rotation, zoom * 2)
draw_string(font, Vector2(x_offset, font.get_height()), string)


func outside_canvas() -> bool:
Expand Down
26 changes: 19 additions & 7 deletions src/UI/Canvas/Rulers/HorizontalRuler.gd
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ func _draw() -> void:
var zoom: float = 1 / Global.camera.zoom.x
transform.x = Vector2(zoom, zoom)

transform.origin = Global.main_viewport.rect_size / 2 + Global.camera.offset * -zoom
# This tracks the "true" top left corner of the drawing:
transform.origin = Global.main_viewport.rect_size / 2 + Global.camera.offset.rotated(-Global.camera.rotation) * -zoom

var proj_size := Global.current_project.size

# Calculating the rotated corners of the image, use min to find the farthest left
var a := Vector2.ZERO # Top left
var b := Vector2(proj_size.x, 0).rotated(-Global.camera.rotation) # Top right
var c := Vector2(0, proj_size.y).rotated(-Global.camera.rotation) # Bottom left
var d := Vector2(proj_size.x, proj_size.y).rotated(-Global.camera.rotation) # Bottom right
transform.origin.x += min(min(a.x, b.x), min(c.x, d.x)) * zoom

var basic_rule := 100.0
var i := 0
Expand Down Expand Up @@ -63,12 +73,14 @@ func _on_HorizontalRuler_pressed() -> void:
if mouse_pos.x < RULER_WIDTH: # For double guides
Global.vertical_ruler._on_VerticalRuler_pressed()
var guide := Guide.new()
guide.type = guide.Types.HORIZONTAL
guide.add_point(Vector2(-19999, Global.canvas.current_pixel.y))
guide.add_point(Vector2(19999, Global.canvas.current_pixel.y))
if guide.points.size() < 2:
guide.queue_free()
return
if abs(Global.camera.rotation_degrees) < 45 or abs(Global.camera.rotation_degrees) > 135:
guide.type = guide.Types.HORIZONTAL
guide.add_point(Vector2(-19999, Global.canvas.current_pixel.y))
guide.add_point(Vector2(19999, Global.canvas.current_pixel.y))
else:
guide.type = guide.Types.VERTICAL
guide.add_point(Vector2(Global.canvas.current_pixel.x, -19999))
guide.add_point(Vector2(Global.canvas.current_pixel.x, 19999))
Global.canvas.add_child(guide)
Global.has_focus = false
update()
Expand Down
26 changes: 19 additions & 7 deletions src/UI/Canvas/Rulers/VerticalRuler.gd
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ func _draw() -> void:
var zoom: float = 1 / Global.camera.zoom.x
transform.y = Vector2(zoom, zoom)

transform.origin = Global.main_viewport.rect_size / 2 + Global.camera.offset * -zoom
# This tracks the "true" top left corner of the drawing:
transform.origin = Global.main_viewport.rect_size / 2 + Global.camera.offset.rotated(-Global.camera.rotation) * -zoom

var proj_size := Global.current_project.size

# Calculating the rotated corners of the image, use min to find the top one
var a := Vector2.ZERO # Top left
var b := Vector2(proj_size.x, 0).rotated(-Global.camera.rotation) # Top right
var c := Vector2(0, proj_size.y).rotated(-Global.camera.rotation) # Bottom left
var d := Vector2(proj_size.x, proj_size.y).rotated(-Global.camera.rotation) # Bottom right
transform.origin.y += min(min(a.y, b.y), min(c.y, d.y)) * zoom

var basic_rule := 100.0
var i := 0
Expand Down Expand Up @@ -63,12 +73,14 @@ func _on_VerticalRuler_pressed() -> void:
if !Global.show_guides:
return
var guide := Guide.new()
guide.type = guide.Types.VERTICAL
guide.add_point(Vector2(Global.canvas.current_pixel.x, -19999))
guide.add_point(Vector2(Global.canvas.current_pixel.x, 19999))
if guide.points.size() < 2:
guide.queue_free()
return
if abs(Global.camera.rotation_degrees) < 45 or abs(Global.camera.rotation_degrees) > 135:
guide.type = guide.Types.VERTICAL
guide.add_point(Vector2(Global.canvas.current_pixel.x, -19999))
guide.add_point(Vector2(Global.canvas.current_pixel.x, 19999))
else:
guide.type = guide.Types.HORIZONTAL
guide.add_point(Vector2(-19999, Global.canvas.current_pixel.y))
guide.add_point(Vector2(19999, Global.canvas.current_pixel.y))
Global.canvas.add_child(guide)
Global.has_focus = false
update()
2 changes: 1 addition & 1 deletion src/UI/Canvas/Selection.gd
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func move_with_arrow_keys(event : InputEvent) -> void:
var step := Vector2.ONE
if Input.is_key_pressed(KEY_CONTROL):
step = Vector2(Global.grid_width, Global.grid_height)
move_content(Vector2(int(event.is_action("ui_right")) - int(event.is_action("ui_left")), int(event.is_action("ui_down")) - int(event.is_action("ui_up"))) * step)
move_content(Vector2(int(event.is_action("ui_right")) - int(event.is_action("ui_left")), int(event.is_action("ui_down")) - int(event.is_action("ui_up"))).rotated(stepify(Global.camera.rotation, PI / 2)) * step)


# Check if an event is a ui_up/down/left/right event-press
Expand Down
Loading

0 comments on commit a9039cc

Please sign in to comment.