diff --git a/src/Shaders/ColorReplace.shader b/src/Shaders/ColorReplace.shader new file mode 100644 index 000000000..99f9cb6ff --- /dev/null +++ b/src/Shaders/ColorReplace.shader @@ -0,0 +1,37 @@ +shader_type canvas_item; +render_mode unshaded; + +uniform vec2 size; + +uniform vec4 old_color; //Our description +uniform vec4 new_color; +uniform float similarity_percent : hint_range(0.0, 100.0); + +// Must be the same size as image +// Selected pixels are 1,1,1,1 and unselected 0,0,0,0 +uniform sampler2D selection; + +uniform bool has_pattern; +uniform sampler2D pattern; +uniform vec2 pattern_size; +uniform vec2 pattern_uv_offset; + +void fragment() { // applies on each pixel seperately + vec4 original_color = texture(TEXTURE, UV); // The drawing we have to use on + vec4 selection_color = texture(selection, UV); // use its alpha to get portion we can ignore + + vec4 col = original_color; // Innocent till proven Guilty + + float max_diff = distance(original_color, old_color); // How much this pixel matches our description + + float similarity = abs(2.0 - ((similarity_percent/100.0) * 2.0)); + + if (max_diff <= similarity) // We found our match and pixel is proven Guilty (small is precise) + if (has_pattern) + col = textureLod(pattern, UV * (size / pattern_size) + pattern_uv_offset, 0.0); + else + col = new_color; + + // Mix selects original color if there is selection or col if there is none + COLOR = mix(original_color, col, selection_color.a); +} diff --git a/src/Tools/Bucket.gd b/src/Tools/Bucket.gd index b270b6b43..abf294106 100644 --- a/src/Tools/Bucket.gd +++ b/src/Tools/Bucket.gd @@ -1,7 +1,10 @@ extends BaseTool +const ColorReplaceShader := preload("res://src/Shaders/ColorReplace.shader") + var _prev_mode := 0 var _pattern: Patterns.Pattern +var _similarity := 100 var _fill_area := 0 var _fill_with := 0 var _offset_x := 0 @@ -20,9 +23,11 @@ func _input(event: InputEvent) -> void: if event.is_action("ctrl"): options.selected = _prev_mode ^ 1 _fill_area = options.selected + $Similarity.visible = (_fill_area == 1) if event.is_action_released("ctrl"): options.selected = _prev_mode _fill_area = options.selected + $Similarity.visible = (_fill_area == 1) func _on_FillAreaOptions_item_selected(index: int) -> void: @@ -33,6 +38,20 @@ func _on_FillAreaOptions_item_selected(index: int) -> void: func _on_FillWithOptions_item_selected(index: int) -> void: _fill_with = index + $Similarity/Value.value = _similarity + update_config() + save_config() + + +func _on_Value_value_changed(value: float) -> void: + _similarity = value + $Similarity/Slider.value = _similarity + update_config() + save_config() + + +func _on_Slider_value_changed(value: float) -> void: + _similarity = value update_config() save_config() @@ -64,11 +83,12 @@ func _on_PatternOffsetY_value_changed(value: float) -> void: func get_config() -> Dictionary: if !_pattern: - return {} + return {"fill_area": _fill_area, "fill_with": _fill_with, "similarity": _similarity} return { "pattern_index": _pattern.index, "fill_area": _fill_area, "fill_with": _fill_with, + "similarity": _similarity, "offset_x": _offset_x, "offset_y": _offset_y, } @@ -80,6 +100,7 @@ func set_config(config: Dictionary) -> void: _pattern = Global.patterns_popup.get_pattern(index) _fill_area = config.get("fill_area", _fill_area) _fill_with = config.get("fill_with", _fill_with) + _similarity = config.get("similarity", _similarity) _offset_x = config.get("offset_x", _offset_x) _offset_y = config.get("offset_y", _offset_y) update_pattern() @@ -88,6 +109,9 @@ func set_config(config: Dictionary) -> void: func update_config() -> void: $FillAreaOptions.selected = _fill_area $FillWithOptions.selected = _fill_with + $Similarity.visible = (_fill_area == 1) + $Similarity/Value.value = _similarity + $Similarity/Slider.value = _similarity $Mirror.visible = _fill_area == 0 $FillPattern.visible = _fill_with == 1 $FillPattern/XOffset/OffsetX.value = _offset_x @@ -151,13 +175,37 @@ func fill_in_color(position: Vector2) -> void: if tool_slot.color.is_equal_approx(color): return - for x in Global.current_project.size.x: - for y in Global.current_project.size.y: - var pos := Vector2(x, y) - if project.has_selection and not project.can_pixel_get_drawn(pos): - continue - if image.get_pixelv(pos).is_equal_approx(color): - _set_pixel(image, x, y, tool_slot.color) + var selection: Image + var selection_tex := ImageTexture.new() + if project.has_selection: + selection = project.bitmap_to_image(project.selection_bitmap, false) + else: + selection = Image.new() + selection.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + selection.fill(Color(1, 1, 1, 1)) + + selection_tex.create_from_image(selection) + + var pattern_tex := ImageTexture.new() + if _pattern: + pattern_tex.create_from_image(_pattern.image) + + var params := { + "size": project.size, + "old_color": color, + "new_color": tool_slot.color, + "similarity_percent": _similarity, + "selection": selection_tex, + "pattern": pattern_tex, + "pattern_size": pattern_tex.get_size(), + # pixel offset converted to pattern uv offset + "pattern_uv_offset": + Vector2.ONE / pattern_tex.get_size() * Vector2(_offset_x, _offset_y), + "has_pattern": true if _fill_with == 1 else false + } + var gen := ShaderImageEffect.new() + gen.generate_image(image, ColorReplaceShader, params, project.size) + yield(gen, "done") func fill_in_area(position: Vector2) -> void: diff --git a/src/Tools/Bucket.tscn b/src/Tools/Bucket.tscn index c3adb6a70..19eacccea 100644 --- a/src/Tools/Bucket.tscn +++ b/src/Tools/Bucket.tscn @@ -22,6 +22,8 @@ corner_radius_bottom_left = 5 anti_aliasing = false [node name="ToolOptions" instance=ExtResource( 2 )] +margin_right = 138.0 +margin_bottom = 105.0 script = ExtResource( 3 ) [node name="Label" parent="." index="0"] @@ -43,10 +45,58 @@ 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 ] +items = [ "Same color area", null, false, 0, null, "Similar color pixels", null, false, 1, null ] selected = 0 -[node name="FillWith" type="Label" parent="." index="3"] +[node name="Similarity" type="VBoxContainer" parent="." index="3"] +visible = false +margin_top = 60.0 +margin_right = 131.0 +margin_bottom = 122.0 +alignment = 1 + +[node name="Label" type="Label" parent="Similarity" index="0"] +margin_left = 33.0 +margin_right = 97.0 +margin_bottom = 14.0 +size_flags_horizontal = 4 +text = "Similarity:" + +[node name="Value" type="SpinBox" parent="Similarity" index="1"] +margin_left = 28.0 +margin_top = 18.0 +margin_right = 102.0 +margin_bottom = 42.0 +hint_tooltip = "How much two colors are Similar/Close together" +mouse_default_cursor_shape = 2 +size_flags_horizontal = 4 +step = 0.001 +value = 100.0 +align = 1 +suffix = "%" +__meta__ = { +"_editor_description_": "" +} + +[node name="Slider" type="HSlider" parent="Similarity" index="2"] +margin_left = 19.0 +margin_top = 46.0 +margin_right = 111.0 +margin_bottom = 62.0 +rect_min_size = Vector2( 92, 0 ) +hint_tooltip = "How much two colors are Similar/Close together" +focus_mode = 0 +mouse_default_cursor_shape = 2 +size_flags_horizontal = 4 +size_flags_vertical = 1 +step = 0.001 +value = 100.0 +ticks_on_borders = true +__meta__ = { +"_editor_description_": "" +} + +[node name="FillWith" type="Label" parent="." index="4"] margin_left = 38.0 margin_top = 60.0 margin_right = 92.0 @@ -54,7 +104,7 @@ margin_bottom = 74.0 size_flags_horizontal = 4 text = "Fill with:" -[node name="FillWithOptions" type="OptionButton" parent="." index="4"] +[node name="FillWithOptions" type="OptionButton" parent="." index="5"] margin_left = 5.0 margin_top = 78.0 margin_right = 126.0 @@ -65,7 +115,7 @@ 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"] +[node name="FillPattern" type="VBoxContainer" parent="." index="6"] visible = false margin_left = 22.0 margin_top = 102.0 @@ -137,16 +187,16 @@ margin_right = 85.0 margin_bottom = 24.0 mouse_default_cursor_shape = 2 -[node name="PixelPerfect" parent="." index="6"] +[node name="PixelPerfect" parent="." index="7"] visible = false -[node name="EmptySpacer" parent="." index="7"] +[node name="EmptySpacer" parent="." index="8"] visible = false margin_top = 212.0 margin_right = 131.0 margin_bottom = 224.0 -[node name="Mirror" parent="." index="8"] +[node name="Mirror" parent="." index="9"] visible = false margin_top = 212.0 margin_right = 131.0 @@ -161,6 +211,8 @@ margin_left = 86.0 margin_right = 103.0 [connection signal="item_selected" from="FillAreaOptions" to="." method="_on_FillAreaOptions_item_selected"] +[connection signal="value_changed" from="Similarity/Value" to="." method="_on_Value_value_changed"] +[connection signal="value_changed" from="Similarity/Slider" to="." method="_on_Slider_value_changed"] [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"]