-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added a simple drawing demo (GD paint) #89
Changes from all commits
a159361
7ad4382
2c5b4a4
0d40b69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
extends Control | ||
|
||
# The TL position of the canvas | ||
var TL_node | ||
|
||
# A list to hold all of the dictionaries that make up each brush. | ||
var brush_data_list = [] | ||
|
||
# A boolean to hold whether or not the mouse is inside the drawing area, the mouse position last _process call | ||
# and the position of the mouse when the left mouse button was pressed | ||
var is_mouse_in_drawing_area = false | ||
var last_mouse_pos = Vector2() | ||
var mouse_click_start_pos = null | ||
|
||
# A boolean to tell whether we've set undo_elements_list_num, which holds the size of draw_elements_list | ||
# before a new stroke is added (unless the current brush mode is 'rectangle shape' or 'circle shape', in | ||
# which case we do things a litte differently. See the undo_stroke function for more details) | ||
var undo_set = false | ||
var undo_element_list_num = -1 | ||
|
||
# A constant for whether or not we're needing to undo a shape | ||
const UNDO_MODE_SHAPE = -2 | ||
# A constant for whether or not we can undo | ||
const UNDO_NONE = -1 | ||
|
||
# Enums for the various modes and brush shapes that can be applied | ||
enum BRUSH_MODES { | ||
pencil, eraser, circle_shape, rectangle_shape | ||
} | ||
enum BRUSH_SHAPES { | ||
rectangle, circle | ||
} | ||
|
||
# The current brush settings: The mode, size, color, and shape we have currently selected | ||
var brush_mode = BRUSH_MODES.pencil | ||
var brush_size = 32 | ||
var brush_color = Color(1, 1, 1, 1) | ||
var brush_shape = BRUSH_SHAPES.circle; | ||
|
||
# The color of the background. We need this for the eraser (see the how we handle the eraser | ||
# in the _draw function for more details) | ||
var bg_color = Color(1, 1, 1, 1) | ||
|
||
# How large is the image (it's actually the size of DrawingAreaBG, because that's our background canvas) | ||
const IMAGE_SIZE = Vector2(930, 720) | ||
|
||
|
||
func _ready(): | ||
# Get the top left position node. We need this to find out whether or not the mouse is inside the canvas | ||
TL_node = get_node("TLPos") | ||
set_process(true) | ||
|
||
|
||
func _process(_delta): | ||
var mouse_pos = get_viewport().get_mouse_position() | ||
|
||
# Check if the mouse is currently inside the canvas/drawing-area | ||
is_mouse_in_drawing_area = false | ||
if mouse_pos.x > TL_node.global_position.x: | ||
if mouse_pos.y > TL_node.global_position.y: | ||
is_mouse_in_drawing_area = true | ||
|
||
if Input.is_mouse_button_pressed(BUTTON_LEFT): | ||
# If we do not have a position for when the mouse was first clicked, then this most | ||
# be the first time is_mouse_button_pressed has been called since the mouse button was | ||
# released, so we need to store the position | ||
if mouse_click_start_pos == null: | ||
mouse_click_start_pos = mouse_pos | ||
|
||
# If the mouse is inside the canvas and the mouse is 1px away from the position of the mouse last _process call | ||
if check_if_mouse_is_inside_canvas(): | ||
if mouse_pos.distance_to(last_mouse_pos) >= 1: | ||
# If we are in pencil or eraser mode, then we need to draw | ||
if brush_mode == BRUSH_MODES.pencil or brush_mode == BRUSH_MODES.eraser: | ||
# If undo has not been set, meaning we've started a new stroke, then store the size of the | ||
# draw_elements_list so we can undo from this point in time | ||
if undo_set == false: | ||
undo_set = true | ||
undo_element_list_num = brush_data_list.size() | ||
# Add the brush object to draw_elements_array | ||
add_brush(mouse_pos, brush_mode) | ||
|
||
else: | ||
# We've finished our stroke, so we can set a new undo (if a new storke is made) | ||
undo_set = false | ||
|
||
# If the mouse is inside the canvas | ||
if check_if_mouse_is_inside_canvas(): | ||
# If we're using either the circle shape mode, or the rectangle shape mode, then | ||
# add the brush object to draw_elements_array | ||
if brush_mode == BRUSH_MODES.circle_shape or brush_mode == BRUSH_MODES.rectangle_shape: | ||
add_brush(mouse_pos, brush_mode) | ||
# We handle undo's differently than either pencil or eraser mode, so we need to set undo | ||
# element_list_num to -2 so we can tell if we need to undo a shape. See undo_stroke for details | ||
undo_element_list_num = UNDO_MODE_SHAPE | ||
# Since we've released the left mouse, we need to get a new mouse_click_start_pos next time | ||
#is_mouse_button_pressed is true. | ||
mouse_click_start_pos = null | ||
|
||
# Store mouse_pos as last_mouse_pos now that we're done with _process | ||
last_mouse_pos = mouse_pos | ||
|
||
|
||
func check_if_mouse_is_inside_canvas(): | ||
# Make sure we have a mouse click starting position | ||
if mouse_click_start_pos != null: | ||
# Make sure the mouse click starting position is inside the canvas. | ||
# This is so if we start out click outside the canvas (say chosing a color from the color picker) | ||
# and then move our mouse back into the canvas, it won't start painting | ||
if mouse_click_start_pos.x > TL_node.global_position.x: | ||
if mouse_click_start_pos.y > TL_node.global_position.y: | ||
# Make sure the current mouse position is inside the canvas | ||
if is_mouse_in_drawing_area == true: | ||
return true | ||
return false | ||
|
||
|
||
func undo_stroke(): | ||
# Only undo a stroke if we have one | ||
if undo_element_list_num == UNDO_NONE: | ||
return | ||
|
||
# If we are undoing a shape, then we can just remove the latest brush | ||
if undo_element_list_num == UNDO_MODE_SHAPE: | ||
if brush_data_list.size() > 0: | ||
brush_data_list.remove(brush_data_list.size() - 1) | ||
|
||
# Now that we've undone a shape, we cannot undo again until another stoke is added | ||
undo_element_list_num = UNDO_NONE | ||
# NOTE: if we only had shape brushes, then we could remove the above line and could let the user | ||
# undo until we have a empty element list | ||
|
||
# Otherwise we're removing a either a pencil stroke or a eraser stroke. | ||
else: | ||
# Figure out how many elements/brushes we've added in the last stroke | ||
var elements_to_remove = brush_data_list.size() - undo_element_list_num | ||
# Remove all of the elements we've added this in the last stroke | ||
#warning-ignore:unused_variable | ||
for elment_num in range(0, elements_to_remove): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Above the line with the |
||
brush_data_list.pop_back() | ||
|
||
# Now that we've undone a stoke, we cannot undo again until another stoke is added | ||
undo_element_list_num = UNDO_NONE | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be possible to allow many undos? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be possible, as I mentioned in the initial post. It has been awhile so I don't remember exactly how the demo works, but if I recall correctly the elements drawn are stored in a list. The current undo functionality works because it stores the index of the element before drawing. When the undo button is pressed, it just removes all of the elements going forward after the stored index. While this works for a single undo, now I can think of better ways to go about doing something like this. If there is interest and the demo is still desired, then I could see about rewriting the code to allow for multiple undos. |
||
|
||
# Redraw the brushes | ||
update() | ||
|
||
|
||
func add_brush(mouse_pos, type): | ||
# Make new brush dictionary that will hold all of the data we need for the brush. | ||
var new_brush = {} | ||
|
||
# Populate the dictionary with values based on the global brush variables. | ||
# We will override these as needed if the brush is a rectange or circle. | ||
new_brush.brush_type = type | ||
new_brush.brush_pos = mouse_pos | ||
new_brush.brush_shape = brush_shape | ||
new_brush.brush_size = brush_size | ||
new_brush.brush_color = brush_color | ||
|
||
# If the new bursh is a rectangle shape, we need to calculate the top left corner of the rectangle and the | ||
# bottom right corner of the rectangle | ||
if type == BRUSH_MODES.rectangle_shape: | ||
var TL_pos = Vector2() | ||
var BR_pos = Vector2() | ||
|
||
# Figure out the left and right positions of the corners and assign them to the proper variable | ||
if mouse_pos.x < mouse_click_start_pos.x: | ||
TL_pos.x = mouse_pos.x | ||
BR_pos.x = mouse_click_start_pos.x | ||
else: | ||
TL_pos.x = mouse_click_start_pos.x | ||
BR_pos.x = mouse_pos.x | ||
|
||
# Figure out the top and bottom positions of the corners and assign them to the proper variable | ||
if mouse_pos.y < mouse_click_start_pos.y: | ||
TL_pos.y = mouse_pos.y | ||
BR_pos.y = mouse_click_start_pos.y | ||
else: | ||
TL_pos.y = mouse_click_start_pos.y | ||
BR_pos.y = mouse_pos.y | ||
|
||
# Assign the positions to the brush | ||
new_brush.brush_pos = TL_pos | ||
new_brush.brush_shape_rect_pos_BR = BR_pos | ||
|
||
# If the brush isa circle shape, then we need to calculate the radius of the circle | ||
if type == BRUSH_MODES.circle_shape: | ||
# Get the center point inbetween the mouse position and the position of the mouse when we clicked | ||
var center_pos = Vector2((mouse_pos.x + mouse_click_start_pos.x) / 2, (mouse_pos.y + mouse_click_start_pos.y) / 2) | ||
# Assign the brush position to the center point, and calculate the radius of the circle using the distance from | ||
# the center to the top/bottom positon of the mouse | ||
new_brush.brush_pos = center_pos | ||
new_brush.brush_shape_circle_radius = center_pos.distance_to(Vector2(center_pos.x, mouse_pos.y)) | ||
|
||
# Add the brush and update/draw all of the brushes | ||
brush_data_list.append(new_brush) | ||
update() | ||
|
||
|
||
func _draw(): | ||
# Go through all of the brushes in brush_data_list | ||
for brush in brush_data_list: | ||
|
||
# If the brush is a pencil | ||
if brush.brush_type == BRUSH_MODES.pencil: | ||
# If the brush shape is a rectangle, then we need to make a Rect2 so we can use draw_rect. | ||
# Draw_rect draws a rectagle at the top left corner, using the scale for the size. | ||
# So we offset the position by half of the brush size so the rectangle's center is at mouse position | ||
if brush.brush_shape == BRUSH_SHAPES.rectangle: | ||
var rect = Rect2(brush.brush_pos - Vector2(brush.brush_size / 2, brush.brush_size / 2), Vector2(brush.brush_size, brush.brush_size)) | ||
draw_rect(rect, brush.brush_color) | ||
# If the brush shape is a circle, then we draw a circle at the mouse position, | ||
# making the radius half of brush size (so the circle is brush size pixels in diameter) | ||
elif brush.brush_shape == BRUSH_SHAPES.circle: | ||
draw_circle(brush.brush_pos, brush.brush_size / 2, brush.brush_color) | ||
|
||
# If the brush is a eraser | ||
elif brush.brush_type == BRUSH_MODES.eraser: | ||
# NOTE: this is a really cheap way of erasing that isn't really erasing! | ||
# However, this gives similar results in a fairy simple way! | ||
|
||
# Erasing works exactly the same was as pencil does for both the rectangle shape and the circle shape, | ||
# but instead of using brush.brush_color, we instead use bg_color instead. | ||
if brush.brush_shape == BRUSH_SHAPES.rectangle: | ||
var rect = Rect2(brush.brush_pos - Vector2(brush.brush_size / 2, brush.brush_size / 2), Vector2(brush.brush_size, brush.brush_size)) | ||
draw_rect(rect, bg_color) | ||
elif brush.brush_shape == BRUSH_SHAPES.circle: | ||
draw_circle(brush.brush_pos, brush.brush_size / 2, bg_color) | ||
|
||
# If the brush is a rectangle shape | ||
elif brush.brush_type == BRUSH_MODES.rectangle_shape: | ||
# We make a Rect2 with the postion at the top left. To get the size we take the bottom right position | ||
# and subtract the top left corner's position | ||
var rect = Rect2(brush.brush_pos, brush.brush_shape_rect_pos_BR - brush.brush_pos) | ||
draw_rect(rect, brush.brush_color) | ||
|
||
# If the brush is a circle shape | ||
elif brush.brush_type == BRUSH_MODES.circle_shape: | ||
# We simply draw a circle using stored in brush | ||
draw_circle(brush.brush_pos, brush.brush_shape_circle_radius, brush.brush_color) | ||
|
||
|
||
|
||
func save_picture(path): | ||
# Wait a couple frames so the save dialog isn't in the way | ||
yield (get_tree(), "idle_frame") | ||
yield (get_tree(), "idle_frame") | ||
|
||
# Get the viewport image | ||
var img = get_viewport().get_texture().get_data() | ||
# Crop the image so we only have canvas area. | ||
var cropped_image = img.get_rect(Rect2(TL_node.global_position, IMAGE_SIZE)) | ||
# Flip the image on the Y-axis (it's flipped upside down by default) | ||
cropped_image.flip_y() | ||
|
||
# Save the image with the passed in path we got from the save dialog | ||
cropped_image.save_png(path) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say this change is debatable.
First, I feel the name
global_pos
isn't very descriptive of what the variable actually contains. The TL_Node is the top left corner of the drawing area. Using the nameglobal_pos
to me sounds like it is the global position of the node that PaintControl.gd is attached to. I don't know if defining a variable makes sense in this case, as it is only used once, but regardless I think the variable should be named something different, liketopleft_global_pos
ortl_global_pos
, as that better defines what the variable stores.Also, I'm not the biggest fan of assigning variables directly through a conditional, as it makes the code harder to read and understand (in my opinion), which is kinda opposite of the goal of a demo.
I do think that merging both of the if statements together makes sense. So the code would be the following:
or if using a variable for the global position: