mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-01-31 15:39:49 +00:00
077c57c53a
* 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
343 lines
13 KiB
GDScript
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()
|