diff --git a/Translations/Translations.pot b/Translations/Translations.pot
index bc212b0a7..437a7b783 100644
--- a/Translations/Translations.pot
+++ b/Translations/Translations.pot
@@ -311,6 +311,10 @@ msgstr ""
msgid "Show Mouse Guides"
msgstr ""
+#. Found under the View menu. When enabled, non-destructive layer effects will be visible on the canvas.
+msgid "Display Layer Effects"
+msgstr ""
+
#. Found under the View menu.
msgid "Snap To"
msgstr ""
@@ -1620,6 +1624,9 @@ msgstr ""
msgid "Zoom out"
msgstr ""
+msgid "Options"
+msgstr ""
+
msgid "Options:"
msgstr ""
@@ -2716,3 +2723,15 @@ msgstr ""
msgid "Blacks out the image and makes all opaque pixels a dark color."
msgstr ""
+
+#. Used in checkbuttons (like on/off switches) that enable/disable something.
+msgid "Enabled"
+msgstr ""
+
+#. Refers to non-destructive effects (such as outline, drop shadow etc) that are applied to layers. Found in the title of the layer effects dialog.
+msgid "Layer effects"
+msgstr ""
+
+#. A button that, when pressed, shows a list of effects to add. Found in the the layer effects dialog.
+msgid "Add effect"
+msgstr ""
diff --git a/assets/graphics/misc/close.svg b/assets/graphics/misc/close.svg
new file mode 100644
index 000000000..81822a3aa
--- /dev/null
+++ b/assets/graphics/misc/close.svg
@@ -0,0 +1 @@
+
diff --git a/assets/graphics/misc/close.svg.import b/assets/graphics/misc/close.svg.import
new file mode 100644
index 000000000..4d68de7c9
--- /dev/null
+++ b/assets/graphics/misc/close.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b4e44lb8k2pmt"
+path="res://.godot/imported/close.svg-11f3414f2f3de5550eee4cc42f4941c6.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/graphics/misc/close.svg"
+dest_files=["res://.godot/imported/close.svg-11f3414f2f3de5550eee4cc42f4941c6.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.25
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/assets/graphics/misc/move_down_arrow.svg b/assets/graphics/misc/move_down_arrow.svg
new file mode 100644
index 000000000..731338539
--- /dev/null
+++ b/assets/graphics/misc/move_down_arrow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/graphics/misc/move_down_arrow.svg.import b/assets/graphics/misc/move_down_arrow.svg.import
new file mode 100644
index 000000000..38ef6055b
--- /dev/null
+++ b/assets/graphics/misc/move_down_arrow.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d1qjs2ci67se4"
+path="res://.godot/imported/move_down_arrow.svg-76570684c2341024db5505cd94fb3ba5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/graphics/misc/move_down_arrow.svg"
+dest_files=["res://.godot/imported/move_down_arrow.svg-76570684c2341024db5505cd94fb3ba5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.7
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/assets/graphics/misc/move_up_arrow.svg b/assets/graphics/misc/move_up_arrow.svg
new file mode 100644
index 000000000..f7354308d
--- /dev/null
+++ b/assets/graphics/misc/move_up_arrow.svg
@@ -0,0 +1 @@
+
diff --git a/assets/graphics/misc/move_up_arrow.svg.import b/assets/graphics/misc/move_up_arrow.svg.import
new file mode 100644
index 000000000..f53a39531
--- /dev/null
+++ b/assets/graphics/misc/move_up_arrow.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cxj0mtixk466v"
+path="res://.godot/imported/move_up_arrow.svg-01a22b2c21ca40bb8e863f736d2606de.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/graphics/misc/move_up_arrow.svg"
+dest_files=["res://.godot/imported/move_up_arrow.svg-01a22b2c21ca40bb8e863f736d2606de.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.7
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/project.godot b/project.godot
index 7daa669a8..19ff6fbfd 100644
--- a/project.godot
+++ b/project.godot
@@ -683,6 +683,10 @@ posterize={
"deadzone": 0.5,
"events": []
}
+display_layer_effects={
+"deadzone": 0.5,
+"events": []
+}
view_splash_screen={
"deadzone": 0.5,
"events": []
diff --git a/src/Autoload/DrawingAlgos.gd b/src/Autoload/DrawingAlgos.gd
index 96daa28ec..01d59d914 100644
--- a/src/Autoload/DrawingAlgos.gd
+++ b/src/Autoload/DrawingAlgos.gd
@@ -20,11 +20,13 @@ func blend_all_layers(
for i in Global.current_project.layers.size():
if current_cels[i] is GroupCel:
continue
- if not Global.current_project.layers[i].is_visible_in_hierarchy():
+ var layer := Global.current_project.layers[i]
+ if not layer.is_visible_in_hierarchy():
continue
- textures.append(current_cels[i].get_image())
+ var cel_image := layer.display_effects(current_cels[i])
+ textures.append(cel_image)
opacities.append(current_cels[i].opacity)
- blend_modes.append(Global.current_project.layers[i].blend_mode)
+ blend_modes.append(layer.blend_mode)
var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures)
var params := {
@@ -51,12 +53,14 @@ func blend_selected_cels(
continue
if frame.cels[cel_ind] is GroupCel:
continue
- if not project.layers[cel_ind].is_visible_in_hierarchy():
+ var layer := project.layers[cel_ind]
+ if not layer.is_visible_in_hierarchy():
continue
var cel := frame.cels[cel_ind]
- textures.append(cel.get_image())
+ var cel_image := layer.display_effects(cel)
+ textures.append(cel_image)
opacities.append(cel.opacity)
- blend_modes.append(Global.current_project.layers[cel_ind].blend_mode)
+ blend_modes.append(layer.blend_mode)
var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures)
var params := {
diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd
index 3aa252654..a7f9c69df 100644
--- a/src/Autoload/Global.gd
+++ b/src/Autoload/Global.gd
@@ -34,6 +34,7 @@ enum ViewMenu {
SHOW_RULERS,
SHOW_GUIDES,
SHOW_MOUSE_GUIDES,
+ DISPLAY_LAYER_EFFECTS,
SNAP_TO,
}
## Enumeration of items present in the Window Menu.
@@ -69,6 +70,8 @@ const OVERRIDE_FILE := "override.cfg"
const HOME_SUBDIR_NAME := "pixelorama"
## The name of folder that contains subdirectories for users to place brushes, palettes, patterns.
const CONFIG_SUBDIR_NAME := "pixelorama_data"
+const VALUE_SLIDER_V2_TSCN := preload("res://src/UI/Nodes/ValueSliderV2.tscn")
+const GRADIENT_EDIT_TSCN := preload("res://src/UI/Nodes/GradientEdit.tscn")
## It is path to the executable's base drectory.
var root_directory := "."
@@ -392,6 +395,12 @@ var show_rulers := true
var show_guides := true
## If [code]true[/code], the mouse guides are visible.
var show_mouse_guides := false
+var display_layer_effects := true:
+ set(value):
+ display_layer_effects = value
+ if is_instance_valid(top_menu_container):
+ top_menu_container.view_menu.set_item_checked(ViewMenu.DISPLAY_LAYER_EFFECTS, value)
+ canvas.queue_redraw()
## If [code]true[/code], cursor snaps to the boundary of rectangular grid boxes.
var snap_to_rectangular_grid_boundary := false
## If [code]true[/code], cursor snaps to the center of rectangular grid boxes.
@@ -607,6 +616,7 @@ func _initialize_keychain() -> void:
"show_pixel_grid": Keychain.InputAction.new("", "View menu", true),
"show_guides": Keychain.InputAction.new("", "View menu", true),
"show_rulers": Keychain.InputAction.new("", "View menu", true),
+ &"display_layer_effects": Keychain.InputAction.new("", "View menu", true),
"moveable_panels": Keychain.InputAction.new("", "Window menu", true),
"zen_mode": Keychain.InputAction.new("", "Window menu", true),
"toggle_fullscreen": Keychain.InputAction.new("", "Window menu", true),
@@ -900,3 +910,220 @@ func undo_redo_move(diff: Vector2i, images: Array[Image]) -> void:
image_copy.copy_from(image)
image.fill(Color(0, 0, 0, 0))
image.blit_rect(image_copy, Rect2i(Vector2i.ZERO, image.get_size()), diff)
+
+
+func create_ui_for_shader_uniforms(
+ shader: Shader,
+ params: Dictionary,
+ parent_node: Control,
+ value_changed: Callable,
+ file_selected: Callable
+) -> void:
+ var code := shader.code.split("\n")
+ var uniforms: PackedStringArray = []
+ for line in code:
+ if line.begins_with("uniform"):
+ uniforms.append(line)
+
+ for uniform in uniforms:
+ # Example uniform:
+ # uniform float parameter_name : hint_range(0, 255) = 100.0;
+ var uniform_split := uniform.split("=")
+ var u_value := ""
+ if uniform_split.size() > 1:
+ u_value = uniform_split[1].replace(";", "").strip_edges()
+ else:
+ uniform_split[0] = uniform_split[0].replace(";", "").strip_edges()
+
+ var u_left_side := uniform_split[0].split(":")
+ var u_hint := ""
+ if u_left_side.size() > 1:
+ u_hint = u_left_side[1].strip_edges()
+ u_hint = u_hint.replace(";", "")
+
+ var u_init := u_left_side[0].split(" ")
+ var u_type := u_init[1]
+ var u_name := u_init[2]
+ var humanized_u_name := Keychain.humanize_snake_case(u_name) + ":"
+
+ if u_type == "float" or u_type == "int":
+ var label := Label.new()
+ label.text = humanized_u_name
+ label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ var slider := ValueSlider.new()
+ slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ var min_value := 0.0
+ var max_value := 255.0
+ var step := 1.0
+ var range_values_array: PackedStringArray
+ if "hint_range" in u_hint:
+ var range_values: String = u_hint.replace("hint_range(", "")
+ range_values = range_values.replace(")", "").strip_edges()
+ range_values_array = range_values.split(",")
+
+ if u_type == "float":
+ if range_values_array.size() >= 1:
+ min_value = float(range_values_array[0])
+ else:
+ min_value = 0.01
+
+ if range_values_array.size() >= 2:
+ max_value = float(range_values_array[1])
+
+ if range_values_array.size() >= 3:
+ step = float(range_values_array[2])
+ else:
+ step = 0.01
+
+ if u_value != "":
+ slider.value = float(u_value)
+ else:
+ if range_values_array.size() >= 1:
+ min_value = int(range_values_array[0])
+
+ if range_values_array.size() >= 2:
+ max_value = int(range_values_array[1])
+
+ if range_values_array.size() >= 3:
+ step = int(range_values_array[2])
+
+ if u_value != "":
+ slider.value = int(u_value)
+ if params.has(u_name):
+ slider.value = params[u_name]
+ else:
+ params[u_name] = slider.value
+ slider.min_value = min_value
+ slider.max_value = max_value
+ slider.step = step
+ slider.value_changed.connect(value_changed.bind(u_name))
+ slider.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ var hbox := HBoxContainer.new()
+ hbox.add_child(label)
+ hbox.add_child(slider)
+ parent_node.add_child(hbox)
+ elif u_type == "vec2":
+ var label := Label.new()
+ label.text = humanized_u_name
+ label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ var vector2 := _vec2str_to_vector2(u_value)
+ var slider := VALUE_SLIDER_V2_TSCN.instantiate() as ValueSliderV2
+ slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ slider.value = vector2
+ if params.has(u_name):
+ slider.value = params[u_name]
+ else:
+ params[u_name] = slider.value
+ slider.value_changed.connect(value_changed.bind(u_name))
+ var hbox := HBoxContainer.new()
+ hbox.add_child(label)
+ hbox.add_child(slider)
+ parent_node.add_child(hbox)
+ elif u_type == "vec4":
+ if "source_color" in u_hint:
+ var label := Label.new()
+ label.text = humanized_u_name
+ label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ var color := _vec4str_to_color(u_value)
+ var color_button := ColorPickerButton.new()
+ color_button.custom_minimum_size = Vector2(20, 20)
+ color_button.color = color
+ if params.has(u_name):
+ color_button.color = params[u_name]
+ else:
+ params[u_name] = color_button.color
+ color_button.color_changed.connect(value_changed.bind(u_name))
+ color_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ color_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ var hbox := HBoxContainer.new()
+ hbox.add_child(label)
+ hbox.add_child(color_button)
+ parent_node.add_child(hbox)
+ elif u_type == "sampler2D":
+ if u_name == "selection":
+ continue
+ var label := Label.new()
+ label.text = humanized_u_name
+ label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ var hbox := HBoxContainer.new()
+ hbox.add_child(label)
+ if u_name.begins_with("gradient_"):
+ var gradient_edit := GRADIENT_EDIT_TSCN.instantiate() as GradientEditNode
+ gradient_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ if params.has(u_name) and params[u_name] is GradientTexture2D:
+ gradient_edit.set_gradient_texture(params[u_name])
+ else:
+ params[u_name] = gradient_edit.texture
+ value_changed.call(gradient_edit.get_node("TextureRect").texture, u_name)
+ gradient_edit.updated.connect(
+ func(_gradient, _cc): value_changed.call(gradient_edit.texture, u_name)
+ )
+ hbox.add_child(gradient_edit)
+ else:
+ var file_dialog := FileDialog.new()
+ file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
+ file_dialog.access = FileDialog.ACCESS_FILESYSTEM
+ file_dialog.size = Vector2(384, 281)
+ file_dialog.file_selected.connect(file_selected.bind(u_name))
+ var button := Button.new()
+ button.text = "Load texture"
+ button.pressed.connect(file_dialog.popup_centered)
+ button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ hbox.add_child(button)
+ parent_node.add_child(file_dialog)
+ parent_node.add_child(hbox)
+
+ elif u_type == "bool":
+ var label := Label.new()
+ label.text = humanized_u_name
+ label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ var checkbox := CheckBox.new()
+ checkbox.text = "On"
+ if u_value == "true":
+ checkbox.button_pressed = true
+ if params.has(u_name):
+ checkbox.button_pressed = params[u_name]
+ else:
+ params[u_name] = checkbox.button_pressed
+ checkbox.toggled.connect(value_changed.bind(u_name))
+ checkbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ checkbox.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ var hbox := HBoxContainer.new()
+ hbox.add_child(label)
+ hbox.add_child(checkbox)
+ parent_node.add_child(hbox)
+
+
+func _vec2str_to_vector2(vec2: String) -> Vector2:
+ vec2 = vec2.replace("vec2(", "")
+ vec2 = vec2.replace(")", "")
+ var vec_values := vec2.split(",")
+ if vec_values.size() == 0:
+ return Vector2.ZERO
+ var y := float(vec_values[0])
+ if vec_values.size() == 2:
+ y = float(vec_values[1])
+ var vector2 := Vector2(float(vec_values[0]), y)
+ return vector2
+
+
+func _vec4str_to_color(vec4: String) -> Color:
+ vec4 = vec4.replace("vec4(", "")
+ vec4 = vec4.replace(")", "")
+ var rgba_values := vec4.split(",")
+ var red := float(rgba_values[0])
+
+ var green := float(rgba_values[0])
+ if rgba_values.size() >= 2:
+ green = float(rgba_values[1])
+
+ var blue := float(rgba_values[0])
+ if rgba_values.size() >= 3:
+ blue = float(rgba_values[2])
+
+ var alpha := float(rgba_values[0])
+ if rgba_values.size() == 4:
+ alpha = float(rgba_values[3])
+ var color := Color(red, green, blue, alpha)
+ return color
diff --git a/src/Classes/BaseLayer.gd b/src/Classes/BaseLayer.gd
index 889773972..6531fa4ce 100644
--- a/src/Classes/BaseLayer.gd
+++ b/src/Classes/BaseLayer.gd
@@ -29,14 +29,16 @@ enum BlendModes {
}
var name := "" ## Name of the layer.
-var project: Project ## Project, the layer belongs to.
-var index: int ## Index of layer in the timeline.
-var parent: BaseLayer ## Parent of the layer.
-var visible := true ## Sets visibility of the layer.
-var locked := false ## Images of a locked layer won't be overritten.
-var new_cels_linked := false ## Determines if new cel of the layer should be linked or not.
-var blend_mode := BlendModes.NORMAL ## Blend mode of the current layer.
+var project: Project ## The project the layer belongs to.
+var index: int ## Index of layer in the timeline.
+var parent: BaseLayer ## Parent of the layer.
+var visible := true ## Sets visibility of the layer.
+var locked := false ## Images of a locked layer won't be overritten.
+var new_cels_linked := false ## Determines if new cel of the layer should be linked or not.
+var blend_mode := BlendModes.NORMAL ## Blend mode of the current layer.
var cel_link_sets: Array[Dictionary] = [] ## Each Dictionary represents a cel's "link set"
+var effects: Array[LayerEffect] ## An array for non-destructive effects of the layer.
+var effects_enabled := true ## If [code]true[/code], the effects are being applied.
## Returns true if this is a direct or indirect parent of layer
@@ -48,7 +50,7 @@ func is_ancestor_of(layer: BaseLayer) -> bool:
return false
-## Returns an [Array] of layers that are children of this layer.
+## Returns an [Array] of [BaseLayer]s that are children of this layer.
## The process is recursive if [param recursive] is [code]true[/code].
func get_children(recursive: bool) -> Array[BaseLayer]:
var children: Array[BaseLayer] = []
@@ -110,6 +112,16 @@ func is_locked_in_hierarchy() -> bool:
return locked
+## Returns an [Array] of [BaseLayer]s that are ancestors of this layer.
+## If there are no ancestors, returns an empty array.
+func get_ancestors() -> Array[BaseLayer]:
+ var ancestors: Array[BaseLayer] = []
+ if is_instance_valid(parent):
+ ancestors.append(parent)
+ ancestors.append_array(parent.get_ancestors())
+ return ancestors
+
+
## Returns the number of parents above this layer.
func get_hierarchy_depth() -> int:
if is_instance_valid(parent):
@@ -117,7 +129,7 @@ func get_hierarchy_depth() -> int:
return 0
-## Returns the path of the layer in the timeline as a [String]
+## Returns the path of the layer in the timeline as a [String].
func get_layer_path() -> String:
if is_instance_valid(parent):
return str(parent.get_layer_path(), "/", name)
@@ -162,6 +174,32 @@ func link_cel(cel: BaseCel, link_set = null) -> void:
cel_link_sets.append(link_set)
+## Returns a copy of the [param cel]'s [Image] with all of the effects applied to it.
+## This method is not destructive as it does NOT change the data of the image,
+## it just returns a copy.
+func display_effects(cel: BaseCel) -> Image:
+ var image := Image.new()
+ image.copy_from(cel.get_image())
+ if not effects_enabled:
+ return image
+ var image_size := image.get_size()
+ for effect in effects:
+ if not effect.enabled:
+ continue
+ var shader_image_effect := ShaderImageEffect.new()
+ shader_image_effect.generate_image(image, effect.shader, effect.params, image_size)
+ # Inherit effects from the parents
+ for ancestor in get_ancestors():
+ if not ancestor.effects_enabled:
+ continue
+ for effect in ancestor.effects:
+ if not effect.enabled:
+ continue
+ var shader_image_effect := ShaderImageEffect.new()
+ shader_image_effect.generate_image(image, effect.shader, effect.params, image_size)
+ return image
+
+
# Methods to Override:
diff --git a/src/Classes/ImageEffect.gd b/src/Classes/ImageEffect.gd
index 3bc42269b..4913fb918 100644
--- a/src/Classes/ImageEffect.gd
+++ b/src/Classes/ImageEffect.gd
@@ -43,6 +43,8 @@ func _about_to_popup() -> void:
# prepares "animate_panel.frames" according to affect
func prepare_animator(project: Project) -> void:
+ if not is_instance_valid(animate_panel):
+ return
var frames: PackedInt32Array = []
if affect == SELECTED_CELS:
for frame_layer in project.selected_cels:
diff --git a/src/Classes/LayerEffect.gd b/src/Classes/LayerEffect.gd
new file mode 100644
index 000000000..9a2ea211f
--- /dev/null
+++ b/src/Classes/LayerEffect.gd
@@ -0,0 +1,17 @@
+class_name LayerEffect
+extends RefCounted
+
+var name := ""
+var shader: Shader
+var params := {}
+var enabled := true
+
+
+func _init(_name: String, _shader: Shader, _params := {}) -> void:
+ name = _name
+ shader = _shader
+ params = _params
+
+
+func duplicate() -> LayerEffect:
+ return LayerEffect.new(name, shader, params.duplicate())
diff --git a/src/Shaders/Effects/Desaturate.gdshader b/src/Shaders/Effects/Desaturate.gdshader
index c27ecbeed..61d166537 100644
--- a/src/Shaders/Effects/Desaturate.gdshader
+++ b/src/Shaders/Effects/Desaturate.gdshader
@@ -1,10 +1,10 @@
shader_type canvas_item;
render_mode unshaded;
-uniform bool red;
-uniform bool blue;
-uniform bool green;
-uniform bool alpha;
+uniform bool red = true;
+uniform bool blue = true;
+uniform bool green = true;
+uniform bool alpha = false;
uniform sampler2D selection;
float stolChannel(float x) {
diff --git a/src/Shaders/Effects/DropShadow.gdshader b/src/Shaders/Effects/DropShadow.gdshader
index 66c0a426b..148077833 100644
--- a/src/Shaders/Effects/DropShadow.gdshader
+++ b/src/Shaders/Effects/DropShadow.gdshader
@@ -1,21 +1,21 @@
shader_type canvas_item;
render_mode unshaded;
-uniform vec2 shadow_offset; // Offset, in pixel coordinate [0, 1, 2, and so on]
-uniform vec4 shadow_color;
+uniform vec2 offset = vec2(5.0, 5.0); // Offset, in pixel coordinate [0, 1, 2, and so on]
+uniform vec4 shadow_color : source_color = vec4(0.08, 0.08, 0.08, 0.63);
uniform sampler2D selection;
void fragment() {
- vec2 offset = shadow_offset * TEXTURE_PIXEL_SIZE; // Normalize shadow_offset to [0..1]
+ vec2 normalized_offset = offset * TEXTURE_PIXEL_SIZE; // Normalize offset to [0..1]
vec4 original = texture(TEXTURE, UV); // Original texture
- float shadow = texture(TEXTURE, UV - offset).a; // Shadow, alpha only
+ float shadow = texture(TEXTURE, UV - normalized_offset).a; // Shadow, alpha only
shadow *= shadow_color.a; // Multiply this mask by shadow alpha
shadow = mix(0.0, shadow, texture(selection, UV).a); // Clip shadow by selection mask
shadow = mix(shadow, 0.0, original.a); // Erase shadow alpha on original area
// Make a border to prevent stretching pixels on the edge
- vec2 border_uv = UV - offset;
+ vec2 border_uv = UV - normalized_offset;
border_uv -= 0.5;
border_uv *= 2.0;
border_uv = abs(border_uv);
diff --git a/src/Shaders/Effects/GradientMap.gdshader b/src/Shaders/Effects/GradientMap.gdshader
index 74902b4d0..0e9c56c2d 100644
--- a/src/Shaders/Effects/GradientMap.gdshader
+++ b/src/Shaders/Effects/GradientMap.gdshader
@@ -1,7 +1,7 @@
shader_type canvas_item;
render_mode unshaded;
-uniform sampler2D map; // GradientTexture
+uniform sampler2D gradient_map : filter_nearest; // GradientTexture
uniform sampler2D selection;
void fragment() {
@@ -9,7 +9,7 @@ void fragment() {
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));
+ vec4 gradient_color = texture(gradient_map, vec2(value, 0.0));
output.rgb = gradient_color.rgb;
output.a *= gradient_color.a;
diff --git a/src/Shaders/Effects/HSV.gdshader b/src/Shaders/Effects/HSV.gdshader
index ccb1a8928..cb0595551 100644
--- a/src/Shaders/Effects/HSV.gdshader
+++ b/src/Shaders/Effects/HSV.gdshader
@@ -1,9 +1,9 @@
shader_type canvas_item;
render_mode unshaded;
-uniform float hue_shift : hint_range(-1, 1);
-uniform float sat_shift : hint_range(-1, 1);
-uniform float val_shift : hint_range(-1, 1);
+uniform float hue : hint_range(-1, 1);
+uniform float saturation : hint_range(-1, 1);
+uniform float value : hint_range(-1, 1);
uniform sampler2D selection;
vec3 rgb2hsb(vec3 c){
@@ -40,20 +40,20 @@ void fragment() {
// If not greyscale
if(col[0] != col[1] || col[1] != col[2]) {
// Shift the color by shift_amount, but rolling over the value goes over 1
- hsb.x = mod(hsb.x + hue_shift, 1.0);
+ hsb.x = mod(hsb.x + hue, 1.0);
}
- if(sat_shift > 0.0) {
- hsb.y = mix(hsb.y, 1 , sat_shift);
+ if(saturation > 0.0) {
+ hsb.y = mix(hsb.y, 1 , saturation);
}
- else if (sat_shift < 0.0) {
- hsb.y = mix(0, hsb.y , 1.0 - abs(sat_shift));
+ else if (saturation < 0.0) {
+ hsb.y = mix(0, hsb.y , 1.0 - abs(saturation));
}
- if(val_shift > 0.0) {
- hsb.z = mix(hsb.z, 1 , val_shift);
+ if(value > 0.0) {
+ hsb.z = mix(hsb.z, 1 , value);
}
- else if (val_shift < 0.0) {
- hsb.z = mix(0, hsb.z , 1.0 - abs(val_shift));
+ else if (value < 0.0) {
+ hsb.z = mix(0, hsb.z , 1.0 - abs(value));
}
col = hsb2rgb(hsb);
diff --git a/src/Shaders/Effects/Invert.gdshader b/src/Shaders/Effects/Invert.gdshader
index 917f650e2..3d87425bb 100644
--- a/src/Shaders/Effects/Invert.gdshader
+++ b/src/Shaders/Effects/Invert.gdshader
@@ -1,10 +1,10 @@
shader_type canvas_item;
render_mode unshaded;
-uniform bool red;
-uniform bool blue;
-uniform bool green;
-uniform bool alpha;
+uniform bool red = true;
+uniform bool blue = true;
+uniform bool green = true;
+uniform bool alpha = false;
uniform sampler2D selection;
diff --git a/src/Shaders/Effects/Posterize.gdshader b/src/Shaders/Effects/Posterize.gdshader
index 016fc02f3..886dfed78 100644
--- a/src/Shaders/Effects/Posterize.gdshader
+++ b/src/Shaders/Effects/Posterize.gdshader
@@ -3,7 +3,7 @@ shader_type canvas_item;
uniform sampler2D selection;
uniform float colors : hint_range(1.0, 255.0) = 2.0;
-uniform float dither : hint_range(0.0, 0.5) = 0.0;
+uniform float dither_intensity : hint_range(0.0, 0.5) = 0.0;
void fragment()
{
@@ -14,13 +14,13 @@ void fragment()
float b = floor(mod(UV.y / TEXTURE_PIXEL_SIZE.y, 2.0));
float c = mod(a + b, 2.0);
vec4 col;
- col.r = (round(color.r * colors + dither) / colors) * c;
- col.g = (round(color.g * colors + dither) / colors) * c;
- col.b = (round(color.b * colors + dither) / colors) * c;
+ col.r = (round(color.r * colors + dither_intensity) / colors) * c;
+ col.g = (round(color.g * colors + dither_intensity) / colors) * c;
+ col.b = (round(color.b * colors + dither_intensity) / colors) * c;
c = 1.0 - c;
- col.r += (round(color.r * colors - dither) / colors) * c;
- col.g += (round(color.g * colors - dither) / colors) * c;
- col.b += (round(color.b * colors - dither) / colors) * c;
+ col.r += (round(color.r * colors - dither_intensity) / colors) * c;
+ col.g += (round(color.g * colors - dither_intensity) / colors) * c;
+ col.b += (round(color.b * colors - dither_intensity) / colors) * c;
col.a = color.a;
vec4 output = mix(color.rgba, col, selection_color.a);
COLOR = output;
diff --git a/src/UI/Canvas/Canvas.gd b/src/UI/Canvas/Canvas.gd
index 598a7a904..4d08799a5 100644
--- a/src/UI/Canvas/Canvas.gd
+++ b/src/UI/Canvas/Canvas.gd
@@ -120,7 +120,8 @@ func update_selected_cels_textures(project := Global.current_project) -> void:
func draw_layers() -> void:
- var current_cels := Global.current_project.frames[Global.current_project.current_frame].cels
+ var current_frame := Global.current_project.frames[Global.current_project.current_frame]
+ var current_cels := current_frame.cels
var textures: Array[Image] = []
var opacities := PackedFloat32Array()
var blend_modes := PackedInt32Array()
@@ -131,7 +132,11 @@ func draw_layers() -> void:
continue
var layer := Global.current_project.layers[i]
if layer.is_visible_in_hierarchy():
- var cel_image := current_cels[i].get_image()
+ var cel_image: Image
+ if Global.display_layer_effects:
+ cel_image = layer.display_effects(current_cels[i])
+ else:
+ cel_image = current_cels[i].get_image()
textures.append(cel_image)
opacities.append(current_cels[i].opacity)
if [Global.current_project.current_frame, i] in Global.current_project.selected_cels:
diff --git a/src/UI/Canvas/CanvasPreview.gd b/src/UI/Canvas/CanvasPreview.gd
index fe93c74b4..05e886037 100644
--- a/src/UI/Canvas/CanvasPreview.gd
+++ b/src/UI/Canvas/CanvasPreview.gd
@@ -72,7 +72,8 @@ func _draw() -> void:
func _draw_layers() -> void:
- var current_cels := Global.current_project.frames[frame_index].cels
+ var current_frame := Global.current_project.frames[frame_index]
+ var current_cels := current_frame.cels
var textures: Array[Image] = []
var opacities := PackedFloat32Array()
var blend_modes := PackedInt32Array()
@@ -80,10 +81,16 @@ func _draw_layers() -> void:
for i in Global.current_project.layers.size():
if current_cels[i] is GroupCel:
continue
- if Global.current_project.layers[i].is_visible_in_hierarchy():
- textures.append(current_cels[i].get_image())
+ var layer := Global.current_project.layers[i]
+ if layer.is_visible_in_hierarchy():
+ var cel_image: Image
+ if Global.display_layer_effects:
+ cel_image = layer.display_effects(current_cels[i])
+ else:
+ cel_image = current_cels[i].get_image()
+ textures.append(cel_image)
opacities.append(current_cels[i].opacity)
- blend_modes.append(Global.current_project.layers[i].blend_mode)
+ blend_modes.append(layer.blend_mode)
var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures)
material.set_shader_parameter("layers", texture_array)
diff --git a/src/UI/Dialogs/ImageEffects/DropShadowDialog.gd b/src/UI/Dialogs/ImageEffects/DropShadowDialog.gd
index ec2f8378e..93e57bfac 100644
--- a/src/UI/Dialogs/ImageEffects/DropShadowDialog.gd
+++ b/src/UI/Dialogs/ImageEffects/DropShadowDialog.gd
@@ -32,9 +32,7 @@ func commit_action(cel: Image, project := Global.current_project) -> void:
selection_tex = ImageTexture.create_from_image(project.selection_map)
var params := {
- "shadow_offset": Vector2(offset_x, offset_y),
- "shadow_color": color,
- "selection": selection_tex,
+ "offset": Vector2(offset_x, offset_y), "shadow_color": color, "selection": selection_tex
}
if !has_been_confirmed:
for param in params:
diff --git a/src/UI/Dialogs/ImageEffects/GradientMapDialog.gd b/src/UI/Dialogs/ImageEffects/GradientMapDialog.gd
index 8b8ff7b08..21729610a 100644
--- a/src/UI/Dialogs/ImageEffects/GradientMapDialog.gd
+++ b/src/UI/Dialogs/ImageEffects/GradientMapDialog.gd
@@ -15,7 +15,7 @@ func commit_action(cel: Image, project := Global.current_project) -> void:
if selection_checkbox.button_pressed and project.has_selection:
selection_tex = ImageTexture.create_from_image(project.selection_map)
- var params := {"selection": selection_tex, "map": $VBoxContainer/GradientEdit.texture}
+ var params := {"selection": selection_tex, "gradient_map": $VBoxContainer/GradientEdit.texture}
if !has_been_confirmed:
for param in params:
diff --git a/src/UI/Dialogs/ImageEffects/HSVDialog.gd b/src/UI/Dialogs/ImageEffects/HSVDialog.gd
index ea64de834..f08e45a53 100644
--- a/src/UI/Dialogs/ImageEffects/HSVDialog.gd
+++ b/src/UI/Dialogs/ImageEffects/HSVDialog.gd
@@ -32,7 +32,7 @@ func commit_action(cel: Image, project := Global.current_project) -> void:
if selection_checkbox.button_pressed and project.has_selection:
selection_tex = ImageTexture.create_from_image(project.selection_map)
- var params := {"hue_shift": hue, "sat_shift": sat, "val_shift": val, "selection": selection_tex}
+ var params := {"hue": hue, "saturation": sat, "value": val, "selection": selection_tex}
if !has_been_confirmed:
for param in params:
preview.material.set_shader_parameter(param, params[param])
diff --git a/src/UI/Dialogs/ImageEffects/Posterize.gd b/src/UI/Dialogs/ImageEffects/Posterize.gd
index 2ebb56f37..f86742370 100644
--- a/src/UI/Dialogs/ImageEffects/Posterize.gd
+++ b/src/UI/Dialogs/ImageEffects/Posterize.gd
@@ -17,7 +17,7 @@ func commit_action(cel: Image, project := Global.current_project) -> void:
if selection_checkbox.button_pressed and project.has_selection:
selection_tex = ImageTexture.create_from_image(project.selection_map)
- var params := {"colors": levels, "dither": dither, "selection": selection_tex}
+ var params := {"colors": levels, "dither_intensity": dither, "selection": selection_tex}
if !has_been_confirmed:
for param in params:
diff --git a/src/UI/Dialogs/ImageEffects/Posterize.tscn b/src/UI/Dialogs/ImageEffects/Posterize.tscn
index a97486c1c..e70ddd2a3 100644
--- a/src/UI/Dialogs/ImageEffects/Posterize.tscn
+++ b/src/UI/Dialogs/ImageEffects/Posterize.tscn
@@ -6,10 +6,12 @@
[node name="Posterize" instance=ExtResource("1")]
title = "Posterize"
+position = Vector2i(0, 36)
+size = Vector2i(360, 348)
script = ExtResource("3")
[node name="VBoxContainer" parent="." index="3"]
-offset_bottom = 316.0
+offset_bottom = 299.0
[node name="ShowAnimate" parent="VBoxContainer" index="0"]
visible = false
diff --git a/src/UI/Dialogs/ImageEffects/ShaderEffect.gd b/src/UI/Dialogs/ImageEffects/ShaderEffect.gd
index af92bf2f1..020a5a2fb 100644
--- a/src/UI/Dialogs/ImageEffects/ShaderEffect.gd
+++ b/src/UI/Dialogs/ImageEffects/ShaderEffect.gd
@@ -1,7 +1,7 @@
extends ImageEffect
var shader: Shader
-var param_names: PackedStringArray = []
+var params := {}
@onready var shader_loaded_label: Label = $VBoxContainer/ShaderLoadedLabel
@onready var shader_params: BoxContainer = $VBoxContainer/ShaderParams
@@ -21,10 +21,6 @@ func commit_action(cel: Image, project := Global.current_project) -> void:
if !shader:
return
- var params := {}
- for param in param_names:
- var param_data = preview.material.get_shader_parameter(param)
- params[param] = param_data
var gen := ShaderImageEffect.new()
gen.generate_image(cel, shader, params, project.size)
@@ -51,210 +47,19 @@ func change_shader(shader_tmp: Shader, shader_name: String) -> void:
shader = shader_tmp
preview.material.shader = shader_tmp
shader_loaded_label.text = tr("Shader loaded:") + " " + shader_name
- param_names.clear()
+ params.clear()
for child in shader_params.get_children():
child.queue_free()
- var code := shader.code.split("\n")
- var uniforms: PackedStringArray = []
- for line in code:
- if line.begins_with("uniform"):
- uniforms.append(line)
-
- for uniform in uniforms:
- # Example uniform:
- # uniform float parameter_name : hint_range(0, 255) = 100.0;
- var uniform_split := uniform.split("=")
- var u_value := ""
- if uniform_split.size() > 1:
- u_value = uniform_split[1].replace(";", "").strip_edges()
- else:
- uniform_split[0] = uniform_split[0].replace(";", "").strip_edges()
-
- var u_left_side := uniform_split[0].split(":")
- var u_hint := ""
- if u_left_side.size() > 1:
- u_hint = u_left_side[1].strip_edges()
- u_hint = u_hint.replace(";", "")
-
- var u_init := u_left_side[0].split(" ")
- var u_type := u_init[1]
- var u_name := u_init[2]
- param_names.append(u_name)
-
- if u_type == "float" or u_type == "int":
- var label := Label.new()
- label.text = u_name
- label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- var slider := ValueSlider.new()
- var min_value := 0.0
- var max_value := 255.0
- var step := 1.0
- var range_values_array: PackedStringArray
- if "hint_range" in u_hint:
- var range_values: String = u_hint.replace("hint_range(", "")
- range_values = range_values.replace(")", "").strip_edges()
- range_values_array = range_values.split(",")
-
- if u_type == "float":
- if range_values_array.size() >= 1:
- min_value = float(range_values_array[0])
- else:
- min_value = 0.01
-
- if range_values_array.size() >= 2:
- max_value = float(range_values_array[1])
-
- if range_values_array.size() >= 3:
- step = float(range_values_array[2])
- else:
- step = 0.01
-
- if u_value != "":
- slider.value = float(u_value)
- else:
- if range_values_array.size() >= 1:
- min_value = int(range_values_array[0])
-
- if range_values_array.size() >= 2:
- max_value = int(range_values_array[1])
-
- if range_values_array.size() >= 3:
- step = int(range_values_array[2])
-
- if u_value != "":
- slider.value = int(u_value)
- slider.min_value = min_value
- slider.max_value = max_value
- slider.step = step
- slider.value_changed.connect(set_shader_parameter.bind(u_name))
- var hbox := HBoxContainer.new()
- hbox.add_child(label)
- hbox.add_child(slider)
- shader_params.add_child(hbox)
- elif u_type == "vec2":
- var label := Label.new()
- label.text = u_name
- label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- var vector2 := _vec2str_to_vector2(u_value)
- var slider1 := ValueSlider.new()
- slider1.value = vector2.x
- slider1.value_changed.connect(_set_vector2_shader_param.bind(u_name, true))
- var slider2 := ValueSlider.new()
- slider2.value = vector2.y
- slider2.value_changed.connect(_set_vector2_shader_param.bind(u_name, false))
- var hbox := HBoxContainer.new()
- hbox.add_child(label)
- hbox.add_child(slider1)
- hbox.add_child(slider2)
- shader_params.add_child(hbox)
- elif u_type == "vec4":
- if "source_color" in u_hint:
- var label := Label.new()
- label.text = u_name
- label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- var color := _vec4str_to_color(u_value)
- var color_button := ColorPickerButton.new()
- color_button.custom_minimum_size = Vector2(20, 20)
- color_button.color = color
- color_button.color_changed.connect(set_shader_parameter.bind(u_name))
- color_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- var hbox := HBoxContainer.new()
- hbox.add_child(label)
- hbox.add_child(color_button)
- shader_params.add_child(hbox)
- elif u_type == "sampler2D":
- var label := Label.new()
- label.text = u_name
- label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- var file_dialog := FileDialog.new()
- file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
- file_dialog.access = FileDialog.ACCESS_FILESYSTEM
- file_dialog.resizable = true
- file_dialog.custom_minimum_size = Vector2(200, 70)
- file_dialog.size = Vector2(384, 281)
- file_dialog.file_selected.connect(_load_texture.bind(u_name))
- var button := Button.new()
- button.text = "Load texture"
- button.pressed.connect(file_dialog.popup_centered)
- button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- var hbox := HBoxContainer.new()
- hbox.add_child(label)
- hbox.add_child(button)
- shader_params.add_child(hbox)
- shader_params.add_child(file_dialog)
- elif u_type == "bool":
- var label := Label.new()
- label.text = u_name
- label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- var checkbox := CheckBox.new()
- checkbox.text = "On"
- if u_value == "true":
- checkbox.button_pressed = true
- checkbox.toggled.connect(set_shader_parameter.bind(u_name))
- checkbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- var hbox := HBoxContainer.new()
- hbox.add_child(label)
- hbox.add_child(checkbox)
- shader_params.add_child(hbox)
+ Global.create_ui_for_shader_uniforms(
+ shader_tmp, params, shader_params, _set_shader_parameter, _load_texture
+ )
-# print("---")
-# print(uniform_split)
-# print(u_type)
-# print(u_name)
-# print(u_hint)
-# print(u_value)
-# print("--")
-
-
-func set_shader_parameter(value, param: String) -> void:
+func _set_shader_parameter(value, param: String) -> void:
var mat: ShaderMaterial = preview.material
mat.set_shader_parameter(param, value)
-
-
-func _set_vector2_shader_param(value: float, param: String, x: bool) -> void:
- var mat: ShaderMaterial = preview.material
- var vector2: Vector2 = mat.get_shader_parameter(param)
- if x:
- vector2.x = value
- else:
- vector2.y = value
- set_shader_parameter(vector2, param)
-
-
-func _vec2str_to_vector2(vec2: String) -> Vector2:
- vec2 = vec2.replace("vec2(", "")
- vec2 = vec2.replace(")", "")
- var vec_values: PackedStringArray = vec2.split(",")
- if vec_values.size() == 0:
- return Vector2.ZERO
- var y := float(vec_values[0])
- if vec_values.size() == 2:
- y = float(vec_values[1])
- var vector2 := Vector2(float(vec_values[0]), y)
- return vector2
-
-
-func _vec4str_to_color(vec4: String) -> Color:
- vec4 = vec4.replace("vec4(", "")
- vec4 = vec4.replace(")", "")
- var rgba_values: PackedStringArray = vec4.split(",")
- var red := float(rgba_values[0])
-
- var green := float(rgba_values[0])
- if rgba_values.size() >= 2:
- green = float(rgba_values[1])
-
- var blue := float(rgba_values[0])
- if rgba_values.size() >= 3:
- blue = float(rgba_values[2])
-
- var alpha := float(rgba_values[0])
- if rgba_values.size() == 4:
- alpha = float(rgba_values[3])
- var color: Color = Color(red, green, blue, alpha)
- return color
+ params[param] = value
func _load_texture(path: String, param: String) -> void:
@@ -264,4 +69,4 @@ func _load_texture(path: String, param: String) -> void:
print("Error loading texture")
return
var image_tex := ImageTexture.create_from_image(image)
- set_shader_parameter(image_tex, param)
+ _set_shader_parameter(image_tex, param)
diff --git a/src/UI/Dialogs/ImageEffects/ShaderEffect.tscn b/src/UI/Dialogs/ImageEffects/ShaderEffect.tscn
index b3263b2ec..ccecefc55 100644
--- a/src/UI/Dialogs/ImageEffects/ShaderEffect.tscn
+++ b/src/UI/Dialogs/ImageEffects/ShaderEffect.tscn
@@ -55,7 +55,7 @@ layout_mode = 2
[node name="FileDialog" type="FileDialog" parent="."]
access = 2
-filters = PackedStringArray("*shader; Godot Shader File")
+filters = PackedStringArray("*gdshader; Godot Shader File")
show_hidden_files = true
[connection signal="pressed" from="VBoxContainer/ChooseShader" to="." method="_on_ChooseShader_pressed"]
diff --git a/src/UI/Nodes/CollapsibleContainer.gd b/src/UI/Nodes/CollapsibleContainer.gd
index 862a25884..02a3edff3 100644
--- a/src/UI/Nodes/CollapsibleContainer.gd
+++ b/src/UI/Nodes/CollapsibleContainer.gd
@@ -24,7 +24,7 @@ func _init() -> void:
func _ready() -> void:
_button.toggle_mode = true
_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
- _button.toggled.connect(_on_Button_toggled)
+ _button.toggled.connect(set_visible_children)
add_child(_button, false, Node.INTERNAL_MODE_FRONT)
_texture_rect.anchor_top = 0.5
_texture_rect.anchor_bottom = 0.5
@@ -53,11 +53,8 @@ func _notification(what: int) -> void:
_texture_rect.texture = get_theme_icon("arrow_normal", "CollapsibleContainer")
-func _on_Button_toggled(button_pressed: bool) -> void:
- _set_visible(button_pressed)
-
-
-func _set_visible(pressed: bool) -> void:
+## Toggles whether the children of the container are visible or not
+func set_visible_children(pressed: bool) -> void:
var angle := 0.0 if pressed else -90.0
create_tween().tween_property(_texture_rect, "rotation_degrees", angle, 0.05)
for child in get_children():
diff --git a/src/UI/Nodes/GradientEdit.gd b/src/UI/Nodes/GradientEdit.gd
index 73d7405ba..4b57d80cb 100644
--- a/src/UI/Nodes/GradientEdit.gd
+++ b/src/UI/Nodes/GradientEdit.gd
@@ -100,6 +100,8 @@ class GradientCursor:
func _ready() -> void:
_create_cursors()
+ %InterpolationOptionButton.select(gradient.interpolation_mode)
+ %ColorSpaceOptionButton.select(gradient.interpolation_color_space)
func _create_cursors() -> void:
@@ -163,12 +165,18 @@ func get_gradient_color(x: float) -> Color:
return gradient.sample(x / x_offset)
+func set_gradient_texture(new_texture: GradientTexture2D) -> void:
+ $TextureRect.texture = new_texture
+ texture = new_texture
+ gradient = texture.gradient
+
+
func _on_ColorPicker_color_changed(color: Color) -> void:
active_cursor.set_color(color)
func _on_GradientEdit_resized() -> void:
- if not gradient:
+ if not is_instance_valid(texture_rect):
return
x_offset = size.x - GradientCursor.WIDTH
_create_cursors()
@@ -176,10 +184,12 @@ func _on_GradientEdit_resized() -> void:
func _on_InterpolationOptionButton_item_selected(index: Gradient.InterpolationMode) -> void:
gradient.interpolation_mode = index
+ updated.emit(gradient, continuous_change)
func _on_color_space_option_button_item_selected(index: Gradient.ColorSpace) -> void:
gradient.interpolation_color_space = index
+ updated.emit(gradient, continuous_change)
func _on_DivideButton_pressed() -> void:
diff --git a/src/UI/Nodes/GradientEdit.tscn b/src/UI/Nodes/GradientEdit.tscn
index f0dc71822..f0a044347 100644
--- a/src/UI/Nodes/GradientEdit.tscn
+++ b/src/UI/Nodes/GradientEdit.tscn
@@ -2,10 +2,12 @@
[ext_resource type="Script" path="res://src/UI/Nodes/GradientEdit.gd" id="1"]
-[sub_resource type="Gradient" id="1"]
+[sub_resource type="Gradient" id="Gradient_1k8kj"]
+resource_local_to_scene = true
-[sub_resource type="GradientTexture2D" id="2"]
-gradient = SubResource("1")
+[sub_resource type="GradientTexture2D" id="GradientTexture2D_fau1l"]
+resource_local_to_scene = true
+gradient = SubResource("Gradient_1k8kj")
[node name="GradientEdit" type="VBoxContainer"]
anchors_preset = 15
@@ -17,7 +19,7 @@ script = ExtResource("1")
custom_minimum_size = Vector2(0, 30)
layout_mode = 2
size_flags_vertical = 3
-texture = SubResource("2")
+texture = SubResource("GradientTexture2D_fau1l")
expand_mode = 1
[node name="Value" type="Label" parent="TextureRect"]
@@ -34,10 +36,10 @@ offset_bottom = 7.0
[node name="Popup" type="PopupPanel" parent="."]
[node name="ColorPicker" type="ColorPicker" parent="Popup"]
-offset_left = 12.0
-offset_top = 8.0
-offset_right = 298.0
-offset_bottom = 576.0
+offset_left = 4.0
+offset_top = 4.0
+offset_right = 294.0
+offset_bottom = 572.0
[node name="InterpolationContainer" type="HBoxContainer" parent="."]
layout_mode = 2
@@ -48,6 +50,7 @@ size_flags_horizontal = 3
text = "Interpolation:"
[node name="InterpolationOptionButton" type="OptionButton" parent="InterpolationContainer"]
+unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
@@ -69,6 +72,7 @@ size_flags_horizontal = 3
text = "Color space:"
[node name="ColorSpaceOptionButton" type="OptionButton" parent="ColorSpaceContainer"]
+unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd
index 7aa31b919..06f53dd55 100644
--- a/src/UI/Timeline/AnimationTimeline.gd
+++ b/src/UI/Timeline/AnimationTimeline.gd
@@ -907,9 +907,10 @@ func _on_MergeDownLayer_pressed() -> void:
project.undo_redo.create_action("Merge Layer")
for frame in project.frames:
- top_cels.append(frame.cels[top_layer.index]) # Store for undo purposes
+ var top_cel := frame.cels[top_layer.index]
+ top_cels.append(top_cel) # Store for undo purposes
- var top_image := frame.cels[top_layer.index].get_image()
+ var top_image := top_layer.display_effects(top_cel)
var bottom_cel := frame.cels[bottom_layer.index]
var textures: Array[Image] = []
var opacities := PackedFloat32Array()
@@ -1094,3 +1095,8 @@ func project_cel_removed(frame: int, layer: int) -> void:
var cel_hbox := Global.cel_vbox.get_child(Global.cel_vbox.get_child_count() - 1 - layer)
cel_hbox.get_child(frame).queue_free()
cel_hbox.remove_child(cel_hbox.get_child(frame))
+
+
+func _on_layer_fx_pressed() -> void:
+ $LayerEffectsSettings.popup_centered()
+ Global.dialog_open(true)
diff --git a/src/UI/Timeline/AnimationTimeline.tscn b/src/UI/Timeline/AnimationTimeline.tscn
index a422dbaf1..305985a0b 100644
--- a/src/UI/Timeline/AnimationTimeline.tscn
+++ b/src/UI/Timeline/AnimationTimeline.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=75 format=3 uid="uid://dbr6mulku2qju"]
+[gd_scene load_steps=76 format=3 uid="uid://dbr6mulku2qju"]
[ext_resource type="Script" path="res://src/UI/Timeline/AnimationTimeline.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://d36mlbmq06q4e" path="res://assets/graphics/layers/new.png" id="2"]
@@ -20,6 +20,7 @@
[ext_resource type="Texture2D" uid="uid://esistdjfbrc4" path="res://assets/graphics/timeline/play_backwards.png" id="24"]
[ext_resource type="Texture2D" uid="uid://l4jj86y1hukm" path="res://assets/graphics/timeline/go_to_last_frame.png" id="25"]
[ext_resource type="Texture2D" uid="uid://b2ndrc0cvy1m5" path="res://assets/graphics/timeline/next_frame.png" id="26"]
+[ext_resource type="PackedScene" uid="uid://dd1fkkc3vjh78" path="res://src/UI/Timeline/LayerEffects/LayerEffectsSettings.tscn" id="26_vbrbd"]
[ext_resource type="Texture2D" uid="uid://cerkv5yx4cqeh" path="res://assets/graphics/timeline/copy_frame.png" id="27"]
[ext_resource type="Texture2D" uid="uid://i13jhsg117kd" path="res://assets/graphics/timeline/tag.png" id="28"]
[ext_resource type="Texture2D" uid="uid://dukip7mvotxsp" path="res://assets/graphics/timeline/onion_skinning_off.png" id="29"]
@@ -380,6 +381,11 @@ texture = ExtResource("5")
[node name="HBoxContainer" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer"]
layout_mode = 2
+[node name="LayerFX" type="Button" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+mouse_default_cursor_shape = 2
+text = "FX"
+
[node name="Label" type="Label" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Blend mode:"
@@ -922,6 +928,8 @@ autowrap_mode = 3
[node name="FrameTagDialog" parent="." instance=ExtResource("42")]
+[node name="LayerEffectsSettings" parent="." instance=ExtResource("26_vbrbd")]
+
[node name="DragHighlight" type="ColorRect" parent="."]
visible = false
layout_mode = 0
@@ -939,6 +947,7 @@ script = ExtResource("12")
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/MoveDownLayer" to="." method="change_layer_order" binds= [false]]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/CloneLayer" to="." method="_on_CloneLayer_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/MergeDownLayer" to="." method="_on_MergeDownLayer_pressed"]
+[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/HBoxContainer/LayerFX" to="." method="_on_layer_fx_pressed"]
[connection signal="item_selected" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/HBoxContainer/BlendModes" to="." method="_on_blend_modes_item_selected"]
[connection signal="value_changed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/BlendingHBox/OpacitySlider" to="." method="_on_OpacitySlider_value_changed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons/AddFrame" to="." method="add_frame"]
diff --git a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd
new file mode 100644
index 000000000..c03c4769a
--- /dev/null
+++ b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd
@@ -0,0 +1,211 @@
+extends AcceptDialog
+
+const DELETE_TEXTURE := preload("res://assets/graphics/misc/close.svg")
+const MOVE_UP_TEXTURE := preload("res://assets/graphics/misc/move_up_arrow.svg")
+const MOVE_DOWN_TEXTURE := preload("res://assets/graphics/misc/move_down_arrow.svg")
+
+var effects: Array[LayerEffect] = [
+ LayerEffect.new("Offset", preload("res://src/Shaders/Effects/OffsetPixels.gdshader")),
+ LayerEffect.new("Outline", preload("res://src/Shaders/Effects/OutlineInline.gdshader")),
+ LayerEffect.new("Drop Shadow", preload("res://src/Shaders/Effects/DropShadow.gdshader")),
+ LayerEffect.new("Invert Colors", preload("res://src/Shaders/Effects/Invert.gdshader")),
+ LayerEffect.new("Desaturation", preload("res://src/Shaders/Effects/Desaturate.gdshader")),
+ LayerEffect.new(
+ "Adjust Hue/Saturation/Value", preload("res://src/Shaders/Effects/HSV.gdshader")
+ ),
+ LayerEffect.new("Posterize", preload("res://src/Shaders/Effects/Posterize.gdshader")),
+ LayerEffect.new("Gradient Map", preload("res://src/Shaders/Effects/GradientMap.gdshader")),
+]
+
+@onready var enabled_button: CheckButton = $VBoxContainer/HBoxContainer/EnabledButton
+@onready var effect_list: MenuButton = $VBoxContainer/HBoxContainer/EffectList
+@onready var effect_container: VBoxContainer = $VBoxContainer/ScrollContainer/EffectContainer
+
+
+func _ready() -> void:
+ for effect in effects:
+ effect_list.get_popup().add_item(effect.name)
+ effect_list.get_popup().id_pressed.connect(_on_effect_list_id_pressed)
+
+
+func _on_about_to_popup() -> void:
+ var layer := Global.current_project.layers[Global.current_project.current_layer]
+ enabled_button.button_pressed = layer.effects_enabled
+ for effect in layer.effects:
+ _create_effect_ui(layer, effect)
+
+
+func _on_visibility_changed() -> void:
+ if not visible:
+ Global.dialog_open(false)
+ for child in effect_container.get_children():
+ child.queue_free()
+
+
+func _on_effect_list_id_pressed(index: int) -> void:
+ var layer := Global.current_project.layers[Global.current_project.current_layer]
+ var effect := effects[index].duplicate()
+ Global.current_project.undos += 1
+ Global.current_project.undo_redo.create_action("Add layer effect")
+ Global.current_project.undo_redo.add_do_method(func(): layer.effects.append(effect))
+ Global.current_project.undo_redo.add_do_method(Global.canvas.queue_redraw)
+ Global.current_project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
+ Global.current_project.undo_redo.add_undo_method(func(): layer.effects.erase(effect))
+ Global.current_project.undo_redo.add_undo_method(Global.canvas.queue_redraw)
+ Global.current_project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
+ Global.current_project.undo_redo.commit_action()
+ _create_effect_ui(layer, effect)
+
+
+func _create_effect_ui(layer: BaseLayer, effect: LayerEffect) -> void:
+ var panel_container := PanelContainer.new()
+ var vbox := VBoxContainer.new()
+ var hbox := HBoxContainer.new()
+ var enable_checkbox := CheckButton.new()
+ enable_checkbox.button_pressed = effect.enabled
+ enable_checkbox.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ enable_checkbox.toggled.connect(_enable_effect.bind(effect))
+ var label := Label.new()
+ label.text = effect.name
+ label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ var move_up_button := TextureButton.new()
+ move_up_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ move_up_button.texture_normal = MOVE_UP_TEXTURE
+ move_up_button.add_to_group(&"UIButtons")
+ move_up_button.modulate = Global.modulate_icon_color
+ move_up_button.pressed.connect(_re_order_effect.bind(effect, layer, panel_container, -1))
+ var move_down_button := TextureButton.new()
+ move_down_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ move_down_button.texture_normal = MOVE_DOWN_TEXTURE
+ move_down_button.add_to_group(&"UIButtons")
+ move_down_button.modulate = Global.modulate_icon_color
+ move_down_button.pressed.connect(_re_order_effect.bind(effect, layer, panel_container, 1))
+ var delete_button := TextureButton.new()
+ delete_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ delete_button.texture_normal = DELETE_TEXTURE
+ delete_button.add_to_group(&"UIButtons")
+ delete_button.modulate = Global.modulate_icon_color
+ delete_button.pressed.connect(_delete_effect.bind(effect))
+ hbox.add_child(enable_checkbox)
+ hbox.add_child(label)
+ if layer is PixelLayer:
+ var apply_button := Button.new()
+ apply_button.text = "Apply"
+ apply_button.pressed.connect(_apply_effect.bind(layer, effect))
+ hbox.add_child(apply_button)
+ hbox.add_child(move_up_button)
+ hbox.add_child(move_down_button)
+ hbox.add_child(delete_button)
+ var parameter_vbox := CollapsibleContainer.new()
+ parameter_vbox.text = "Options"
+ Global.create_ui_for_shader_uniforms(
+ effect.shader,
+ effect.params,
+ parameter_vbox,
+ _set_parameter.bind(effect),
+ _load_parameter_texture.bind(effect)
+ )
+ vbox.add_child(hbox)
+ vbox.add_child(parameter_vbox)
+ panel_container.add_child(vbox)
+ effect_container.add_child(panel_container)
+ parameter_vbox.set_visible_children(false)
+
+
+func _enable_effect(button_pressed: bool, effect: LayerEffect) -> void:
+ effect.enabled = button_pressed
+ Global.canvas.queue_redraw()
+
+
+func _re_order_effect(
+ effect: LayerEffect, layer: BaseLayer, container: Container, direction: int
+) -> void:
+ assert(layer.effects.size() == effect_container.get_child_count())
+ var effect_index := container.get_index()
+ var new_index := effect_index + direction
+ if new_index < 0:
+ return
+ if new_index >= effect_container.get_child_count():
+ return
+ Global.current_project.undos += 1
+ Global.current_project.undo_redo.create_action("Re-arrange layer effect")
+ Global.current_project.undo_redo.add_do_method(
+ swap_array.bind(layer.effects, effect_index, new_index, effect)
+ )
+ Global.current_project.undo_redo.add_do_method(Global.canvas.queue_redraw)
+ Global.current_project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
+ Global.current_project.undo_redo.add_undo_method(
+ swap_array.bind(layer.effects, new_index, effect_index, effect)
+ )
+ Global.current_project.undo_redo.add_undo_method(Global.canvas.queue_redraw)
+ Global.current_project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
+ Global.current_project.undo_redo.commit_action()
+ effect_container.move_child(container, new_index)
+
+
+func swap_array(array: Array, old_index: int, new_index: int, new_item: Variant) -> void:
+ var temp = array[new_index]
+ array[new_index] = new_item
+ array[old_index] = temp
+
+
+func _delete_effect(effect: LayerEffect) -> void:
+ var layer := Global.current_project.layers[Global.current_project.current_layer]
+ var index := layer.effects.find(effect)
+ Global.current_project.undos += 1
+ Global.current_project.undo_redo.create_action("Delete layer effect")
+ Global.current_project.undo_redo.add_do_method(func(): layer.effects.erase(effect))
+ Global.current_project.undo_redo.add_do_method(Global.canvas.queue_redraw)
+ Global.current_project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
+ Global.current_project.undo_redo.add_undo_method(func(): layer.effects.insert(index, effect))
+ Global.current_project.undo_redo.add_undo_method(Global.canvas.queue_redraw)
+ Global.current_project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
+ Global.current_project.undo_redo.commit_action()
+ effect_container.get_child(index).queue_free()
+
+
+func _apply_effect(layer: BaseLayer, effect: LayerEffect) -> void:
+ var index := layer.effects.find(effect)
+ var redo_data := {}
+ var undo_data := {}
+ for frame in Global.current_project.frames:
+ var cel := frame.cels[layer.index]
+ var new_image := Image.new()
+ new_image.copy_from(cel.get_image())
+ var image_size := new_image.get_size()
+ var shader_image_effect := ShaderImageEffect.new()
+ shader_image_effect.generate_image(new_image, effect.shader, effect.params, image_size)
+ redo_data[cel.image] = new_image.data
+ undo_data[cel.image] = cel.image.data
+ Global.current_project.undos += 1
+ Global.current_project.undo_redo.create_action("Apply layer effect")
+ Global.undo_redo_compress_images(redo_data, undo_data)
+ Global.current_project.undo_redo.add_do_method(func(): layer.effects.erase(effect))
+ Global.current_project.undo_redo.add_do_method(Global.canvas.queue_redraw)
+ Global.current_project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
+ Global.current_project.undo_redo.add_undo_method(func(): layer.effects.insert(index, effect))
+ Global.current_project.undo_redo.add_undo_method(Global.canvas.queue_redraw)
+ Global.current_project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
+ Global.current_project.undo_redo.commit_action()
+ effect_container.get_child(index).queue_free()
+
+
+func _set_parameter(value, param: String, effect: LayerEffect) -> void:
+ effect.params[param] = value
+ Global.canvas.queue_redraw()
+
+
+func _load_parameter_texture(path: String, effect: LayerEffect, param: String) -> void:
+ var image := Image.new()
+ image.load(path)
+ if !image:
+ print("Error loading texture")
+ return
+ var image_tex := ImageTexture.create_from_image(image)
+ _set_parameter(image_tex, param, effect)
+
+
+func _on_enabled_button_toggled(button_pressed: bool) -> void:
+ var layer := Global.current_project.layers[Global.current_project.current_layer]
+ layer.effects_enabled = button_pressed
+ Global.canvas.queue_redraw()
diff --git a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.tscn b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.tscn
new file mode 100644
index 000000000..9dcf291d8
--- /dev/null
+++ b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.tscn
@@ -0,0 +1,43 @@
+[gd_scene load_steps=2 format=3 uid="uid://dd1fkkc3vjh78"]
+
+[ext_resource type="Script" path="res://src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd" id="1_h6h7b"]
+
+[node name="LayerEffectsSettings" type="AcceptDialog"]
+title = "Layer effects"
+size = Vector2i(400, 400)
+exclusive = false
+script = ExtResource("1_h6h7b")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+offset_left = 8.0
+offset_top = 8.0
+offset_right = 392.0
+offset_bottom = 351.0
+size_flags_horizontal = 3
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="EnabledButton" type="CheckButton" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+mouse_default_cursor_shape = 2
+text = "Enabled"
+
+[node name="EffectList" type="MenuButton" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 10
+mouse_default_cursor_shape = 2
+text = "Add effect"
+
+[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="EffectContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[connection signal="about_to_popup" from="." to="." method="_on_about_to_popup"]
+[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
+[connection signal="toggled" from="VBoxContainer/HBoxContainer/EnabledButton" to="." method="_on_enabled_button_toggled"]
diff --git a/src/UI/TopMenuContainer/TopMenuContainer.gd b/src/UI/TopMenuContainer/TopMenuContainer.gd
index 77d8cb47c..badb7001a 100644
--- a/src/UI/TopMenuContainer/TopMenuContainer.gd
+++ b/src/UI/TopMenuContainer/TopMenuContainer.gd
@@ -129,6 +129,7 @@ func _setup_view_menu() -> void:
"Show Rulers": "show_rulers",
"Show Guides": "show_guides",
"Show Mouse Guides": "",
+ "Display Layer Effects": &"display_layer_effects",
"Snap To": "",
}
view_menu = view_menu_button.get_popup()
@@ -145,6 +146,7 @@ func _setup_view_menu() -> void:
_set_menu_shortcut(view_menu_items[item], view_menu, i)
view_menu.set_item_checked(Global.ViewMenu.SHOW_RULERS, true)
view_menu.set_item_checked(Global.ViewMenu.SHOW_GUIDES, true)
+ view_menu.set_item_checked(Global.ViewMenu.DISPLAY_LAYER_EFFECTS, true)
view_menu.hide_on_checkable_item_selection = false
view_menu.id_pressed.connect(view_menu_id_pressed)
@@ -346,7 +348,7 @@ func _setup_help_menu() -> void:
help_menu.id_pressed.connect(help_menu_id_pressed)
-func _set_menu_shortcut(action: String, menu: PopupMenu, index: int) -> void:
+func _set_menu_shortcut(action: StringName, menu: PopupMenu, index: int) -> void:
if action.is_empty():
return
var shortcut := Shortcut.new()
@@ -497,6 +499,8 @@ func view_menu_id_pressed(id: int) -> void:
_toggle_show_guides()
Global.ViewMenu.SHOW_MOUSE_GUIDES:
_toggle_show_mouse_guides()
+ Global.ViewMenu.DISPLAY_LAYER_EFFECTS:
+ Global.display_layer_effects = not Global.display_layer_effects
_:
_handle_metadata(id, view_menu_button)