1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-31 15:39:49 +00:00
Pixelorama/src/Classes/Layers/BaseLayer.gd
Emmanouil Papadeas 077c57c53a
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
2024-08-15 15:52:55 +03:00

343 lines
13 KiB
GDScript

# gdlint: ignore=max-public-methods
class_name BaseLayer
extends RefCounted
## Base class for layer properties. Different layer types extend from this class.
signal name_changed ## Emits when [member name] is changed.
signal visibility_changed ## Emits when [member visible] is changed.
## All currently supported layer blend modes between two layers. The upper layer
## 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 {
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.
LINEAR_BURN, ## Darkens the base colors based on the value of the blend colors.
LIGHTEN, ## Keeps the lighter colors between the blend and the base layers.
SCREEN, ## Lightens the colors by multiplying the inverse of the blend and base colors.
COLOR_DODGE, ## Lightens by decreasing the contrast between the blend and base colors.
ADD, ## Lightens by adding the numerical values of the two colors. Also known as linear dodge.
OVERLAY, ## Like Screen mode in bright base colors and Multiply mode in darker base colors.
SOFT_LIGHT, ## Similar to Overlay, but more subtle.
HARD_LIGHT, ## Like Screen mode in bright blending colors and Multiply mode in darker colors.
DIFFERENCE, ## Subtracts the blend color from the base or vice versa, depending on the brightness.
EXCLUSION, ## Similar to Difference mode, but with less contrast between the colors.
SUBTRACT, ## Darkens by subtracting the numerical values of the blend colors from the base.
DIVIDE, ## Divides the numerical values of the base colors by the blend.
HUE, ## Uses the blend hue while preserving the base saturation and luminosity.
SATURATION, ## Uses the blend saturation while preserving the base hue and luminosity.
COLOR, ## Uses the blend hue and saturation while preserving the base luminosity.
LUMINOSITY ## Uses the blend luminosity while preserving the base hue and saturation.
}
var name := "": ## Name of the layer.
set(value):
name = value
name_changed.emit()
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.
set(value):
visible = value
visibility_changed.emit()
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 clipping_mask := false ## If [code]true[/code], the layer acts as a clipping mask.
var opacity := 1.0 ## The opacity of the layer, affects all frames that belong to that 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.
var user_data := "" ## User defined data, set in the layer properties.
## Returns true if this is a direct or indirect parent of layer
func is_ancestor_of(layer: BaseLayer) -> bool:
if layer.parent == self:
return true
elif is_instance_valid(layer.parent):
return is_ancestor_of(layer.parent)
return false
## 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] = []
if recursive:
for i in index:
if is_ancestor_of(project.layers[i]):
children.append(project.layers[i])
else:
for i in index:
if project.layers[i].parent == self:
children.append(project.layers[i])
return children
## Returns the number of child nodes.
## The process is recursive if [param recursive] is [code]true[/code].
func get_child_count(recursive: bool) -> int:
var count := 0
if recursive:
for i in index:
if is_ancestor_of(project.layers[i]):
count += 1
else:
for i in index:
if project.layers[i].parent == self:
count += 1
return count
## Tells if the layer has child layers ([code]true[/code]) or not ([code]false[/code]).
func has_children() -> bool:
if index == 0:
return false
return project.layers[index - 1].parent == self
## Tells if the layer is expanded ([code]true[/code]) or collapsed ([code]false[/code])
## in the hierarchy.
func is_expanded_in_hierarchy() -> bool:
if is_instance_valid(parent):
# "expanded" variable is located in GroupLayer.gd
return parent.expanded and parent.is_expanded_in_hierarchy()
return true
## Tells if the layer's content is visible ([code]true[/code]) or hidden ([code]false[/code])
## in the layer tree. This is influenced by the eye button.
func is_visible_in_hierarchy() -> bool:
if is_instance_valid(parent) and visible:
return parent.is_visible_in_hierarchy()
return visible
## Tells if the layer's content is locked ([code]true[/code]) or not ([code]false[/code])
## in the layer tree. This is influenced by the lock button.
func is_locked_in_hierarchy() -> bool:
if is_instance_valid(parent) and not locked:
return parent.is_locked_in_hierarchy()
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]:
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):
return parent.get_hierarchy_depth() + 1
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):
return str(parent.get_layer_path(), "/", name)
return name
## Links a cel to link_set if its a Dictionary, or unlinks if null.
## Content/image_texture are handled separately for undo related reasons
func link_cel(cel: BaseCel, link_set = null) -> void:
# Erase from the cel's current link_set
if cel.link_set != null:
if cel.link_set.has("cels"):
cel.link_set["cels"].erase(cel)
if cel.link_set["cels"].is_empty():
cel_link_sets.erase(cel.link_set)
else:
cel_link_sets.erase(cel.link_set)
# Add to link_set
cel.link_set = link_set
if link_set != null:
if not link_set.has("cels"):
link_set["cels"] = []
link_set["cels"].append(cel)
if not cel_link_sets.has(link_set):
if not link_set.has("hue"):
var hues := PackedFloat32Array()
for other_link_set in cel_link_sets:
hues.append(other_link_set["hue"])
if hues.is_empty():
link_set["hue"] = Color.GREEN.h
else: # Calculate the largest gap in hue between existing link sets:
hues.sort()
# Start gap between the highest and lowest hues, otherwise its hard to include
var largest_gap_pos := hues[-1]
var largest_gap_size := 1.0 - (hues[-1] - hues[0])
for h in hues.size() - 1:
var gap_size: float = hues[h + 1] - hues[h]
if gap_size > largest_gap_size:
largest_gap_pos = hues[h]
largest_gap_size = gap_size
link_set["hue"] = wrapf(largest_gap_pos + largest_gap_size / 2.0, 0, 1)
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_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
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, 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:
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:
## Returns a curated [Dictionary] containing the layer data.
func serialize() -> Dictionary:
assert(index == project.layers.find(self))
var effect_data: Array[Dictionary] = []
for effect in effects:
effect_data.append(effect.serialize())
var dict := {
"name": name,
"visible": visible,
"locked": locked,
"blend_mode": blend_mode,
"clipping_mask": clipping_mask,
"opacity": opacity,
"parent": parent.index if is_instance_valid(parent) else -1,
"effects": effect_data
}
if not user_data.is_empty():
dict["user_data"] = user_data
if not cel_link_sets.is_empty():
var cels := [] # Cels array for easy finding of the frame index for link_set saving
for frame in project.frames:
cels.append(frame.cels[index])
dict["link_sets"] = []
for link_set in cel_link_sets:
dict["link_sets"].append({"cels": [], "hue": link_set["hue"]})
for cel in link_set["cels"]:
dict["link_sets"][-1]["cels"].append(cels.find(cel))
return dict
## Sets the layer data according to a curated [Dictionary] obtained from [method serialize].
func deserialize(dict: Dictionary) -> void:
name = dict.get("name", "")
visible = dict.get("visible", true)
locked = dict.get("locked", false)
blend_mode = dict.get("blend_mode", BlendModes.NORMAL)
clipping_mask = dict.get("clipping_mask", false)
opacity = dict.get("opacity", 1.0)
user_data = dict.get("user_data", user_data)
if dict.get("parent", -1) != -1:
parent = project.layers[dict.parent]
if dict.has("linked_cels") and not dict["linked_cels"].is_empty(): # Backwards compatibility
dict["link_sets"] = [{"cels": dict["linked_cels"], "hue": Color.GREEN.h}]
if dict.has("link_sets"):
for serialized_link_set in dict["link_sets"]:
var link_set := {"cels": [], "hue": serialized_link_set["hue"]}
for linked_cel_index in serialized_link_set["cels"]:
var cel: BaseCel = project.frames[linked_cel_index].cels[index]
link_set["cels"].append(cel)
cel.link_set = link_set
var linked_cel: BaseCel = link_set["cels"][0]
cel.set_content(linked_cel.get_content(), linked_cel.image_texture)
cel_link_sets.append(link_set)
if dict.has("effects"):
for effect_dict in dict["effects"]:
if not typeof(effect_dict) == TYPE_DICTIONARY:
print("Loading effect failed, not a dictionary.")
continue
var effect := LayerEffect.new()
effect.deserialize(effect_dict)
effects.append(effect)
## Returns a layer type that is one of the [param LayerTypes]
## enum in ["src/Autoload/Global.gd"] Autoload.
func get_layer_type() -> int:
return -1
## Returns a new empty [BaseCel]
func new_empty_cel() -> BaseCel:
return null
## Sets layer name to the default name followed by [param number].
func set_name_to_default(number: int) -> void:
name = tr("Layer") + " %s" % number
## Tells if the user is allowed to draw on current layer ([code]true[/code])
## or not ([code]false[/code]).
func can_layer_get_drawn() -> bool:
return false
## Tells if the layer allows child layers ([code]true[/code]) or not ([code]true[/code])
func accepts_child(_layer: BaseLayer) -> bool:
return false
## Returns an instance of the layer button that will be added to the timeline.
func instantiate_layer_button() -> Node:
return Global.layer_button_node.instantiate()