diff --git a/Translations/Translations.pot b/Translations/Translations.pot index cf774aa5d..522571def 100644 --- a/Translations/Translations.pot +++ b/Translations/Translations.pot @@ -2143,6 +2143,10 @@ msgstr "" msgid "Blend mode:" msgstr "" +#. Found in the layer's section of the timeline, as a blend mode option, only available for group layers. If enabled, group blending is disabled and the group simply acts as a way to organize layers instead of affecting blending. +msgid "Pass through" +msgstr "" + #. Adjective, refers to something usual/regular, such as the normal blend mode. msgid "Normal" msgstr "" diff --git a/src/Autoload/DrawingAlgos.gd b/src/Autoload/DrawingAlgos.gd index befb565e7..4dda47d4b 100644 --- a/src/Autoload/DrawingAlgos.gd +++ b/src/Autoload/DrawingAlgos.gd @@ -53,8 +53,18 @@ func blend_layers( if DisplayServer.get_name() == "headless": blend_layers_headless(image, project, layer, cel, origin) else: - var cel_image := layer.display_effects(cel) - textures.append(cel_image) + if layer is GroupLayer and layer.blend_mode != BaseLayer.BlendModes.PASS_THROUGH: + var cel_image := (layer as GroupLayer).blend_children(frame) + textures.append(cel_image) + else: + var cel_image := layer.display_effects(cel) + textures.append(cel_image) + if ( + layer.is_blended_by_ancestor() + and not only_selected_cels + and not only_selected_layers + ): + include = false set_layer_metadata_image(layer, cel, metadata_image, ordered_index, include) if DisplayServer.get_name() != "headless": var texture_array := Texture2DArray.new() @@ -84,9 +94,14 @@ func set_layer_metadata_image( image.set_pixel(index, 1, Color()) # Store the clipping mask boolean if layer.clipping_mask: - image.set_pixel(index, 3, Color.WHITE) + image.set_pixel(index, 3, Color.RED) else: image.set_pixel(index, 3, Color.BLACK) + if not include: + # Store a small red value as a way to indicate that this layer should be skipped + # Used for layers such as child layers of a group, so that the group layer itself can + # sucessfuly be used as a clipping mask with the layer below it. + image.set_pixel(index, 3, Color(0.2, 0.0, 0.0, 0.0)) func blend_layers_headless( diff --git a/src/Classes/Layers/BaseLayer.gd b/src/Classes/Layers/BaseLayer.gd index aa5898d07..d45ba7333 100644 --- a/src/Classes/Layers/BaseLayer.gd +++ b/src/Classes/Layers/BaseLayer.gd @@ -1,3 +1,4 @@ +# gdlint: ignore=max-public-methods class_name BaseLayer extends RefCounted ## Base class for layer properties. Different layer types extend from this class. @@ -9,7 +10,8 @@ signal visibility_changed ## Emits when [member visible] is changed. ## is the blend layer, and the bottom layer is the base layer. ## For more information, refer to: [url]https://en.wikipedia.org/wiki/Blend_modes[/url] enum BlendModes { - NORMAL, ## The blend layer colors are simply placed on top of the base colors. + PASS_THROUGH = -2, ## Only for group layers. Ignores group blending, like it doesn't exist. + NORMAL = 0, ## The blend layer colors are simply placed on top of the base colors. DARKEN, ## Keeps the darker colors between the blend and the base layers. MULTIPLY, ## Multiplies the numerical values of the two colors, giving a darker result. COLOR_BURN, ## Darkens by increasing the contrast between the blend and base colors. @@ -124,6 +126,17 @@ func is_locked_in_hierarchy() -> bool: return locked +## Returns [code]true[/code] if the layer has at least one ancestor +## that does not have its blend mode set to pass through. +func is_blended_by_ancestor() -> bool: + var is_blended := false + for ancestor in get_ancestors(): + if ancestor.blend_mode != BlendModes.PASS_THROUGH: + is_blended = true + break + return is_blended + + ## 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]: @@ -141,6 +154,20 @@ func get_hierarchy_depth() -> int: return 0 +## Returns the layer's top most parent that is responsible for its blending. +## For example, if a layer belongs in a group with its blend mode set to anything but pass through, +## and that group has no parents of its own, then that group gets returned. +## If that group is a child of another non-pass through group, +## then the grandparent group is returned, and so on. +## If the layer has no ancestors, or if they are set to pass through mode, it returns self. +func get_blender_ancestor() -> BaseLayer: + var blender := self + for ancestor in get_ancestors(): + if ancestor.blend_mode != BlendModes.PASS_THROUGH: + blender = ancestor + return blender + + ## Returns the path of the layer in the timeline as a [String]. func get_layer_path() -> String: if is_instance_valid(parent): @@ -189,9 +216,12 @@ func link_cel(cel: BaseCel, link_set = null) -> void: ## 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: +func display_effects(cel: BaseCel, image_override: Image = null) -> Image: var image := Image.new() - image.copy_from(cel.get_image()) + if is_instance_valid(image_override): + image.copy_from(image_override) + else: + image.copy_from(cel.get_image()) if not effects_enabled: return image var image_size := image.get_size() @@ -200,8 +230,10 @@ func display_effects(cel: BaseCel) -> Image: continue var shader_image_effect := ShaderImageEffect.new() shader_image_effect.generate_image(image, effect.shader, effect.params, image_size) - # Inherit effects from the parents + # Inherit effects from the parents, if their blend mode is set to pass through for ancestor in get_ancestors(): + if ancestor.blend_mode != BlendModes.PASS_THROUGH: + break if not ancestor.effects_enabled: continue for effect in ancestor.effects: diff --git a/src/Classes/Layers/GroupLayer.gd b/src/Classes/Layers/GroupLayer.gd index f073dd3f5..d31c49ead 100644 --- a/src/Classes/Layers/GroupLayer.gd +++ b/src/Classes/Layers/GroupLayer.gd @@ -11,44 +11,130 @@ func _init(_project: Project, _name := "") -> void: ## Blends all of the images of children layer of the group layer into a single image. -func blend_children(frame: Frame, origin := Vector2i.ZERO) -> Image: +func blend_children(frame: Frame, origin := Vector2i.ZERO, apply_effects := true) -> Image: var image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) var children := get_children(false) if children.size() <= 0: return image - var blend_rect := Rect2i(Vector2i.ZERO, project.size) var textures: Array[Image] = [] - var metadata_image := Image.create(children.size(), 4, false, Image.FORMAT_R8) + var metadata_image := Image.create(children.size(), 4, false, Image.FORMAT_RG8) + var current_child_index := 0 for i in children.size(): var layer := children[i] if not layer.is_visible_in_hierarchy(): + current_child_index += 1 continue - var cel := frame.cels[layer.index] if layer is GroupLayer: - var blended_children: Image = layer.blend_children(frame, origin) - if DisplayServer.get_name() == "headless": - image.blend_rect(blended_children, blend_rect, origin) - else: - textures.append(blended_children) - DrawingAlgos.set_layer_metadata_image(layer, cel, metadata_image, i) + current_child_index = _blend_child_group( + image, + layer, + frame, + textures, + metadata_image, + current_child_index, + origin, + apply_effects + ) else: - if DisplayServer.get_name() == "headless": - DrawingAlgos.blend_layers_headless(image, project, layer, cel, origin) - else: - textures.append(layer.display_effects(cel)) - DrawingAlgos.set_layer_metadata_image(layer, cel, metadata_image, i) + _include_child_in_blending( + image, + layer, + frame, + textures, + metadata_image, + current_child_index, + origin, + apply_effects + ) + current_child_index += 1 - if DisplayServer.get_name() != "headless": + if DisplayServer.get_name() != "headless" and textures.size() > 0: var texture_array := Texture2DArray.new() texture_array.create_from_images(textures) var params := { - "layers": texture_array, "metadata": ImageTexture.create_from_image(metadata_image) + "layers": texture_array, + "metadata": ImageTexture.create_from_image(metadata_image), + "origin_x_positive": origin.x > 0, + "origin_y_positive": origin.y > 0, } var gen := ShaderImageEffect.new() gen.generate_image(image, DrawingAlgos.blend_layers_shader, params, project.size) + if apply_effects: + image = display_effects(frame.cels[index], image) return image +func _include_child_in_blending( + image: Image, + layer: BaseLayer, + frame: Frame, + textures: Array[Image], + metadata_image: Image, + i: int, + origin: Vector2i, + apply_effects: bool +) -> void: + var cel := frame.cels[layer.index] + if DisplayServer.get_name() == "headless": + DrawingAlgos.blend_layers_headless(image, project, layer, cel, origin) + else: + var cel_image: Image + if apply_effects: + cel_image = layer.display_effects(cel) + else: + cel_image = cel.get_image() + textures.append(cel_image) + DrawingAlgos.set_layer_metadata_image(layer, cel, metadata_image, i) + if origin != Vector2i.ZERO: + # Only used as a preview for the move tool, when used on a group's children + var test_array := [project.frames.find(frame), project.layers.find(layer)] + if test_array in project.selected_cels: + var origin_fixed := Vector2(origin).abs() / Vector2(cel_image.get_size()) + metadata_image.set_pixel(i, 2, Color(origin_fixed.x, origin_fixed.y, 0.0, 0.0)) + + +## Include a child group in the blending process. +## If the child group is set to pass through mode, loop through its children +## and include them as separate images, instead of blending them all together. +## Gets called recursively if the child group has children groups of its own, +## and they are also set to pass through mode. +func _blend_child_group( + image: Image, + layer: BaseLayer, + frame: Frame, + textures: Array[Image], + metadata_image: Image, + i: int, + origin: Vector2i, + apply_effects: bool +) -> int: + var new_i := i + var blend_rect := Rect2i(Vector2i.ZERO, project.size) + var cel := frame.cels[layer.index] + if layer.blend_mode == BlendModes.PASS_THROUGH: + var children := layer.get_children(false) + for j in children.size(): + var child := children[j] + if child is GroupLayer: + new_i = _blend_child_group( + image, child, frame, textures, metadata_image, i + j, origin, apply_effects + ) + else: + new_i += j + metadata_image.crop(metadata_image.get_width() + 1, metadata_image.get_height()) + _include_child_in_blending( + image, child, frame, textures, metadata_image, new_i, origin, apply_effects + ) + else: + var blended_children := (layer as GroupLayer).blend_children(frame, origin) + if DisplayServer.get_name() == "headless": + image.blend_rect(blended_children, blend_rect, origin) + else: + textures.append(blended_children) + DrawingAlgos.set_layer_metadata_image(layer, cel, metadata_image, i) + return new_i + + # Overridden Methods: diff --git a/src/Shaders/BlendLayers.gdshader b/src/Shaders/BlendLayers.gdshader index 4aa0576f1..974394e3f 100644 --- a/src/Shaders/BlendLayers.gdshader +++ b/src/Shaders/BlendLayers.gdshader @@ -144,6 +144,20 @@ float border_trim(vec4 color, vec2 uv) { } +int find_previous_opaque_layer(int index, vec2 metadata_size_float) { + for (int i = index - 1; i > 0; i--) { + float layer_index = float(i) / metadata_size_float.x; + float clipping_mask = texture(metadata, vec2(layer_index, 3.0 / metadata_size_float.y)).r; + // If the red value is ~0.2, it means that it should be skipped. + // Otherwise, return the index. + if (clipping_mask > 0.25 || clipping_mask < 0.15) { + return i; + } + } + return 0; +} + + void fragment() { ivec2 metadata_size = textureSize(metadata, 0) - 1; vec2 metadata_size_float = vec2(metadata_size); @@ -173,17 +187,18 @@ void fragment() { current_origin.y = -current_origin.y; } // get origin of previous layer (used for clipping masks to work correctly) - vec2 prev_origin = texture(metadata, vec2(float(i - 1), 2.0 / metadata_size_float.y)).rg; + float clipping_mask_index = float(find_previous_opaque_layer(i, metadata_size_float)); + vec2 prev_origin = texture(metadata, vec2(clipping_mask_index, 2.0 / metadata_size_float.y)).rg; if (!origin_x_positive) { - prev_origin.x = -prev_origin.x; + prev_origin.x = -prev_origin.x; } if (!origin_y_positive) { - prev_origin.y = -prev_origin.y; + prev_origin.y = -prev_origin.y; } float current_opacity = texture(metadata, vec2(layer_index, 1.0 / metadata_size_float.y)).r; vec2 uv = UV - current_origin; vec4 layer_color = texture(layers, vec3(uv, float(i))); - vec4 prev_layer_color = texture(layers, vec3(UV - prev_origin, float(i - 1))); + vec4 prev_layer_color = texture(layers, vec3(UV - prev_origin, clipping_mask_index)); float clipping_mask = texture(metadata, vec2(layer_index, 3.0 / metadata_size_float.y)).r; layer_color.a *= prev_layer_color.a * step(0.5, clipping_mask) + 1.0 * step(clipping_mask, 0.5); layer_color.a = border_trim(layer_color, uv); diff --git a/src/UI/Canvas/Canvas.gd b/src/UI/Canvas/Canvas.gd index 9b5891ee0..79694f977 100644 --- a/src/UI/Canvas/Canvas.gd +++ b/src/UI/Canvas/Canvas.gd @@ -117,17 +117,22 @@ func update_texture(layer_i: int, frame_i := -1, project := Global.current_proje if frame_i != project.current_frame: # Don't update if the cel is on a different frame (can happen with undo/redo) return - var layer := project.layers[layer_i] + var layer := project.layers[layer_i].get_blender_ancestor() var cel_image: Image - if Global.display_layer_effects: - cel_image = layer.display_effects(current_cel) + if layer is GroupLayer: + cel_image = layer.blend_children( + project.frames[project.current_frame], Vector2i.ZERO, Global.display_layer_effects + ) else: - cel_image = current_cel.get_image() + if Global.display_layer_effects: + cel_image = layer.display_effects(current_cel) + else: + cel_image = current_cel.get_image() if ( cel_image.get_size() == Vector2i(layer_texture_array.get_width(), layer_texture_array.get_height()) ): - layer_texture_array.update_layer(cel_image, project.ordered_layers[layer_i]) + layer_texture_array.update_layer(cel_image, project.ordered_layers[layer.index]) func update_selected_cels_textures(project := Global.current_project) -> void: @@ -141,7 +146,6 @@ func update_selected_cels_textures(project := Global.current_project) -> void: func draw_layers(force_recreate := false) -> void: var project := Global.current_project - var current_cels := project.frames[project.current_frame].cels var recreate_texture_array := ( layer_texture_array.get_layers() != project.layers.size() or layer_texture_array.get_width() != project.size.x @@ -157,16 +161,11 @@ func draw_layers(force_recreate := false) -> void: layer_metadata_image = Image.create(project.layers.size(), 4, false, Image.FORMAT_RG8) # Draw current frame layers for i in project.layers.size(): - var ordered_index := project.ordered_layers[i] var layer := project.layers[i] - var cel := current_cels[i] - var cel_image: Image - if Global.display_layer_effects: - cel_image = layer.display_effects(cel) - else: - cel_image = cel.get_image() + var ordered_index := project.ordered_layers[layer.index] + var cel_image := Image.new() + _update_texture_array_layer(project, layer, cel_image, false) textures[ordered_index] = cel_image - DrawingAlgos.set_layer_metadata_image(layer, cel, layer_metadata_image, ordered_index) # Store the origin if [project.current_frame, i] in project.selected_cels: var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size()) @@ -185,18 +184,14 @@ func draw_layers(force_recreate := false) -> void: var test_array := [project.current_frame, i] if not test_array in project.selected_cels: continue - var ordered_index := project.ordered_layers[i] var layer := project.layers[i] - var cel := current_cels[i] - var cel_image: Image - if Global.display_layer_effects: - cel_image = layer.display_effects(cel) - else: - cel_image = cel.get_image() - layer_texture_array.update_layer(cel_image, ordered_index) - DrawingAlgos.set_layer_metadata_image( - layer, cel, layer_metadata_image, ordered_index - ) + var ordered_index := project.ordered_layers[layer.index] + var cel_image := Image.new() + _update_texture_array_layer(project, layer, cel_image, true) + var parent_layer := layer.get_blender_ancestor() + if layer != parent_layer: + # True when the layer has parents. In that case, update its top-most parent. + _update_texture_array_layer(project, parent_layer, Image.new(), true) # Update the origin var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size()) layer_metadata_image.set_pixel( @@ -209,6 +204,32 @@ func draw_layers(force_recreate := false) -> void: update_all_layers = false +func _update_texture_array_layer( + project: Project, layer: BaseLayer, cel_image: Image, update_layer: bool +) -> void: + var ordered_index := project.ordered_layers[layer.index] + var cel := project.frames[project.current_frame].cels[layer.index] + var include := true + if layer is GroupLayer and layer.blend_mode != BaseLayer.BlendModes.PASS_THROUGH: + cel_image.copy_from( + layer.blend_children( + project.frames[project.current_frame], + move_preview_location, + Global.display_layer_effects + ) + ) + else: + if Global.display_layer_effects: + cel_image.copy_from(layer.display_effects(cel)) + else: + cel_image.copy_from(cel.get_image()) + if layer.is_blended_by_ancestor(): + include = false + if update_layer: + layer_texture_array.update_layer(cel_image, ordered_index) + DrawingAlgos.set_layer_metadata_image(layer, cel, layer_metadata_image, ordered_index, include) + + func refresh_onion() -> void: onion_past.queue_redraw() onion_future.queue_redraw() diff --git a/src/UI/Canvas/CanvasPreview.gd b/src/UI/Canvas/CanvasPreview.gd index bcd85f74d..23f710efc 100644 --- a/src/UI/Canvas/CanvasPreview.gd +++ b/src/UI/Canvas/CanvasPreview.gd @@ -83,14 +83,17 @@ func _draw_layers() -> void: # Draw current frame layers for i in project.ordered_layers: var cel := current_cels[i] - if current_cels[i] is GroupCel: - continue var layer := project.layers[i] var cel_image: Image - if Global.display_layer_effects: - cel_image = layer.display_effects(cel) + if layer is GroupLayer and layer.blend_mode != BaseLayer.BlendModes.PASS_THROUGH: + cel_image = layer.blend_children( + current_frame, Vector2i.ZERO, Global.display_layer_effects + ) else: - cel_image = cel.get_image() + if Global.display_layer_effects: + cel_image = layer.display_effects(cel) + else: + cel_image = cel.get_image() textures.append(cel_image) DrawingAlgos.set_layer_metadata_image(layer, cel, metadata_image, i) var texture_array := Texture2DArray.new() diff --git a/src/UI/Dialogs/ImageEffects/OffsetImage.gd b/src/UI/Dialogs/ImageEffects/OffsetImage.gd index eed79dd01..bfd94d303 100644 --- a/src/UI/Dialogs/ImageEffects/OffsetImage.gd +++ b/src/UI/Dialogs/ImageEffects/OffsetImage.gd @@ -66,7 +66,7 @@ func recalculate_preview(params: Dictionary) -> void: ## Altered version of blend_layers() located in DrawingAlgos.gd -## This function is REQUIRED in order for offset effect to work correctly with cliping masks +## This function is REQUIRED in order for offset effect to work correctly with clipping masks func blend_layers( image: Image, frame: Frame, @@ -101,8 +101,14 @@ func blend_layers( if not layer_is_selected: include = false var cel := frame.cels[ordered_index] - var cel_image := layer.display_effects(cel) - if include: ## apply offset effect to it + var cel_image: Image + if layer is GroupLayer and layer.blend_mode != BaseLayer.BlendModes.PASS_THROUGH: + cel_image = (layer as GroupLayer).blend_children(frame) + else: + cel_image = layer.display_effects(cel) + if layer.is_blended_by_ancestor() and not only_selected_cels and not only_selected_layers: + include = false + if include: # Apply offset effect to it gen.generate_image(cel_image, shader, effect_params, project.size) textures.append(cel_image) DrawingAlgos.set_layer_metadata_image(layer, cel, metadata_image, ordered_index, include) diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index ca6405850..12670d988 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -65,34 +65,7 @@ func _ready() -> void: frame_scroll_bar.value_changed.connect(_frame_scroll_changed) Global.animation_timer.wait_time = 1 / Global.current_project.fps fps_spinbox.value = Global.current_project.fps - - # Fill the blend modes OptionButton with items - blend_modes_button.add_item("Normal", BaseLayer.BlendModes.NORMAL) - blend_modes_button.add_separator("Darken") - blend_modes_button.add_item("Darken", BaseLayer.BlendModes.DARKEN) - blend_modes_button.add_item("Multiply", BaseLayer.BlendModes.MULTIPLY) - blend_modes_button.add_item("Color burn", BaseLayer.BlendModes.COLOR_BURN) - blend_modes_button.add_item("Linear burn", BaseLayer.BlendModes.LINEAR_BURN) - blend_modes_button.add_separator("Lighten") - blend_modes_button.add_item("Lighten", BaseLayer.BlendModes.LIGHTEN) - blend_modes_button.add_item("Screen", BaseLayer.BlendModes.SCREEN) - blend_modes_button.add_item("Color dodge", BaseLayer.BlendModes.COLOR_DODGE) - blend_modes_button.add_item("Add", BaseLayer.BlendModes.ADD) - blend_modes_button.add_separator("Contrast") - blend_modes_button.add_item("Overlay", BaseLayer.BlendModes.OVERLAY) - blend_modes_button.add_item("Soft light", BaseLayer.BlendModes.SOFT_LIGHT) - blend_modes_button.add_item("Hard light", BaseLayer.BlendModes.HARD_LIGHT) - blend_modes_button.add_separator("Inversion") - blend_modes_button.add_item("Difference", BaseLayer.BlendModes.DIFFERENCE) - blend_modes_button.add_item("Exclusion", BaseLayer.BlendModes.EXCLUSION) - blend_modes_button.add_item("Subtract", BaseLayer.BlendModes.SUBTRACT) - blend_modes_button.add_item("Divide", BaseLayer.BlendModes.DIVIDE) - blend_modes_button.add_separator("Component") - blend_modes_button.add_item("Hue", BaseLayer.BlendModes.HUE) - blend_modes_button.add_item("Saturation", BaseLayer.BlendModes.SATURATION) - blend_modes_button.add_item("Color", BaseLayer.BlendModes.COLOR) - blend_modes_button.add_item("Luminosity", BaseLayer.BlendModes.LUMINOSITY) - + _fill_blend_modes_option_button() # Config loading layer_frame_h_split.split_offset = Global.config_cache.get_value("timeline", "layer_size", 0) cel_size = Global.config_cache.get_value("timeline", "cel_size", cel_size) # Call setter @@ -220,6 +193,48 @@ func _cel_size_changed(value: int) -> void: tag_c.update_position_and_size() +## Fill the blend modes OptionButton with items +func _fill_blend_modes_option_button() -> void: + blend_modes_button.clear() + var selected_layers_are_groups := true + if Global.current_project.layers.size() == 0: + selected_layers_are_groups = false + else: + for idx_pair in Global.current_project.selected_cels: + var layer := Global.current_project.layers[idx_pair[1]] + if not layer is GroupLayer: + selected_layers_are_groups = false + break + if selected_layers_are_groups: + # Special blend mode that appears only when group layers are selected + blend_modes_button.add_item("Pass through", BaseLayer.BlendModes.PASS_THROUGH) + blend_modes_button.add_item("Normal", BaseLayer.BlendModes.NORMAL) + blend_modes_button.add_separator("Darken") + blend_modes_button.add_item("Darken", BaseLayer.BlendModes.DARKEN) + blend_modes_button.add_item("Multiply", BaseLayer.BlendModes.MULTIPLY) + blend_modes_button.add_item("Color burn", BaseLayer.BlendModes.COLOR_BURN) + blend_modes_button.add_item("Linear burn", BaseLayer.BlendModes.LINEAR_BURN) + blend_modes_button.add_separator("Lighten") + blend_modes_button.add_item("Lighten", BaseLayer.BlendModes.LIGHTEN) + blend_modes_button.add_item("Screen", BaseLayer.BlendModes.SCREEN) + blend_modes_button.add_item("Color dodge", BaseLayer.BlendModes.COLOR_DODGE) + blend_modes_button.add_item("Add", BaseLayer.BlendModes.ADD) + blend_modes_button.add_separator("Contrast") + blend_modes_button.add_item("Overlay", BaseLayer.BlendModes.OVERLAY) + blend_modes_button.add_item("Soft light", BaseLayer.BlendModes.SOFT_LIGHT) + blend_modes_button.add_item("Hard light", BaseLayer.BlendModes.HARD_LIGHT) + blend_modes_button.add_separator("Inversion") + blend_modes_button.add_item("Difference", BaseLayer.BlendModes.DIFFERENCE) + blend_modes_button.add_item("Exclusion", BaseLayer.BlendModes.EXCLUSION) + blend_modes_button.add_item("Subtract", BaseLayer.BlendModes.SUBTRACT) + blend_modes_button.add_item("Divide", BaseLayer.BlendModes.DIVIDE) + blend_modes_button.add_separator("Component") + blend_modes_button.add_item("Hue", BaseLayer.BlendModes.HUE) + blend_modes_button.add_item("Saturation", BaseLayer.BlendModes.SATURATION) + blend_modes_button.add_item("Color", BaseLayer.BlendModes.COLOR) + blend_modes_button.add_item("Luminosity", BaseLayer.BlendModes.LUMINOSITY) + + func _on_blend_modes_item_selected(index: int) -> void: var project := Global.current_project var current_mode := blend_modes_button.get_item_id(index) @@ -231,13 +246,18 @@ func _on_blend_modes_item_selected(index: int) -> void: project.undo_redo.add_undo_property(layer, "blend_mode", previous_mode) project.undo_redo.add_do_method(Global.undo_or_redo.bind(false)) project.undo_redo.add_do_method(_update_layer_ui) - project.undo_redo.add_do_method(Global.canvas.draw_layers) + project.undo_redo.add_do_method(_update_layers) project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true)) project.undo_redo.add_undo_method(_update_layer_ui) - project.undo_redo.add_undo_method(Global.canvas.draw_layers) + project.undo_redo.add_undo_method(_update_layers) project.undo_redo.commit_action() +func _update_layers() -> void: + Global.canvas.update_all_layers = true + Global.canvas.draw_layers() + + func add_frame() -> void: var project := Global.current_project var frame_add_index := project.current_frame + 1 @@ -1068,6 +1088,7 @@ func _on_timeline_settings_visibility_changed() -> void: func _cel_switched() -> void: _toggle_frame_buttons() _toggle_layer_buttons() + _fill_blend_modes_option_button() # Temporarily disconnect it in order to prevent layer opacity changing # in the rest of the selected layers, if there are any. opacity_slider.value_changed.disconnect(_on_opacity_slider_value_changed) @@ -1077,10 +1098,9 @@ func _cel_switched() -> void: func _update_layer_ui() -> void: var project := Global.current_project - opacity_slider.value = project.layers[project.current_layer].opacity * 100 - var blend_mode_index := blend_modes_button.get_item_index( - project.layers[project.current_layer].blend_mode - ) + var layer := project.layers[project.current_layer] + opacity_slider.value = layer.opacity * 100 + var blend_mode_index := blend_modes_button.get_item_index(layer.blend_mode) blend_modes_button.selected = blend_mode_index diff --git a/src/UI/Timeline/CelProperties.gd b/src/UI/Timeline/CelProperties.gd index b35e06ba3..0aefdd9da 100644 --- a/src/UI/Timeline/CelProperties.gd +++ b/src/UI/Timeline/CelProperties.gd @@ -37,6 +37,7 @@ func _on_opacity_slider_value_changed(value: float) -> void: for cel_index in cel_indices: var cel := Global.current_project.frames[cel_index[0]].cels[cel_index[1]] cel.opacity = value / 100.0 + Global.canvas.update_all_layers = true Global.canvas.queue_redraw() diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index 4ac8667b6..cbb4dcfdb 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -145,9 +145,7 @@ func _on_main_button_gui_input(event: InputEvent) -> void: line_edit.grab_focus() elif event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: - var layer := Global.current_project.layers[layer_index] - if not layer is GroupLayer: - popup_menu.popup_on_parent(Rect2(get_global_mouse_position(), Vector2.ONE)) + popup_menu.popup_on_parent(Rect2(get_global_mouse_position(), Vector2.ONE)) func _on_layer_name_line_edit_focus_exited() -> void: @@ -222,6 +220,7 @@ func _on_popup_menu_id_pressed(id: int) -> void: layer.clipping_mask = not layer.clipping_mask popup_menu.set_item_checked(id, layer.clipping_mask) clipping_mask_icon.visible = layer.clipping_mask + Global.canvas.update_all_layers = true Global.canvas.draw_layers() diff --git a/src/UI/Timeline/LayerProperties.gd b/src/UI/Timeline/LayerProperties.gd index 9196933fb..eb58bdff9 100644 --- a/src/UI/Timeline/LayerProperties.gd +++ b/src/UI/Timeline/LayerProperties.gd @@ -10,8 +10,34 @@ var layer_indices: PackedInt32Array @onready var user_data_text_edit := $GridContainer/UserDataTextEdit as TextEdit -func _ready() -> void: - # Fill the blend modes OptionButton with items +func _on_visibility_changed() -> void: + if layer_indices.size() == 0: + return + Global.dialog_open(visible) + var first_layer := Global.current_project.layers[layer_indices[0]] + if visible: + _fill_blend_modes_option_button() + name_line_edit.text = first_layer.name + opacity_slider.value = first_layer.opacity * 100.0 + var blend_mode_index := blend_modes_button.get_item_index(first_layer.blend_mode) + blend_modes_button.selected = blend_mode_index + user_data_text_edit.text = first_layer.user_data + else: + layer_indices = [] + + +## Fill the blend modes OptionButton with items +func _fill_blend_modes_option_button() -> void: + blend_modes_button.clear() + var selected_layers_are_groups := true + for layer_index in layer_indices: + var layer := Global.current_project.layers[layer_index] + if not layer is GroupLayer: + selected_layers_are_groups = false + break + if selected_layers_are_groups: + # Special blend mode that appears only when group layers are selected + blend_modes_button.add_item("Pass through", BaseLayer.BlendModes.PASS_THROUGH) blend_modes_button.add_item("Normal", BaseLayer.BlendModes.NORMAL) blend_modes_button.add_item("Darken", BaseLayer.BlendModes.DARKEN) blend_modes_button.add_item("Multiply", BaseLayer.BlendModes.MULTIPLY) @@ -34,20 +60,6 @@ func _ready() -> void: blend_modes_button.add_item("Luminosity", BaseLayer.BlendModes.LUMINOSITY) -func _on_visibility_changed() -> void: - if layer_indices.size() == 0: - return - Global.dialog_open(visible) - var first_layer := Global.current_project.layers[layer_indices[0]] - if visible: - name_line_edit.text = first_layer.name - opacity_slider.value = first_layer.opacity * 100.0 - blend_modes_button.selected = first_layer.blend_mode - user_data_text_edit.text = first_layer.user_data - else: - layer_indices = [] - - func _on_name_line_edit_text_changed(new_text: String) -> void: if layer_indices.size() == 0: return @@ -63,18 +75,21 @@ func _on_opacity_slider_value_changed(value: float) -> void: var layer := Global.current_project.layers[layer_index] layer.opacity = value / 100.0 _emit_layer_property_signal() + Global.canvas.update_all_layers = true Global.canvas.queue_redraw() func _on_blend_mode_option_button_item_selected(index: BaseLayer.BlendModes) -> void: if layer_indices.size() == 0: return + Global.canvas.update_all_layers = true var project := Global.current_project + var current_mode := blend_modes_button.get_item_id(index) project.undo_redo.create_action("Set Blend Mode") for layer_index in layer_indices: var layer := project.layers[layer_index] var previous_mode := layer.blend_mode - project.undo_redo.add_do_property(layer, "blend_mode", index) + project.undo_redo.add_do_property(layer, "blend_mode", current_mode) project.undo_redo.add_undo_property(layer, "blend_mode", previous_mode) project.undo_redo.add_do_method(Global.undo_or_redo.bind(false)) project.undo_redo.add_do_method(Global.canvas.draw_layers)