1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 17:19:50 +00:00

Implement support for group layer blending (#1077)

* Blend group layers on `DrawingAlgos.blend_layers()`

* Support group layer blending on the canvas

* Allow editing of group layer properties

* Fix issues with group layer blending in canvas, and unite common code

* Group layers can now be used as clipping masks

* Make move tool preview work on child layers

* Change OffsetImage's `blend_layers()` to support group layer blending

* Support group layer blending in the canvas preview

* Fix layer blending mode, clipping mask opacity and cel opacity not being updated automatically if the layer/cel changed is not selected

* Add a pass through blending mode to layer groups

Fingers crossed that no bugs were introduced

* Fix issue with layers that belong to pass through groups not updating their textures on the canvas automatically on undo
This commit is contained in:
Emmanouil Papadeas 2024-08-15 15:52:55 +03:00 committed by GitHub
parent f6f40e03e5
commit 077c57c53a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 332 additions and 115 deletions

View file

@ -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 ""

View file

@ -52,9 +52,19 @@ func blend_layers(
var cel := frame.cels[ordered_index]
if DisplayServer.get_name() == "headless":
blend_layers_headless(image, project, layer, cel, origin)
else:
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(

View file

@ -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,8 +216,11 @@ 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()
if is_instance_valid(image_override):
image.copy_from(image_override)
else:
image.copy_from(cel.get_image())
if not effects_enabled:
return image
@ -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:

View file

@ -11,42 +11,128 @@ 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)
current_child_index = _blend_child_group(
image,
layer,
frame,
textures,
metadata_image,
current_child_index,
origin,
apply_effects
)
else:
_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" 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),
"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)
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)
if DisplayServer.get_name() != "headless":
var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures)
var params := {
"layers": texture_array, "metadata": ImageTexture.create_from_image(metadata_image)
}
var gen := ShaderImageEffect.new()
gen.generate_image(image, DrawingAlgos.blend_layers_shader, params, project.size)
return image
return new_i
# Overridden Methods:

View file

@ -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,7 +187,8 @@ 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;
}
@ -183,7 +198,7 @@ void fragment() {
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);

View file

@ -117,8 +117,13 @@ 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 layer is GroupLayer:
cel_image = layer.blend_children(
project.frames[project.current_frame], Vector2i.ZERO, Global.display_layer_effects
)
else:
if Global.display_layer_effects:
cel_image = layer.display_effects(current_cel)
else:
@ -127,7 +132,7 @@ func update_texture(layer_i: int, frame_i := -1, project := Global.current_proje
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()

View file

@ -83,10 +83,13 @@ 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 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:
if Global.display_layer_effects:
cel_image = layer.display_effects(cel)
else:

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -145,8 +145,6 @@ 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))
@ -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()

View file

@ -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)