1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 17:19:50 +00:00

Refactoring tools (#281)

* Refactoring tools

* Remove unused code

* Fixed some inferring errors and added translations

* Attempt to fix some Script Errors found in the CI workflow

* Fix bucket crash.

* Fix static type convert.

Co-authored-by: OverloadedOrama <35376950+OverloadedOrama@users.noreply.github.com>
This commit is contained in:
Kinwailo 2020-07-09 20:22:17 +08:00 committed by GitHub
parent e1724148fc
commit 4a668f71f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 2489 additions and 2389 deletions

View file

@ -97,6 +97,15 @@ msgstr ""
msgid "Redo"
msgstr ""
msgid "Copy"
msgstr ""
msgid "Paste"
msgstr ""
msgid "Delete"
msgstr ""
msgid "Scale Image"
msgstr ""

View file

@ -14,6 +14,11 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://src/Classes/AnimationTag.gd"
}, {
"base": "Popup",
"class": "Brushes",
"language": "GDScript",
"path": "res://src/UI/BrushesPopup.gd"
}, {
"base": "Node2D",
"class": "Canvas",
"language": "GDScript",
@ -59,6 +64,11 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://src/Palette/PaletteColor.gd"
}, {
"base": "PopupPanel",
"class": "Patterns",
"language": "GDScript",
"path": "res://src/UI/PatternsPopup.gd"
}, {
"base": "Reference",
"class": "Project",
"language": "GDScript",
@ -66,6 +76,7 @@ _global_script_classes=[ {
} ]
_global_script_class_icons={
"AnimationTag": "",
"Brushes": "",
"Canvas": "",
"Cel": "",
"Drawer": "",
@ -75,6 +86,7 @@ _global_script_class_icons={
"LayerButton": "",
"Palette": "",
"PaletteColor": "",
"Patterns": "",
"Project": ""
}
@ -97,6 +109,7 @@ Global="*res://src/Autoload/Global.gd"
Import="*res://src/Autoload/Import.gd"
OpenSave="*res://src/Autoload/OpenSave.gd"
DrawingAlgos="*res://src/Autoload/DrawingAlgos.gd"
Tools="*res://src/Autoload/Tools.gd"
Html5FileExchange="*res://src/Autoload/HTML5FileExchange.gd"
[debug]

View file

@ -1,336 +1,6 @@
extends Node
var drawer := Drawer.new()
var mouse_press_pixels := [] # Cleared after mouse release
var mouse_press_pressure_values := [] # Cleared after mouse release
func reset() -> void:
drawer.reset()
mouse_press_pixels.clear()
mouse_press_pressure_values.clear()
func draw_pixel_blended(sprite : Image, pos : Vector2, color : Color, pen_pressure : float, current_mouse_button := -1, current_action := -1) -> void:
var x_min = Global.current_project.x_min
var x_max = Global.current_project.x_max
var y_min = Global.current_project.y_min
var y_max = Global.current_project.y_max
# Check if Tiling is enabled and whether mouse is in TilingPreviews
if Global.tile_mode and point_in_rectangle(pos,Vector2( - Global.current_project.size.x - 1 , - Global.current_project.size.y -1 ), Vector2(2 * Global.current_project.size.x, 2 * Global.current_project.size.y)):
pos = pos.posmodv(Global.current_project.size)
if !point_in_rectangle(pos, Vector2(x_min - 1, y_min - 1), Vector2(x_max, y_max)):
return
var pos_floored := pos.floor()
var current_pixel_color = sprite.get_pixelv(pos)
var saved_pixel_index := mouse_press_pixels.find(pos_floored)
if current_action == Global.Tools.PENCIL && color.a < 1:
color = blend_colors(color, current_pixel_color)
if current_pixel_color != color && (saved_pixel_index == -1 || pen_pressure > mouse_press_pressure_values[saved_pixel_index]):
if current_action == Global.Tools.LIGHTENDARKEN:
var ld : int = Global.ld_modes[current_mouse_button]
var ld_amount : float = Global.ld_amounts[current_mouse_button]
if ld == Global.Lighten_Darken_Mode.LIGHTEN:
color = current_pixel_color.lightened(ld_amount)
else:
color = current_pixel_color.darkened(ld_amount)
if saved_pixel_index == -1:
mouse_press_pixels.append(pos_floored)
mouse_press_pressure_values.append(pen_pressure)
else:
mouse_press_pressure_values[saved_pixel_index] = pen_pressure
drawer.set_pixel(sprite, pos, color)
func draw_brush(sprite : Image, pos : Vector2, color : Color, current_mouse_button : int, pen_pressure : float, current_action := -1) -> void:
if Global.can_draw && Global.has_focus:
var x_min = Global.current_project.x_min
var x_max = Global.current_project.x_max
var y_min = Global.current_project.y_min
var y_max = Global.current_project.y_max
if Global.pressure_sensitivity_mode == Global.Pressure_Sensitivity.ALPHA:
if current_action == Global.Tools.PENCIL:
color.a *= pen_pressure
elif current_action == Global.Tools.ERASER: # This is not working
color.a *= (1.0 - pen_pressure)
var brush_size : int = Global.brush_sizes[current_mouse_button]
var brush_type : int = Global.current_brush_types[current_mouse_button]
var horizontal_mirror : bool = Global.horizontal_mirror[current_mouse_button]
var vertical_mirror : bool = Global.vertical_mirror[current_mouse_button]
var pixel_perfect : bool = Global.pixel_perfect[current_mouse_button]
drawer.pixel_perfect = pixel_perfect if brush_size == 1 else false
drawer.h_mirror = horizontal_mirror
drawer.v_mirror = vertical_mirror
if brush_type == Global.Brush_Types.PIXEL || current_action == Global.Tools.LIGHTENDARKEN:
var start_pos_x = floor(pos.x - (brush_size >> 1))
var start_pos_y = floor(pos.y - (brush_size >> 1))
var end_pos_x = floor(start_pos_x + brush_size)
var end_pos_y = floor(start_pos_y + brush_size)
for cur_pos_x in range(start_pos_x, end_pos_x):
for cur_pos_y in range(start_pos_y, end_pos_y):
draw_pixel_blended(sprite, Vector2(cur_pos_x, cur_pos_y), color, pen_pressure, current_mouse_button, current_action)
Global.canvas.sprite_changed_this_frame = true
elif brush_type == Global.Brush_Types.CIRCLE || brush_type == Global.Brush_Types.FILLED_CIRCLE:
plot_circle(sprite, pos.x, pos.y, brush_size, color, pen_pressure, brush_type == Global.Brush_Types.FILLED_CIRCLE)
Global.canvas.sprite_changed_this_frame = true
else:
var brush_index : int = Global.custom_brush_indexes[current_mouse_button]
var custom_brush_image : Image
if brush_type != Global.Brush_Types.RANDOM_FILE:
custom_brush_image = Global.brush_images[current_mouse_button]
else: # Handle random brush
var brush_button = Global.file_brush_container.get_child(brush_index + 3)
var random_index = randi() % brush_button.random_brushes.size()
custom_brush_image = Image.new()
custom_brush_image.copy_from(brush_button.random_brushes[random_index])
var custom_brush_size = custom_brush_image.get_size()
custom_brush_image.resize(custom_brush_size.x * brush_size, custom_brush_size.y * brush_size, Image.INTERPOLATE_NEAREST)
custom_brush_image = Global.blend_image_with_color(custom_brush_image, color, Global.interpolate_spinboxes[current_mouse_button].value / 100)
custom_brush_image.lock()
var custom_brush_size := custom_brush_image.get_size() - Vector2.ONE
pos = pos.floor()
# #Check if Tiling is enabled and whether mouse is in TilingPreviews
if Global.tile_mode and point_in_rectangle(pos,Vector2( - Global.current_project.size.x - 1 , - Global.current_project.size.y -1 ), Vector2(2 * Global.current_project.size.x, 2 * Global.current_project.size.y)):
pos = pos.posmodv(Global.current_project.size)
var dst := rectangle_center(pos, custom_brush_size)
var src_rect := Rect2(Vector2.ZERO, custom_brush_size + Vector2.ONE)
# Rectangle with the same size as the brush, but at cursor's position
var pos_rect := Rect2(dst, custom_brush_size + Vector2.ONE)
# The selection rectangle
# If there's no rectangle, the whole canvas is considered a selection
var selection_rect := Rect2()
selection_rect.position = Vector2(x_min, y_min)
selection_rect.end = Vector2(x_max, y_max)
# Intersection of the position rectangle and selection
var pos_rect_clipped := pos_rect.clip(selection_rect)
# If the size is 0, that means that the brush wasn't positioned inside the selection
if pos_rect_clipped.size == Vector2.ZERO:
return
# Re-position src_rect and dst based on the clipped position
var pos_difference := (pos_rect.position - pos_rect_clipped.position).abs()
# Obviously, if pos_rect and pos_rect_clipped are the same, pos_difference is Vector2.ZERO
src_rect.position = pos_difference
dst += pos_difference
src_rect.end -= pos_rect.end - pos_rect_clipped.end
# If the selection rectangle is smaller than the brush, ...
# ... make sure pixels aren't being drawn outside the selection by adjusting src_rect's size
src_rect.size.x = min(src_rect.size.x, selection_rect.size.x)
src_rect.size.y = min(src_rect.size.y, selection_rect.size.y)
# Handle mirroring
var mirror_x = x_max + x_min - pos.x - (pos.x - dst.x)
var mirror_y = y_max + y_min - pos.y - (pos.y - dst.y)
if int(pos_rect_clipped.size.x) % 2 != 0:
mirror_x -= 1
if int(pos_rect_clipped.size.y) % 2 != 0:
mirror_y -= 1
# Use custom blend function cause of godot's issue #31124
if color.a > 0: # If it's the pencil
sprite.blend_rect(custom_brush_image, src_rect, dst)
if horizontal_mirror:
sprite.blend_rect(custom_brush_image, src_rect, Vector2(mirror_x, dst.y))
if vertical_mirror:
sprite.blend_rect(custom_brush_image, src_rect, Vector2(dst.x, mirror_y))
if horizontal_mirror && vertical_mirror:
sprite.blend_rect(custom_brush_image, src_rect, Vector2(mirror_x, mirror_y))
else: # if it's transparent - if it's the eraser
var custom_brush := Image.new()
if brush_type == Global.Brush_Types.CUSTOM:
custom_brush.copy_from(Global.current_project.brushes[brush_index])
else:
custom_brush.copy_from(Global.file_brushes[brush_index])
custom_brush_size = custom_brush.get_size()
custom_brush.resize(custom_brush_size.x * brush_size, custom_brush_size.y * brush_size, Image.INTERPOLATE_NEAREST)
var custom_brush_blended = Global.blend_image_with_color(custom_brush, color, 1)
sprite.blit_rect_mask(custom_brush_blended, custom_brush, src_rect, dst)
if horizontal_mirror:
sprite.blit_rect_mask(custom_brush_blended, custom_brush, src_rect, Vector2(mirror_x, dst.y))
if vertical_mirror:
sprite.blit_rect_mask(custom_brush_blended, custom_brush, src_rect, Vector2(dst.x, mirror_y))
if horizontal_mirror && vertical_mirror:
sprite.blit_rect_mask(custom_brush_blended, custom_brush, src_rect, Vector2(mirror_x, mirror_y))
sprite.lock()
Global.canvas.sprite_changed_this_frame = true
Global.canvas.previous_mouse_pos_for_lines = pos.floor() + Vector2(0.5, 0.5)
Global.canvas.previous_mouse_pos_for_lines.x = clamp(Global.canvas.previous_mouse_pos_for_lines.x, Global.canvas.location.x, Global.canvas.location.x + Global.current_project.size.x)
Global.canvas.previous_mouse_pos_for_lines.y = clamp(Global.canvas.previous_mouse_pos_for_lines.y, Global.canvas.location.y, Global.canvas.location.y + Global.current_project.size.y)
if Global.canvas.is_making_line:
Global.canvas.line_pos[0] = Global.canvas.previous_mouse_pos_for_lines
# Bresenham's Algorithm
# Thanks to https://godotengine.org/qa/35276/tile-based-line-drawing-algorithm-efficiency
func fill_gaps(sprite : Image, end_pos : Vector2, start_pos : Vector2, color : Color, current_mouse_button : int, pen_pressure : float, current_action := -1) -> void:
var previous_mouse_pos_floored = start_pos.floor()
var mouse_pos_floored = end_pos.floor()
var dx := int(abs(mouse_pos_floored.x - previous_mouse_pos_floored.x))
var dy := int(-abs(mouse_pos_floored.y - previous_mouse_pos_floored.y))
var err := dx + dy
var e2 := err << 1 # err * 2
var sx = 1 if previous_mouse_pos_floored.x < mouse_pos_floored.x else -1
var sy = 1 if previous_mouse_pos_floored.y < mouse_pos_floored.y else -1
var x = previous_mouse_pos_floored.x
var y = previous_mouse_pos_floored.y
while !(x == mouse_pos_floored.x && y == mouse_pos_floored.y):
draw_brush(sprite, Vector2(x, y), color, current_mouse_button, pen_pressure, current_action)
e2 = err << 1
if e2 >= dy:
err += dy
x += sx
if e2 <= dx:
err += dx
y += sy
# Algorithm based on http://members.chello.at/easyfilter/bresenham.html
func plot_circle(sprite : Image, xm : int, ym : int, r : int, color : Color, pen_pressure : float, fill := false) -> void:
var radius := r # Used later for filling
var x := -r
var y := 0
var err := 2 - r * 2 # II. Quadrant
while x < 0:
var quadrant_1 := Vector2(xm - x, ym + y)
var quadrant_2 := Vector2(xm - y, ym - x)
var quadrant_3 := Vector2(xm + x, ym - y)
var quadrant_4 := Vector2(xm + y, ym + x)
draw_pixel_blended(sprite, quadrant_1, color, pen_pressure)
draw_pixel_blended(sprite, quadrant_2, color, pen_pressure)
draw_pixel_blended(sprite, quadrant_3, color, pen_pressure)
draw_pixel_blended(sprite, quadrant_4, color, pen_pressure)
r = err
if r <= y:
y += 1
err += y * 2 + 1
if r > x || err > y:
x += 1
err += x * 2 + 1
if fill:
for j in range (-radius, radius + 1):
for i in range (-radius, radius + 1):
if i * i + j * j <= radius * radius:
var draw_pos := Vector2(i + xm, j + ym)
draw_pixel_blended(sprite, draw_pos, color, Global.canvas.pen_pressure)
# Thanks to https://en.wikipedia.org/wiki/Flood_fill
func flood_fill(sprite : Image, pos : Vector2, target_color : Color, replace_color : Color) -> void:
var x_min = Global.current_project.x_min
var x_max = Global.current_project.x_max
var y_min = Global.current_project.y_min
var y_max = Global.current_project.y_max
pos = pos.floor()
var pixel = sprite.get_pixelv(pos)
if target_color == replace_color:
return
elif pixel != target_color:
return
else:
if !point_in_rectangle(pos, Vector2(x_min - 1, y_min - 1), Vector2(x_max, y_max)):
return
var q = [pos]
for n in q:
# If the difference in colors is very small, break the loop (thanks @azagaya on GitHub!)
if target_color == replace_color:
break
var west : Vector2 = n
var east : Vector2 = n
while west.x >= x_min && sprite.get_pixelv(west) == target_color:
west += Vector2.LEFT
while east.x < x_max && sprite.get_pixelv(east) == target_color:
east += Vector2.RIGHT
for px in range(west.x + 1, east.x):
var p := Vector2(px, n.y)
# Draw
sprite.set_pixelv(p, replace_color)
replace_color = sprite.get_pixelv(p)
var north := p + Vector2.UP
var south := p + Vector2.DOWN
if north.y >= y_min && sprite.get_pixelv(north) == target_color:
q.append(north)
if south.y < y_max && sprite.get_pixelv(south) == target_color:
q.append(south)
Global.canvas.sprite_changed_this_frame = true
func pattern_fill(sprite : Image, pos : Vector2, pattern : Image, target_color : Color, var offset : Vector2) -> void:
var x_min = Global.current_project.x_min
var x_max = Global.current_project.x_max
var y_min = Global.current_project.y_min
var y_max = Global.current_project.y_max
pos = pos.floor()
if !point_in_rectangle(pos, Vector2(x_min - 1, y_min - 1), Vector2(x_max, y_max)):
return
pattern.lock()
var pattern_size := pattern.get_size()
var q = [pos]
for n in q:
var west : Vector2 = n
var east : Vector2 = n
while west.x >= x_min && sprite.get_pixelv(west) == target_color:
west += Vector2.LEFT
while east.x < x_max && sprite.get_pixelv(east) == target_color:
east += Vector2.RIGHT
for px in range(west.x + 1, east.x):
var p := Vector2(px, n.y)
var xx : int = int(px + offset.x) % int(pattern_size.x)
var yy : int = int(n.y + offset.y) % int(pattern_size.y)
var pattern_color : Color = pattern.get_pixel(xx, yy)
if pattern_color == target_color:
continue
sprite.set_pixelv(p, pattern_color)
var north := p + Vector2.UP
var south := p + Vector2.DOWN
if north.y >= y_min && sprite.get_pixelv(north) == target_color:
q.append(north)
if south.y < y_max && sprite.get_pixelv(south) == target_color:
q.append(south)
pattern.unlock()
Global.canvas.sprite_changed_this_frame = true
func blend_colors(color_1 : Color, color_2 : Color) -> Color:
var color := Color()
color.a = color_1.a + color_2.a * (1 - color_1.a) # Blend alpha
if color.a != 0:
# Blend colors
color.r = (color_1.r * color_1.a + color_2.r * color_2.a * (1-color_1.a)) / color.a
color.g = (color_1.g * color_1.a + color_2.g * color_2.a * (1-color_1.a)) / color.a
color.b = (color_1.b * color_1.a + color_2.b * color_2.a * (1-color_1.a)) / color.a
return color
func scale3X(sprite : Image, tol : float = 50) -> Image:
var scaled = Image.new()
scaled.create(sprite.get_width()*3, sprite.get_height()*3, false, Image.FORMAT_RGBA8)
@ -829,13 +499,3 @@ func adjust_hsv(img: Image, id : int, delta : float) -> void:
img.set_pixel(i,j,c)
img.unlock()
# Checks if a point is inside a rectangle
func point_in_rectangle(p : Vector2, coord1 : Vector2, coord2 : Vector2) -> bool:
return p.x > coord1.x && p.y > coord1.y && p.x < coord2.x && p.y < coord2.y
# Returns the position in the middle of a rectangle
func rectangle_center(rect_position : Vector2, rect_size : Vector2) -> Vector2:
return (rect_position - rect_size / 2).floor()

View file

@ -3,15 +3,8 @@ extends Node
enum Grid_Types {CARTESIAN, ISOMETRIC, ALL}
enum Pressure_Sensitivity {NONE, ALPHA, SIZE, ALPHA_AND_SIZE}
enum Brush_Types {PIXEL, CIRCLE, FILLED_CIRCLE, FILE, RANDOM_FILE, CUSTOM}
enum Direction {UP, DOWN, LEFT, RIGHT}
enum Mouse_Button {LEFT, RIGHT}
enum Tools {PENCIL, ERASER, BUCKET, LIGHTENDARKEN, RECTSELECT, COLORPICKER, ZOOM}
enum Theme_Types {DARK, BLUE, CARAMEL, LIGHT}
enum Fill_Area {SAME_COLOR_AREA, SAME_COLOR_PIXELS}
enum Fill_With {COLOR, PATTERN}
enum Lighten_Darken_Mode {LIGHTEN, DARKEN}
enum Zoom_Mode {ZOOM_IN, ZOOM_OUT}
# Stuff for arrowkey-based canvas movements nyaa ^.^
const low_speed_move_rate := 150.0
@ -44,8 +37,8 @@ var pressure_sensitivity_mode = Pressure_Sensitivity.NONE
var open_last_project := false
var smooth_zoom := true
var cursor_image = preload("res://assets/graphics/cursor_icons/cursor.png")
var left_cursor_tool_texture : ImageTexture
var right_cursor_tool_texture : ImageTexture
var left_cursor_tool_texture := ImageTexture.new()
var right_cursor_tool_texture := ImageTexture.new()
var image_clipboard : Image
var play_only_tags := true
@ -68,27 +61,11 @@ var autosave_interval := 5.0
var enable_autosave := true
# Tools & options
var current_tools := [Tools.PENCIL, Tools.ERASER]
var show_left_tool_icon := true
var show_right_tool_icon := true
var left_square_indicator_visible := true
var right_square_indicator_visible := false
var fill_areas := [Fill_Area.SAME_COLOR_AREA, Fill_Area.SAME_COLOR_AREA]
var fill_with := [Fill_With.COLOR, Fill_With.COLOR]
var fill_pattern_offsets := [Vector2.ZERO, Vector2.ZERO]
var ld_modes := [Lighten_Darken_Mode.LIGHTEN, Lighten_Darken_Mode.LIGHTEN]
var ld_amounts := [0.1, 0.1]
var color_picker_for := [Mouse_Button.LEFT, Mouse_Button.RIGHT]
var zoom_modes := [Zoom_Mode.ZOOM_IN, Zoom_Mode.ZOOM_OUT]
var horizontal_mirror := [false, false]
var vertical_mirror := [false, false]
var pixel_perfect := [false, false]
# View menu options
var tile_mode := false
var draw_grid := false
@ -102,25 +79,6 @@ var onion_skinning_past_rate := 1.0
var onion_skinning_future_rate := 1.0
var onion_skinning_blue_red := false
# Brushes
var file_brushes := []
var brush_sizes := [1, 1]
var current_brush_types := [Brush_Types.PIXEL, Brush_Types.PIXEL]
var brush_images := [Image.new(), Image.new()]
var brush_textures := [ImageTexture.new(), ImageTexture.new()]
var brush_type_window_position : int = Mouse_Button.LEFT
var left_circle_points := []
var right_circle_points := []
var brushes_from_files := 0
var custom_brush_indexes := [-1, -1]
# Patterns
var patterns := []
var pattern_window_position : int = Mouse_Button.LEFT
var pattern_images := [Image.new(), Image.new()]
# Palettes
var palettes := {}
@ -158,41 +116,11 @@ var export_dialog : AcceptDialog
var preferences_dialog : AcceptDialog
var unsaved_changes_dialog : ConfirmationDialog
var color_pickers := []
var color_switch_button : BaseButton
var tool_options_containers := []
var brush_type_containers := []
var brush_type_buttons := []
var brushes_popup : Popup
var file_brush_container : GridContainer
var project_brush_container : GridContainer
var patterns_popup : Popup
var brush_size_edits := []
var brush_size_sliders := []
var pixel_perfect_containers := []
var color_interpolation_containers := []
var interpolate_spinboxes := []
var interpolate_sliders := []
var fill_area_containers := []
var fill_pattern_containers := []
var ld_containers := []
var ld_amount_sliders := []
var ld_amount_spinboxes := []
var colorpicker_containers := []
var zoom_containers := []
var mirror_containers := []
var animation_timeline : Panel
var animation_timer : Timer
@ -251,21 +179,6 @@ func _ready() -> void:
right_cursor = find_node_by_name(root, "RightCursor")
canvas = find_node_by_name(root, "Canvas")
var pencil_cursor_image = preload("res://assets/graphics/cursor_icons/pencil_cursor.png")
var eraser_cursor_image = preload("res://assets/graphics/cursor_icons/eraser_cursor.png")
left_cursor_tool_texture = ImageTexture.new()
if pencil_cursor_image is Image:
left_cursor_tool_texture.create_from_image(pencil_cursor_image)
elif pencil_cursor_image is ImageTexture:
left_cursor_tool_texture.create_from_image(pencil_cursor_image.get_data())
right_cursor_tool_texture = ImageTexture.new()
if eraser_cursor_image is Image:
right_cursor_tool_texture.create_from_image(eraser_cursor_image)
elif eraser_cursor_image is ImageTexture:
right_cursor_tool_texture.create_from_image(eraser_cursor_image.get_data())
tabs = find_node_by_name(root, "Tabs")
main_viewport = find_node_by_name(root, "ViewportContainer")
second_viewport = find_node_by_name(root, "ViewportContainer2")
@ -294,58 +207,11 @@ func _ready() -> void:
preferences_dialog = find_node_by_name(root, "PreferencesDialog")
unsaved_changes_dialog = find_node_by_name(root, "UnsavedCanvasDialog")
tool_options_containers.append(find_node_by_name(root, "LeftToolOptions"))
tool_options_containers.append(find_node_by_name(root, "RightToolOptions"))
color_pickers.append(find_node_by_name(root, "LeftColorPickerButton"))
color_pickers.append(find_node_by_name(root, "RightColorPickerButton"))
color_switch_button = find_node_by_name(root, "ColorSwitch")
brush_type_containers.append(find_node_by_name(tool_options_containers[0], "LeftBrushType"))
brush_type_containers.append(find_node_by_name(tool_options_containers[1], "RightBrushType"))
brush_type_buttons.append(find_node_by_name(brush_type_containers[0], "LeftBrushTypeButton"))
brush_type_buttons.append(find_node_by_name(brush_type_containers[1], "RightBrushTypeButton"))
brushes_popup = find_node_by_name(root, "BrushesPopup")
file_brush_container = find_node_by_name(brushes_popup, "FileBrushContainer")
project_brush_container = find_node_by_name(brushes_popup, "ProjectBrushContainer")
patterns_popup = find_node_by_name(root, "PatternsPopup")
brush_size_edits.append(find_node_by_name(root, "LeftBrushSizeEdit"))
brush_size_sliders.append(find_node_by_name(root, "LeftBrushSizeSlider"))
brush_size_edits.append(find_node_by_name(root, "RightBrushSizeEdit"))
brush_size_sliders.append(find_node_by_name(root, "RightBrushSizeSlider"))
pixel_perfect_containers.append(find_node_by_name(root, "LeftBrushPixelPerfectMode"))
pixel_perfect_containers.append(find_node_by_name(root, "RightBrushPixelPerfectMode"))
color_interpolation_containers.append(find_node_by_name(root, "LeftColorInterpolation"))
color_interpolation_containers.append(find_node_by_name(root, "RightColorInterpolation"))
interpolate_spinboxes.append(find_node_by_name(root, "LeftInterpolateFactor"))
interpolate_sliders.append(find_node_by_name(root, "LeftInterpolateSlider"))
interpolate_spinboxes.append(find_node_by_name(root, "RightInterpolateFactor"))
interpolate_sliders.append(find_node_by_name(root, "RightInterpolateSlider"))
fill_area_containers.append(find_node_by_name(root, "LeftFillArea"))
fill_pattern_containers.append(find_node_by_name(root, "LeftFillPattern"))
fill_area_containers.append(find_node_by_name(root, "RightFillArea"))
fill_pattern_containers.append(find_node_by_name(root, "RightFillPattern"))
ld_containers.append(find_node_by_name(root, "LeftLDOptions"))
ld_amount_sliders.append(find_node_by_name(root, "LeftLDAmountSlider"))
ld_amount_spinboxes.append(find_node_by_name(root, "LeftLDAmountSpinbox"))
ld_containers.append(find_node_by_name(root, "RightLDOptions"))
ld_amount_sliders.append(find_node_by_name(root, "RightLDAmountSlider"))
ld_amount_spinboxes.append(find_node_by_name(root, "RightLDAmountSpinbox"))
colorpicker_containers.append(find_node_by_name(root, "LeftColorPickerOptions"))
colorpicker_containers.append(find_node_by_name(root, "RightColorPickerOptions"))
zoom_containers.append(find_node_by_name(root, "LeftZoomOptions"))
zoom_containers.append(find_node_by_name(root, "RightZoomOptions"))
mirror_containers.append(find_node_by_name(root, "LeftMirrorButtons"))
mirror_containers.append(find_node_by_name(root, "RightMirrorButtons"))
animation_timeline = find_node_by_name(root, "AnimationTimeline")
layers_container = find_node_by_name(animation_timeline, "LayersContainer")
@ -605,143 +471,6 @@ Hold %s to make a line""") % [InputMap.get_action_list("left_eraser_tool")[0].as
(%s)""") % InputMap.get_action_list("go_to_last_frame")[0].as_text()
func create_brush_button(brush_img : Image, brush_type := Brush_Types.CUSTOM, hint_tooltip := "") -> void:
var brush_container
var brush_button = load("res://src/UI/BrushButton.tscn").instance()
brush_button.brush_type = brush_type
if brush_type == Brush_Types.FILE || brush_type == Brush_Types.RANDOM_FILE:
brush_button.custom_brush_index = file_brushes.size() - 1
brush_container = file_brush_container
else:
brush_button.custom_brush_index = current_project.brushes.size() - 1
brush_container = project_brush_container
var brush_tex := ImageTexture.new()
brush_tex.create_from_image(brush_img, 0)
brush_button.get_child(0).texture = brush_tex
brush_button.hint_tooltip = hint_tooltip
brush_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
if brush_type == Brush_Types.RANDOM_FILE:
brush_button.random_brushes.append(brush_img)
brush_container.add_child(brush_button)
func create_pattern_button(image : Image, hint_tooltip := "") -> void:
var pattern_button : BaseButton = load("res://src/UI/PatternButton.tscn").instance()
pattern_button.image = image
var pattern_tex := ImageTexture.new()
pattern_tex.create_from_image(image, 0)
pattern_button.get_child(0).texture = pattern_tex
pattern_button.hint_tooltip = hint_tooltip
pattern_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
patterns_popup.get_node("ScrollContainer/PatternContainer").add_child(pattern_button)
func remove_brush_buttons() -> void:
current_brush_types[0] = Brush_Types.PIXEL
current_brush_types[1] = Brush_Types.PIXEL
for child in project_brush_container.get_children():
child.queue_free()
func undo_custom_brush(_brush_button : BaseButton = null) -> void:
general_undo()
var action_name : String = current_project.undo_redo.get_current_action_name()
if action_name == "Delete Custom Brush":
project_brush_container.add_child(_brush_button)
project_brush_container.move_child(_brush_button, _brush_button.custom_brush_index)
_brush_button.get_node("DeleteButton").visible = false
func redo_custom_brush(_brush_button : BaseButton = null) -> void:
general_redo()
var action_name : String = current_project.undo_redo.get_current_action_name()
if action_name == "Delete Custom Brush":
project_brush_container.remove_child(_brush_button)
func update_custom_brush(mouse_button : int) -> void:
var brush_type : int = current_brush_types[mouse_button]
if brush_type == Brush_Types.PIXEL:
var pixel = preload("res://assets/graphics/pixel_image.png")
if pixel is Image:
brush_type_buttons[mouse_button].get_child(0).texture.create_from_image(pixel, 0)
elif pixel is ImageTexture:
brush_type_buttons[mouse_button].get_child(0).texture.create_from_image(pixel.get_data(), 0)
elif brush_type == Brush_Types.CIRCLE:
var pixel = preload("res://assets/graphics/circle_9x9.png")
if pixel is Image:
brush_type_buttons[mouse_button].get_child(0).texture.create_from_image(pixel, 0)
elif pixel is ImageTexture:
brush_type_buttons[mouse_button].get_child(0).texture.create_from_image(pixel.get_data(), 0)
left_circle_points = plot_circle(brush_sizes[0])
right_circle_points = plot_circle(brush_sizes[1])
elif brush_type == Brush_Types.FILLED_CIRCLE:
var pixel = preload("res://assets/graphics/circle_filled_9x9.png")
if pixel is Image:
brush_type_buttons[mouse_button].get_child(0).texture.create_from_image(pixel, 0)
elif pixel is ImageTexture:
brush_type_buttons[mouse_button].get_child(0).texture.create_from_image(pixel.get_data(), 0)
left_circle_points = plot_circle(brush_sizes[0])
right_circle_points = plot_circle(brush_sizes[1])
else:
var custom_brush := Image.new()
if brush_type == Brush_Types.FILE or brush_type == Brush_Types.RANDOM_FILE:
custom_brush.copy_from(file_brushes[custom_brush_indexes[mouse_button]])
else:
custom_brush.copy_from(current_project.brushes[custom_brush_indexes[mouse_button]])
var custom_brush_size = custom_brush.get_size()
custom_brush.resize(custom_brush_size.x * brush_sizes[mouse_button], custom_brush_size.y * brush_sizes[mouse_button], Image.INTERPOLATE_NEAREST)
brush_images[mouse_button] = blend_image_with_color(custom_brush, color_pickers[mouse_button].color, interpolate_spinboxes[mouse_button].value / 100)
brush_textures[mouse_button].create_from_image(brush_images[mouse_button], 0)
brush_type_buttons[mouse_button].get_child(0).texture = brush_textures[mouse_button]
func blend_image_with_color(image : Image, color : Color, interpolate_factor : float) -> Image:
var blended_image := Image.new()
blended_image.copy_from(image)
var size := image.get_size()
blended_image.lock()
for xx in size.x:
for yy in size.y:
if color.a > 0: # If it's the pencil
var current_color := blended_image.get_pixel(xx, yy)
if current_color.a > 0:
var new_color := current_color.linear_interpolate(color, interpolate_factor)
new_color.a = current_color.a
blended_image.set_pixel(xx, yy, new_color)
else: # If color is transparent - if it's the eraser
blended_image.set_pixel(xx, yy, Color(0, 0, 0, 0))
return blended_image
# Algorithm based on http://members.chello.at/easyfilter/bresenham.html
# This is not used for drawing, rather for finding the points required
# for the mouse cursor/position indicator
func plot_circle(r : int) -> Array:
var circle_points := []
var xm := 0
var ym := 0
var x := -r
var y := 0
var err := 2 - r * 2
while x < 0:
circle_points.append(Vector2(xm - x, ym + y))
circle_points.append(Vector2(xm - y, ym - x))
circle_points.append(Vector2(xm + x, ym - y))
circle_points.append(Vector2(xm + y, ym + x))
r = err
if r <= y:
y += 1
err += y * 2 + 1
if r > x || err > y:
x += 1
err += x * 2 + 1
return circle_points
func _exit_tree() -> void:
config_cache.set_value("window", "screen", OS.current_screen)
config_cache.set_value("window", "maximized", OS.window_maximized || OS.window_fullscreen)

View file

@ -105,18 +105,7 @@ func add_randomised_brush(fpaths : Array, tooltip_name : String) -> void:
if len(loaded_images) > 0: # actually have images
# to use.
# take initial image...
var first_image : Image = loaded_images.pop_front()
# The index which this random brush will be at
var next_random_brush_index : int = Global.file_brush_container.get_child_count()
Global.file_brushes.append(first_image)
Global.create_brush_button(first_image, Global.Brush_Types.RANDOM_FILE, tooltip_name)
# # Process the rest
for remaining_image in loaded_images:
var brush_button = Global.file_brush_container.get_child(next_random_brush_index)
brush_button.random_brushes.append(remaining_image)
Brushes.add_file_brush(loaded_images, tooltip_name)
# Add a plain brush from the given path to the list of brushes.
# Taken, again, from find_brushes
@ -127,8 +116,7 @@ func add_plain_brush(path: String, tooltip_name: String) -> void:
return
# do the standard conversion thing...
image.convert(Image.FORMAT_RGBA8)
Global.file_brushes.append(image)
Global.create_brush_button(image, Global.Brush_Types.FILE, tooltip_name)
Brushes.add_file_brush([image], tooltip_name)
# Import brushes, in priority order, from the paths in question in priority order
@ -214,8 +202,6 @@ func import_brushes(priority_ordered_search_path: Array) -> void:
# Mark this as a processed relpath
processed_subdir_paths[nonrandomised_subdir][relative_path] = true
Global.brushes_from_files = Global.file_brushes.size()
func import_patterns(priority_ordered_search_path: Array) -> void:
for path in priority_ordered_search_path:
@ -235,25 +221,7 @@ func import_patterns(priority_ordered_search_path: Array) -> void:
var err := image.load(path.plus_file(pattern))
if err == OK:
image.convert(Image.FORMAT_RGBA8)
Global.patterns.append(image)
Global.create_pattern_button(image, pattern)
if Global.patterns.size() > 0:
var image_size = Global.patterns[0].get_size()
Global.pattern_images[0] = Global.patterns[0]
var pattern_left_tex := ImageTexture.new()
pattern_left_tex.create_from_image(Global.pattern_images[0], 0)
Global.fill_pattern_containers[0].get_child(0).get_child(0).texture = pattern_left_tex
Global.fill_pattern_containers[0].get_child(2).get_child(1).max_value = image_size.x - 1
Global.fill_pattern_containers[0].get_child(3).get_child(1).max_value = image_size.y - 1
Global.pattern_images[1] = Global.patterns[0]
var pattern_right_tex := ImageTexture.new()
pattern_right_tex.create_from_image(Global.pattern_images[1], 0)
Global.fill_pattern_containers[1].get_child(0).get_child(0).texture = pattern_right_tex
Global.fill_pattern_containers[1].get_child(2).get_child(1).max_value = image_size.x - 1
Global.fill_pattern_containers[1].get_child(3).get_child(1).max_value = image_size.y - 1
Global.patterns_popup.add(image)
func import_gpl(path : String, text : String) -> Palette:

View file

@ -93,7 +93,7 @@ func open_pxo_file(path : String, untitled_backup : bool = false) -> void:
var image := Image.new()
image.create_from_data(b_width, b_height, false, Image.FORMAT_RGBA8, buffer)
new_project.brushes.append(image)
Global.create_brush_button(image)
Brushes.add_project_brush(image)
file.close()
if !empty_project:
@ -232,19 +232,13 @@ func open_old_pxo_file(file : File, new_project : Project, first_line : String)
guide_line = file.get_line()
# Load tool options
Global.color_pickers[0].color = file.get_var()
Global.color_pickers[1].color = file.get_var()
Global.brush_sizes[0] = file.get_8()
Global.brush_size_edits[0].value = Global.brush_sizes[0]
Global.brush_sizes[1] = file.get_8()
Global.brush_size_edits[1].value = Global.brush_sizes[1]
file.get_var()
file.get_var()
file.get_8()
file.get_8()
if file_major_version == 0 and file_minor_version < 7:
var left_palette = file.get_var()
var right_palette = file.get_var()
for color in left_palette:
Global.color_pickers[0].get_picker().add_preset(color)
for color in right_palette:
Global.color_pickers[1].get_picker().add_preset(color)
file.get_var()
file.get_var()
# Load custom brushes
var brush_line := file.get_line()
@ -255,7 +249,7 @@ func open_old_pxo_file(file : File, new_project : Project, first_line : String)
var image := Image.new()
image.create_from_data(b_width, b_height, false, Image.FORMAT_RGBA8, buffer)
new_project.brushes.append(image)
Global.create_brush_button(image)
Brushes.add_project_brush(image)
brush_line = file.get_line()
if file_major_version >= 0 and file_minor_version > 6:

201
src/Autoload/Tools.gd Normal file
View file

@ -0,0 +1,201 @@
extends Node
class Slot:
var name : String
var kname : String
var tool_node : Node = null
var button : int
var color : Color
var pixel_perfect := false
var horizontal_mirror := false
var vertical_mirror := false
func _init(slot_name : String) -> void:
name = slot_name
kname = name.replace(" ", "_").to_lower()
load_config()
func save_config() -> void:
var config := {
"pixel_perfect" : pixel_perfect,
"horizontal_mirror" : horizontal_mirror,
"vertical_mirror" : vertical_mirror,
}
Global.config_cache.set_value(kname, "slot", config)
func load_config() -> void:
var config = Global.config_cache.get_value(kname, "slot", {})
pixel_perfect = config.get("pixel_perfect", pixel_perfect)
horizontal_mirror = config.get("horizontal_mirror", horizontal_mirror)
vertical_mirror = config.get("vertical_mirror", vertical_mirror)
signal color_changed(color, button)
var _tools = {
"RectSelect" : "res://src/Tools/RectSelect.tscn",
"Zoom" : "res://src/Tools/Zoom.tscn",
"ColorPicker" : "res://src/Tools/ColorPicker.tscn",
"Pencil" : "res://src/Tools/Pencil.tscn",
"Eraser" : "res://src/Tools/Eraser.tscn",
"Bucket" : "res://src/Tools/Bucket.tscn",
"LightenDarken" : "res://src/Tools/LightenDarken.tscn",
}
var _slots = {}
var _panels = {}
var _tool_buttons : Node
var _active_button := -1
var _last_position := Vector2.INF
var pen_pressure := 1.0
var control := false
var shift := false
var alt := false
func _ready():
yield(get_tree(), "idle_frame")
_slots[BUTTON_LEFT] = Slot.new("Left tool")
_slots[BUTTON_RIGHT] = Slot.new("Right tool")
_panels[BUTTON_LEFT] = Global.find_node_by_name(Global.control, "LeftPanelContainer")
_panels[BUTTON_RIGHT] = Global.find_node_by_name(Global.control, "RightPanelContainer")
_tool_buttons = Global.find_node_by_name(Global.control, "ToolButtons")
var value = Global.config_cache.get_value(_slots[BUTTON_LEFT].kname, "tool", "Pencil")
set_tool(value, BUTTON_LEFT)
value = Global.config_cache.get_value(_slots[BUTTON_RIGHT].kname, "tool", "Eraser")
set_tool(value, BUTTON_RIGHT)
value = Global.config_cache.get_value(_slots[BUTTON_LEFT].kname, "color", Color.black)
assign_color(value, BUTTON_LEFT)
value = Global.config_cache.get_value(_slots[BUTTON_RIGHT].kname, "color", Color.white)
assign_color(value, BUTTON_RIGHT)
update_tool_buttons()
update_tool_cursors()
func set_tool(name : String, button : int) -> void:
var slot = _slots[button]
var panel : Node = _panels[button]
var node : Node = load(_tools[name]).instance()
node.name = name
node.tool_slot = slot
slot.tool_node = node
slot.button = button
panel.add_child(slot.tool_node)
func assign_tool(name : String, button : int) -> void:
var slot = _slots[button]
var panel : Node = _panels[button]
if slot.tool_node != null:
if slot.tool_node.name == name:
return
panel.remove_child(slot.tool_node)
slot.tool_node.queue_free()
set_tool(name, button)
update_tool_buttons()
update_tool_cursors()
Global.config_cache.set_value(slot.kname, "tool", name)
func default_color() -> void:
assign_color(Color.black, BUTTON_LEFT)
assign_color(Color.white, BUTTON_RIGHT)
func swap_color() -> void:
var left = _slots[BUTTON_LEFT].color
var right = _slots[BUTTON_RIGHT].color
assign_color(right, BUTTON_LEFT)
assign_color(left, BUTTON_RIGHT)
func assign_color(color : Color, button : int) -> void:
var c : Color = _slots[button].color
if color.a == 0:
if color.r != c.r or color.g != c.g or color.b != c.b:
color.a = 1
_slots[button].color = color
Global.config_cache.set_value(_slots[button].kname, "color", color)
emit_signal("color_changed", color, button)
func get_assigned_color(button : int) -> Color:
return _slots[button].color
func update_tool_buttons() -> void:
for child in _tool_buttons.get_children():
var texture : TextureRect = child.get_child(0)
var filename = child.name.to_lower()
if _slots[BUTTON_LEFT].tool_node.name == child.name:
filename += "_l"
if _slots[BUTTON_RIGHT].tool_node.name == child.name:
filename += "_r"
filename += ".png"
Global.change_button_texturerect(texture, filename)
func update_tool_cursors() -> void:
var image = "res://assets/graphics/cursor_icons/%s_cursor.png" % _slots[BUTTON_LEFT].tool_node.name.to_lower()
Global.left_cursor_tool_texture.create_from_image(load(image), 0)
image = "res://assets/graphics/cursor_icons/%s_cursor.png" % _slots[BUTTON_RIGHT].tool_node.name.to_lower()
Global.right_cursor_tool_texture.create_from_image(load(image), 0)
func draw_indicator() -> void:
if Global.left_square_indicator_visible:
_slots[BUTTON_LEFT].tool_node.draw_indicator()
if Global.right_square_indicator_visible:
_slots[BUTTON_RIGHT].tool_node.draw_indicator()
func handle_draw(position : Vector2, event : InputEvent) -> void:
if not (Global.can_draw and Global.has_focus):
return
if event is InputEventWithModifiers:
control = event.control
shift = event.shift
alt = event.alt
if event is InputEventMouseButton:
if event.button_index in [BUTTON_LEFT, BUTTON_RIGHT]:
if event.pressed and _active_button == -1:
_active_button = event.button_index
_slots[_active_button].tool_node.draw_start(position)
elif not event.pressed and event.button_index == _active_button:
_slots[_active_button].tool_node.draw_end(position)
_active_button = -1
if event is InputEventMouseMotion:
if Engine.get_version_info().major == 3 && Engine.get_version_info().minor >= 2:
pen_pressure = event.pressure
if Global.pressure_sensitivity_mode == Global.Pressure_Sensitivity.NONE:
pen_pressure = 1.0
if not position.is_equal_approx(_last_position):
_last_position = position
_slots[BUTTON_LEFT].tool_node.cursor_move(position)
_slots[BUTTON_RIGHT].tool_node.cursor_move(position)
if _active_button != -1:
_slots[_active_button].tool_node.draw_move(position)
var project : Project = Global.current_project
var text := "[%s×%s]" % [project.size.x, project.size.y]
if Global.has_focus:
text += " %s, %s" % [position.x, position.y]
if not _slots[BUTTON_LEFT].tool_node.cursor_text.empty():
text += " %s" % _slots[BUTTON_LEFT].tool_node.cursor_text
if not _slots[BUTTON_RIGHT].tool_node.cursor_text.empty():
text += " %s" % _slots[BUTTON_RIGHT].tool_node.cursor_text
Global.cursor_position_label.text = text

View file

@ -5,17 +5,9 @@ extends Node2D
var location := Vector2.ZERO
var fill_color := Color(0, 0, 0, 0)
var current_pixel := Vector2.ZERO # pretty much same as mouse_pos, but can be accessed externally
var previous_mouse_pos := Vector2.ZERO
var previous_mouse_pos_for_lines := Vector2.ZERO
var can_undo := true
var cursor_image_has_changed := false
var previous_action := -1
var sprite_changed_this_frame := false # for optimization purposes
var is_making_line := false
var made_line := false
var is_making_selection := -1
var line_pos = []
var pen_pressure := 1.0 # For tablet pressure sensitivity
# Called when the node enters the scene tree for the first time.
@ -23,7 +15,6 @@ func _ready() -> void:
var frame : Frame = new_empty_frame(true)
Global.current_project.frames.append(frame)
camera_zoom()
line_pos = [previous_mouse_pos_for_lines, previous_mouse_pos_for_lines]
func _draw() -> void:
@ -54,111 +45,40 @@ func _draw() -> void:
draw_grid(Global.grid_type)
# Draw rectangle to indicate the pixel currently being hovered on
if Global.can_draw:
var mouse_pos := current_pixel
mouse_pos = mouse_pos.floor()
var visible_indicators := [Global.left_square_indicator_visible, Global.right_square_indicator_visible]
for i in range(0, 1):
if visible_indicators[i]:
if Global.current_brush_types[i] == Global.Brush_Types.PIXEL || Global.current_tools[i] == Global.Tools.LIGHTENDARKEN:
if Global.current_tools[i] == Global.Tools.PENCIL || Global.current_tools[i] == Global.Tools.ERASER || Global.current_tools[i] == Global.Tools.LIGHTENDARKEN:
var start_pos_x = mouse_pos.x - (Global.brush_sizes[i] >> 1)
var start_pos_y = mouse_pos.y - (Global.brush_sizes[i] >> 1)
draw_rect(Rect2(start_pos_x, start_pos_y, Global.brush_sizes[i], Global.brush_sizes[i]), Color.blue, false)
#Check for tile mode
if Global.tile_mode and point_in_rectangle(mouse_pos,Vector2( - Global.current_project.size.x - 1 , - Global.current_project.size.y -1 ), Vector2(2 * Global.current_project.size.x, 2 * Global.current_project.size.y)):
if !point_in_rectangle(mouse_pos, Vector2(Global.current_project.x_min - 1,Global.current_project.y_min - 1), Vector2(Global.current_project.x_max,Global.current_project.y_max)):
var new_start_pos_x = posmod(start_pos_x,Global.current_project.size.x)
var new_start_pos_y = posmod(start_pos_y,Global.current_project.size.y)
draw_rect(Rect2(new_start_pos_x, new_start_pos_y, Global.brush_sizes[i], Global.brush_sizes[i]), Color.green, false)
if is_making_line:
var line_rect = plot_line(line_pos[1], line_pos[0])
for rect in line_rect:
draw_rect(Rect2(rect, Vector2.ONE), Color.blue, false)
#Check for tile mode
if Global.tile_mode and point_in_rectangle(mouse_pos,Vector2( - Global.current_project.size.x - 1 , - Global.current_project.size.y -1 ), Vector2(2 * Global.current_project.size.x, 2 * Global.current_project.size.y)):
if !point_in_rectangle(mouse_pos, Vector2(Global.current_project.x_min - 1,Global.current_project.y_min - 1), Vector2(Global.current_project.x_max,Global.current_project.y_max)):
rect.x = posmod(rect.x,Global.current_project.size.x)
rect.y = posmod(rect.y,Global.current_project.size.y)
draw_rect(Rect2(rect, Vector2.ONE), Color.green, false)
elif Global.current_brush_types[i] == Global.Brush_Types.CIRCLE || Global.current_brush_types[i] == Global.Brush_Types.FILLED_CIRCLE:
if Global.current_tools[i] == Global.Tools.PENCIL || Global.current_tools[i] == Global.Tools.ERASER:
draw_set_transform(mouse_pos, rotation, scale)
for rect in Global.left_circle_points:
draw_rect(Rect2(rect, Vector2.ONE), Color.blue, false)
#Check for tile mode
if Global.tile_mode and point_in_rectangle(mouse_pos,Vector2( - Global.current_project.size.x - 1 , - Global.current_project.size.y -1 ), Vector2(2 * Global.current_project.size.x, 2 * Global.current_project.size.y)):
if !point_in_rectangle(mouse_pos, Vector2(Global.current_project.x_min - 1,Global.current_project.y_min - 1), Vector2(Global.current_project.x_max,Global.current_project.y_max)):
var pos = mouse_pos.posmodv(Global.current_project.size)
if pos != mouse_pos:
draw_set_transform(pos,rotation,scale)
for rect in Global.left_circle_points:
draw_rect(Rect2(rect, Vector2.ONE), Color.green, false)
draw_set_transform(position, rotation, scale)
else:
if Global.current_tools[i] == Global.Tools.PENCIL || Global.current_tools[i] == Global.Tools.ERASER:
var custom_brush_size = Global.brush_images[i].get_size() - Vector2.ONE
var dst : Vector2 = DrawingAlgos.rectangle_center(mouse_pos, custom_brush_size)
draw_texture(Global.brush_textures[i], dst)
if Global.has_focus and Global.can_draw:
Tools.draw_indicator()
func _input(event : InputEvent) -> void:
# Don't process anything below if the input isn't a mouse event, or Shift/Ctrl.
# This decreases CPU/GPU usage slightly.
if not event is InputEventMouse:
if event is InputEventKey:
if event.scancode != KEY_SHIFT && event.scancode != KEY_CONTROL:
return
else:
if not event is InputEventKey:
return
if (Input.is_action_just_released("left_mouse") && !Input.is_action_pressed("right_mouse")) || (Input.is_action_just_released("right_mouse") && !Input.is_action_pressed("left_mouse")):
made_line = false
DrawingAlgos.reset()
can_undo = true
elif not event.scancode in [KEY_SHIFT, KEY_CONTROL]:
return
# elif not get_viewport_rect().has_point(event.position):
# return
current_pixel = get_local_mouse_position() + location
if Global.has_focus:
update()
# Godot 3.2 and above only code
if Engine.get_version_info().major == 3 && Engine.get_version_info().minor >= 2:
if event is InputEventMouseMotion:
if Global.pressure_sensitivity_mode == Global.Pressure_Sensitivity.NONE:
pen_pressure = 1
else:
pen_pressure = event.pressure
sprite_changed_this_frame = false
var mouse_pos := current_pixel
var mouse_pos_floored := mouse_pos.floor()
var current_mouse_button := -1
var current_project : Project = Global.current_project
var current_project : Project = Global.current_project
current_project.x_min = location.x
current_project.x_max = location.x + current_project.size.x
current_project.y_min = location.y
current_project.y_max = location.y + current_project.size.y
if current_project.selected_pixels.size() != 0:
current_project.x_min = max(current_project.x_min, Global.selection_rectangle.polygon[0].x)
current_project.x_max = min(current_project.x_max, Global.selection_rectangle.polygon[2].x)
current_project.y_min = max(current_project.y_min, Global.selection_rectangle.polygon[0].y)
current_project.y_max = min(current_project.y_max, Global.selection_rectangle.polygon[2].y)
if Input.is_mouse_button_pressed(BUTTON_LEFT):
current_mouse_button = Global.Mouse_Button.LEFT
elif Input.is_mouse_button_pressed(BUTTON_RIGHT):
current_mouse_button = Global.Mouse_Button.RIGHT
var current_action : int = Global.current_tools[current_mouse_button] if current_mouse_button != -1 else -1
if not current_project.selected_rect.has_no_area():
current_project.x_min = max(current_project.x_min, current_project.selected_rect.position.x)
current_project.x_max = min(current_project.x_max, current_project.selected_rect.end.x)
current_project.y_min = max(current_project.y_min, current_project.selected_rect.position.y)
current_project.y_max = min(current_project.y_max, current_project.selected_rect.end.y)
if Global.has_focus:
Global.cursor_position_label.text = "[%s×%s] %s, %s" % [current_project.size.x, current_project.size.y, mouse_pos_floored.x, mouse_pos_floored.y]
if !cursor_image_has_changed:
cursor_image_has_changed = true
if Global.show_left_tool_icon:
@ -166,80 +86,13 @@ func _input(event : InputEvent) -> void:
if Global.show_right_tool_icon:
Global.right_cursor.visible = true
else:
Global.cursor_position_label.text = "[%s×%s]" % [current_project.size.x, current_project.size.y]
if cursor_image_has_changed:
cursor_image_has_changed = false
Global.left_cursor.visible = false
Global.right_cursor.visible = false
# Handle Undo/Redo
var can_handle : bool = Global.can_draw && Global.has_focus && !made_line
var mouse_pressed : bool = (Input.is_action_just_pressed("left_mouse") && !Input.is_action_pressed("right_mouse")) || (Input.is_action_just_pressed("right_mouse") && !Input.is_action_pressed("left_mouse"))
Tools.handle_draw(current_pixel.floor(), event)
if mouse_pressed:
if can_handle || is_making_line:
if current_action != -1 && current_action != Global.Tools.COLORPICKER && current_action != Global.Tools.ZOOM:
if current_action == Global.Tools.RECTSELECT:
handle_undo("Rectangle Select")
else:
handle_undo("Draw")
elif (Input.is_action_just_released("left_mouse") && !Input.is_action_pressed("right_mouse")) || (Input.is_action_just_released("right_mouse") && !Input.is_action_pressed("left_mouse")):
if can_handle || current_project.undos == current_project.undo_redo.get_version():
if previous_action != -1 && previous_action != Global.Tools.RECTSELECT && current_action != Global.Tools.COLORPICKER && current_action != Global.Tools.ZOOM:
handle_redo("Draw")
handle_tools(current_mouse_button, current_action, mouse_pos, can_handle)
if Global.can_draw && Global.has_focus && Input.is_action_just_pressed("shift") && ([Global.Tools.PENCIL, Global.Tools.ERASER, Global.Tools.LIGHTENDARKEN].has(Global.current_tools[0]) || [Global.Tools.PENCIL, Global.Tools.ERASER, Global.Tools.LIGHTENDARKEN].has(Global.current_tools[1])):
is_making_line = true
line_pos[0] = previous_mouse_pos_for_lines
elif Input.is_action_just_released("shift"):
is_making_line = false
line_pos[1] = line_pos[0]
if is_making_line:
var point0 : Vector2 = line_pos[0]
var angle := stepify(rad2deg(mouse_pos.angle_to_point(point0)), 0.01)
if Input.is_action_pressed("ctrl"):
angle = round(angle / 15) * 15
var distance : float = point0.distance_to(mouse_pos)
line_pos[1] = point0 + Vector2.RIGHT.rotated(deg2rad(angle)) * distance
else:
line_pos[1] = mouse_pos
if angle < 0:
angle = 360 + angle
Global.cursor_position_label.text += " %s°" % str(angle)
if is_making_selection != -1: # If we're making a selection
var mouse_button_string := "left_mouse" if is_making_selection == Global.Mouse_Button.LEFT else "right_mouse"
if Input.is_action_just_released(mouse_button_string): # Finish selection when button is released
var start_pos = Global.selection_rectangle.polygon[0]
var end_pos = Global.selection_rectangle.polygon[2]
if start_pos.x > end_pos.x:
var temp = end_pos.x
end_pos.x = start_pos.x
start_pos.x = temp
if start_pos.y > end_pos.y:
var temp = end_pos.y
end_pos.y = start_pos.y
start_pos.y = temp
Global.selection_rectangle.polygon[0] = start_pos
Global.selection_rectangle.polygon[1] = Vector2(end_pos.x, start_pos.y)
Global.selection_rectangle.polygon[2] = end_pos
Global.selection_rectangle.polygon[3] = Vector2(start_pos.x, end_pos.y)
for xx in range(start_pos.x, end_pos.x):
for yy in range(start_pos.y, end_pos.y):
current_project.selected_pixels.append(Vector2(xx, yy))
is_making_selection = -1
handle_redo("Rectangle Select")
previous_action = current_action
previous_mouse_pos = current_pixel
if sprite_changed_this_frame:
update_texture(current_project.current_layer)
@ -284,143 +137,6 @@ func new_empty_frame(first_time := false, single_layer := false, size := Global.
return frame
func handle_tools(current_mouse_button : int, current_action : int, mouse_pos : Vector2, can_handle : bool) -> void:
var current_project : Project = Global.current_project
var current_cel : Cel = current_project.frames[current_project.current_frame].cels[current_project.current_layer]
var sprite : Image = current_cel.image
var mouse_pos_floored := mouse_pos.floor()
var mouse_pos_ceiled := mouse_pos.ceil()
var current_color : Color = Global.color_pickers[current_mouse_button].color
var fill_area : int = Global.fill_areas[current_mouse_button]
var ld : int = Global.ld_modes[current_mouse_button]
var ld_amount : float = Global.ld_amounts[current_mouse_button]
var color_picker_for : int = Global.color_picker_for[current_mouse_button]
var zoom_mode : int = Global.zoom_modes[current_mouse_button]
match current_action: # Handle current tool
Global.Tools.PENCIL:
pencil_and_eraser(sprite, mouse_pos, current_color, current_mouse_button, current_action)
Global.Tools.ERASER:
pencil_and_eraser(sprite, mouse_pos, Color(0, 0, 0, 0), current_mouse_button, current_action)
Global.Tools.BUCKET:
if can_handle:
var fill_with : int = Global.fill_with[current_mouse_button]
var pattern_image : Image = Global.pattern_images[current_mouse_button]
var pattern_offset : Vector2 = Global.fill_pattern_offsets[current_mouse_button]
if fill_area == Global.Fill_Area.SAME_COLOR_AREA: # Paint the specific area of the same color
var mirror_x := current_project.x_max + current_project.x_min - mouse_pos_floored.x - 1
var mirror_y := current_project.y_max + current_project.y_min - mouse_pos_floored.y - 1
var horizontal_mirror : bool = Global.horizontal_mirror[current_mouse_button]
var vertical_mirror : bool = Global.vertical_mirror[current_mouse_button]
if fill_with == Global.Fill_With.PATTERN && pattern_image: # Pattern fill
DrawingAlgos.pattern_fill(sprite, mouse_pos, pattern_image, sprite.get_pixelv(mouse_pos), pattern_offset)
if horizontal_mirror:
var pos := Vector2(mirror_x, mouse_pos.y)
DrawingAlgos.pattern_fill(sprite, pos, pattern_image, sprite.get_pixelv(mouse_pos), pattern_offset)
if vertical_mirror:
var pos := Vector2(mouse_pos.x, mirror_y)
DrawingAlgos.pattern_fill(sprite, pos, pattern_image, sprite.get_pixelv(mouse_pos), pattern_offset)
if horizontal_mirror && vertical_mirror:
var pos := Vector2(mirror_x, mirror_y)
DrawingAlgos.pattern_fill(sprite, pos, pattern_image, sprite.get_pixelv(mouse_pos), pattern_offset)
else: # Flood fill
DrawingAlgos.flood_fill(sprite, mouse_pos, sprite.get_pixelv(mouse_pos), current_color)
if horizontal_mirror:
var pos := Vector2(mirror_x, mouse_pos.y)
DrawingAlgos.flood_fill(sprite, pos, sprite.get_pixelv(pos), current_color)
if vertical_mirror:
var pos := Vector2(mouse_pos.x, mirror_y)
DrawingAlgos.flood_fill(sprite, pos, sprite.get_pixelv(pos), current_color)
if horizontal_mirror && vertical_mirror:
var pos := Vector2(mirror_x, mirror_y)
DrawingAlgos.flood_fill(sprite, pos, sprite.get_pixelv(pos), current_color)
else: # Paint all pixels of the same color
var pixel_color : Color = sprite.get_pixelv(mouse_pos)
for xx in range(current_project.x_min, current_project.x_max):
for yy in range(current_project.y_min, current_project.y_max):
var c : Color = sprite.get_pixel(xx, yy)
if c == pixel_color:
if fill_with == Global.Fill_With.PATTERN && pattern_image: # Pattern fill
pattern_image.lock()
var pattern_size := pattern_image.get_size()
var xxx : int = int(xx + pattern_offset.x) % int(pattern_size.x)
var yyy : int = int(yy + pattern_offset.y) % int(pattern_size.y)
var pattern_color : Color = pattern_image.get_pixel(xxx, yyy)
sprite.set_pixel(xx, yy, pattern_color)
pattern_image.unlock()
else:
sprite.set_pixel(xx, yy, current_color)
sprite_changed_this_frame = true
Global.Tools.LIGHTENDARKEN:
if can_handle:
var pixel_color : Color = sprite.get_pixelv(mouse_pos)
var color_changed : Color
if ld == Global.Lighten_Darken_Mode.LIGHTEN:
color_changed = pixel_color.lightened(ld_amount)
else: # Darken
color_changed = pixel_color.darkened(ld_amount)
pencil_and_eraser(sprite, mouse_pos, color_changed, current_mouse_button, current_action)
Global.Tools.RECTSELECT:
# Check SelectionRectangle.gd for more code on Rectangle Selection
if Global.can_draw && Global.has_focus:
# If we're creating a new selection
if current_project.selected_pixels.size() == 0 || !point_in_rectangle(mouse_pos_floored, Global.selection_rectangle.polygon[0] - Vector2.ONE, Global.selection_rectangle.polygon[2]):
var mouse_button_string := "left_mouse" if current_mouse_button == Global.Mouse_Button.LEFT else "right_mouse"
if Input.is_action_just_pressed(mouse_button_string):
Global.selection_rectangle.polygon[0] = mouse_pos_floored
Global.selection_rectangle.polygon[1] = mouse_pos_floored
Global.selection_rectangle.polygon[2] = mouse_pos_floored
Global.selection_rectangle.polygon[3] = mouse_pos_floored
is_making_selection = current_mouse_button
current_project.selected_pixels.clear()
else:
if is_making_selection != -1: # If we're making a new selection...
var start_pos = Global.selection_rectangle.polygon[0]
if start_pos != mouse_pos_floored:
var end_pos := Vector2(mouse_pos_ceiled.x, mouse_pos_ceiled.y)
if mouse_pos.x < start_pos.x:
end_pos.x = mouse_pos_ceiled.x - 1
if mouse_pos.y < start_pos.y:
end_pos.y = mouse_pos_ceiled.y - 1
Global.selection_rectangle.polygon[1] = Vector2(end_pos.x, start_pos.y)
Global.selection_rectangle.polygon[2] = end_pos
Global.selection_rectangle.polygon[3] = Vector2(start_pos.x, end_pos.y)
Global.Tools.COLORPICKER:
var canvas_rect := Rect2(location, current_project.size)
if can_handle && canvas_rect.has_point(mouse_pos):
var image_data := Image.new()
image_data.copy_from(sprite)
image_data.lock()
var pixel_color : Color = image_data.get_pixelv(mouse_pos)
Global.color_pickers[color_picker_for].color = pixel_color
Global.update_custom_brush(color_picker_for)
Global.Tools.ZOOM:
if can_handle:
if zoom_mode == Global.Zoom_Mode.ZOOM_IN:
Global.camera.zoom_camera(-1)
else:
Global.camera.zoom_camera(1)
func pencil_and_eraser(sprite : Image, mouse_pos : Vector2, color : Color, current_mouse_button : int, current_action := -1) -> void:
if made_line:
return
if is_making_line:
DrawingAlgos.fill_gaps(sprite, line_pos[1], previous_mouse_pos_for_lines, color, current_mouse_button, pen_pressure, current_action)
DrawingAlgos.draw_brush(sprite, line_pos[1], color, current_mouse_button, pen_pressure, current_action)
made_line = true
else:
# Draw
DrawingAlgos.draw_brush(sprite, mouse_pos, color, current_mouse_button, pen_pressure, current_action)
DrawingAlgos.fill_gaps(sprite, mouse_pos, previous_mouse_pos, color, current_mouse_button, pen_pressure, current_action) # Fill the gaps
func handle_undo(action : String) -> void:
if !can_undo:
return
@ -442,16 +158,12 @@ func handle_undo(action : String) -> void:
var data = f.cels[Global.current_project.current_layer].image.data
f.cels[Global.current_project.current_layer].image.lock()
Global.current_project.undo_redo.add_undo_property(f.cels[Global.current_project.current_layer].image, "data", data)
if action == "Rectangle Select":
var selected_pixels = Global.current_project.selected_pixels.duplicate()
Global.current_project.undo_redo.add_undo_property(Global.selection_rectangle, "polygon", Global.selection_rectangle.polygon)
Global.current_project.undo_redo.add_undo_property(Global.current_project, "selected_pixels", selected_pixels)
Global.current_project.undo_redo.add_undo_method(Global, "undo", frame_index, layer_index)
can_undo = false
func handle_redo(action : String) -> void:
func handle_redo(_action : String) -> void:
can_undo = true
if Global.current_project.undos < Global.current_project.undo_redo.get_version():
@ -467,9 +179,6 @@ func handle_redo(action : String) -> void:
frames = Global.current_project.frames
for f in frames:
Global.current_project.undo_redo.add_do_property(f.cels[Global.current_project.current_layer].image, "data", f.cels[Global.current_project.current_layer].image.data)
if action == "Rectangle Select":
Global.current_project.undo_redo.add_do_property(Global.selection_rectangle, "polygon", Global.selection_rectangle.polygon)
Global.current_project.undo_redo.add_do_property(Global.current_project, "selected_pixels", Global.current_project.selected_pixels)
Global.current_project.undo_redo.add_do_method(Global, "redo", frame_index, layer_index)
Global.current_project.undo_redo.commit_action()
@ -561,34 +270,3 @@ func draw_grid(grid_type : int) -> void:
for x in range(0, size.x, Global.grid_height * 2):
var yy2 = (size.x - x) * tan(deg2rad(26.565)) # 30 degrees
draw_line(Vector2(x, size.y), Vector2(size.x, size.y - yy2), Global.grid_color)
# Bresenham's Algorithm
# Thanks to https://godotengine.org/qa/35276/tile-based-line-drawing-algorithm-efficiency
func plot_line(end_pos : Vector2, start_pos : Vector2) -> Array:
start_pos = start_pos.floor()
end_pos = end_pos.floor()
var line_points := []
var dx := int(abs(end_pos.x - start_pos.x))
var dy := int(-abs(end_pos.y - start_pos.y))
var err := dx + dy
var e2 := err << 1 # err * 2
var sx = 1 if start_pos.x < end_pos.x else -1
var sy = 1 if start_pos.y < end_pos.y else -1
var x = start_pos.x
var y = start_pos.y
while !(x == end_pos.x && y == end_pos.y):
line_points.append(Vector2(x, y))
e2 = err << 1
if e2 >= dy:
err += dy
x += sx
if e2 <= dx:
err += dx
y += sy
return line_points
# Checks if a point is inside a rectangle
func point_in_rectangle(p : Vector2, coord1 : Vector2, coord2 : Vector2) -> bool:
return p.x > coord1.x && p.y > coord1.y && p.x < coord2.x && p.y < coord2.y

View file

@ -1,8 +1,20 @@
class_name Drawer
class ColorOp:
var strength := 1.0
func process(src: Color, _dst: Color) -> Color:
return src
class SimpleDrawer:
func set_pixel(_sprite: Image, _pos: Vector2, _new_color: Color) -> void:
_sprite.set_pixel(_pos.x, _pos.y, _new_color)
func set_pixel(image: Image, position: Vector2, color: Color, op : ColorOp) -> void:
var color_old := image.get_pixelv(position)
var color_new := op.process(color, color_old)
if not color_new.is_equal_approx(color_old):
image.set_pixelv(position, color_new)
class PixelPerfectDrawer:
@ -15,9 +27,10 @@ class PixelPerfectDrawer:
last_pixels = [null, null]
func set_pixel(_sprite: Image, _pos: Vector2, _new_color: Color) -> void:
last_pixels.push_back([_pos, _sprite.get_pixel(_pos.x, _pos.y)])
_sprite.set_pixel(_pos.x, _pos.y, _new_color)
func set_pixel(image: Image, position: Vector2, color: Color, op : ColorOp) -> void:
var color_old = image.get_pixelv(position)
last_pixels.push_back([position, color_old])
image.set_pixelv(position, op.process(color, color_old))
var corner = last_pixels.pop_front()
var neighbour = last_pixels[0]
@ -25,14 +38,15 @@ class PixelPerfectDrawer:
if corner == null or neighbour == null:
return
if _pos - corner[0] in corners and _pos - neighbour[0] in neighbours:
_sprite.set_pixel(neighbour[0].x, neighbour[0].y, neighbour[1])
if position - corner[0] in corners and position - neighbour[0] in neighbours:
image.set_pixel(neighbour[0].x, neighbour[0].y, neighbour[1])
last_pixels[0] = corner
var pixel_perfect := false setget set_pixel_perfect
var h_mirror := false
var v_mirror := false
var horizontal_mirror := false
var vertical_mirror := false
var color_op := ColorOp.new()
var simple_drawer := SimpleDrawer.new()
var pixel_perfect_drawers = [PixelPerfectDrawer.new(), PixelPerfectDrawer.new(), PixelPerfectDrawer.new(), PixelPerfectDrawer.new()]
@ -52,14 +66,14 @@ func set_pixel_perfect(value: bool) -> void:
drawers = [simple_drawer, simple_drawer, simple_drawer, simple_drawer]
func set_pixel(_sprite: Image, _pos: Vector2, _new_color: Color) -> void:
var mirror_x = Global.current_project.x_max + Global.current_project.x_min - _pos.x - 1
var mirror_y = Global.current_project.y_max + Global.current_project.y_min - _pos.y - 1
func set_pixel(image: Image, position: Vector2, color: Color) -> void:
var mirror_x = Global.current_project.x_max + Global.current_project.x_min - position.x - 1
var mirror_y = Global.current_project.y_max + Global.current_project.y_min - position.y - 1
drawers[0].set_pixel(_sprite, _pos, _new_color)
if h_mirror:
drawers[1].set_pixel(_sprite, Vector2(mirror_x, _pos.y), _new_color)
if v_mirror:
drawers[2].set_pixel(_sprite, Vector2(mirror_x, mirror_y), _new_color)
if v_mirror:
drawers[3].set_pixel(_sprite, Vector2(_pos.x, mirror_y), _new_color)
drawers[0].set_pixel(image, position, color, color_op)
if horizontal_mirror:
drawers[1].set_pixel(image, Vector2(mirror_x, position.y), color, color_op)
if vertical_mirror:
drawers[2].set_pixel(image, Vector2(mirror_x, mirror_y), color, color_op)
if vertical_mirror:
drawers[3].set_pixel(image, Vector2(position.x, mirror_y), color, color_op)

View file

@ -16,12 +16,13 @@ var guides := [] # Array of Guides
var brushes := [] # Array of Images
var selected_pixels := []
var x_min := 0
var x_max := 64
var y_min := 0
var y_max := 64
var selected_rect := Rect2(0, 0, 0, 0) setget _set_selected_rect
# For every camera (currently there are 3)
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
@ -39,6 +40,11 @@ func _init(_frames := [], _name := tr("untitled")) -> void:
OpenSave.backup_save_paths.append("")
func _set_selected_rect(value : Rect2) -> void:
selected_rect = value
Global.selection_rectangle.set_rect(value)
func change_project() -> void:
# Remove old nodes
for container in Global.layers_container.get_children():
@ -94,16 +100,7 @@ func change_project() -> void:
self.animation_tags = animation_tags
# Change the selection rectangle
if selected_pixels.size() != 0:
Global.selection_rectangle.polygon[0] = Vector2(x_min, y_min)
Global.selection_rectangle.polygon[1] = Vector2(x_max, y_min)
Global.selection_rectangle.polygon[2] = Vector2(x_max, y_max)
Global.selection_rectangle.polygon[3] = Vector2(x_min, y_max)
else:
Global.selection_rectangle.polygon[0] = Vector2.ZERO
Global.selection_rectangle.polygon[1] = Vector2.ZERO
Global.selection_rectangle.polygon[2] = Vector2.ZERO
Global.selection_rectangle.polygon[3] = Vector2.ZERO
Global.selection_rectangle.set_rect(selected_rect)
# Change the guides
for guide in Global.canvas.get_children():
@ -114,11 +111,9 @@ func change_project() -> void:
guide.visible = false
# Change the project brushes
for child in Global.project_brush_container.get_children():
child.queue_free()
Brushes.clear_project_brush()
for brush in brushes:
Global.create_brush_button(brush)
Brushes.add_project_brush(brush)
var cameras = [Global.camera, Global.camera2, Global.camera_preview]
var i := 0

View file

@ -19,9 +19,6 @@ func _ready() -> void:
Import.import_brushes(Global.directory_module.get_brushes_search_path_in_order())
Import.import_patterns(Global.directory_module.get_patterns_search_path_in_order())
Global.color_pickers[0].get_picker().presets_visible = false
Global.color_pickers[1].get_picker().presets_visible = false
$QuitAndSaveDialog.add_button("Save & Exit", false, "Save")
$QuitAndSaveDialog.get_ok().text = "Exit without saving"

View file

@ -27,8 +27,8 @@ func open(palette : String) -> void:
self.popup_centered()
Global.dialog_open(true)
left_color_button.modulate = Global.color_pickers[0].color
right_color_button.modulate = Global.color_pickers[1].color
left_color_button.modulate = Tools.get_assigned_color(BUTTON_LEFT)
right_color_button.modulate = Tools.get_assigned_color(BUTTON_RIGHT)
func _display_palette() -> void:
@ -177,12 +177,12 @@ func _refresh_hint_tooltip(_index : int) -> void:
func _on_LeftColor_pressed() -> void:
color_picker.color = Global.color_pickers[0].color
color_picker.color = Tools.get_assigned_color(BUTTON_LEFT)
_on_EditPaletteColorPicker_color_changed(color_picker.color)
func _on_RightColor_pressed() -> void:
color_picker.color = Global.color_pickers[1].color
color_picker.color = Tools.get_assigned_color(BUTTON_RIGHT)
_on_EditPaletteColorPicker_color_changed(color_picker.color)

View file

@ -220,11 +220,9 @@ func on_color_select(index : int) -> void:
var color : Color = Global.palettes[current_palette].get_color(index)
if Input.is_action_just_pressed("left_mouse"):
Global.color_pickers[0].color = color
Global.update_custom_brush(0)
Tools.assign_color(color, BUTTON_LEFT)
elif Input.is_action_just_pressed("right_mouse"):
Global.color_pickers[1].color = color
Global.update_custom_brush(1)
Tools.assign_color(color, BUTTON_RIGHT)
func _load_palettes() -> void:

View file

@ -1,144 +1,156 @@
extends Polygon2D
var img : Image
var tex : ImageTexture
var is_dragging := false
var move_pixels := false
var diff_x := 0.0
var diff_y := 0.0
var orig_x := 0.0
var orig_y := 0.0
var orig_colors := []
var _selected_rect := Rect2(0, 0, 0, 0)
var _clipped_rect := Rect2(0, 0, 0, 0)
var _move_image := Image.new()
var _move_texture := ImageTexture.new()
var _clear_image := Image.new()
var _move_pixel := false
var _clipboard := Image.new()
var _undo_data := {}
func _ready() -> void:
img = Image.new()
img.create(1, 1, false, Image.FORMAT_RGBA8)
img.lock()
tex = ImageTexture.new()
tex.create_from_image(img, 0)
func _process(_delta : float) -> void:
if Global.current_project.layers[Global.current_project.current_layer].locked:
return
var mouse_pos: Vector2 = get_local_mouse_position() - Global.canvas.location
var mouse_pos_floored := mouse_pos.floor()
var start_pos := polygon[0]
var end_pos := polygon[2]
var current_layer_index : int = Global.current_project.current_layer
var layer : Image = Global.current_project.frames[Global.current_project.current_frame].cels[current_layer_index].image
if end_pos == start_pos:
visible = false
else:
visible = true
if Global.can_draw and Global.has_focus and point_in_rectangle(mouse_pos, polygon[0], polygon[2]) and Global.current_project.selected_pixels.size() > 0 and (Global.current_tools[0] == Global.Tools.RECTSELECT or Global.current_tools[1] == Global.Tools.RECTSELECT):
get_parent().get_parent().mouse_default_cursor_shape = Input.CURSOR_MOVE
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
if (Global.current_tools[0] == Global.Tools.RECTSELECT && Input.is_action_just_pressed("left_mouse")) || (Global.current_tools[1] == Global.Tools.RECTSELECT && Input.is_action_just_pressed("right_mouse")):
# Begin dragging
is_dragging = true
if Input.is_key_pressed(KEY_SHIFT):
move_pixels = true
else:
move_pixels = false
img.fill(Color(0, 0, 0, 0))
diff_x = end_pos.x - mouse_pos_floored.x
diff_y = end_pos.y - mouse_pos_floored.y
orig_x = start_pos.x - mouse_pos_floored.x
orig_y = start_pos.y - mouse_pos_floored.y
if move_pixels:
img.unlock()
img.resize(polygon[2].x - polygon[0].x, polygon[2].y - polygon[0].y, 0)
img.lock()
for i in range(Global.current_project.selected_pixels.size()):
var curr_px = Global.current_project.selected_pixels[i]
if point_in_rectangle(curr_px, Global.canvas.location - Vector2.ONE, Global.current_project.size):
orig_colors.append(layer.get_pixelv(curr_px)) # Color of pixel
var px = curr_px - Global.current_project.selected_pixels[0]
img.set_pixelv(px, orig_colors[i])
layer.set_pixelv(curr_px, Color(0, 0, 0, 0))
else: # If part of selection is outside canvas
orig_colors.append(Color(0, 0, 0, 0))
Global.canvas.update_texture(current_layer_index)
tex.create_from_image(img, 0)
update()
else:
get_parent().get_parent().mouse_default_cursor_shape = Input.CURSOR_CROSS
if is_dragging:
if (Global.current_tools[0] == Global.Tools.RECTSELECT && Input.is_action_pressed("left_mouse")) || (Global.current_tools[1] == Global.Tools.RECTSELECT && Input.is_action_pressed("right_mouse")):
# Drag
start_pos.x = orig_x + mouse_pos_floored.x
end_pos.x = diff_x + mouse_pos_floored.x
start_pos.y = orig_y + mouse_pos_floored.y
end_pos.y = diff_y + mouse_pos_floored.y
polygon[0] = start_pos
polygon[1] = Vector2(end_pos.x, start_pos.y)
polygon[2] = end_pos
polygon[3] = Vector2(start_pos.x, end_pos.y)
if (Global.current_tools[0] == Global.Tools.RECTSELECT && Input.is_action_just_released("left_mouse")) || (Global.current_tools[1] == Global.Tools.RECTSELECT && Input.is_action_just_released("right_mouse")):
# Release Drag
is_dragging = false
if move_pixels:
for i in range(orig_colors.size()):
if orig_colors[i].a > 0:
var px = polygon[0] + Global.current_project.selected_pixels[i] - Global.current_project.selected_pixels[0]
if point_in_rectangle(px, Global.canvas.location - Vector2.ONE, Global.current_project.size):
layer.set_pixelv(px, orig_colors[i])
Global.canvas.update_texture(current_layer_index)
img.fill(Color(0, 0, 0, 0))
tex.create_from_image(img, 0)
update()
orig_colors.clear()
Global.current_project.selected_pixels.clear()
for xx in range(start_pos.x, end_pos.x):
for yy in range(start_pos.y, end_pos.y):
Global.current_project.selected_pixels.append(Vector2(xx, yy))
Global.canvas.handle_redo("Rectangle Select") # Redo
if Global.current_project.selected_pixels.size() > 0:
# Handle copy
if Input.is_action_just_pressed("copy"):
# Save as custom brush
var brush_img := Image.new()
brush_img = layer.get_rect(Rect2(polygon[0], polygon[2] - polygon[0]))
if brush_img.is_invisible():
return
brush_img = brush_img.get_rect(brush_img.get_used_rect()) # Save only the visible pixels
Global.current_project.brushes.append(brush_img)
Global.create_brush_button(brush_img)
# Have it in the clipboard so it can be pasted later
Global.image_clipboard = layer.get_rect(Rect2(polygon[0], polygon[2] - polygon[0]))
# Handle paste
if Input.is_action_just_pressed("paste") && Global.image_clipboard.get_size() > Vector2.ZERO:
Global.canvas.handle_undo("Draw")
layer.blend_rect(Global.image_clipboard, Rect2(Vector2.ZERO, polygon[2]-polygon[0]), polygon[0])
layer.lock()
Global.canvas.handle_redo("Draw")
if Input.is_action_just_pressed("delete"):
Global.canvas.handle_undo("Draw")
for xx in range(start_pos.x, end_pos.x):
for yy in range(start_pos.y, end_pos.y):
if point_in_rectangle(Vector2(xx, yy), Global.canvas.location - Vector2.ONE, Global.canvas.location + Global.current_project.size):
layer.set_pixel(xx, yy, Color(0, 0, 0, 0))
Global.canvas.handle_redo("Draw")
_clear_image.create(1, 1, false, Image.FORMAT_RGBA8)
_clear_image.fill(Color(0, 0, 0, 0))
func _draw() -> void:
if img.get_size() == polygon[2] - polygon[0]:
draw_texture(tex, polygon[0], Color(1, 1, 1, 0.5))
if _move_pixel:
draw_texture(_move_texture, _clipped_rect.position, Color(1, 1, 1, 0.5))
func point_in_rectangle(p : Vector2, coord1 : Vector2, coord2 : Vector2) -> bool:
return p.x > coord1.x && p.y > coord1.y && p.x < coord2.x && p.y < coord2.y
func has_point(position : Vector2) -> bool:
return _selected_rect.has_point(position)
func get_rect() -> Rect2:
return _selected_rect
func set_rect(rect : Rect2) -> void:
_selected_rect = rect
polygon[0] = rect.position
polygon[1] = Vector2(rect.end.x, rect.position.y)
polygon[2] = rect.end
polygon[3] = Vector2(rect.position.x, rect.end.y)
visible = not rect.has_no_area()
func move_rect(move : Vector2) -> void:
_selected_rect.position += move
_clipped_rect.position += move
set_rect(_selected_rect)
func select_rect() -> void:
var undo_data = _get_undo_data(false)
Global.current_project.selected_rect = _selected_rect
commit_undo("Rectangle Select", undo_data)
func move_start(move_pixel : bool) -> void:
if not move_pixel:
return
_undo_data = _get_undo_data(true)
var project := Global.current_project
var image : Image = project.frames[project.current_frame].cels[project.current_layer].image
var rect = Rect2(Vector2.ZERO, project.size)
_clipped_rect = rect.clip(_selected_rect)
_move_image = image.get_rect(_clipped_rect)
_move_texture.create_from_image(_move_image, 0)
var size := _clipped_rect.size
rect = Rect2(Vector2.ZERO, size)
_clear_image.resize(size.x, size.y, Image.INTERPOLATE_NEAREST)
image.blit_rect(_clear_image, rect, _clipped_rect.position)
Global.canvas.update_texture(project.current_layer)
_move_pixel = true
update()
func move_end() -> void:
var undo_data = _undo_data if _move_pixel else _get_undo_data(false)
if _move_pixel:
var project := Global.current_project
var image : Image = project.frames[project.current_frame].cels[project.current_layer].image
var size := _clipped_rect.size
var rect = Rect2(Vector2.ZERO, size)
image.blit_rect_mask(_move_image, _move_image, rect, _clipped_rect.position)
_move_pixel = false
update()
Global.current_project.selected_rect = _selected_rect
commit_undo("Rectangle Select", undo_data)
_undo_data.clear()
func copy() -> void:
if _selected_rect.has_no_area():
return
var project := Global.current_project
var image : Image = project.frames[project.current_frame].cels[project.current_layer].image
_clipboard = image.get_rect(_selected_rect)
if _clipboard.is_invisible():
return
var brush = _clipboard.get_rect(_clipboard.get_used_rect())
project.brushes.append(brush)
Brushes.add_project_brush(brush)
func paste() -> void:
if _clipboard.get_size() <= Vector2.ZERO:
return
var undo_data = _get_undo_data(true)
var project := Global.current_project
var image : Image = project.frames[project.current_frame].cels[project.current_layer].image
var size := _selected_rect.size
var rect = Rect2(Vector2.ZERO, size)
image.blend_rect(_clipboard, rect, _selected_rect.position)
commit_undo("Draw", undo_data)
func delete() -> void:
var undo_data = _get_undo_data(true)
var project := Global.current_project
var image : Image = project.frames[project.current_frame].cels[project.current_layer].image
var size := _selected_rect.size
var rect = Rect2(Vector2.ZERO, size)
_clear_image.resize(size.x, size.y, Image.INTERPOLATE_NEAREST)
image.blit_rect(_clear_image, rect, _selected_rect.position)
commit_undo("Draw", undo_data)
func commit_undo(action : String, undo_data : Dictionary) -> void:
var redo_data = _get_undo_data("image_data" in undo_data)
var project := Global.current_project
project.undos += 1
project.undo_redo.create_action(action)
project.undo_redo.add_do_property(project, "selected_rect", redo_data["selected_rect"])
project.undo_redo.add_undo_property(project, "selected_rect", undo_data["selected_rect"])
if "image_data" in undo_data:
var image : Image = project.frames[project.current_frame].cels[project.current_layer].image
project.undo_redo.add_do_property(image, "data", redo_data["image_data"])
project.undo_redo.add_undo_property(image, "data", undo_data["image_data"])
project.undo_redo.add_do_method(Global, "redo", project.current_frame, project.current_layer)
project.undo_redo.add_undo_method(Global, "undo", project.current_frame, project.current_layer)
project.undo_redo.commit_action()
func _get_undo_data(undo_image : bool) -> Dictionary:
var data = {}
var project := Global.current_project
data["selected_rect"] = Global.current_project.selected_rect
if undo_image:
var image : Image = project.frames[project.current_frame].cels[project.current_layer].image
image.unlock()
data["image_data"] = image.data
image.lock()
return data

130
src/Tools/Base.gd Normal file
View file

@ -0,0 +1,130 @@
extends VBoxContainer
var kname : String
var tool_slot : Tools.Slot = null
var cursor_text := ""
var _cursor := Vector2.INF
func _ready():
kname = name.replace(" ", "_").to_lower()
$Label.text = tool_slot.name
yield(get_tree(), "idle_frame")
load_config()
$PixelPerfect.pressed = tool_slot.pixel_perfect
$Mirror/Horizontal.pressed = tool_slot.horizontal_mirror
$Mirror/Vertical.pressed = tool_slot.vertical_mirror
func _on_PixelPerfect_toggled(button_pressed : bool):
tool_slot.pixel_perfect = button_pressed
tool_slot.save_config()
func _on_Horizontal_toggled(button_pressed : bool):
tool_slot.horizontal_mirror = button_pressed
tool_slot.save_config()
func _on_Vertical_toggled(button_pressed : bool):
tool_slot.vertical_mirror = button_pressed
tool_slot.save_config()
func save_config() -> void:
var config := get_config()
Global.config_cache.set_value(tool_slot.kname, kname, config)
func load_config() -> void:
var value = Global.config_cache.get_value(tool_slot.kname, kname, {})
set_config(value)
update_config()
func get_config() -> Dictionary:
return {}
func set_config(_config : Dictionary) -> void:
pass
func update_config() -> void:
pass
func cursor_move(position : Vector2) -> void:
_cursor = position
func draw_indicator() -> void:
var rect := Rect2(_cursor, Vector2.ONE)
Global.canvas.draw_rect(rect, Color.blue, false)
func _get_draw_rect() -> Rect2:
var x_min : int = Global.current_project.x_min
var x_max : int = Global.current_project.x_max
var y_min : int = Global.current_project.y_min
var y_max : int = Global.current_project.y_max
return Rect2(x_min, y_min, x_max - x_min, y_max - y_min)
func _get_tile_mode_rect() -> Rect2:
return Rect2(-Global.current_project.size, Global.current_project.size * 3)
func _get_draw_image() -> Image:
var project : Project = Global.current_project
return project.frames[project.current_frame].cels[project.current_layer].image
func _flip_rect(rect : Rect2, size : Vector2, horizontal : bool, vertical : bool) -> Rect2:
var result := rect
if horizontal:
result.position.x = size.x - rect.end.x
result.end.x = size.x - rect.position.x
if vertical:
result.position.y = size.y - rect.end.y
result.end.y = size.y - rect.position.y
return result.abs()
func _create_polylines(bitmap : BitMap) -> Array:
var lines := []
var size := bitmap.get_size()
for y in size.y:
for x in size.x:
var p := Vector2(x, y)
if not bitmap.get_bit(p):
continue
if x <= 0 or not bitmap.get_bit(p - Vector2(1, 0)):
_add_polylines_segment(lines, p, p + Vector2(0, 1))
if y <= 0 or not bitmap.get_bit(p - Vector2(0, 1)):
_add_polylines_segment(lines, p, p + Vector2(1, 0))
if x + 1 >= size.x or not bitmap.get_bit(p + Vector2(1, 0)):
_add_polylines_segment(lines, p + Vector2(1, 0), p + Vector2(1, 1))
if y + 1 >= size.y or not bitmap.get_bit(p + Vector2(0, 1)):
_add_polylines_segment(lines, p + Vector2(0, 1), p + Vector2(1, 1))
return lines
func _add_polylines_segment(lines : Array, start : Vector2, end : Vector2) -> void:
for line in lines:
if line[0] == start:
line.insert(0, end)
return
if line[0] == end:
line.insert(0, start)
return
if line[line.size() - 1] == start:
line.append(end)
return
if line[line.size() - 1] == end:
line.append(start)
return
lines.append([start, end])

77
src/Tools/Base.tscn Normal file
View file

@ -0,0 +1,77 @@
[gd_scene load_steps=6 format=2]
[ext_resource path="res://src/Tools/Base.gd" type="Script" id=1]
[ext_resource path="res://assets/graphics/dark_themes/tools/horizontal_mirror_on.png" type="Texture" id=2]
[ext_resource path="res://assets/graphics/dark_themes/tools/horizontal_mirror_off.png" type="Texture" id=3]
[ext_resource path="res://assets/graphics/dark_themes/tools/vertical_mirror_on.png" type="Texture" id=4]
[ext_resource path="res://assets/graphics/dark_themes/tools/vertical_mirror_off.png" type="Texture" id=5]
[node name="ToolOptions" type="VBoxContainer"]
margin_left = 7.0
margin_top = 7.0
margin_right = 123.0
margin_bottom = 65.0
size_flags_horizontal = 3
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Label" type="Label" parent="."]
margin_right = 116.0
margin_bottom = 14.0
text = "Tool"
align = 1
autowrap = true
[node name="PixelPerfect" type="CheckBox" parent="."]
margin_left = 4.0
margin_top = 18.0
margin_right = 112.0
margin_bottom = 42.0
grow_horizontal = 2
grow_vertical = 2
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
text = "Pixel Perfect"
align = 1
[node name="EmptySpacer" type="Control" parent="."]
margin_top = 46.0
margin_right = 116.0
margin_bottom = 58.0
rect_min_size = Vector2( 0, 12 )
[node name="Mirror" type="HBoxContainer" parent="."]
margin_top = 62.0
margin_right = 116.0
margin_bottom = 79.0
custom_constants/separation = 44
alignment = 1
[node name="Horizontal" type="TextureButton" parent="Mirror" groups=[
"UIButtons",
]]
margin_left = 20.0
margin_right = 35.0
margin_bottom = 17.0
hint_tooltip = "Enable horizontal mirrored drawing"
mouse_default_cursor_shape = 2
toggle_mode = true
texture_normal = ExtResource( 3 )
texture_pressed = ExtResource( 2 )
[node name="Vertical" type="TextureButton" parent="Mirror" groups=[
"UIButtons",
]]
margin_left = 79.0
margin_right = 96.0
margin_bottom = 17.0
hint_tooltip = "Enable vertical mirrored drawing"
mouse_default_cursor_shape = 2
toggle_mode = true
texture_normal = ExtResource( 5 )
texture_pressed = ExtResource( 4 )
[connection signal="toggled" from="PixelPerfect" to="." method="_on_PixelPerfect_toggled"]
[connection signal="toggled" from="Mirror/Horizontal" to="." method="_on_Horizontal_toggled"]
[connection signal="toggled" from="Mirror/Vertical" to="." method="_on_Vertical_toggled"]

204
src/Tools/Bucket.gd Normal file
View file

@ -0,0 +1,204 @@
extends "res://src/Tools/Base.gd"
var _pattern : Patterns.Pattern
var _fill_area := 0
var _fill_with := 0
var _offset_x := 0
var _offset_y := 0
func _ready() -> void:
update_pattern()
func _on_FillAreaOptions_item_selected(index : int) -> void:
_fill_area = index
update_config()
save_config()
func _on_FillWithOptions_item_selected(index : int) -> void:
_fill_with = index
update_config()
save_config()
func _on_PatternType_pressed():
Global.patterns_popup.connect("pattern_selected", self, "_on_Pattern_selected", [], CONNECT_ONESHOT)
Global.patterns_popup.popup(Rect2($FillPattern/Type.rect_global_position, Vector2(226, 72)))
func _on_Pattern_selected(pattern : Patterns.Pattern) -> void:
_pattern = pattern
update_pattern()
save_config()
func _on_PatternOffsetX_value_changed(value : float):
_offset_x = int(value)
update_config()
save_config()
func _on_PatternOffsetY_value_changed(value : float):
_offset_y = int(value)
update_config()
save_config()
func get_config() -> Dictionary:
return {
"pattern_index" : _pattern.index,
"fill_area" : _fill_area,
"fill_with" : _fill_with,
"offset_x" : _offset_x,
"offset_y" : _offset_y,
}
func set_config(config : Dictionary) -> void:
var index = config.get("pattern_index", _pattern.index)
_pattern = Global.patterns_popup.get_pattern(index)
_fill_area = config.get("fill_area", _fill_area)
_fill_with = config.get("fill_with", _fill_with)
_offset_x = config.get("offset_x", _offset_x)
_offset_y = config.get("offset_y", _offset_y)
update_pattern()
func update_config() -> void:
$FillAreaOptions.selected = _fill_area
$FillWithOptions.selected = _fill_with
$Mirror.visible = _fill_area == 0
$FillPattern.visible = _fill_with == 1
$FillPattern/XOffset/OffsetX.value = _offset_x
$FillPattern/YOffset/OffsetY.value = _offset_y
func update_pattern() -> void:
if _pattern == null:
_pattern = Global.patterns_popup.default_pattern
var tex := ImageTexture.new()
tex.create_from_image(_pattern.image, 0)
$FillPattern/Type/Texture.texture = tex
var size := _pattern.image.get_size()
$FillPattern/XOffset/OffsetX.max_value = size.x - 1
$FillPattern/YOffset/OffsetY.max_value = size.y - 1
func draw_start(position : Vector2) -> void:
if not _get_draw_rect().has_point(position):
return
var undo_data = _get_undo_data()
if _fill_area == 0:
fill_in_area(position)
else:
fill_in_color(position)
commit_undo("Draw", undo_data)
func draw_move(_position : Vector2) -> void:
pass
func draw_end(_position : Vector2) -> void:
pass
func fill_in_color(position : Vector2) -> void:
var project : Project = Global.current_project
var image := _get_draw_image()
var color := image.get_pixelv(position)
if _fill_with == 0 or _pattern == null:
if tool_slot.color.is_equal_approx(color):
return
image.lock()
for y in range(project.y_min, project.y_max):
for x in range(project.x_min, project.x_max):
if image.get_pixel(x, y).is_equal_approx(color):
_set_pixel(image, x, y, tool_slot.color)
func fill_in_area(position : Vector2) -> void:
var project : Project = Global.current_project
var mirror_x := project.x_max + project.x_min - position.x - 1
var mirror_y := project.y_max + project.y_min - position.y - 1
_flood_fill(position)
if tool_slot.horizontal_mirror:
_flood_fill(Vector2(mirror_x, position.y))
if tool_slot.vertical_mirror:
_flood_fill(Vector2(mirror_x, mirror_y))
if tool_slot.vertical_mirror:
_flood_fill(Vector2(position.x, mirror_y))
func _flood_fill(position : Vector2) -> void:
var project : Project = Global.current_project
var image := _get_draw_image()
var color := image.get_pixelv(position)
if _fill_with == 0 or _pattern == null:
if tool_slot.color.is_equal_approx(color):
return
image.lock()
var processed := BitMap.new()
processed.create(image.get_size())
var q = [position]
for n in q:
if processed.get_bit(n):
continue
var west : Vector2 = n
var east : Vector2 = n
while west.x >= project.x_min && image.get_pixelv(west).is_equal_approx(color):
west += Vector2.LEFT
while east.x < project.x_max && image.get_pixelv(east).is_equal_approx(color):
east += Vector2.RIGHT
for px in range(west.x + 1, east.x):
var p := Vector2(px, n.y)
_set_pixel(image, p.x, p.y, tool_slot.color)
processed.set_bit(p, true)
var north := p + Vector2.UP
var south := p + Vector2.DOWN
if north.y >= project.y_min && image.get_pixelv(north).is_equal_approx(color):
q.append(north)
if south.y < project.y_max && image.get_pixelv(south).is_equal_approx(color):
q.append(south)
func _set_pixel(image : Image, x : int, y : int, color : Color) -> void:
if _fill_with == 0 or _pattern == null:
image.set_pixel(x, y, color)
else:
_pattern.image.lock()
var size := _pattern.image.get_size()
var px := int(x + _offset_x) % int(size.x)
var py := int(y + _offset_y) % int(size.y)
var pc := _pattern.image.get_pixel(px, py)
image.set_pixel(x, y, pc)
func commit_undo(action : String, undo_data : Dictionary) -> void:
var redo_data = _get_undo_data()
var project : Project = Global.current_project
var image : Image = project.frames[project.current_frame].cels[project.current_layer].image
project.undos += 1
project.undo_redo.create_action(action)
project.undo_redo.add_do_property(image, "data", redo_data["image_data"])
project.undo_redo.add_undo_property(image, "data", undo_data["image_data"])
project.undo_redo.add_do_method(Global, "redo", project.current_frame, project.current_layer)
project.undo_redo.add_undo_method(Global, "undo", project.current_frame, project.current_layer)
project.undo_redo.commit_action()
func _get_undo_data() -> Dictionary:
var data = {}
var project : Project = Global.current_project
var image : Image = project.frames[project.current_frame].cels[project.current_layer].image
image.unlock()
data["image_data"] = image.data
image.lock()
return data

144
src/Tools/Bucket.tscn Normal file
View file

@ -0,0 +1,144 @@
[gd_scene load_steps=4 format=2]
[ext_resource path="res://assets/graphics/brush_button.png" type="Texture" id=1]
[ext_resource path="res://src/Tools/Base.tscn" type="PackedScene" id=2]
[ext_resource path="res://src/Tools/Bucket.gd" type="Script" id=3]
[node name="ToolOptions" instance=ExtResource( 2 )]
script = ExtResource( 3 )
[node name="Label" parent="." index="0"]
margin_right = 131.0
[node name="FillArea" type="Label" parent="." index="1"]
margin_left = 38.0
margin_top = 18.0
margin_right = 92.0
margin_bottom = 32.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
text = "Fill area:"
[node name="FillAreaOptions" type="OptionButton" parent="." index="2"]
margin_top = 36.0
margin_right = 131.0
margin_bottom = 56.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
text = "Same color area"
items = [ "Same color area", null, false, 0, null, "Same color pixels", null, false, 1, null ]
selected = 0
[node name="FillWith" type="Label" parent="." index="3"]
margin_left = 38.0
margin_top = 60.0
margin_right = 92.0
margin_bottom = 74.0
size_flags_horizontal = 4
text = "Fill with:"
[node name="FillWithOptions" type="OptionButton" parent="." index="4"]
margin_left = 5.0
margin_top = 78.0
margin_right = 126.0
margin_bottom = 98.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
text = "Selected Color"
items = [ "Selected Color", null, false, 0, null, "Pattern", null, false, 1, null ]
selected = 0
[node name="FillPattern" type="VBoxContainer" parent="." index="5"]
visible = false
margin_left = 22.0
margin_top = 102.0
margin_right = 108.0
margin_bottom = 208.0
size_flags_horizontal = 4
[node name="Type" type="TextureButton" parent="FillPattern" index="0"]
margin_left = 27.0
margin_right = 59.0
margin_bottom = 32.0
hint_tooltip = "Select a brush"
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
texture_normal = ExtResource( 1 )
[node name="Texture" type="TextureRect" parent="FillPattern/Type" index="0"]
margin_right = 32.0
margin_bottom = 32.0
expand = true
stretch_mode = 6
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Offset" type="Label" parent="FillPattern" index="1"]
margin_top = 36.0
margin_right = 86.0
margin_bottom = 50.0
text = "Offset"
align = 1
[node name="XOffset" type="HBoxContainer" parent="FillPattern" index="2"]
margin_top = 54.0
margin_right = 86.0
margin_bottom = 78.0
[node name="Label" type="Label" parent="FillPattern/XOffset" index="0"]
margin_top = 5.0
margin_right = 8.0
margin_bottom = 19.0
text = "X"
[node name="OffsetX" type="SpinBox" parent="FillPattern/XOffset" index="1"]
margin_left = 12.0
margin_right = 86.0
margin_bottom = 24.0
mouse_default_cursor_shape = 2
[node name="YOffset" type="HBoxContainer" parent="FillPattern" index="3"]
margin_top = 82.0
margin_right = 86.0
margin_bottom = 106.0
[node name="Label" type="Label" parent="FillPattern/YOffset" index="0"]
margin_top = 5.0
margin_right = 7.0
margin_bottom = 19.0
text = "Y"
[node name="OffsetY" type="SpinBox" parent="FillPattern/YOffset" index="1"]
margin_left = 11.0
margin_right = 85.0
margin_bottom = 24.0
mouse_default_cursor_shape = 2
[node name="PixelPerfect" parent="." index="6"]
visible = false
[node name="EmptySpacer" parent="." index="7"]
visible = false
margin_top = 212.0
margin_right = 131.0
margin_bottom = 224.0
[node name="Mirror" parent="." index="8"]
visible = false
margin_top = 212.0
margin_right = 131.0
margin_bottom = 229.0
[node name="Horizontal" parent="Mirror" index="0"]
margin_left = 27.0
margin_right = 42.0
[node name="Vertical" parent="Mirror" index="1"]
margin_left = 86.0
margin_right = 103.0
[connection signal="item_selected" from="FillAreaOptions" to="." method="_on_FillAreaOptions_item_selected"]
[connection signal="item_selected" from="FillWithOptions" to="." method="_on_FillWithOptions_item_selected"]
[connection signal="pressed" from="FillPattern/Type" to="." method="_on_PatternType_pressed"]
[connection signal="value_changed" from="FillPattern/XOffset/OffsetX" to="." method="_on_PatternOffsetX_value_changed"]
[connection signal="value_changed" from="FillPattern/YOffset/OffsetY" to="." method="_on_PatternOffsetY_value_changed"]

45
src/Tools/ColorPicker.gd Normal file
View file

@ -0,0 +1,45 @@
extends "res://src/Tools/Base.gd"
var _color_slot := 0
func _on_Options_item_selected(id):
_color_slot = id
update_config()
save_config()
func get_config() -> Dictionary:
return {
"color_slot" : _color_slot,
}
func set_config(config : Dictionary) -> void:
_color_slot = config.get("color_slot", _color_slot)
func update_config() -> void:
$ColorPicker/Options.selected = _color_slot
func draw_start(position : Vector2) -> void:
_pick_color(position)
func draw_move(position : Vector2) -> void:
_pick_color(position)
func draw_end(_position : Vector2) -> void:
pass
func _pick_color(position : Vector2) -> void:
var image := Image.new()
image.copy_from(_get_draw_image())
image.lock()
var color := image.get_pixelv(position)
var button := BUTTON_LEFT if _color_slot == 0 else BUTTON_RIGHT
Tools.assign_color(color, button)

View file

@ -0,0 +1,47 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://src/Tools/Base.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/Tools/ColorPicker.gd" type="Script" id=2]
[node name="ToolOptions" instance=ExtResource( 1 )]
script = ExtResource( 2 )
[node name="ColorPicker" type="VBoxContainer" parent="." index="1"]
margin_top = 18.0
margin_right = 116.0
margin_bottom = 56.0
[node name="Label" type="Label" parent="ColorPicker" index="0"]
margin_left = 32.0
margin_right = 83.0
margin_bottom = 14.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
text = "Pick for:"
[node name="Options" type="OptionButton" parent="ColorPicker" index="1"]
margin_left = 13.0
margin_top = 18.0
margin_right = 103.0
margin_bottom = 38.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
text = "Left Color"
items = [ "Left Color", null, false, 0, null, "Right Color", null, false, 1, null ]
selected = 0
[node name="PixelPerfect" parent="." index="2"]
visible = false
margin_top = 60.0
margin_bottom = 84.0
[node name="EmptySpacer" parent="." index="3"]
visible = false
margin_top = 60.0
margin_bottom = 72.0
[node name="Mirror" parent="." index="4"]
visible = false
margin_top = 60.0
margin_bottom = 77.0
[connection signal="item_selected" from="ColorPicker/Options" to="." method="_on_Options_item_selected"]

494
src/Tools/Draw.gd Normal file
View file

@ -0,0 +1,494 @@
extends "res://src/Tools/Base.gd"
var _brush := Brushes.get_default_brush()
var _brush_size := 1
var _brush_interpolate := 0
var _brush_image := Image.new()
var _brush_texture := ImageTexture.new()
var _strength := 1.0
var _undo_data := {}
var _drawer := Drawer.new()
var _mask := PoolByteArray()
var _mirror_brushes := {}
var _draw_line := false
var _line_start := Vector2.ZERO
var _line_end := Vector2.ZERO
var _indicator := BitMap.new()
var _polylines := []
var _line_polylines := []
func _ready() -> void:
Tools.connect("color_changed", self, "_on_Color_changed")
Global.brushes_popup.connect("brush_removed", self, "_on_Brush_removed")
func _on_BrushType_pressed() -> void:
if not Global.brushes_popup.is_connected("brush_selected", self, "_on_Brush_selected"):
Global.brushes_popup.connect("brush_selected", self, "_on_Brush_selected", [], CONNECT_ONESHOT)
Global.brushes_popup.popup(Rect2($Brush/Type.rect_global_position, Vector2(226, 72)))
func _on_Brush_selected(brush : Brushes.Brush) -> void:
_brush = brush
update_brush()
save_config()
func _on_BrushSize_value_changed(value : float) -> void:
_brush_size = int(value)
update_config()
save_config()
func _on_InterpolateFactor_value_changed(value : float) -> void:
_brush_interpolate = int(value)
update_config()
save_config()
func _on_Color_changed(_color : Color, _button : int) -> void:
update_brush()
func _on_Brush_removed(brush : Brushes.Brush) -> void:
if brush == _brush:
_brush = Brushes.get_default_brush()
update_brush()
save_config()
func get_config() -> Dictionary:
return {
"brush_type" : _brush.type,
"brush_index" : _brush.index,
"brush_size" : _brush_size,
"brush_interpolate" : _brush_interpolate,
}
func set_config(config : Dictionary) -> void:
var type = config.get("brush_type", _brush.type)
var index = config.get("brush_index", _brush.index)
_brush = Global.brushes_popup.get_brush(type, index)
_brush_size = config.get("brush_size", _brush_size)
_brush_interpolate = config.get("brush_interpolate", _brush_interpolate)
func update_config() -> void:
$Brush/Size.value = _brush_size
$BrushSize.value = _brush_size
$ColorInterpolation/Factor.value = _brush_interpolate
$ColorInterpolation/Slider.value = _brush_interpolate
update_brush()
func update_brush() -> void:
match _brush.type:
Brushes.PIXEL:
_brush_texture.create_from_image(load("res://assets/graphics/pixel_image.png"), 0)
Brushes.CIRCLE:
_brush_texture.create_from_image(load("res://assets/graphics/circle_9x9.png"), 0)
Brushes.FILLED_CIRCLE:
_brush_texture.create_from_image(load("res://assets/graphics/circle_filled_9x9.png"), 0)
Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM:
if _brush.random.size() <= 1:
_brush_image = _create_blended_brush_image(_brush.image)
else:
var random = randi() % _brush.random.size()
_brush_image = _create_blended_brush_image(_brush.random[random])
_brush_image.lock()
_brush_texture.create_from_image(_brush_image, 0)
update_mirror_brush()
_indicator = _create_brush_indicator()
_polylines = _create_polylines(_indicator)
$Brush/Type/Texture.texture = _brush_texture
$ColorInterpolation.visible = _brush.type in [Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM]
func update_random_image() -> void:
if _brush.type != Brushes.RANDOM_FILE:
return
var random = randi() % _brush.random.size()
_brush_image = _create_blended_brush_image(_brush.random[random])
_brush_image.lock()
_brush_texture.create_from_image(_brush_image, 0)
_indicator = _create_brush_indicator()
update_mirror_brush()
func update_mirror_brush() -> void:
_mirror_brushes.x = _brush_image.duplicate()
_mirror_brushes.x.flip_x()
_mirror_brushes.y = _brush_image.duplicate()
_mirror_brushes.y.flip_y()
_mirror_brushes.xy = _mirror_brushes.x.duplicate()
_mirror_brushes.xy.flip_y()
func update_mask() -> void:
var size := _get_draw_image().get_size()
_mask = PoolByteArray()
_mask.resize(size.x * size.y)
for i in _mask.size():
_mask[i] = 0
func update_line_polylines(start : Vector2, end : Vector2) -> void:
var indicator := _create_line_indicator(_indicator, start, end)
_line_polylines = _create_polylines(indicator)
func restore_image() -> void:
var project : Project = Global.current_project
var image = project.frames[project.current_frame].cels[project.current_layer].image
image.unlock()
image.data = _undo_data[image]
image.lock()
func prepare_undo() -> void:
_undo_data = _get_undo_data()
func commit_undo(action : String) -> void:
var redo_data = _get_undo_data()
var project : Project = Global.current_project
var frame := -1
var layer := -1
if Global.animation_timer.is_stopped():
frame = project.current_frame
layer = project.current_layer
project.undos += 1
project.undo_redo.create_action(action)
for image in redo_data:
project.undo_redo.add_do_property(image, "data", redo_data[image])
for image in _undo_data:
project.undo_redo.add_undo_property(image, "data", _undo_data[image])
project.undo_redo.add_do_method(Global, "redo", frame, layer)
project.undo_redo.add_undo_method(Global, "undo", frame, layer)
project.undo_redo.commit_action()
_undo_data.clear()
func draw_tool(position : Vector2) -> void:
var strength := _strength
if Global.pressure_sensitivity_mode == Global.Pressure_Sensitivity.ALPHA:
strength *= Tools.pen_pressure
_drawer.pixel_perfect = tool_slot.pixel_perfect if _brush_size == 1 else false
_drawer.horizontal_mirror = tool_slot.horizontal_mirror
_drawer.vertical_mirror = tool_slot.vertical_mirror
_drawer.color_op.strength = strength
match _brush.type:
Brushes.PIXEL:
draw_tool_pixel(position)
Brushes.CIRCLE:
draw_tool_circle(position, false)
Brushes.FILLED_CIRCLE:
draw_tool_circle(position, true)
_:
draw_tool_brush(position)
# Bresenham's Algorithm
# Thanks to https://godotengine.org/qa/35276/tile-based-line-drawing-algorithm-efficiency
func draw_fill_gap(start : Vector2, end : Vector2) -> void:
var dx := int(abs(end.x - start.x))
var dy := int(-abs(end.y - start.y))
var err := dx + dy
var e2 := err << 1
var sx = 1 if start.x < end.x else -1
var sy = 1 if start.y < end.y else -1
var x = start.x
var y = start.y
while !(x == end.x && y == end.y):
e2 = err << 1
if e2 >= dy:
err += dy
x += sx
if e2 <= dx:
err += dx
y += sy
draw_tool(Vector2(x, y))
func draw_tool_pixel(position : Vector2) -> void:
var start := position - Vector2.ONE * (_brush_size >> 1)
var end := start + Vector2.ONE * _brush_size
for y in range(start.y, end.y):
for x in range(start.x, end.x):
_set_pixel(Vector2(x, y))
# Algorithm based on http://members.chello.at/easyfilter/bresenham.html
func draw_tool_circle(position : Vector2, fill := false) -> void:
var r := _brush_size
var x := -r
var y := 0
var err := 2 - r * 2
var draw := true
if fill:
_set_pixel(position)
while x < 0:
if draw:
for i in range(1 if fill else -x, -x + 1):
_set_pixel(position + Vector2(-i, y))
_set_pixel(position + Vector2(-y, -i))
_set_pixel(position + Vector2(i, -y))
_set_pixel(position + Vector2(y, i))
draw = not fill
r = err
if r <= y:
y += 1
err += y * 2 + 1
draw = true
if r > x || err > y:
x += 1
err += x * 2 + 1
func draw_tool_brush(position : Vector2) -> void:
if Global.tile_mode and _get_tile_mode_rect().has_point(position):
position = position.posmodv(Global.current_project.size)
var size := _brush_image.get_size()
var dst := position - (size / 2).floor()
var dst_rect := Rect2(dst, size)
var draw_rect := _get_draw_rect()
dst_rect = dst_rect.clip(draw_rect)
if dst_rect.size == Vector2.ZERO:
return
var src_rect := Rect2(dst_rect.position - dst, dst_rect.size)
dst = dst_rect.position
var mirror_x = draw_rect.end.x + draw_rect.position.x - dst.x - src_rect.size.x
var mirror_y = draw_rect.end.y + draw_rect.position.y - dst.y - src_rect.size.y
_draw_brush_image(_brush_image, src_rect, dst)
if tool_slot.horizontal_mirror:
_draw_brush_image(_mirror_brushes.x, _flip_rect(src_rect, size, true, false), Vector2(mirror_x, dst.y))
if tool_slot.vertical_mirror:
_draw_brush_image(_mirror_brushes.xy, _flip_rect(src_rect, size, true, true), Vector2(mirror_x, mirror_y))
if tool_slot.vertical_mirror:
_draw_brush_image(_mirror_brushes.y, _flip_rect(src_rect, size, false, true), Vector2(dst.x, mirror_y))
func draw_indicator() -> void:
draw_indicator_at(_cursor, Vector2.ZERO, Color.blue)
if Global.tile_mode and _get_tile_mode_rect().has_point(_cursor):
var tile := _line_start if _draw_line else _cursor
if not _get_draw_rect().has_point(tile):
var offset := tile - tile.posmodv(Global.current_project.size)
draw_indicator_at(_cursor, offset, Color.green)
func draw_indicator_at(position : Vector2, offset : Vector2, color : Color) -> void:
var canvas = Global.canvas
if _brush.type in [Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM] and not _draw_line:
position -= (_brush_image.get_size() / 2).floor()
position -= offset
canvas.draw_texture(_brush_texture, position)
else:
if _draw_line:
position.x = _line_end.x if _line_end.x < _line_start.x else _line_start.x
position.y = _line_end.y if _line_end.y < _line_start.y else _line_start.y
position -= (_indicator.get_size() / 2).floor()
position -= offset
canvas.draw_set_transform(position, canvas.rotation, canvas.scale)
var polylines := _line_polylines if _draw_line else _polylines
for line in polylines:
var pool := PoolVector2Array(line)
canvas.draw_polyline(pool, color)
canvas.draw_set_transform(canvas.position, canvas.rotation, canvas.scale)
func _set_pixel(position : Vector2) -> void:
if Global.tile_mode and _get_tile_mode_rect().has_point(position):
position = position.posmodv(Global.current_project.size)
if not _get_draw_rect().has_point(position):
return
var image := _get_draw_image()
var i := int(position.x + position.y * image.get_size().x)
if _mask[i] < Tools.pen_pressure:
_mask[i] = Tools.pen_pressure
_drawer.set_pixel(image, position, tool_slot.color)
func _draw_brush_image(_image : Image, _src_rect: Rect2, _dst: Vector2) -> void:
pass
func _create_blended_brush_image(image : Image) -> Image:
var size := image.get_size() * _brush_size
var brush := Image.new()
brush.copy_from(image)
brush = _blend_image(brush, tool_slot.color, _brush_interpolate / 100.0)
brush.unlock()
brush.resize(size.x, size.y, Image.INTERPOLATE_NEAREST)
return brush
func _blend_image(image : Image, color : Color, factor : float) -> Image:
var size := image.get_size()
image.lock()
for y in size.y:
for x in size.x:
var color_old := image.get_pixel(x, y)
if color_old.a > 0:
var color_new := color_old.linear_interpolate(color, factor)
color_new.a = color_old.a
image.set_pixel(x, y, color_new)
return image
func _create_brush_indicator() -> BitMap:
match _brush.type:
Brushes.PIXEL:
return _create_pixel_indicator(_brush_size)
Brushes.CIRCLE:
return _create_circle_indicator(_brush_size, false)
Brushes.FILLED_CIRCLE:
return _create_circle_indicator(_brush_size, true)
_:
return _create_image_indicator(_brush_image)
func _create_image_indicator(image : Image) -> BitMap:
var bitmap := BitMap.new()
bitmap.create_from_image_alpha(image, 0.0)
return bitmap
func _create_pixel_indicator(size : int) -> BitMap:
var bitmap := BitMap.new()
bitmap.create(Vector2.ONE * size)
bitmap.set_bit_rect(Rect2(Vector2.ZERO, Vector2.ONE * size), true)
return bitmap
func _create_circle_indicator(size : int, fill := false) -> BitMap:
var bitmap := BitMap.new()
bitmap.create(Vector2.ONE * (size * 2 + 1))
var position := Vector2(size, size)
var r := size
var x := -r
var y := 0
var err := 2 - r * 2
var draw := true
if fill:
bitmap.set_bit(position, true)
while x < 0:
if draw:
for i in range(1 if fill else -x, -x + 1):
bitmap.set_bit(position + Vector2(-i, y), true)
bitmap.set_bit(position + Vector2(-y, -i), true)
bitmap.set_bit(position + Vector2(i, -y), true)
bitmap.set_bit(position + Vector2(y, i), true)
draw = not fill
r = err
if r <= y:
y += 1
err += y * 2 + 1
draw = true
if r > x || err > y:
x += 1
err += x * 2 + 1
return bitmap
func _create_line_indicator(indicator : BitMap, start : Vector2, end : Vector2) -> BitMap:
var bitmap := BitMap.new()
var size := (end - start).abs() + indicator.get_size()
bitmap.create(size)
var offset := (indicator.get_size() / 2).floor()
var diff := end - start
start.x = -diff.x if diff.x < 0 else 0.0
end.x = 0.0 if diff.x < 0 else diff.x
start.y = -diff.y if diff.y < 0 else 0.0
end.y = 0.0 if diff.y < 0 else diff.y
start += offset
end += offset
var dx := int(abs(end.x - start.x))
var dy := int(-abs(end.y - start.y))
var err := dx + dy
var e2 := err << 1
var sx = 1 if start.x < end.x else -1
var sy = 1 if start.y < end.y else -1
var x = start.x
var y = start.y
while !(x == end.x && y == end.y):
_blit_indicator(bitmap, indicator, Vector2(x, y))
e2 = err << 1
if e2 >= dy:
err += dy
x += sx
if e2 <= dx:
err += dx
y += sy
_blit_indicator(bitmap, indicator, Vector2(x, y))
return bitmap
func _blit_indicator(dst : BitMap, indicator : BitMap, position : Vector2) -> void:
var rect := Rect2(Vector2.ZERO, dst.get_size())
var size := indicator.get_size()
position -= (size / 2).floor()
for y in size.y:
for x in size.x:
var pos := Vector2(x, y)
var bit := indicator.get_bit(pos)
pos += position
if bit and rect.has_point(pos):
dst.set_bit(pos, bit)
func _line_angle_constraint(start : Vector2, end : Vector2) -> Dictionary:
var result := {}
var angle := rad2deg(end.angle_to_point(start))
var distance := start.distance_to(end)
if Tools.control:
if tool_slot.pixel_perfect:
angle = stepify(angle, 22.5)
if step_decimals(angle) != 0:
var diff := end - start
var v := Vector2(2 , 1) if abs(diff.x) > abs(diff.y) else Vector2(1 , 2)
var p := diff.project(diff.sign() * v).abs().round()
var f := p.y if abs(diff.x) > abs(diff.y) else p.x
end = start + diff.sign() * v * f - diff.sign()
angle = rad2deg(atan2(sign(diff.y) * v.y, sign(diff.x) * v.x))
else:
end = start + Vector2.RIGHT.rotated(deg2rad(angle)) * distance
else:
angle = stepify(angle, 15)
end = start + Vector2.RIGHT.rotated(deg2rad(angle)) * distance
angle *= -1
angle += 360 if angle < 0 else 0
result.text = str(stepify(angle, 0.01)) + "°"
result.position = end.round()
return result
func _get_undo_data() -> Dictionary:
var data = {}
var project : Project = Global.current_project
var frames := project.frames
if Global.animation_timer.is_stopped():
frames = [project.frames[project.current_frame]]
for frame in frames:
var image : Image = frame.cels[project.current_layer].image
image.unlock()
data[image] = image.data
image.lock()
return data

114
src/Tools/Draw.tscn Normal file
View file

@ -0,0 +1,114 @@
[gd_scene load_steps=4 format=2]
[ext_resource path="res://assets/graphics/brush_button.png" type="Texture" id=1]
[ext_resource path="res://src/Tools/Base.tscn" type="PackedScene" id=2]
[ext_resource path="res://src/Tools/Draw.gd" type="Script" id=3]
[node name="ToolOptions" instance=ExtResource( 2 )]
script = ExtResource( 3 )
[node name="Brush" type="HBoxContainer" parent="." index="1"]
margin_top = 18.0
margin_right = 116.0
margin_bottom = 50.0
alignment = 1
[node name="Type" type="TextureButton" parent="Brush" index="0"]
margin_left = 1.0
margin_right = 37.0
margin_bottom = 32.0
rect_min_size = Vector2( 36, 32 )
hint_tooltip = "Select a brush"
mouse_default_cursor_shape = 2
size_flags_horizontal = 0
texture_normal = ExtResource( 1 )
[node name="Texture" type="TextureRect" parent="Brush/Type" index="0"]
margin_right = 32.0
margin_bottom = 32.0
expand = true
stretch_mode = 6
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Size" type="SpinBox" parent="Brush" index="1"]
margin_left = 41.0
margin_right = 115.0
margin_bottom = 32.0
mouse_default_cursor_shape = 2
min_value = 1.0
value = 1.0
align = 1
suffix = "px"
[node name="BrushSize" type="HSlider" parent="." index="2"]
margin_left = 12.0
margin_top = 54.0
margin_right = 104.0
margin_bottom = 70.0
rect_min_size = Vector2( 92, 0 )
focus_mode = 0
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
size_flags_vertical = 1
min_value = 1.0
value = 1.0
allow_greater = true
ticks_on_borders = true
[node name="PixelPerfect" parent="." index="3"]
margin_top = 74.0
margin_bottom = 98.0
[node name="ColorInterpolation" type="VBoxContainer" parent="." index="4"]
visible = false
margin_top = 102.0
margin_right = 116.0
margin_bottom = 164.0
alignment = 1
[node name="Label" type="Label" parent="ColorInterpolation" index="0"]
margin_left = 4.0
margin_right = 111.0
margin_bottom = 14.0
hint_tooltip = "0: Color from the brush itself, 100: the currently selected color"
mouse_filter = 1
size_flags_horizontal = 4
text = "Brush color from"
[node name="Factor" type="SpinBox" parent="ColorInterpolation" index="1"]
margin_left = 21.0
margin_top = 18.0
margin_right = 95.0
margin_bottom = 42.0
hint_tooltip = "0: Color from the brush itself, 100: the currently selected color"
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
align = 1
[node name="Slider" type="HSlider" parent="ColorInterpolation" index="2"]
margin_left = 12.0
margin_top = 46.0
margin_right = 104.0
margin_bottom = 62.0
rect_min_size = Vector2( 92, 0 )
hint_tooltip = "0: Color from the brush itself, 100: the currently selected color"
focus_mode = 0
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
size_flags_vertical = 1
ticks_on_borders = true
[node name="EmptySpacer" parent="." index="5"]
margin_top = 102.0
margin_bottom = 114.0
[node name="Mirror" parent="." index="6"]
margin_top = 118.0
margin_bottom = 135.0
[connection signal="pressed" from="Brush/Type" to="." method="_on_BrushType_pressed"]
[connection signal="value_changed" from="Brush/Size" to="." method="_on_BrushSize_value_changed"]
[connection signal="value_changed" from="BrushSize" to="." method="_on_BrushSize_value_changed"]
[connection signal="value_changed" from="ColorInterpolation/Factor" to="." method="_on_InterpolateFactor_value_changed"]
[connection signal="value_changed" from="ColorInterpolation/Slider" to="." method="_on_InterpolateFactor_value_changed"]

75
src/Tools/Eraser.gd Normal file
View file

@ -0,0 +1,75 @@
extends "res://src/Tools/Draw.gd"
var _last_position := Vector2.INF
var _clear_image := Image.new()
var _changed := false
class EraseOp extends Drawer.ColorOp:
var changed := false
func process(_src: Color, _dst: Color) -> Color:
changed = true
# dst.a -= src.a * strength
# return dst
return Color(0, 0, 0, 0)
func _init() -> void:
_drawer.color_op = EraseOp.new()
_clear_image.create(1, 1, false, Image.FORMAT_RGBA8)
_clear_image.fill(Color(0, 0, 0, 0))
func draw_start(position : Vector2) -> void:
update_mask()
_changed = false
_drawer.color_op.changed = false
prepare_undo()
_drawer.reset()
_draw_line = Tools.shift
if _draw_line:
_line_start = position
_line_end = position
update_line_polylines(_line_start, _line_end)
else:
draw_tool(position)
_last_position = position
Global.canvas.sprite_changed_this_frame = true
cursor_text = ""
func draw_move(position : Vector2) -> void:
if _draw_line:
var d = _line_angle_constraint(_line_start, position)
_line_end = d.position
cursor_text = d.text
update_line_polylines(_line_start, _line_end)
else:
draw_fill_gap(_last_position, position)
_last_position = position
cursor_text = ""
Global.canvas.sprite_changed_this_frame = true
func draw_end(_position : Vector2) -> void:
if _draw_line:
draw_tool(_line_start)
draw_fill_gap(_line_start, _line_end)
_draw_line = false
if _changed or _drawer.color_op.changed:
commit_undo("Draw")
cursor_text = ""
update_random_image()
func _draw_brush_image(_image : Image, src_rect: Rect2, dst: Vector2) -> void:
_changed = true
var size := _image.get_size()
if _clear_image.get_size() != size:
_clear_image.resize(size.x, size.y, Image.INTERPOLATE_NEAREST)
_get_draw_image().blit_rect_mask(_clear_image, _image, src_rect, dst)

7
src/Tools/Eraser.tscn Normal file
View file

@ -0,0 +1,7 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://src/Tools/Draw.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/Tools/Eraser.gd" type="Script" id=2]
[node name="ToolOptions" instance=ExtResource( 1 )]
script = ExtResource( 2 )

112
src/Tools/LightenDarken.gd Normal file
View file

@ -0,0 +1,112 @@
extends "res://src/Tools/Draw.gd"
var _last_position := Vector2.INF
var _changed := false
var _mode := 0
var _amount := 10
class LightenDarkenOp extends Drawer.ColorOp:
var changed := false
func process(_src: Color, dst: Color) -> Color:
changed = true
if strength > 0:
return dst.lightened(strength)
elif strength < 0:
return dst.darkened(-strength)
else:
return dst
func _init() -> void:
_drawer.color_op = LightenDarkenOp.new()
func _on_LightenDarken_item_selected(id : int):
_mode = id
update_config()
save_config()
func _on_LightenDarken_value_changed(value : float):
_amount = int(value)
update_config()
save_config()
func get_config() -> Dictionary:
var config := .get_config()
config["mode"] = _mode
config["amount"] = _amount
return config
func set_config(config : Dictionary) -> void:
.set_config(config)
_mode = config.get("mode", _mode)
_amount = config.get("amount", _amount)
func update_config() -> void:
.update_config()
$LightenDarken.selected = _mode
$Amount/Spinbox.value = _amount
$Amount/Slider.value = _amount
update_strength()
func update_strength() -> void:
var factor = 1 if _mode == 0 else -1
_strength = _amount * factor / 100.0
func draw_start(position : Vector2) -> void:
update_mask()
_changed = false
_drawer.color_op.changed = false
prepare_undo()
_drawer.reset()
_draw_line = Tools.shift
if _draw_line:
_line_start = position
_line_end = position
update_line_polylines(_line_start, _line_end)
else:
draw_tool(position)
_last_position = position
Global.canvas.sprite_changed_this_frame = true
cursor_text = ""
func draw_move(position : Vector2) -> void:
if _draw_line:
var d = _line_angle_constraint(_line_start, position)
_line_end = d.position
cursor_text = d.text
update_line_polylines(_line_start, _line_end)
else:
draw_fill_gap(_last_position, position)
_last_position = position
cursor_text = ""
Global.canvas.sprite_changed_this_frame = true
func draw_end(_position : Vector2) -> void:
if _draw_line:
draw_tool(_line_start)
draw_fill_gap(_line_start, _line_end)
_draw_line = false
if _changed or _drawer.color_op.changed:
commit_undo("Draw")
cursor_text = ""
update_random_image()
func _draw_brush_image(_image : Image, _src_rect: Rect2, _dst: Vector2) -> void:
_changed = true
draw_tool_pixel(_cursor.floor())

View file

@ -0,0 +1,68 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://src/Tools/Draw.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/Tools/LightenDarken.gd" type="Script" id=2]
[node name="ToolOptions" instance=ExtResource( 1 )]
script = ExtResource( 2 )
[node name="LightenDarken" type="OptionButton" parent="." index="4"]
margin_left = 12.0
margin_top = 102.0
margin_right = 104.0
margin_bottom = 122.0
rect_min_size = Vector2( 92, 0 )
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
text = "Lighten"
items = [ "Lighten", null, false, 0, null, "Darken", null, false, 1, null ]
selected = 0
[node name="Amount" type="VBoxContainer" parent="." index="5"]
margin_top = 126.0
margin_right = 116.0
margin_bottom = 188.0
alignment = 1
[node name="Label" type="Label" parent="Amount" index="0"]
margin_left = 30.0
margin_right = 85.0
margin_bottom = 14.0
size_flags_horizontal = 4
text = "Amount:"
[node name="Spinbox" type="SpinBox" parent="Amount" index="1"]
margin_left = 21.0
margin_top = 18.0
margin_right = 95.0
margin_bottom = 42.0
hint_tooltip = "Lighten/Darken amount"
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
value = 10.0
align = 1
[node name="Slider" type="HSlider" parent="Amount" index="2"]
margin_left = 12.0
margin_top = 46.0
margin_right = 104.0
margin_bottom = 62.0
rect_min_size = Vector2( 92, 0 )
hint_tooltip = "Lighten/Darken amount"
focus_mode = 0
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
size_flags_vertical = 1
value = 10.0
ticks_on_borders = true
[node name="EmptySpacer" parent="." index="7"]
margin_top = 192.0
margin_bottom = 204.0
[node name="Mirror" parent="." index="8"]
margin_top = 208.0
margin_bottom = 225.0
[connection signal="item_selected" from="LightenDarken" to="." method="_on_LightenDarken_item_selected"]
[connection signal="value_changed" from="Amount/Spinbox" to="." method="_on_LightenDarken_value_changed"]
[connection signal="value_changed" from="Amount/Slider" to="." method="_on_LightenDarken_value_changed"]

68
src/Tools/Pencil.gd Normal file
View file

@ -0,0 +1,68 @@
extends "res://src/Tools/Draw.gd"
var _last_position := Vector2.INF
var _changed := false
class AlphaBlendOp extends Drawer.ColorOp:
var changed := false
func process(src: Color, dst: Color) -> Color:
changed = true
src.a *= strength
return dst.blend(src)
func _init() -> void:
_drawer.color_op = AlphaBlendOp.new()
func draw_start(position : Vector2) -> void:
update_mask()
_changed = false
_drawer.color_op.changed = false
prepare_undo()
_drawer.reset()
_draw_line = Tools.shift
if _draw_line:
_line_start = position
_line_end = position
update_line_polylines(_line_start, _line_end)
else:
draw_tool(position)
_last_position = position
Global.canvas.sprite_changed_this_frame = true
cursor_text = ""
func draw_move(position : Vector2) -> void:
if _draw_line:
var d = _line_angle_constraint(_line_start, position)
_line_end = d.position
cursor_text = d.text
update_line_polylines(_line_start, _line_end)
else:
draw_fill_gap(_last_position, position)
_last_position = position
cursor_text = ""
Global.canvas.sprite_changed_this_frame = true
func draw_end(_position : Vector2) -> void:
if _draw_line:
draw_tool(_line_start)
draw_fill_gap(_line_start, _line_end)
_draw_line = false
if _changed or _drawer.color_op.changed:
commit_undo("Draw")
cursor_text = ""
update_random_image()
func _draw_brush_image(image : Image, src_rect: Rect2, dst: Vector2) -> void:
_changed = true
_get_draw_image().blend_rect(image, src_rect, dst)

7
src/Tools/Pencil.tscn Normal file
View file

@ -0,0 +1,7 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://src/Tools/Draw.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/Tools/Pencil.gd" type="Script" id=3]
[node name="ToolOptions" instance=ExtResource( 1 )]
script = ExtResource( 3 )

59
src/Tools/RectSelect.gd Normal file
View file

@ -0,0 +1,59 @@
extends "res://src/Tools/Base.gd"
var _start := Rect2(0, 0, 0, 0)
var _offset := Vector2.ZERO
var _drag := false
var _move := false
func draw_start(position : Vector2) -> void:
if Global.selection_rectangle.has_point(position):
_move = true
_offset = position
Global.selection_rectangle.move_start(Tools.shift)
_set_cursor_text(Global.selection_rectangle.get_rect())
else:
_drag = true
_start = Rect2(position, Vector2.ZERO)
Global.selection_rectangle.set_rect(_start)
func draw_move(position : Vector2) -> void:
if _move:
Global.selection_rectangle.move_rect(position - _offset)
_offset = position
_set_cursor_text(Global.selection_rectangle.get_rect())
else:
var rect := _start.expand(position).abs()
rect = rect.grow_individual(0, 0, 1, 1)
Global.selection_rectangle.set_rect(rect)
_set_cursor_text(rect)
func draw_end(_position : Vector2) -> void:
if _move:
Global.selection_rectangle.move_end()
else:
Global.selection_rectangle.select_rect()
_drag = false
_move = false
cursor_text = ""
func cursor_move(position : Vector2) -> void:
if _drag:
_cursor = Vector2.INF
elif Global.selection_rectangle.has_point(position):
_cursor = Vector2.INF
Global.main_viewport.mouse_default_cursor_shape = Input.CURSOR_MOVE
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
else:
_cursor = position
Global.main_viewport.mouse_default_cursor_shape = Input.CURSOR_CROSS
func _set_cursor_text(rect : Rect2) -> void:
cursor_text = "%s, %s" % [rect.position.x, rect.position.y]
cursor_text += " -> %s, %s" % [rect.end.x - 1, rect.end.y - 1]
cursor_text += " (%s, %s)" % [rect.size.x, rect.size.y]

20
src/Tools/RectSelect.tscn Normal file
View file

@ -0,0 +1,20 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://src/Tools/Base.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/Tools/RectSelect.gd" type="Script" id=2]
[node name="ToolOptions" instance=ExtResource( 1 )]
script = ExtResource( 2 )
[node name="PixelPerfect" parent="." index="1"]
visible = false
[node name="EmptySpacer" parent="." index="2"]
visible = false
margin_top = 18.0
margin_bottom = 30.0
[node name="Mirror" parent="." index="3"]
visible = false
margin_top = 18.0
margin_bottom = 35.0

48
src/Tools/Zoom.gd Normal file
View file

@ -0,0 +1,48 @@
extends "res://src/Tools/Base.gd"
var _zoom_mode := 0
func _on_ModeOptions_item_selected(id):
_zoom_mode = id
update_config()
save_config()
func _on_FitToFrame_pressed():
Global.camera.fit_to_frame(Global.current_project.size)
func _on_100_pressed():
Global.camera.zoom = Vector2.ONE
Global.camera.offset = Global.current_project.size / 2
Global.zoom_level_label.text = str(round(100 / Global.camera.zoom.x)) + " %"
Global.horizontal_ruler.update()
Global.vertical_ruler.update()
func get_config() -> Dictionary:
return {
"zoom_mode" : _zoom_mode,
}
func set_config(config : Dictionary) -> void:
_zoom_mode = config.get("zoom_mode", _zoom_mode)
func update_config() -> void:
$ModeOptions.selected = _zoom_mode
func draw_start(_position : Vector2) -> void:
Global.camera.zoom_camera(_zoom_mode * 2 - 1)
func draw_move(_position : Vector2) -> void:
pass
func draw_end(_position : Vector2) -> void:
pass

75
src/Tools/Zoom.tscn Normal file
View file

@ -0,0 +1,75 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://src/Tools/Base.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/Tools/Zoom.gd" type="Script" id=2]
[node name="ToolOptions" instance=ExtResource( 1 )]
script = ExtResource( 2 )
[node name="Mode" type="Label" parent="." index="1"]
margin_left = 38.0
margin_top = 18.0
margin_right = 78.0
margin_bottom = 32.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
text = "Mode:"
[node name="ModeOptions" type="OptionButton" parent="." index="2"]
margin_left = 12.0
margin_top = 36.0
margin_right = 104.0
margin_bottom = 56.0
rect_min_size = Vector2( 92, 0 )
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
text = "Zoom in"
items = [ "Zoom in", null, false, 0, null, "Zoom out", null, false, 1, null ]
selected = 0
[node name="Options" type="Label" parent="." index="3"]
margin_left = 30.0
margin_top = 60.0
margin_right = 85.0
margin_bottom = 74.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
text = "Options:"
[node name="FitToFrame" type="Button" parent="." index="4"]
margin_left = 12.0
margin_top = 78.0
margin_right = 104.0
margin_bottom = 98.0
rect_min_size = Vector2( 92, 0 )
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
text = "Fit to frame"
[node name="100%" type="Button" parent="." index="5"]
margin_left = 12.0
margin_top = 102.0
margin_right = 104.0
margin_bottom = 122.0
rect_min_size = Vector2( 92, 0 )
mouse_default_cursor_shape = 2
size_flags_horizontal = 4
text = "100% Zoom"
[node name="PixelPerfect" parent="." index="6"]
visible = false
margin_top = 126.0
margin_bottom = 150.0
[node name="EmptySpacer" parent="." index="7"]
visible = false
margin_top = 126.0
margin_bottom = 138.0
[node name="Mirror" parent="." index="8"]
visible = false
margin_top = 126.0
margin_bottom = 143.0
[connection signal="item_selected" from="ModeOptions" to="." method="_on_ModeOptions_item_selected"]
[connection signal="pressed" from="FitToFrame" to="." method="_on_FitToFrame_pressed"]
[connection signal="pressed" from="100%" to="." method="_on_100_pressed"]

View file

@ -1,70 +1,29 @@
extends BaseButton
export var brush_type := 0 # Global.Brush_Types.PIXEL
export var custom_brush_index := -3
var random_brushes := []
var brush := Brushes.Brush.new()
func _on_BrushButton_pressed() -> void:
# Delete the brush on middle mouse press
if Input.is_action_just_released("middle_mouse"):
_on_DeleteButton_pressed()
return
# Change brush
Global.current_brush_types[Global.brush_type_window_position] = brush_type
Global.custom_brush_indexes[Global.brush_type_window_position] = custom_brush_index
if brush_type == Global.Brush_Types.FILE or brush_type == Global.Brush_Types.RANDOM_FILE or brush_type == Global.Brush_Types.CUSTOM:
if Global.current_tools[Global.brush_type_window_position] == Global.Tools.PENCIL:
Global.color_interpolation_containers[Global.brush_type_window_position].visible = true
else:
Global.color_interpolation_containers[Global.brush_type_window_position].visible = false
Global.update_custom_brush(Global.brush_type_window_position)
Global.brushes_popup.hide()
Global.brushes_popup.select_brush(brush)
func _on_DeleteButton_pressed() -> void:
if brush_type != Global.Brush_Types.CUSTOM:
if brush.type != Brushes.CUSTOM:
return
if Global.custom_brush_indexes[0] == custom_brush_index:
Global.custom_brush_indexes[0] = -3
Global.current_brush_types[0] = Global.Brush_Types.PIXEL
Global.update_custom_brush(0)
if Global.custom_brush_indexes[1] == custom_brush_index:
Global.custom_brush_indexes[1] = -3
Global.current_brush_types[1] = Global.Brush_Types.PIXEL
Global.update_custom_brush(1)
Global.current_project.undos += 1
Global.current_project.undo_redo.create_action("Delete Custom Brush")
for i in range(Global.project_brush_container.get_child_count()):
var bb = Global.project_brush_container.get_child(i)
if Global.custom_brush_indexes[0] == bb.custom_brush_index:
Global.custom_brush_indexes[0] -= 1
if Global.custom_brush_indexes[1] == bb.custom_brush_index:
Global.custom_brush_indexes[1] -= 1
Global.current_project.undo_redo.add_do_property(bb, "custom_brush_index", bb.custom_brush_index - 1)
Global.current_project.undo_redo.add_undo_property(bb, "custom_brush_index", bb.custom_brush_index)
var custom_brushes: Array = Global.current_project.brushes.duplicate()
custom_brushes.remove(custom_brush_index)
Global.current_project.undo_redo.add_do_property(Global.current_project, "brushes", custom_brushes)
Global.current_project.undo_redo.add_undo_property(Global.current_project, "brushes", Global.current_project.brushes)
Global.current_project.undo_redo.add_do_method(Global, "redo_custom_brush", self)
Global.current_project.undo_redo.add_undo_method(Global, "undo_custom_brush", self)
Global.current_project.undo_redo.commit_action()
Global.brushes_popup.remove_brush(self)
func _on_BrushButton_mouse_entered() -> void:
if brush_type == Global.Brush_Types.CUSTOM:
if brush.type == Brushes.CUSTOM:
$DeleteButton.visible = true
func _on_BrushButton_mouse_exited() -> void:
if brush_type == Global.Brush_Types.CUSTOM:
if brush.type == Brushes.CUSTOM:
$DeleteButton.visible = false

132
src/UI/BrushesPopup.gd Normal file
View file

@ -0,0 +1,132 @@
extends Popup
class_name Brushes
class Brush:
var type : int
var image : Image
var random := []
var index : int
signal brush_selected(brush)
signal brush_removed(brush)
enum {PIXEL, CIRCLE, FILLED_CIRCLE, FILE, RANDOM_FILE, CUSTOM}
var pixel_image = preload("res://assets/graphics/pixel_image.png")
var circle_image = preload("res://assets/graphics/circle_9x9.png")
var circle_filled_image = preload("res://assets/graphics/circle_filled_9x9.png")
func _ready() -> void:
var container = Global.brushes_popup.get_node("TabContainer/File/FileBrushContainer")
var button = create_button(pixel_image)
button.brush.type = PIXEL
button.hint_tooltip = "Pixel brush"
container.add_child(button)
button.brush.index = button.get_index()
button = create_button(circle_image)
button.brush.type = CIRCLE
button.hint_tooltip = "Circle brush"
container.add_child(button)
button.brush.index = button.get_index()
button = create_button(circle_filled_image)
button.brush.type = FILLED_CIRCLE
button.hint_tooltip = "Filled circle brush"
container.add_child(button)
button.brush.index = button.get_index()
func select_brush(brush : Brush) -> void:
emit_signal("brush_selected", brush)
hide()
static func get_default_brush() -> Brush:
var brush = Brush.new()
brush.type = PIXEL
brush.index = 0
return brush
static func create_button(image : Image) -> Node:
var button : BaseButton = load("res://src/UI/BrushButton.tscn").instance()
var tex := ImageTexture.new()
tex.create_from_image(image, 0)
button.get_child(0).texture = tex
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
return button
static func add_file_brush(images : Array, hint := "") -> void:
var button = create_button(images[0])
button.brush.type = FILE if images.size() == 1 else RANDOM_FILE
button.brush.image = images[0]
button.brush.random = images
button.hint_tooltip = hint
var container = Global.brushes_popup.get_node("TabContainer/File/FileBrushContainer")
container.add_child(button)
button.brush.index = button.get_index()
static func add_project_brush(image : Image) -> void:
var button = create_button(image)
button.brush.type = CUSTOM
button.brush.image = image
var container = Global.brushes_popup.get_node("TabContainer/Project/ProjectBrushContainer")
container.add_child(button)
button.brush.index = button.get_index()
static func clear_project_brush() -> void:
var container = Global.brushes_popup.get_node("TabContainer/Project/ProjectBrushContainer")
for child in container.get_children():
child.queue_free()
Global.brushes_popup.emit_signal("brush_removed", child.brush)
func get_brush(type : int, index : int) -> Brush:
var container
if type == CUSTOM:
container = Global.brushes_popup.get_node("TabContainer/Project/ProjectBrushContainer")
else:
container = Global.brushes_popup.get_node("TabContainer/File/FileBrushContainer")
var brush = get_default_brush()
if index < container.get_child_count():
brush = container.get_child(index).brush
return brush
func remove_brush(brush_button : Node) -> void:
emit_signal("brush_removed", brush_button.brush)
var project = Global.current_project
var undo_brushes = project.brushes.duplicate()
project.brushes.erase(brush_button.brush.image)
project.undos += 1
project.undo_redo.create_action("Delete Custom Brush")
project.undo_redo.add_do_property(project, "brushes", project.brushes)
project.undo_redo.add_undo_property(project, "brushes", undo_brushes)
project.undo_redo.add_do_method(self, "redo_custom_brush", brush_button)
project.undo_redo.add_undo_method(self, "undo_custom_brush", brush_button)
project.undo_redo.add_undo_reference(brush_button)
project.undo_redo.commit_action()
func undo_custom_brush(brush_button : BaseButton = null) -> void:
Global.general_undo()
var action_name : String = Global.current_project.undo_redo.get_current_action_name()
if action_name == "Delete Custom Brush":
$TabContainer/Project/ProjectBrushContainer.add_child(brush_button)
$TabContainer/Project/ProjectBrushContainer.move_child(brush_button, brush_button.brush.index)
brush_button.get_node("DeleteButton").visible = false
func redo_custom_brush(brush_button : BaseButton = null) -> void:
Global.general_redo()
var action_name : String = Global.current_project.undo_redo.get_current_action_name()
if action_name == "Delete Custom Brush":
$TabContainer/Project/ProjectBrushContainer.remove_child(brush_button)

View file

@ -1,41 +1,12 @@
[gd_scene load_steps=6 format=2]
[gd_scene load_steps=2 format=2]
[ext_resource path="res://src/UI/BrushButton.tscn" type="PackedScene" id=2]
[sub_resource type="Image" id=5]
data = {
"data": PoolByteArray( 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 0 ),
"format": "LumAlpha8",
"height": 9,
"mipmaps": false,
"width": 9
}
[sub_resource type="ImageTexture" id=2]
flags = 3
flags = 3
image = SubResource( 5 )
size = Vector2( 9, 9 )
[sub_resource type="Image" id=6]
data = {
"data": PoolByteArray( 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 0 ),
"format": "LumAlpha8",
"height": 9,
"mipmaps": false,
"width": 9
}
[sub_resource type="ImageTexture" id=4]
flags = 3
flags = 3
image = SubResource( 6 )
size = Vector2( 9, 9 )
[ext_resource path="res://src/UI/BrushesPopup.gd" type="Script" id=1]
[node name="BrushesPopup" type="Popup"]
margin_right = 226.0
margin_bottom = 144.0
rect_min_size = Vector2( 0, 144 )
script = ExtResource( 1 )
[node name="TabContainer" type="TabContainer" parent="."]
anchor_right = 1.0
@ -55,36 +26,8 @@ size_flags_horizontal = 3
scroll_horizontal_enabled = false
[node name="FileBrushContainer" type="GridContainer" parent="TabContainer/File"]
margin_right = 104.0
margin_bottom = 32.0
columns = 6
[node name="PixelBrushButton" parent="TabContainer/File/FileBrushContainer" instance=ExtResource( 2 )]
hint_tooltip = "Pixel brush"
mouse_default_cursor_shape = 2
[node name="CircleBrushButton" parent="TabContainer/File/FileBrushContainer" instance=ExtResource( 2 )]
margin_left = 36.0
margin_right = 68.0
hint_tooltip = "Filled circle brush"
mouse_default_cursor_shape = 2
brush_type = 1
custom_brush_index = -2
[node name="BrushTexture" parent="TabContainer/File/FileBrushContainer/CircleBrushButton" index="0"]
texture = SubResource( 2 )
[node name="FilledCircleBrushButton" parent="TabContainer/File/FileBrushContainer" instance=ExtResource( 2 )]
margin_left = 72.0
margin_right = 104.0
hint_tooltip = "Circle brush"
mouse_default_cursor_shape = 2
brush_type = 2
custom_brush_index = -1
[node name="BrushTexture" parent="TabContainer/File/FileBrushContainer/FilledCircleBrushButton" index="0"]
texture = SubResource( 4 )
[node name="Project" type="ScrollContainer" parent="TabContainer"]
visible = false
anchor_right = 1.0
@ -99,7 +42,3 @@ scroll_horizontal_enabled = false
[node name="ProjectBrushContainer" type="GridContainer" parent="TabContainer/Project"]
columns = 5
[editable path="TabContainer/File/FileBrushContainer/CircleBrushButton"]
[editable path="TabContainer/File/FileBrushContainer/FilledCircleBrushButton"]

View file

@ -1,25 +1,23 @@
extends VBoxContainer
var previous_colors := [Color.black, Color.white]
onready var left_picker := $ColorButtonsVertical/ColorPickersCenter/ColorPickersHorizontal/LeftColorPickerButton
onready var right_picker := $ColorButtonsVertical/ColorPickersCenter/ColorPickersHorizontal/RightColorPickerButton
func _ready() -> void:
Tools.connect("color_changed", self, "update_color")
left_picker.get_picker().presets_visible = false
right_picker.get_picker().presets_visible = false
func _on_ColorSwitch_pressed() -> void:
var temp : Color = Global.color_pickers[0].color
Global.color_pickers[0].color = Global.color_pickers[1].color
Global.color_pickers[1].color = temp
Global.update_custom_brush(0)
Global.update_custom_brush(1)
Tools.swap_color()
func _on_ColorPickerButton_color_changed(color : Color, right : bool):
var mouse_button := int(right)
# If the color changed while it's on full transparency, make it opaque (GH issue #54)
if color.a == 0:
if previous_colors[mouse_button].r != color.r or previous_colors[mouse_button].g != color.g or previous_colors[mouse_button].b != color.b:
Global.color_pickers[mouse_button].color.a = 1
Global.update_custom_brush(mouse_button)
previous_colors[mouse_button] = color
var button := BUTTON_RIGHT if right else BUTTON_LEFT
Tools.assign_color(color, button)
func _on_ColorPickerButton_pressed() -> void:
@ -31,108 +29,11 @@ func _on_ColorPickerButton_popup_closed() -> void:
func _on_ColorDefaults_pressed() -> void:
Global.color_pickers[0].color = Color.black
Global.color_pickers[1].color = Color.white
Global.update_custom_brush(0)
Global.update_custom_brush(1)
Tools.default_color()
func _on_FitToFrameButton_pressed() -> void:
Global.camera.fit_to_frame(Global.current_project.size)
func _on_100ZoomButton_pressed() -> void:
Global.camera.zoom = Vector2.ONE
Global.camera.offset = Global.current_project.size / 2
Global.zoom_level_label.text = str(round(100 / Global.camera.zoom.x)) + " %"
Global.horizontal_ruler.update()
Global.vertical_ruler.update()
func _on_BrushTypeButton_pressed(right : bool) -> void:
var mouse_button := int(right)
Global.brushes_popup.popup(Rect2(Global.brush_type_buttons[mouse_button].rect_global_position, Vector2(226, 72)))
Global.brush_type_window_position = mouse_button
func _on_BrushSizeEdit_value_changed(value : float, right : bool) -> void:
var mouse_button := int(right)
var new_size = int(value)
Global.brush_size_edits[mouse_button].value = value
Global.brush_size_sliders[mouse_button].value = value
Global.brush_sizes[mouse_button] = new_size
Global.update_custom_brush(mouse_button)
func _on_PixelPerfectMode_toggled(button_pressed : bool, right : bool) -> void:
var mouse_button := int(right)
Global.pixel_perfect[mouse_button] = button_pressed
func _on_InterpolateFactor_value_changed(value : float, right : bool) -> void:
var mouse_button := int(right)
Global.interpolate_spinboxes[mouse_button].value = value
Global.interpolate_sliders[mouse_button].value = value
Global.update_custom_brush(mouse_button)
func _on_FillAreaOptions_item_selected(ID : int, right : bool) -> void:
var mouse_button := int(right)
Global.fill_areas[mouse_button] = ID
func _on_FillWithOptions_item_selected(ID : int, right : bool) -> void:
var mouse_button := int(right)
Global.fill_with[mouse_button] = ID
if ID == 1:
Global.fill_pattern_containers[mouse_button].visible = true
func update_color(color : Color, button : int) -> void:
if button == BUTTON_LEFT:
left_picker.color = color
else:
Global.fill_pattern_containers[mouse_button].visible = false
func _on_PatternTypeButton_pressed(right : bool) -> void:
var mouse_button := int(right)
Global.pattern_window_position = mouse_button
Global.patterns_popup.popup(Rect2(Global.brush_type_buttons[mouse_button].rect_global_position, Vector2(226, 72)))
func _on_PatternOffsetX_value_changed(value : float, right : bool) -> void:
var mouse_button := int(right)
Global.fill_pattern_offsets[mouse_button].x = value
func _on_PatternOffsetY_value_changed(value : float, right : bool) -> void:
var mouse_button := int(right)
Global.fill_pattern_offsets[mouse_button].y = value
func _on_LightenDarken_item_selected(ID : int, right : bool) -> void:
var mouse_button := int(right)
Global.ld_modes[mouse_button] = ID
func _on_LDAmount_value_changed(value : float, right : bool) -> void:
var mouse_button := int(right)
Global.ld_amounts[mouse_button] = value / 100
Global.ld_amount_sliders[mouse_button].value = value
Global.ld_amount_spinboxes[mouse_button].value = value
func _on_ForColorOptions_item_selected(ID : int, right : bool) -> void:
var mouse_button := int(right)
Global.color_picker_for[mouse_button] = ID
func _on_ZoomModeOptions_item_selected(ID : int, right : bool) -> void:
var mouse_button := int(right)
Global.zoom_modes[mouse_button] = ID
func _on_HorizontalMirroring_toggled(button_pressed : bool, right : bool) -> void:
var mouse_button := int(right)
Global.horizontal_mirror[mouse_button] = button_pressed
func _on_VerticalMirroring_toggled(button_pressed : bool, right : bool) -> void:
var mouse_button := int(right)
Global.vertical_mirror[mouse_button] = button_pressed
right_picker.color = color

File diff suppressed because one or more lines are too long

View file

@ -1,22 +1,8 @@
extends TextureButton
var image : Image
var image_size : Vector2
var texture : ImageTexture
func _ready() -> void:
if image:
image_size = image.get_size()
texture = ImageTexture.new()
texture.create_from_image(image, 0)
var pattern := Patterns.Pattern.new()
func _on_PatternButton_pressed() -> void:
Global.pattern_images[Global.pattern_window_position] = image
Global.fill_pattern_containers[Global.pattern_window_position].get_child(0).get_child(0).texture = texture
Global.fill_pattern_containers[Global.pattern_window_position].get_child(2).get_child(1).max_value = image_size.x - 1
Global.fill_pattern_containers[Global.pattern_window_position].get_child(3).get_child(1).max_value = image_size.y - 1
Global.patterns_popup.hide()
Global.patterns_popup.select_pattern(pattern)

45
src/UI/PatternsPopup.gd Normal file
View file

@ -0,0 +1,45 @@
extends PopupPanel
class_name Patterns
class Pattern:
var image : Image
var index : int
signal pattern_selected(pattern)
var default_pattern : Pattern = null
func select_pattern(pattern : Pattern) -> void:
emit_signal("pattern_selected", pattern)
hide()
static func create_button(image : Image) -> Node:
var button : BaseButton = load("res://src/UI/PatternButton.tscn").instance()
var tex := ImageTexture.new()
tex.create_from_image(image, 0)
button.get_child(0).texture = tex
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
return button
static func add(image : Image, hint := "") -> void:
var button = create_button(image)
button.pattern.image = image
button.hint_tooltip = hint
var container = Global.patterns_popup.get_node("ScrollContainer/PatternContainer")
container.add_child(button)
button.pattern.index = button.get_index()
if Global.patterns_popup.default_pattern == null:
Global.patterns_popup.default_pattern = button.pattern
func get_pattern(index : int) -> Pattern:
var container = Global.patterns_popup.get_node("ScrollContainer/PatternContainer")
var pattern = default_pattern
if index < container.get_child_count():
pattern = container.get_child(index).pattern
return pattern

View file

@ -1,8 +1,11 @@
[gd_scene format=2]
[gd_scene load_steps=2 format=2]
[ext_resource path="res://src/UI/PatternsPopup.gd" type="Script" id=1]
[node name="PatternsPopup" type="PopupPanel"]
margin_right = 226.0
margin_bottom = 104.0
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}

View file

@ -1,98 +1,40 @@
extends VBoxContainer
var tools := []
# Node, shortcut
onready var tools := [
[$RectSelect, "rectangle_select"],
[$Zoom, "zoom"],
[$ColorPicker, "colorpicker"],
[$Pencil, "pencil"],
[$Eraser, "eraser"],
[$Bucket, "fill"],
[$LightenDarken, "lightdark"],
]
func _ready() -> void:
# Node, left mouse shortcut, right mouse shortcut
tools.append([Global.find_node_by_name(self, "Pencil"), "left_pencil_tool", "right_pencil_tool"])
tools.append([Global.find_node_by_name(self, "Eraser"), "left_eraser_tool", "right_eraser_tool"])
tools.append([Global.find_node_by_name(self, "Bucket"), "left_fill_tool", "right_fill_tool"])
tools.append([Global.find_node_by_name(self, "LightenDarken"), "left_lightdark_tool", "right_lightdark_tool"])
tools.append([Global.find_node_by_name(self, "RectSelect"), "left_rectangle_select_tool", "right_rectangle_select_tool"])
tools.append([Global.find_node_by_name(self, "ColorPicker"), "left_colorpicker_tool", "right_colorpicker_tool"])
tools.append([Global.find_node_by_name(self, "Zoom"), "left_zoom_tool", "right_zoom_tool"])
for t in tools:
t[0].connect("pressed", self, "_on_Tool_pressed", [t[0]])
Global.update_hint_tooltips()
func _input(event : InputEvent) -> void:
if Global.has_focus:
if event.is_action_pressed("undo") or event.is_action_pressed("redo") or event.is_action_pressed("redo_secondary"):
if not Global.has_focus:
return
for action in ["undo", "redo", "redo_secondary"]:
if event.is_action_pressed(action):
return
for t in tools: # Handle tool shortcuts
if event.is_action_pressed(t[2]): # Shortcut for right button (with Alt)
_on_Tool_pressed(t[0], false, false)
elif event.is_action_pressed(t[1]): # Shortcut for left button
_on_Tool_pressed(t[0], false, true)
for t in tools: # Handle tool shortcuts
if event.is_action_pressed("right_" + t[1] + "_tool"): # Shortcut for right button (with Alt)
Tools.assign_tool(t[0].name, BUTTON_RIGHT)
elif event.is_action_pressed("left_" + t[1] + "_tool"): # Shortcut for left button
Tools.assign_tool(t[0].name, BUTTON_LEFT)
func _on_Tool_pressed(tool_pressed : BaseButton, mouse_press := true, key_for_left := true) -> void:
var current_action := tool_pressed.name
var current_tool : int = Global.Tools.keys().find(current_action.to_upper())
var left_tool_name := str(Global.Tools.keys()[Global.current_tools[0]]).to_lower()
var right_tool_name := str(Global.Tools.keys()[Global.current_tools[1]]).to_lower()
var current_mouse_button := -1
if (mouse_press and Input.is_action_just_released("left_mouse")) or (!mouse_press and key_for_left):
left_tool_name = current_action.to_lower()
current_mouse_button = Global.Mouse_Button.LEFT
elif (mouse_press and Input.is_action_just_released("right_mouse")) or (!mouse_press and !key_for_left):
right_tool_name = current_action.to_lower()
current_mouse_button = Global.Mouse_Button.RIGHT
if current_mouse_button != -1:
Global.current_tools[current_mouse_button] = current_tool
# Start from 1, so the label won't get invisible
for i in range(1, Global.tool_options_containers[current_mouse_button].get_child_count()):
Global.tool_options_containers[current_mouse_button].get_child(i).visible = false
Global.tool_options_containers[current_mouse_button].get_node("EmptySpacer").visible = true
# Tool options visible depending on the selected tool
if current_tool == Global.Tools.PENCIL:
Global.brush_type_containers[current_mouse_button].visible = true
Global.brush_size_sliders[current_mouse_button].visible = true
Global.pixel_perfect_containers[current_mouse_button].visible = true
Global.mirror_containers[current_mouse_button].visible = true
if Global.current_brush_types[current_mouse_button] == Global.Brush_Types.FILE or Global.current_brush_types[current_mouse_button] == Global.Brush_Types.CUSTOM or Global.current_brush_types[current_mouse_button] == Global.Brush_Types.RANDOM_FILE:
Global.color_interpolation_containers[current_mouse_button].visible = true
elif current_tool == Global.Tools.ERASER:
Global.brush_type_containers[current_mouse_button].visible = true
Global.brush_size_sliders[current_mouse_button].visible = true
Global.pixel_perfect_containers[current_mouse_button].visible = true
Global.mirror_containers[current_mouse_button].visible = true
elif current_tool == Global.Tools.BUCKET:
Global.fill_area_containers[current_mouse_button].visible = true
Global.mirror_containers[current_mouse_button].visible = true
elif current_tool == Global.Tools.LIGHTENDARKEN:
Global.brush_type_containers[current_mouse_button].visible = true
Global.brush_size_sliders[current_mouse_button].visible = true
Global.pixel_perfect_containers[current_mouse_button].visible = true
Global.ld_containers[current_mouse_button].visible = true
Global.mirror_containers[current_mouse_button].visible = true
elif current_tool == Global.Tools.COLORPICKER:
Global.colorpicker_containers[current_mouse_button].visible = true
elif current_tool == Global.Tools.ZOOM:
Global.zoom_containers[current_mouse_button].visible = true
for t in tools:
var tool_name : String = t[0].name.to_lower()
var texture_button : TextureRect = t[0].get_child(0)
if tool_name == left_tool_name and tool_name == right_tool_name:
Global.change_button_texturerect(texture_button, "%s_l_r.png" % tool_name.to_lower())
elif tool_name == left_tool_name:
Global.change_button_texturerect(texture_button, "%s_l.png" % tool_name.to_lower())
elif tool_name == right_tool_name:
Global.change_button_texturerect(texture_button, "%s_r.png" % tool_name.to_lower())
else:
Global.change_button_texturerect(texture_button, "%s.png" % tool_name.to_lower())
Global.left_cursor_tool_texture.create_from_image(load("res://assets/graphics/cursor_icons/%s_cursor.png" % left_tool_name), 0)
Global.right_cursor_tool_texture.create_from_image(load("res://assets/graphics/cursor_icons/%s_cursor.png" % right_tool_name), 0)
func _on_Tool_pressed(tool_pressed : BaseButton) -> void:
var button := -1
button = BUTTON_LEFT if Input.is_action_just_released("left_mouse") else button
button = BUTTON_RIGHT if Input.is_action_just_released("right_mouse") else button
if button != -1:
Tools.assign_tool(tool_pressed.name, button)

View file

@ -39,6 +39,9 @@ func setup_edit_menu() -> void:
var edit_menu_items := {
"Undo" : InputMap.get_action_list("undo")[0].get_scancode_with_modifiers(),
"Redo" : InputMap.get_action_list("redo")[0].get_scancode_with_modifiers(),
"Copy" : InputMap.get_action_list("copy")[0].get_scancode_with_modifiers(),
"Paste" : InputMap.get_action_list("paste")[0].get_scancode_with_modifiers(),
"Delete" : InputMap.get_action_list("delete")[0].get_scancode_with_modifiers(),
"Clear Selection" : 0,
"Preferences" : 0
}
@ -201,15 +204,16 @@ func edit_menu_id_pressed(id : int) -> void:
Global.control.redone = true
Global.current_project.undo_redo.redo()
Global.control.redone = false
2: # Clear selection
Global.canvas.handle_undo("Rectangle Select")
Global.selection_rectangle.polygon[0] = Vector2.ZERO
Global.selection_rectangle.polygon[1] = Vector2.ZERO
Global.selection_rectangle.polygon[2] = Vector2.ZERO
Global.selection_rectangle.polygon[3] = Vector2.ZERO
Global.current_project.selected_pixels.clear()
Global.canvas.handle_redo("Rectangle Select")
3: # Preferences
2: # Copy
Global.selection_rectangle.copy()
3: # paste
Global.selection_rectangle.paste()
4: # Delete
Global.selection_rectangle.delete()
5: # Clear selection
Global.selection_rectangle.set_rect(Rect2(0, 0, 0, 0))
Global.selection_rectangle.select_rect()
6: # Preferences
Global.preferences_dialog.popup_centered(Vector2(400, 280))
Global.dialog_open(true)