From c9f0301c79ea8c8d150377f65a3658e12ac93ccb Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas Date: Sat, 11 Jun 2022 16:15:34 +0300 Subject: [PATCH] Add a Gradient Map image effect, implements the second half of #595 The gradient edit code was taken and modified from Material Maker, MIT license. --- project.godot | 10 ++ src/Autoload/Global.gd | 3 + src/Shaders/GradientMap.gdshader | 16 ++ .../Dialogs/ImageEffects/GradientMapDialog.gd | 49 ++++++ .../ImageEffects/GradientMapDialog.tscn | 77 +++++++++ src/UI/Dialogs/ImageEffects/ImageEffects.tscn | 8 +- src/UI/Nodes/GradientEdit.gd | 162 ++++++++++++++++++ src/UI/Nodes/GradientEdit.tscn | 38 ++++ src/UI/TopMenuContainer.gd | 5 + 9 files changed, 364 insertions(+), 4 deletions(-) create mode 100644 src/Shaders/GradientMap.gdshader create mode 100644 src/UI/Dialogs/ImageEffects/GradientMapDialog.gd create mode 100644 src/UI/Dialogs/ImageEffects/GradientMapDialog.tscn create mode 100644 src/UI/Nodes/GradientEdit.gd create mode 100644 src/UI/Nodes/GradientEdit.tscn diff --git a/project.godot b/project.godot index f44936ebe..805c45498 100644 --- a/project.godot +++ b/project.godot @@ -44,6 +44,11 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://src/Classes/Frame.gd" }, { +"base": "TextureRect", +"class": "GradientEditNode", +"language": "GDScript", +"path": "res://src/UI/Nodes/GradientEdit.gd" +}, { "base": "Line2D", "class": "Guide", "language": "GDScript", @@ -127,6 +132,7 @@ _global_script_class_icons={ "Cel": "", "Drawer": "", "Frame": "", +"GradientEditNode": "", "Guide": "", "ImageEffect": "", "Layer": "", @@ -830,6 +836,10 @@ about_pixelorama={ "deadzone": 0.5, "events": [ ] } +gradient_map={ +"deadzone": 0.5, +"events": [ ] +} [locale] diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index c3bd66f04..5a79c26c8 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -33,6 +33,7 @@ enum ImageMenu { DROP_SHADOW, HSV, GRADIENT, + GRADIENT_MAP, SHADER } enum SelectMenu { SELECT_ALL, CLEAR_SELECTION, INVERT } @@ -281,6 +282,8 @@ func _initialize_keychain() -> void: Keychain.MenuInputAction.new("", "Image menu", true, "ImageMenu", ImageMenu.HSV), "gradient": Keychain.MenuInputAction.new("", "Image menu", true, "ImageMenu", ImageMenu.GRADIENT), + "gradient_map": + Keychain.MenuInputAction.new("", "Image menu", true, "ImageMenu", ImageMenu.GRADIENT_MAP), "mirror_view": Keychain.MenuInputAction.new("", "View menu", true, "ViewMenu", ViewMenu.MIRROR_VIEW), "show_grid": diff --git a/src/Shaders/GradientMap.gdshader b/src/Shaders/GradientMap.gdshader new file mode 100644 index 000000000..8a7a216e3 --- /dev/null +++ b/src/Shaders/GradientMap.gdshader @@ -0,0 +1,16 @@ +shader_type canvas_item; + +uniform sampler2D map; // GradientTexture +uniform sampler2D selection; + +void fragment() { + vec4 original_color = texture(TEXTURE, UV); + vec4 selection_color = texture(selection, UV); + vec4 output = original_color; + float value = (0.2126 * original_color.r) + (0.7152 * original_color.g) + (0.0722 * original_color.b); + vec4 gradient_color = texture(map, vec2(value, 0.0)); + output.rgb = gradient_color.rgb; + output.a *= gradient_color.a; + + COLOR = mix(original_color, output, selection_color.a); +} diff --git a/src/UI/Dialogs/ImageEffects/GradientMapDialog.gd b/src/UI/Dialogs/ImageEffects/GradientMapDialog.gd new file mode 100644 index 000000000..98a339efb --- /dev/null +++ b/src/UI/Dialogs/ImageEffects/GradientMapDialog.gd @@ -0,0 +1,49 @@ +extends ImageEffect + +var shader: Shader = preload("res://src/Shaders/GradientMap.gdshader") +var confirmed := false + + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + var sm := ShaderMaterial.new() + sm.shader = shader + preview.set_material(sm) + + +func set_nodes() -> void: + preview = $VBoxContainer/AspectRatioContainer/Preview + selection_checkbox = $VBoxContainer/OptionsContainer/SelectionCheckBox + affect_option_button = $VBoxContainer/OptionsContainer/AffectOptionButton + + +func _about_to_show() -> void: + confirmed = false + ._about_to_show() + + +func _confirmed() -> void: + confirmed = true + ._confirmed() + + +func commit_action(cel: Image, project: Project = Global.current_project) -> void: + var selection_tex := ImageTexture.new() + if selection_checkbox.pressed and project.has_selection: + var selection: Image = project.bitmap_to_image(project.selection_bitmap) + selection_tex.create_from_image(selection, 0) + + var params := {"selection": selection_tex, "map": $VBoxContainer/GradientEdit.texture} + + if !confirmed: + preview.material.shader = shader + for param in params: + preview.material.set_shader_param(param, params[param]) + else: + var gen := ShaderImageEffect.new() + gen.generate_image(cel, shader, params, project.size) + yield(gen, "done") + + +func _on_GradientEdit_updated(_gradient, _cc) -> void: + update_preview() diff --git a/src/UI/Dialogs/ImageEffects/GradientMapDialog.tscn b/src/UI/Dialogs/ImageEffects/GradientMapDialog.tscn new file mode 100644 index 000000000..5d4cfe144 --- /dev/null +++ b/src/UI/Dialogs/ImageEffects/GradientMapDialog.tscn @@ -0,0 +1,77 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://src/UI/TransparentChecker.tscn" type="PackedScene" id=1] +[ext_resource path="res://src/UI/Nodes/GradientEdit.tscn" type="PackedScene" id=2] +[ext_resource path="res://src/UI/Dialogs/ImageEffects/GradientMapDialog.gd" type="Script" id=3] + +[node name="GradientMapDialog" type="ConfirmationDialog"] +margin_right = 200.0 +margin_bottom = 70.0 +window_title = "Gradient Map" +resizable = true +script = ExtResource( 3 ) + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 8.0 +margin_top = 8.0 +margin_right = -8.0 +margin_bottom = -36.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="VBoxContainer"] +margin_right = 278.0 +margin_bottom = 200.0 +size_flags_vertical = 3 + +[node name="Preview" type="TextureRect" parent="VBoxContainer/AspectRatioContainer"] +margin_left = 39.0 +margin_right = 239.0 +margin_bottom = 200.0 +rect_min_size = Vector2( 200, 200 ) +size_flags_horizontal = 5 +size_flags_vertical = 3 +expand = true +stretch_mode = 5 + +[node name="TransparentChecker" parent="VBoxContainer/AspectRatioContainer/Preview" instance=ExtResource( 1 )] +show_behind_parent = true +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_right = 0.0 +margin_bottom = 0.0 + +[node name="GradientEdit" parent="VBoxContainer" instance=ExtResource( 2 )] +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_top = 204.0 +margin_right = 278.0 +margin_bottom = 234.0 +rect_min_size = Vector2( 0, 30 ) + +[node name="OptionsContainer" type="GridContainer" parent="VBoxContainer"] +margin_top = 238.0 +margin_right = 278.0 +margin_bottom = 262.0 +columns = 2 + +[node name="SelectionCheckBox" type="CheckBox" parent="VBoxContainer/OptionsContainer" groups=["gradient_common"]] +margin_right = 160.0 +margin_bottom = 24.0 +mouse_default_cursor_shape = 2 +pressed = true +text = "Only affect selection" + +[node name="AffectOptionButton" type="OptionButton" parent="VBoxContainer/OptionsContainer" groups=["gradient_common"]] +margin_left = 164.0 +margin_right = 278.0 +margin_bottom = 24.0 +mouse_default_cursor_shape = 2 +text = "Selected cels" +items = [ "Selected cels", null, false, 0, null, "Current frame", null, false, 1, null, "All frames", null, false, 2, null, "All projects", null, false, 3, null ] +selected = 0 + +[connection signal="updated" from="VBoxContainer/GradientEdit" to="." method="_on_GradientEdit_updated"] diff --git a/src/UI/Dialogs/ImageEffects/ImageEffects.tscn b/src/UI/Dialogs/ImageEffects/ImageEffects.tscn index eddf0d123..45da9b43a 100644 --- a/src/UI/Dialogs/ImageEffects/ImageEffects.tscn +++ b/src/UI/Dialogs/ImageEffects/ImageEffects.tscn @@ -1,9 +1,10 @@ -[gd_scene load_steps=12 format=2] +[gd_scene load_steps=13 format=2] [ext_resource path="res://src/UI/Dialogs/ImageEffects/FlipImageDialog.tscn" type="PackedScene" id=1] [ext_resource path="res://src/UI/Dialogs/ImageEffects/InvertColorsDialog.tscn" type="PackedScene" id=2] [ext_resource path="res://src/UI/Dialogs/ImageEffects/DesaturateDialog.tscn" type="PackedScene" id=3] [ext_resource path="res://src/UI/Dialogs/ImageEffects/DropShadowDialog.tscn" type="PackedScene" id=4] +[ext_resource path="res://src/UI/Dialogs/ImageEffects/GradientMapDialog.tscn" type="PackedScene" id=5] [ext_resource path="res://src/UI/Dialogs/ImageEffects/ResizeCanvas.tscn" type="PackedScene" id=8] [ext_resource path="res://src/UI/Dialogs/ImageEffects/RotateImage.tscn" type="PackedScene" id=9] [ext_resource path="res://src/UI/Dialogs/ImageEffects/ShaderEffect.tscn" type="PackedScene" id=10] @@ -14,9 +15,6 @@ [node name="ImageEffects" type="Control"] mouse_filter = 2 -__meta__ = { -"_edit_use_anchors_": false -} [node name="ScaleImage" parent="." instance=ExtResource( 14 )] margin_bottom = 127.0 @@ -44,4 +42,6 @@ margin_bottom = 106.0 [node name="GradientDialog" parent="." instance=ExtResource( 12 )] margin_bottom = 214.0 +[node name="GradientMapDialog" parent="." instance=ExtResource( 5 )] + [node name="ShaderEffect" parent="." instance=ExtResource( 10 )] diff --git a/src/UI/Nodes/GradientEdit.gd b/src/UI/Nodes/GradientEdit.gd new file mode 100644 index 000000000..403440de5 --- /dev/null +++ b/src/UI/Nodes/GradientEdit.gd @@ -0,0 +1,162 @@ +# Code taken and modified from Material Maker, licensed under MIT +# gdlint: ignore=max-line-length +# https://github.com/RodZill4/material-maker/blob/master/material_maker/widgets/gradient_editor/gradient_editor.gd +class_name GradientEditNode +extends TextureRect + +signal updated(gradient, cc) + +var continuous_change := true +var active_cursor: GradientCursor # Showing a color picker popup to change a cursor's color + +onready var x_offset: float = rect_size.x - GradientCursor.WIDTH +onready var gradient: Gradient = texture.gradient + + +class GradientCursor: + extends Control + + const WIDTH := 10 + var color: Color + var sliding := false + onready var label: Label = get_parent().get_node("Value") + + func _ready() -> void: + rect_position = Vector2(0, 15) + rect_size = Vector2(WIDTH, 15) + + func _draw() -> void: +# warning-ignore:integer_division + var polygon := PoolVector2Array( + [ + Vector2(0, 5), + Vector2(WIDTH / 2, 0), + Vector2(WIDTH, 5), + Vector2(WIDTH, 15), + Vector2(0, 15), + Vector2(0, 5) + ] + ) + var c := color + c.a = 1.0 + draw_colored_polygon(polygon, c) + draw_polyline(polygon, Color(0.0, 0.0, 0.0) if color.v > 0.5 else Color(1.0, 1.0, 1.0)) + + func _gui_input(ev: InputEvent) -> void: + if ev is InputEventMouseButton: + if ev.button_index == BUTTON_LEFT: + if ev.doubleclick: + get_parent().select_color(self, ev.global_position) + elif ev.pressed: + get_parent().continuous_change = false + sliding = true + label.visible = true + label.text = "%.03f" % get_cursor_position() + else: + sliding = false + label.visible = false + elif ev.button_index == BUTTON_RIGHT and get_parent().get_sorted_cursors().size() > 2: + var parent = get_parent() + parent.remove_child(self) + parent.continuous_change = false + parent.update_from_value() + queue_free() + elif ev is InputEventMouseMotion and (ev.button_mask & BUTTON_MASK_LEFT) != 0 and sliding: + rect_position.x += get_local_mouse_position().x + if ev.control: + rect_position.x = ( + round(get_cursor_position() * 20.0) + * 0.05 + * (get_parent().rect_size.x - WIDTH) + ) + rect_position.x = min(max(0, rect_position.x), get_parent().rect_size.x - rect_size.x) + get_parent().update_from_value() + label.text = "%.03f" % get_cursor_position() + + func get_cursor_position() -> float: + return rect_position.x / (get_parent().rect_size.x - WIDTH) + + func set_color(c: Color) -> void: + color = c + get_parent().update_from_value() + update() + + static func sort(a, b) -> bool: + return a.get_position() < b.get_position() + + func can_drop_data(_position, data) -> bool: + return typeof(data) == TYPE_COLOR + + func drop_data(_position, data) -> void: + set_color(data) + + +func _ready() -> void: + create_cursors() + + +func create_cursors() -> void: + for c in get_children(): + if c is GradientCursor: + remove_child(c) + c.queue_free() + for i in gradient.get_point_count(): + var p: float = gradient.get_offset(i) + add_cursor(p * x_offset, gradient.get_color(i)) + + +func _gui_input(ev: InputEvent) -> void: + if ev.is_action_pressed("left_mouse"): + var p = clamp(ev.position.x, 0, x_offset) + add_cursor(p, get_gradient_color(p)) + continuous_change = false + update_from_value() + + +func update_from_value() -> void: + gradient.offsets = [] + for c in get_children(): + if c is GradientCursor: + var point: float = c.rect_position.x / x_offset + gradient.add_point(point, c.color) + emit_signal("updated", gradient, continuous_change) + continuous_change = true + + +func add_cursor(x: float, color: Color) -> void: + var cursor := GradientCursor.new() + add_child(cursor) + cursor.rect_position.x = x + cursor.color = color + + +func select_color(cursor: GradientCursor, position: Vector2) -> void: + active_cursor = cursor + var color_picker = $Popup.get_node("ColorPicker") + color_picker.color = cursor.color + $Popup.rect_position = position + $Popup.popup() + + +func get_sorted_cursors() -> Array: + var array := [] + for c in get_children(): + if c is GradientCursor: + array.append(c) + array.sort_custom(GradientCursor, "sort") + return array + + +func get_gradient_color(x: float) -> Color: + return gradient.interpolate(x / x_offset) + + +func _on_ColorPicker_color_changed(color: Color) -> void: + active_cursor.set_color(color) + + +func _on_GradientEdit_resized() -> void: + if not gradient: + return + x_offset = rect_size.x - GradientCursor.WIDTH + create_cursors() diff --git a/src/UI/Nodes/GradientEdit.tscn b/src/UI/Nodes/GradientEdit.tscn new file mode 100644 index 000000000..c7ad82bb6 --- /dev/null +++ b/src/UI/Nodes/GradientEdit.tscn @@ -0,0 +1,38 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://src/UI/Nodes/GradientEdit.gd" type="Script" id=1] + +[sub_resource type="Gradient" id=1] + +[sub_resource type="GradientTexture" id=2] +gradient = SubResource( 1 ) + +[node name="GradientEdit" type="TextureRect"] +anchor_right = 1.0 +anchor_bottom = 1.0 +texture = SubResource( 2 ) +expand = true +script = ExtResource( 1 ) + +[node name="Popup" type="PopupPanel" parent="."] +margin_right = 316.0 +margin_bottom = 470.0 + +[node name="ColorPicker" type="ColorPicker" parent="Popup"] +margin_left = 4.0 +margin_top = 4.0 +margin_right = 312.0 +margin_bottom = 466.0 + +[node name="Value" type="Label" parent="."] +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +margin_left = -20.0 +margin_top = -7.0 +margin_right = 20.0 +margin_bottom = 7.0 + +[connection signal="resized" from="." to="." method="_on_GradientEdit_resized"] +[connection signal="color_changed" from="Popup/ColorPicker" to="." method="_on_ColorPicker_color_changed"] diff --git a/src/UI/TopMenuContainer.gd b/src/UI/TopMenuContainer.gd index 744862b06..125cc1d7c 100644 --- a/src/UI/TopMenuContainer.gd +++ b/src/UI/TopMenuContainer.gd @@ -252,6 +252,7 @@ func _setup_image_menu() -> void: "Drop Shadow", "Adjust Hue/Saturation/Value", "Gradient", + "Gradient Map", # "Shader" ] var image_menu: PopupMenu = image_menu_button.get_popup() @@ -620,6 +621,10 @@ func image_menu_id_pressed(id: int) -> void: Global.control.get_node("Dialogs/ImageEffects/GradientDialog").popup_centered() Global.dialog_open(true) + Global.ImageMenu.GRADIENT_MAP: + Global.control.get_node("Dialogs/ImageEffects/GradientMapDialog").popup_centered() + Global.dialog_open(true) + # Global.ImageMenu.SHADER: # Global.control.get_node("Dialogs/ImageEffects/ShaderEffect").popup_centered() # Global.dialog_open(true)