diff --git a/assets/graphics/layers/clipping_mask.png b/assets/graphics/layers/clipping_mask.png new file mode 100644 index 000000000..c88772296 Binary files /dev/null and b/assets/graphics/layers/clipping_mask.png differ diff --git a/assets/graphics/layers/clipping_mask.png.import b/assets/graphics/layers/clipping_mask.png.import new file mode 100644 index 000000000..ae2de06d9 --- /dev/null +++ b/assets/graphics/layers/clipping_mask.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ieo8fsapcgsy" +path="res://.godot/imported/clipping_mask.png-735677b4fff2e062e79993566d07bdd3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/graphics/layers/clipping_mask.png" +dest_files=["res://.godot/imported/clipping_mask.png-735677b4fff2e062e79993566d07bdd3.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 diff --git a/src/Autoload/DrawingAlgos.gd b/src/Autoload/DrawingAlgos.gd index 6cdc596df..ea89668a3 100644 --- a/src/Autoload/DrawingAlgos.gd +++ b/src/Autoload/DrawingAlgos.gd @@ -26,9 +26,10 @@ func blend_layers( only_selected_layers := false, ) -> void: var textures: Array[Image] = [] - # Nx3 texture, where N is the number of layers and the first row are the blend modes, - # the second are the opacities and the third are the origins - var metadata_image := Image.create(project.layers.size(), 3, false, Image.FORMAT_R8) + # Nx4 texture, where N is the number of layers and the first row are the blend modes, + # the second are the opacities, the third are the origins and the fourth are the + # clipping mask booleans. + var metadata_image := Image.create(project.layers.size(), 4, false, Image.FORMAT_R8) var frame_index := project.frames.find(frame) var previous_ordered_layers: Array[int] = project.ordered_layers project.order_layers(frame_index) @@ -51,14 +52,7 @@ func blend_layers( var cel := frame.cels[ordered_index] var cel_image := layer.display_effects(cel) textures.append(cel_image) - # Store the blend mode - metadata_image.set_pixel(ordered_index, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0)) - # Store the opacity - if include: - var opacity := cel.get_final_opacity(layer) - metadata_image.set_pixel(ordered_index, 1, Color(opacity, 0.0, 0.0, 0.0)) - else: - metadata_image.set_pixel(ordered_index, 1, Color()) + set_layer_metadata_image(layer, cel, metadata_image, ordered_index, include) var texture_array := Texture2DArray.new() texture_array.create_from_images(textures) var params := { @@ -73,6 +67,24 @@ func blend_layers( project.ordered_layers = previous_ordered_layers +func set_layer_metadata_image( + layer: BaseLayer, cel: BaseCel, image: Image, index: int, include := true +) -> void: + # Store the blend mode + image.set_pixel(index, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0)) + # Store the opacity + if layer.is_visible_in_hierarchy() and include: + var opacity := cel.get_final_opacity(layer) + image.set_pixel(index, 1, Color(opacity, 0.0, 0.0, 0.0)) + else: + image.set_pixel(index, 1, Color()) + # Store the clipping mask boolean + if layer.clipping_mask: + image.set_pixel(index, 3, Color.WHITE) + else: + image.set_pixel(index, 3, Color.BLACK) + + ## Algorithm based on http://members.chello.at/easyfilter/bresenham.html func get_ellipse_points(pos: Vector2i, size: Vector2i) -> Array[Vector2i]: var array: Array[Vector2i] = [] diff --git a/src/Classes/Layers/BaseLayer.gd b/src/Classes/Layers/BaseLayer.gd index 7ad9b3967..13f334009 100644 --- a/src/Classes/Layers/BaseLayer.gd +++ b/src/Classes/Layers/BaseLayer.gd @@ -36,6 +36,7 @@ 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 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. @@ -215,6 +216,7 @@ func serialize() -> Dictionary: "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 @@ -233,13 +235,12 @@ func serialize() -> Dictionary: ## Sets the layer data according to a curated [Dictionary] obtained from [method serialize]. func deserialize(dict: Dictionary) -> void: - name = dict.name - visible = dict.visible - locked = dict.locked - if dict.has("blend_mode"): - blend_mode = dict.blend_mode - if dict.has("opacity"): - opacity = dict.opacity + 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) 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 diff --git a/src/Shaders/BlendLayers.gdshader b/src/Shaders/BlendLayers.gdshader index 40a667f39..09bdf875d 100644 --- a/src/Shaders/BlendLayers.gdshader +++ b/src/Shaders/BlendLayers.gdshader @@ -5,8 +5,9 @@ const float HCV_EPSILON = 1e-10; const float HSL_EPSILON = 1e-10; uniform sampler2DArray layers : filter_nearest; -// Nx3 texture, where N is the number of layers and the first row are the blend modes, -// the second are the opacities and the third are the origins +// Nx4 texture, where N is the number of layers and the first row are the blend modes, +// the second are the opacities, the third are the origins and the fourth are the +// clipping mask booleans. uniform sampler2D metadata : filter_nearest; uniform bool origin_x_positive = true; uniform bool origin_y_positive = true; @@ -153,9 +154,9 @@ void fragment() { first_origin.y = -first_origin.y; } float first_opacity = texture(metadata, vec2(0.0, 1.0 / float(metadata_size.y))).r; - vec4 col = texture(layers, vec3(UV - first_origin, 0.0)); - col.a = border_trim(col, UV - first_origin); - col.a *= first_opacity; + vec4 result_color = texture(layers, vec3(UV - first_origin, 0.0)); + result_color.a = border_trim(result_color, UV - first_origin); + result_color.a *= first_opacity; for(int i = 1; i < metadata_size.x + 1; i++) // Loops through every layer { float blend_mode_float = texture(metadata, vec2(float(i) / float(metadata_size.x), 0.0)).r; @@ -171,9 +172,12 @@ void fragment() { } float current_opacity = texture(metadata, vec2(float(i) / float(metadata_size.x), 1.0 / float(metadata_size.y))).r; vec2 uv = UV - current_origin; - vec4 texture_color = texture(layers, vec3(uv, float(i))); - texture_color.a = border_trim(texture_color, uv); - col = blend(current_blend_mode, texture_color, col, current_opacity); + vec4 layer_color = texture(layers, vec3(uv, float(i))); + vec4 prev_layer_color = texture(layers, vec3(uv, float(i - 1))); + float clipping_mask = texture(metadata, vec2(float(i) / float(metadata_size.x), 3.0 / float(metadata_size.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); + result_color = blend(current_blend_mode, layer_color, result_color, current_opacity); } - COLOR = col; + COLOR = result_color; } diff --git a/src/UI/Canvas/Canvas.gd b/src/UI/Canvas/Canvas.gd index ec1dd888e..91b758a6b 100644 --- a/src/UI/Canvas/Canvas.gd +++ b/src/UI/Canvas/Canvas.gd @@ -148,9 +148,10 @@ func draw_layers() -> void: if recreate_texture_array: var textures: Array[Image] = [] textures.resize(project.layers.size()) - # Nx3 texture, where N is the number of layers and the first row are the blend modes, - # the second are the opacities and the third are the origins - layer_metadata_image = Image.create(project.layers.size(), 3, false, Image.FORMAT_RG8) + # Nx4 texture, where N is the number of layers and the first row are the blend modes, + # the second are the opacities, the third are the origins and the fourth are the + # clipping mask booleans. + 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] @@ -162,16 +163,7 @@ func draw_layers() -> void: else: cel_image = cel.get_image() textures[ordered_index] = cel_image - # Store the blend mode - layer_metadata_image.set_pixel( - ordered_index, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0) - ) - # Store the opacity - if layer.is_visible_in_hierarchy(): - var opacity := cel.get_final_opacity(layer) - layer_metadata_image.set_pixel(ordered_index, 1, Color(opacity, 0.0, 0.0, 0.0)) - else: - layer_metadata_image.set_pixel(ordered_index, 1, Color()) + 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()) @@ -199,14 +191,10 @@ func draw_layers() -> void: else: cel_image = cel.get_image() layer_texture_array.update_layer(cel_image, ordered_index) - layer_metadata_image.set_pixel( - ordered_index, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0) + DrawingAlgos.set_layer_metadata_image( + layer, cel, layer_metadata_image, ordered_index ) - if layer.is_visible_in_hierarchy(): - var opacity := cel.get_final_opacity(layer) - layer_metadata_image.set_pixel(ordered_index, 1, Color(opacity, 0.0, 0.0, 0.0)) - else: - layer_metadata_image.set_pixel(ordered_index, 1, Color()) + # Update the origin var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size()) layer_metadata_image.set_pixel( ordered_index, 2, Color(origin.x, origin.y, 0.0, 0.0) diff --git a/src/UI/Canvas/CanvasPreview.gd b/src/UI/Canvas/CanvasPreview.gd index da75c2160..bcd85f74d 100644 --- a/src/UI/Canvas/CanvasPreview.gd +++ b/src/UI/Canvas/CanvasPreview.gd @@ -76,9 +76,10 @@ func _draw_layers() -> void: var current_frame := project.frames[frame_index] var current_cels := current_frame.cels var textures: Array[Image] = [] - # Nx3 texture, where N is the number of layers and the first row are the blend modes, - # the second are the opacities and the third are the origins - var metadata_image := Image.create(project.layers.size(), 3, false, Image.FORMAT_R8) + # Nx4 texture, where N is the number of layers and the first row are the blend modes, + # the second are the opacities, the third are the origins and the fourth are the + # clipping mask booleans. + var metadata_image := Image.create(project.layers.size(), 4, false, Image.FORMAT_R8) # Draw current frame layers for i in project.ordered_layers: var cel := current_cels[i] @@ -91,12 +92,7 @@ func _draw_layers() -> void: else: cel_image = cel.get_image() textures.append(cel_image) - metadata_image.set_pixel(i, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0)) - if layer.is_visible_in_hierarchy(): - var opacity := cel.get_final_opacity(layer) - metadata_image.set_pixel(i, 1, Color(opacity, 0.0, 0.0, 0.0)) - else: - metadata_image.set_pixel(i, 1, Color(0.0, 0.0, 0.0, 0.0)) + DrawingAlgos.set_layer_metadata_image(layer, cel, metadata_image, i) var texture_array := Texture2DArray.new() texture_array.create_from_images(textures) material.set_shader_parameter("layers", texture_array) diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index b2612988a..322c38efd 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -940,13 +940,11 @@ func _on_MergeDownLayer_pressed() -> void: var top_image := top_layer.display_effects(top_cel) var bottom_cel := frame.cels[bottom_layer.index] var textures: Array[Image] = [] - var metadata_image := Image.create(2, 3, false, Image.FORMAT_R8) textures.append(bottom_cel.get_image()) - metadata_image.set_pixel(0, 1, Color(1.0, 0.0, 0.0, 0.0)) textures.append(top_image) - metadata_image.set_pixel(1, 0, Color(top_layer.blend_mode / 255.0, 0.0, 0.0, 0.0)) - var opacity := frame.cels[top_layer.index].get_final_opacity(top_layer) - metadata_image.set_pixel(1, 1, Color(opacity, 0.0, 0.0, 0.0)) + var metadata_image := Image.create(2, 4, false, Image.FORMAT_R8) + DrawingAlgos.set_layer_metadata_image(bottom_layer, bottom_cel, metadata_image, 0) + DrawingAlgos.set_layer_metadata_image(top_layer, top_cel, metadata_image, 1) var texture_array := Texture2DArray.new() texture_array.create_from_images(textures) var params := { diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index 52c5c88a7..d31d3fc30 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -12,6 +12,8 @@ var layer_index := 0 @onready var line_edit := %LayerNameLineEdit as LineEdit @onready var hierarchy_spacer := %HierarchySpacer as Control @onready var linked_button := %LinkButton as BaseButton +@onready var clipping_mask_icon := %ClippingMask as TextureRect +@onready var popup_menu := $PopupMenu as PopupMenu func _ready() -> void: @@ -71,6 +73,8 @@ func update_buttons() -> void: visibility_button.modulate.a = 1 lock_button.modulate.a = 1 + popup_menu.set_item_checked(0, layer.clipping_mask) + clipping_mask_icon.visible = layer.clipping_mask if is_instance_valid(layer.parent): if not layer.parent.is_visible_in_hierarchy(): visibility_button.modulate.a = 0.33 @@ -108,7 +112,9 @@ func _input(event: InputEvent) -> void: func _on_LayerContainer_gui_input(event: InputEvent) -> void: var project := Global.current_project - if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: + if not event is InputEventMouseButton: + return + if event.button_index == MOUSE_BUTTON_LEFT: Global.canvas.selection.transform_content_confirm() var prev_curr_layer := project.current_layer if Input.is_action_pressed(&"shift"): @@ -135,6 +141,10 @@ func _on_LayerContainer_gui_input(event: InputEvent) -> void: line_edit.visible = true line_edit.editable = true 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(Rect2(get_global_mouse_position(), Vector2.ONE)) func _on_LineEdit_focus_exited() -> void: @@ -368,3 +378,12 @@ func _get_region_rect(y_begin: float, y_end: float) -> Rect2: rect.position.y += rect.size.y * y_begin rect.size.y *= y_end - y_begin return rect + + +func _on_popup_menu_id_pressed(id: int) -> void: + var layer := Global.current_project.layers[layer_index] + if id == 0: + layer.clipping_mask = not layer.clipping_mask + popup_menu.set_item_checked(0, layer.clipping_mask) + clipping_mask_icon.visible = layer.clipping_mask + Global.canvas.draw_layers() diff --git a/src/UI/Timeline/LayerButton.tscn b/src/UI/Timeline/LayerButton.tscn index 649820b61..9a5f913b8 100644 --- a/src/UI/Timeline/LayerButton.tscn +++ b/src/UI/Timeline/LayerButton.tscn @@ -1,10 +1,11 @@ -[gd_scene load_steps=6 format=3 uid="uid://bai814sqvk68f"] +[gd_scene load_steps=7 format=3 uid="uid://bai814sqvk68f"] [ext_resource type="Script" path="res://src/UI/Timeline/LayerButton.gd" id="1_6hlpe"] [ext_resource type="Texture2D" uid="uid://c2b3htff5yox8" path="res://assets/graphics/layers/layer_visible.png" id="2_ef6fb"] [ext_resource type="Texture2D" uid="uid://dndlglvqc7v6a" path="res://assets/graphics/layers/group_expanded.png" id="2_enrtd"] [ext_resource type="Texture2D" uid="uid://dhc0pnnqojd2m" path="res://assets/graphics/layers/unlock.png" id="3_ah1my"] [ext_resource type="Texture2D" uid="uid://cofw1x6chh4i" path="res://assets/graphics/layers/unlinked_layer.png" id="4_058qm"] +[ext_resource type="Texture2D" uid="uid://ieo8fsapcgsy" path="res://assets/graphics/layers/clipping_mask.png" id="6_73j5q"] [node name="LayerButton" type="Button"] offset_right = 200.0 @@ -147,6 +148,13 @@ unique_name_in_owner = true layout_mode = 2 mouse_filter = 2 +[node name="ClippingMask" type="TextureRect" parent="HBoxContainer/LayerName"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +texture = ExtResource("6_73j5q") +stretch_mode = 5 + [node name="LayerNameLabel" type="Label" parent="HBoxContainer/LayerName"] unique_name_in_owner = true layout_mode = 2 @@ -169,9 +177,17 @@ caret_blink_interval = 0.5 layout_mode = 2 mouse_filter = 2 +[node name="PopupMenu" type="PopupMenu" parent="."] +disable_3d = true +item_count = 1 +item_0/text = "Clipping mask" +item_0/checkable = 1 +item_0/id = 0 + [connection signal="gui_input" from="." to="." method="_on_LayerContainer_gui_input"] [connection signal="pressed" from="HBoxContainer/LayerButtons/ExpandButton" to="." method="_on_ExpandButton_pressed"] [connection signal="pressed" from="HBoxContainer/LayerButtons/VisibilityButton" to="." method="_on_VisibilityButton_pressed"] [connection signal="pressed" from="HBoxContainer/LayerButtons/LockButton" to="." method="_on_LockButton_pressed"] [connection signal="pressed" from="HBoxContainer/LayerButtons/LinkButton" to="." method="_on_LinkButton_pressed"] [connection signal="focus_exited" from="HBoxContainer/LayerName/LayerNameLineEdit" to="." method="_on_LineEdit_focus_exited"] +[connection signal="id_pressed" from="PopupMenu" to="." method="_on_popup_menu_id_pressed"]