Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- extends Node
- var width := 200
- var height := 200
- var selectedColor := 2
- var brushSize := 1
- enum {BRUSH_SQUARE, BRUSH_CIRCLE}
- var brushShape = BRUSH_SQUARE
- var pixelOffset : PoolVector2Array
- var colors = [
- Color8( 0, 0, 0, 0), #Blank
- Color8(255,255,255,255),#white
- Color8( 0, 0, 0,255),#black
- Color8( 0, 60,200,255),#blue
- Color8(255, 23, 23,255),#red
- Color8(255,230, 0,255),#yellow
- Color8( 0,130, 50,255),#green
- Color8(128, 0,255,255),#purple
- Color8(255, 0,128,255) #pink
- ]
- var canvas = Image.new()
- var cursorPosition : Vector2 #Holds the cursor position
- var cursorRelative : Vector2
- var cursorRelativePreviousFrame : Vector2
- var stroke = [] #this will hold 2 coordinates
- var stroke_curve := Curve2D.new()
- #==================================== INIT =====================================
- func _ready():
- OS.vsync_enabled = false
- setup_ui()
- create_main_canvas()
- get_node("toolbar/palette/widthField").text = width as String
- get_node("toolbar/palette/heightField").text = height as String
- func create_main_canvas():
- if not has_node("canvas"):
- var canvasSprite = Sprite.new()
- canvasSprite.name = "canvas"
- add_child(canvasSprite)
- $canvas.position = get_viewport().size/2
- #Creating the canvas. NOTE, in the future this is gonna be replaced with code to load a png
- canvas.create(width,height,false,Image.FORMAT_RGBA4444) #RGBA4444 stores up to 16 colors. Which is perfect for us
- canvas.lock()
- clear_canvas()
- func setup_ui():
- #--- toolbar
- var barHeight = 28
- var bgBar = ColorRect.new()
- bgBar.name = "toolbar"
- bgBar.color = Color.darkslategray
- bgBar.anchor_right = 1
- bgBar.rect_min_size = Vector2(0,barHeight)
- add_child(bgBar)
- var palette = HBoxContainer.new()
- palette.name = "palette"
- palette.anchor_bottom = 1
- palette.anchor_right = 1
- bgBar.add_child(palette)
- var brushTypeButton = Button.new()
- brushTypeButton.name = "btn_brush_shape"
- brushTypeButton.connect("pressed",self,"change_brush_shape")
- brushTypeButton.rect_min_size = Vector2(barHeight,barHeight)
- brushTypeButton.text = "x"
- palette.add_child(brushTypeButton)
- for i in range(colors.size()):
- var button = Button.new()
- button.connect("pressed",self,"set",["selectedColor", i])
- # button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- button.rect_min_size = Vector2(barHeight,barHeight)
- palette.add_child(button)
- var colorDisplay = ColorRect.new()
- colorDisplay.color = colors[i]
- colorDisplay.mouse_filter = Control.MOUSE_FILTER_IGNORE
- colorDisplay.anchor_top = .2
- colorDisplay.anchor_left = .2
- colorDisplay.anchor_bottom = .8
- colorDisplay.anchor_right = .8
- button.add_child(colorDisplay)
- var sizeSlider = HSlider.new()
- sizeSlider.min_value = 1
- sizeSlider.max_value = 21
- sizeSlider.step = 1
- sizeSlider.rect_min_size = Vector2(120,barHeight)
- sizeSlider.connect("value_changed",self,"change_brush_size")
- palette.add_child(sizeSlider)
- var brushSizeLabel = Label.new()
- brushSizeLabel.name = "brushSizeLabel"
- brushSizeLabel.text = "%02d"%brushSize
- palette.add_child(brushSizeLabel)
- var sep00 = VSeparator.new()
- palette.add_child(sep00)
- var clearCanvasButton = Button.new()
- clearCanvasButton.connect("pressed",self,"clear_canvas")
- clearCanvasButton.rect_min_size = Vector2(80,barHeight)
- clearCanvasButton.text = "Clear"
- palette.add_child(clearCanvasButton)
- var widthField = LineEdit.new()
- widthField.name = "widthField"
- widthField.placeholder_text = "width"
- widthField.rect_min_size = Vector2(80, barHeight)
- palette.add_child(widthField)
- var heightField = LineEdit.new()
- heightField.name = "heightField"
- heightField.placeholder_text = "height"
- heightField.rect_min_size = Vector2(80, barHeight)
- palette.add_child(heightField)
- var newCanvas = Button.new()
- newCanvas.connect("pressed", self, "make_new_canvas")
- newCanvas.name = "newCanvas"
- newCanvas.text = "New Canvas"
- palette.add_child(newCanvas)
- #--- infobar
- var bgInfo = ColorRect.new()
- bgInfo.name = "infobar"
- bgInfo.color = Color.darkslategray
- bgInfo.rect_min_size = Vector2(0, barHeight)
- bgInfo.anchor_top = 1
- bgInfo.anchor_right = 1
- add_child(bgInfo)
- bgInfo.margin_top = -barHeight
- var info = HBoxContainer.new()
- info.name = "info"
- info.anchor_bottom = 1
- info.anchor_right = 1
- bgInfo.add_child(info)
- var zoomOutBtn = Button.new()
- zoomOutBtn.connect("pressed", self, "zoom_canvas", [false])
- zoomOutBtn.size_flags_vertical = Control.SIZE_SHRINK_CENTER
- zoomOutBtn.rect_min_size = Vector2(barHeight/2, barHeight/2)
- zoomOutBtn.text = "-"
- info.add_child(zoomOutBtn)
- var zoomInfo = Label.new()
- zoomInfo.name = "zoomInfo"
- info.add_child(zoomInfo)
- var zoomInBtn = Button.new()
- zoomInBtn.connect("pressed", self, "zoom_canvas", [true])
- zoomInBtn.size_flags_vertical = Control.SIZE_SHRINK_CENTER
- zoomInBtn.rect_min_size = Vector2(barHeight/2, barHeight/2)
- zoomInBtn.text = "+"
- info.add_child(zoomInBtn)
- var sep01 = VSeparator.new()
- info.add_child(sep01)
- var cursorPosInfo = Label.new()
- cursorPosInfo.name = "cursorPosInfo"
- info.add_child(cursorPosInfo)
- #================================= PROCESSING ==================================
- func _unhandled_input(event):
- #--- calculate mouse position on canvas
- if event is InputEventMouse:
- $cursor.position = event.global_position
- if event is InputEventMouseMotion or InputEventMouseButton:
- cursorPosition = (event.global_position - $canvas.position)
- cursorPosition += Vector2(width,height)/2*$canvas.scale.x
- cursorPosition /= $canvas.scale.x
- cursorPosition.x = round(cursorPosition.x)
- cursorPosition.y = round(cursorPosition.y)
- update_info()
- if event is InputEventMouseMotion:
- cursorRelativePreviousFrame = cursorRelative
- cursorRelative = event.relative/($canvas.scale.x)
- #--- draw single pixel on the first click
- if event.is_action_pressed("draw"):
- draw_brush([cursorPosition])
- #--- draw while dragging
- if event is InputEventMouseMotion:
- if Input.is_action_pressed("draw"):
- #- clear arrays and curve
- stroke.clear()
- stroke_curve.clear_points()
- stroke_curve.bake_interval = 1
- if false:#cursorRelative.x <= 1 and cursorRelative.y <= 1:
- draw_brush([cursorPosition])
- else:
- #- add two points to the curve
- stroke_curve.add_point(cursorPosition - cursorRelative, cursorRelativePreviousFrame*.3, cursorRelativePreviousFrame*.3)
- stroke_curve.add_point(cursorPosition)
- #- get rounded baked points from the curve
- for bakedPoint in stroke_curve.get_baked_points():
- var rounded_point = Vector2()
- rounded_point.x = round(bakedPoint.x)
- rounded_point.y = round(bakedPoint.y)
- if not rounded_point in stroke:
- stroke.append(rounded_point)
- draw_brush(stroke)
- #--- Zoom and pan
- if Input.is_action_pressed("screen_drag") and event is InputEventMouseMotion:
- $canvas.position += event.relative
- clamp_canvas_position_in_work_area()
- if event.is_action_pressed("zoom_in"):
- zoom_canvas(true, event.global_position)
- if event.is_action_pressed("zoom_out"):
- zoom_canvas(false, event.global_position)
- if event is InputEventMouseButton:
- if event.is_pressed() and event.button_index == BUTTON_RIGHT:
- flood_fill()
- #=================================== PAINT =====================================
- func draw_pixels(positions : Array):
- for pos in positions:
- pos.x = clamp(pos.x, 0, width-1)
- pos.y = clamp(pos.y, 0, height-1)
- canvas.set_pixel(pos.x, pos.y, colors[selectedColor])
- func draw_brush(positions : Array):
- var pixelToDraw : PoolVector2Array = []
- if brushSize == 1:
- draw_pixels(positions)
- else:
- if brushShape == BRUSH_CIRCLE:
- pixelOffset = calculate_pixel_offsets_in_circle()
- elif brushShape == BRUSH_SQUARE:
- pixelOffset = calculate_pixel_offsets_in_square()
- for pos in positions:
- for offset in pixelOffset:
- if not (pos + offset) in pixelToDraw:
- pixelToDraw.append(pos + offset)
- draw_pixels(pixelToDraw)
- update_canvas()
- func flood_fill():
- if not pos_is_in_canvas(cursorPosition):
- return
- var sampled_color = canvas.get_pixel(cursorPosition.x, cursorPosition.y)
- if sampled_color == colors[selectedColor]:
- return
- var t_start = OS.get_ticks_msec()
- var current = Vector2()
- var queue = [cursorPosition]
- var looked = BitMap.new()
- looked.create(Vector2(width+2, height+2))
- var px_to_fill = []
- var iter = 0
- while not queue.empty():
- current = queue.pop_front()
- # px_to_fill.append(current)
- canvas.set_pixel(current.x, current.y, colors[selectedColor])
- for dir in [Vector2.UP, Vector2.LEFT, Vector2.DOWN, Vector2.RIGHT]:
- iter += 1
- var next = (current + dir)
- # if pos_is_in_canvas(next):
- next.x = clamp(next.x, 0, width-1)
- next.y = clamp(next.y, 0, height-1)
- if not looked.get_bit(next):
- looked.set_bit(next, true)
- var next_px = canvas.get_pixel(next.x, next.y)
- if next_px == sampled_color:
- queue.push_front(next)
- # for px in px_to_fill:
- # canvas.set_pixel(px.x, px.y, colors[selectedColor])
- var t_end = OS.get_ticks_msec()
- print("===========")
- print("Flood Fill")
- print("===========")
- print("iterations: %s"%iter)
- print("tot px in canvas: %s"%(height*width))
- print("time in msec: %s"%(t_end-t_start))
- print("Check this link for improvements: https://godotengine.org/qa/56212/texture-floodfill-too-slow")
- print("===========")
- update_canvas()
- #=================================== CANVAS ====================================
- func update_canvas():
- var texture = ImageTexture.new()
- texture.create_from_image(canvas)
- texture.flags = 16 #disables antialiasing
- $canvas.texture = texture
- func clear_canvas():
- for y in height:
- for x in width:
- canvas.set_pixel(x,y,colors[1])
- update_canvas()
- func make_new_canvas():
- width = get_node("toolbar/palette/widthField").text as int
- height = get_node("toolbar/palette/heightField").text as int
- width = clamp(width, 10, 1500)
- height = clamp(height, 10, 1500)
- canvas.unlock()
- create_main_canvas()
- get_node("toolbar/palette/widthField").text = width as String
- get_node("toolbar/palette/heightField").text = height as String
- #==================================== TOOLS ====================================
- func calculate_pixel_offsets_in_circle() -> PoolVector2Array:
- var circleArray : PoolVector2Array = calculate_pixel_offsets_in_square()
- for i in range(circleArray.size()-1, -1, -1):
- if circleArray[i].length() > (brushSize)/2:
- circleArray.remove(i)
- return circleArray
- func calculate_pixel_offsets_in_square() -> PoolVector2Array:
- var squareArray : PoolVector2Array = []
- var r = (brushSize-1)/2
- for y in range(-r,r+1):
- for x in range(-r,r+1):
- squareArray.append(Vector2(x,y))
- return squareArray
- func update_info():
- get_node("infobar/info/zoomInfo").text = "%%%0.02f"%($canvas.scale.x*100)
- get_node("infobar/info/cursorPosInfo").text = "pos - %s | %s"%[cursorPosition.x, cursorPosition.y]
- func zoom_canvas(zooming_in : bool, pos = null):
- $canvas.scale *= 2.0 if zooming_in else 0.5
- $canvas.scale.x = clamp($canvas.scale.x, 0.125, 64)
- $canvas.scale.y = $canvas.scale.x
- #- move the canvas to follow the mouse position after zoom
- if pos:
- # adjust the canvas position only if the cursor is on the canvas sprite
- if pos_is_in_canvas(cursorPosition):
- if zooming_in:
- $canvas.position -= (cursorPosition - Vector2(width, height)/2)*($canvas.scale/2)
- else:
- $canvas.position += (cursorPosition - Vector2(width, height)/2)*$canvas.scale
- clamp_canvas_position_in_work_area()
- update_info()
- func pos_is_in_canvas(pos) -> bool:
- var in_canvas_x = (pos.x >= 0) and (pos.x < width)
- var in_canvas_y = (pos.y >= 0) and (pos.y < height)
- return (in_canvas_x and in_canvas_y)
- func clamp_canvas_position_in_work_area():
- var workareaSize = get_viewport().size
- var min_pos = Vector2()
- var max_pos = Vector2()
- var margin = Vector2(32,32)
- min_pos.x = -(width/2 * $canvas.scale.x) + margin.x
- min_pos.y = -(height/2 * $canvas.scale.x) + margin.y
- max_pos.x = workareaSize.x + (width/2 * $canvas.scale.x) - margin.x
- max_pos.y = workareaSize.y + (height/2 * $canvas.scale.x) - margin.y
- $canvas.position.x = clamp($canvas.position.x, min_pos.x, max_pos.x)
- $canvas.position.y = clamp($canvas.position.y, min_pos.y, max_pos.y)
- #=================================== SIGNALS ===================================
- func _notification(what):
- if what == MainLoop.NOTIFICATION_WM_QUIT_REQUEST:
- # file.close()
- get_tree().quit()
- func change_brush_shape():
- if brushShape == BRUSH_CIRCLE: brushShape = BRUSH_SQUARE
- else: brushShape = BRUSH_CIRCLE
- get_node("toolbar/palette/btn_brush_shape").text = "o" if brushShape == BRUSH_CIRCLE else "x"
- func change_brush_size(val):
- brushSize = val
- get_node("toolbar/palette/brushSizeLabel").text = "%02d"%brushSize
- pixelOffset = calculate_pixel_offsets_in_square() if brushShape == BRUSH_SQUARE else calculate_pixel_offsets_in_circle()
Add Comment
Please, Sign In to add comment