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)