extends BaseTool 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 _picking_color := false 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_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_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(can_skip := true) -> void: if can_skip and Global.pressure_sensitivity_mode == Global.PressureSensitivity.NONE: if _mask: _mask = PoolByteArray() return var size: Vector2 = Global.current_project.size # Faster than zeroing PoolByteArray directly. # See: https://github.com/Orama-Interactive/Pixelorama/pull/439 var nulled_array := [] nulled_array.resize(size.x * size.y) _mask = PoolByteArray(nulled_array) func update_line_polylines(start: Vector2, end: Vector2) -> void: var indicator := _create_line_indicator(_indicator, start, end) _line_polylines = _create_polylines(indicator) func prepare_undo(action: String) -> void: var project: Project = Global.current_project _undo_data = _get_undo_data() project.undo_redo.create_action(action) func commit_undo() -> 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() and project.selected_cels.size() == 1: frame = project.current_frame layer = project.current_layer project.undos += 1 for image in redo_data: project.undo_redo.add_do_property(image, "data", redo_data[image]) image.unlock() for image in _undo_data: project.undo_redo.add_undo_property(image, "data", _undo_data[image]) project.undo_redo.add_do_method(Global, "undo_or_redo", false, frame, layer) project.undo_redo.add_undo_method(Global, "undo_or_redo", true, frame, layer) project.undo_redo.commit_action() _undo_data.clear() func draw_tool(position: Vector2) -> void: if !Global.current_project.layers[Global.current_project.current_layer].can_layer_get_drawn(): return var strength := _strength if Global.pressure_sensitivity_mode == Global.PressureSensitivity.ALPHA: strength *= Tools.pen_pressure _drawer.pixel_perfect = tool_slot.pixel_perfect if _brush_size == 1 else false _drawer.horizontal_mirror = Tools.horizontal_mirror _drawer.vertical_mirror = Tools.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: var project: Project = Global.current_project if project.tile_mode and project.get_tile_mode_rect().has_point(position): position = position.posmodv(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 brush_image: Image = remove_unselected_parts_of_brush(_brush_image, dst) _draw_brush_image(brush_image, src_rect, dst) # Handle Mirroring var mirror_x = (project.x_symmetry_point + 1) - dst.x - src_rect.size.x var mirror_y = (project.y_symmetry_point + 1) - dst.y - src_rect.size.y if Tools.horizontal_mirror: var x_dst := Vector2(mirror_x, dst.y) var mirror_brush_x: Image = remove_unselected_parts_of_brush(_mirror_brushes.x, x_dst) _draw_brush_image(mirror_brush_x, _flip_rect(src_rect, size, true, false), x_dst) if Tools.vertical_mirror: var xy_dst := Vector2(mirror_x, mirror_y) var mirror_brush_xy: Image = remove_unselected_parts_of_brush( _mirror_brushes.xy, xy_dst ) _draw_brush_image(mirror_brush_xy, _flip_rect(src_rect, size, true, true), xy_dst) if Tools.vertical_mirror: var y_dst := Vector2(dst.x, mirror_y) var mirror_brush_y: Image = remove_unselected_parts_of_brush(_mirror_brushes.y, y_dst) _draw_brush_image(mirror_brush_y, _flip_rect(src_rect, size, false, true), y_dst) func remove_unselected_parts_of_brush(brush: Image, dst: Vector2) -> Image: var project: Project = Global.current_project if !project.has_selection: return brush var size := brush.get_size() var new_brush := Image.new() new_brush.copy_from(brush) new_brush.lock() for x in size.x: for y in size.y: var pos := Vector2(x, y) + dst if !project.selection_bitmap.get_bit(pos): new_brush.set_pixel(x, y, Color(0)) new_brush.unlock() return new_brush func draw_indicator() -> void: draw_indicator_at(_cursor, Vector2.ZERO, Color.blue) if ( Global.current_project.tile_mode and Global.current_project.get_tile_mode_rect().has_point(_cursor) ): var tile := _line_start if _draw_line else _cursor if not Global.current_project.tile_mode_rects[Global.TileMode.NONE].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.indicators 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, ignore_mirroring := false) -> void: var project: Project = Global.current_project if project.tile_mode and project.get_tile_mode_rect().has_point(position): position = position.posmodv(project.size) if !project.can_pixel_get_drawn(position): return var images := _get_selected_draw_images() var i := int(position.x + position.y * project.size.x) if _mask.size() >= i + 1: if _mask[i] < Tools.pen_pressure: _mask[i] = Tools.pen_pressure for image in images: _drawer.set_pixel(image, position, tool_slot.color, ignore_mirroring) else: for image in images: _drawer.set_pixel(image, position, tool_slot.color, ignore_mirroring) 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.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) image.unlock() 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 cels := [] # Array of Cels if Global.animation_timer.is_stopped(): for cel_index in project.selected_cels: cels.append(project.frames[cel_index[0]].cels[cel_index[1]]) else: for frame in project.frames: var cel: Cel = frame.cels[project.current_layer] cels.append(cel) for cel in cels: var image: Image = cel.image image.unlock() data[image] = image.data image.lock() return data func _pick_color(position: Vector2) -> void: var project: Project = Global.current_project if project.tile_mode and project.get_tile_mode_rect().has_point(position): position = position.posmodv(project.size) if position.x < 0 or position.y < 0: return var image := Image.new() image.copy_from(_get_draw_image()) if position.x > image.get_width() - 1 or position.y > image.get_height() - 1: return image.lock() var color := image.get_pixelv(position) image.unlock() var button := BUTTON_LEFT if Tools._slots[BUTTON_LEFT].tool_node == self else BUTTON_RIGHT Tools.assign_color(color, button, false)