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

Layer blend modes (#911)

* Preview blend modes

No support for exporting and layer merging yet. Also need to fix the move tool preview.

* Preview blend modes on tile mode

* Raise layer limit to 1024

* Export images with layer blending modes

* Save blend modes in pxo files

* Merge layers with blending modes

* Fix crash when adding a new layer

* Preview blending in the other canvases

* Update DrawingAlgos.gd

* Move tool preview

* Re-arrange blend menu and add lighten, darken, linear burn and exclusion

* Add divide blend mode

* Add hue, saturation, color & luminosity blend modes

* Undo/redo when changing blend modes
This commit is contained in:
Emmanouil Papadeas 2023-10-22 01:57:45 +03:00 committed by GitHub
parent 6247ab2252
commit 8de9697be0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 448 additions and 169 deletions

View file

@ -3,10 +3,73 @@ extends Node
enum GradientDirection { TOP, BOTTOM, LEFT, RIGHT }
## Continuation from Image.Interpolation
enum Interpolation { SCALE3X = 5, CLEANEDGE = 6, OMNISCALE = 7 }
var blend_layers_shader := preload("res://src/Shaders/BlendLayers.gdshader")
var clean_edge_shader: Shader
var omniscale_shader := preload("res://src/Shaders/Rotation/OmniScale.gdshader")
## Blends canvas layers into passed image starting from the origin position
func blend_all_layers(
image: Image, frame: Frame, origin := Vector2i.ZERO, project := Global.current_project
) -> void:
var current_cels := frame.cels
var textures: Array[Image] = []
var opacities := PackedFloat32Array()
var blend_modes := PackedInt32Array()
for i in Global.current_project.layers.size():
if current_cels[i] is GroupCel:
continue
if not Global.current_project.layers[i].is_visible_in_hierarchy():
continue
textures.append(current_cels[i].get_image())
opacities.append(current_cels[i].opacity)
blend_modes.append(Global.current_project.layers[i].blend_mode)
var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures)
var params := {
"layers": texture_array,
"opacities": opacities,
"blend_modes": blend_modes,
}
var blended := Image.create(project.size.x, project.size.y, false, image.get_format())
var gen := ShaderImageEffect.new()
gen.generate_image(blended, blend_layers_shader, params, project.size)
image.blend_rect(blended, Rect2i(Vector2i.ZERO, project.size), origin)
## Blends selected cels of the given frame into passed image starting from the origin position
func blend_selected_cels(
image: Image, frame: Frame, origin := Vector2i.ZERO, project := Global.current_project
) -> void:
var textures: Array[Image] = []
var opacities := PackedFloat32Array()
var blend_modes := PackedInt32Array()
for cel_ind in frame.cels.size():
var test_array := [project.current_frame, cel_ind]
if not test_array in project.selected_cels:
continue
if frame.cels[cel_ind] is GroupCel:
continue
if not project.layers[cel_ind].is_visible_in_hierarchy():
continue
var cel := frame.cels[cel_ind]
textures.append(cel.get_image())
opacities.append(cel.opacity)
blend_modes.append(Global.current_project.layers[cel_ind].blend_mode)
var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures)
var params := {
"layers": texture_array,
"opacities": opacities,
"blend_modes": blend_modes,
}
var blended := Image.create(project.size.x, project.size.y, false, image.get_format())
var gen := ShaderImageEffect.new()
gen.generate_image(blended, blend_layers_shader, params, project.size)
image.blend_rect(blended, Rect2i(Vector2i.ZERO, project.size), origin)
## Algorithm based on http://members.chello.at/easyfilter/bresenham.html
func get_ellipse_points(pos: Vector2i, size: Vector2i) -> Array[Vector2i]:
var array: Array[Vector2i] = []

View file

@ -484,9 +484,9 @@ func _blend_layers(
image: Image, frame: Frame, origin := Vector2i.ZERO, project := Global.current_project
) -> void:
if export_layers == 0:
blend_all_layers(image, frame, origin, project)
DrawingAlgos.blend_all_layers(image, frame, origin, project)
elif export_layers == 1:
blend_selected_cels(image, frame, origin, project)
DrawingAlgos.blend_selected_cels(image, frame, origin, project)
else:
var layer := project.layers[export_layers - 2]
var layer_image := Image.new()
@ -497,57 +497,5 @@ func _blend_layers(
image.blend_rect(layer_image, Rect2i(Vector2i.ZERO, project.size), origin)
## Blends canvas layers into passed image starting from the origin position
func blend_all_layers(
image: Image, frame: Frame, origin := Vector2i.ZERO, project := Global.current_project
) -> void:
var layer_i := 0
for cel in frame.cels:
if not project.layers[layer_i].is_visible_in_hierarchy():
layer_i += 1
continue
if cel is GroupCel:
layer_i += 1
continue
var cel_image := Image.new()
cel_image.copy_from(cel.get_image())
if cel.opacity < 1: # If we have cel transparency
for xx in cel_image.get_size().x:
for yy in cel_image.get_size().y:
var pixel_color := cel_image.get_pixel(xx, yy)
var alpha: float = pixel_color.a * cel.opacity
cel_image.set_pixel(
xx, yy, Color(pixel_color.r, pixel_color.g, pixel_color.b, alpha)
)
image.blend_rect(cel_image, Rect2i(Vector2i.ZERO, project.size), origin)
layer_i += 1
## Blends selected cels of the given frame into passed image starting from the origin position
func blend_selected_cels(
image: Image, frame: Frame, origin := Vector2i.ZERO, project := Global.current_project
) -> void:
for cel_ind in frame.cels.size():
var test_array := [project.current_frame, cel_ind]
if not test_array in project.selected_cels:
continue
if frame.cels[cel_ind] is GroupCel:
continue
if not project.layers[cel_ind].is_visible_in_hierarchy():
continue
var cel: BaseCel = frame.cels[cel_ind]
var cel_image := Image.new()
cel_image.copy_from(cel.get_image())
if cel.opacity < 1: # If we have cel transparency
for xx in cel_image.get_size().x:
for yy in cel_image.get_size().y:
var pixel_color := cel_image.get_pixel(xx, yy)
var alpha: float = pixel_color.a * cel.opacity
cel_image.set_pixel(
xx, yy, Color(pixel_color.r, pixel_color.g, pixel_color.b, alpha)
)
image.blend_rect(cel_image, Rect2i(Vector2i.ZERO, project.size), origin)
func frames_divided_by_spritesheet_lines() -> int:
return ceili(number_of_frames / float(lines_count))

View file

@ -2,10 +2,34 @@ class_name BaseLayer
extends RefCounted
## Base class for layer properties. Different layer types extend from this class.
enum BlendModes {
NORMAL,
DARKEN,
MULTIPLY,
COLOR_BURN,
LINEAR_BURN,
LIGHTEN,
SCREEN,
COLOR_DODGE,
ADD,
OVERLAY,
SOFT_LIGHT,
HARD_LIGHT,
DIFFERENCE,
EXCLUSION,
SUBTRACT,
DIVIDE,
HUE,
SATURATION,
COLOR,
LUMINOSITY
}
var name := ""
var project: Project
var index: int
var parent: BaseLayer
var blend_mode := BlendModes.NORMAL
var visible := true
var locked := false
var new_cels_linked := false
@ -130,6 +154,7 @@ func serialize() -> Dictionary:
"name": name,
"visible": visible,
"locked": locked,
"blend_mode": blend_mode,
"parent": parent.index if is_instance_valid(parent) else -1
}
if not cel_link_sets.is_empty():
@ -148,6 +173,8 @@ 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.get("parent", -1) != -1:
parent = project.layers[dict.parent]
if dict.has("linked_cels") and not dict["linked_cels"].is_empty(): # Backwards compatibility

View file

@ -204,10 +204,10 @@ func set_and_update_preview_image(frame_idx: int) -> void:
var frame := Global.current_project.frames[frame_idx]
selected_cels.resize(Global.current_project.size.x, Global.current_project.size.y)
selected_cels.fill(Color(0, 0, 0, 0))
Export.blend_selected_cels(selected_cels, frame)
DrawingAlgos.blend_selected_cels(selected_cels, frame)
current_frame.resize(Global.current_project.size.x, Global.current_project.size.y)
current_frame.fill(Color(0, 0, 0, 0))
Export.blend_all_layers(current_frame, frame)
DrawingAlgos.blend_all_layers(current_frame, frame)
update_preview()

View file

@ -522,8 +522,9 @@ func change_cel(new_frame: int, new_layer := -1) -> void:
toggle_layer_buttons()
if current_frame < frames.size(): # Set opacity slider
var cel_opacity: float = frames[current_frame].cels[current_layer].opacity
var cel_opacity := frames[current_frame].cels[current_layer].opacity
Global.layer_opacity_slider.value = cel_opacity * 100
Global.animation_timeline.blend_modes_button.selected = layers[current_layer].blend_mode
Global.canvas.queue_redraw()
Global.transparent_checker.update_rect()
Global.cel_changed.emit()

View file

@ -31,7 +31,7 @@ func generate_image(img: Image, shader: Shader, params: Dictionary, size: Vector
RenderingServer.canvas_item_set_material(ci_rid, mat_rid)
for key in params:
var param = params[key]
if param is Texture2D:
if param is Texture2D or param is Texture2DArray:
RenderingServer.material_set_param(mat_rid, key, [param])
else:
RenderingServer.material_set_param(mat_rid, key, param)

View file

@ -0,0 +1,146 @@
shader_type canvas_item;
render_mode unshaded;
const float HCV_EPSILON = 1e-10;
const float HSL_EPSILON = 1e-10;
uniform sampler2DArray layers : filter_nearest;
uniform float[1024] opacities;
uniform int[1024] blend_modes;
uniform vec2[1024] origins;
// Conversion functions from
// https://gist.github.com/unitycoder/aaf94ddfe040ec2da93b58d3c65ab9d9
// licensed under MIT
// Converts from pure Hue to linear RGB
vec3 hue_to_rgb(float hue)
{
float R = abs(hue * 6.0 - 3.0) - 1.0;
float G = 2.0 - abs(hue * 6.0 - 2.0);
float B = 2.0 - abs(hue * 6.0 - 4.0);
return clamp(vec3(R, G, B), 0.0, 1.0);
}
// Converts from HSL to linear RGB
vec3 hsl_to_rgb(vec3 hsl)
{
vec3 rgb = hue_to_rgb(hsl.x);
float C = (1.0 - abs(2.0 * hsl.z - 1.0)) * hsl.y;
return (rgb - 0.5) * C + hsl.z;
}
// Converts a value from linear RGB to HCV (Hue, Chroma, Value)
vec3 rgb_to_hcv(vec3 rgb)
{
// Based on work by Sam Hocevar and Emil Persson
vec4 P = (rgb.g < rgb.b) ? vec4(rgb.bg, -1.0, 2.0/3.0) : vec4(rgb.gb, 0.0, -1.0/3.0);
vec4 Q = (rgb.r < P.x) ? vec4(P.xyw, rgb.r) : vec4(rgb.r, P.yzx);
float C = Q.x - min(Q.w, Q.y);
float H = abs((Q.w - Q.y) / (6.0 * C + HCV_EPSILON) + Q.z);
return vec3(H, C, Q.x);
}
// Converts from linear rgb to HSL
vec3 rgb_to_hsl(vec3 rgb)
{
vec3 HCV = rgb_to_hcv(rgb);
float L = HCV.z - HCV.y * 0.5;
float S = HCV.y / (1.0 - abs(L * 2.0 - 1.0) + HSL_EPSILON);
return vec3(HCV.x, S, L);
}
vec4 blend(int blend_type, vec4 current_color, vec4 prev_color, float opacity) {
vec4 result;
if (current_color.a <= 0.01) {
return prev_color;
}
current_color.rgb = current_color.rgb * opacity; // Premultiply with the layer texture's alpha to prevent semi transparent pixels from being too bright (ALL LAYER TYPES!)
current_color.a = current_color.a * opacity; // Combine the layer opacity
switch(blend_type) {
case 1: // Darken
result.rgb = min(prev_color.rgb, current_color.rgb);
break;
case 2: // Multiply
result.rgb = prev_color.rgb * current_color.rgb;
break;
case 3: // Color burn
result.rgb = 1.0 - (1.0 - prev_color.rgb) / current_color.rgb;
break;
case 4: // Linear burn
result.rgb = prev_color.rgb + current_color.rgb - 1.0;
break;
case 5: // Lighten
result.rgb = max(prev_color.rgb, current_color.rgb);
break;
case 6: // Screen
result.rgb = mix(prev_color.rgb, 1.0 - (1.0 - prev_color.rgb) * (1.0 - current_color.rgb), current_color.a);
break;
case 7: // Color dodge
result.rgb = prev_color.rgb / (1.0 - current_color.rgb);
break;
case 8: // Add (linear dodge)
result.rgb = prev_color.rgb + current_color.rgb;
break;
case 9: // Overlay
result.rgb = mix(2.0 * prev_color.rgb * current_color.rgb, 1.0 - 2.0 * (1.0 - current_color.rgb) * (1.0 - prev_color.rgb), round(prev_color.rgb));
break;
case 10: // Soft light
result.rgb = mix(2.0 * prev_color.rgb * current_color.rgb + prev_color.rgb * prev_color.rgb * (1.0 - 2.0 * current_color.rgb), sqrt(prev_color.rgb) * (2.0 * current_color.rgb - 1.0) + (2.0 * prev_color.rgb) * (1.0 - current_color.rgb), round(prev_color.rgb));
break;
case 11: // Hard light
result.rgb = mix(2.0 * prev_color.rgb * current_color.rgb, 1.0 - 2.0 * (1.0 - current_color.rgb) * (1.0 - prev_color.rgb), round(current_color.rgb));
break;
case 12: // Difference
result.rgb = abs(prev_color.rgb - current_color.rgb);
break;
case 13: // Exclusion
result.rgb = prev_color.rgb + current_color.rgb - 2.0 * prev_color.rgb * current_color.rgb;
break;
case 14: // Subtract
result.rgb = prev_color.rgb - current_color.rgb;
break;
case 15: // Divide
result.rgb = prev_color.rgb / current_color.rgb;
break;
case 16: // Hue
vec3 current_hsl = rgb_to_hsl(current_color.rgb);
vec3 prev_hsl = rgb_to_hsl(prev_color.rgb);
result.rgb = hsl_to_rgb(vec3(current_hsl.r, prev_hsl.g, prev_hsl.b));
break;
case 17: // Saturation
vec3 current_hsl = rgb_to_hsl(current_color.rgb);
vec3 prev_hsl = rgb_to_hsl(prev_color.rgb);
result.rgb = hsl_to_rgb(vec3(prev_hsl.r, current_hsl.g, prev_hsl.b));
break;
case 18: // Color
vec3 current_hsl = rgb_to_hsl(current_color.rgb);
vec3 prev_hsl = rgb_to_hsl(prev_color.rgb);
result.rgb = hsl_to_rgb(vec3(current_hsl.r, current_hsl.g, prev_hsl.b));
break;
case 19: // Luminosity
vec3 current_hsl = rgb_to_hsl(current_color.rgb);
vec3 prev_hsl = rgb_to_hsl(prev_color.rgb);
result.rgb = hsl_to_rgb(vec3(prev_hsl.r, prev_hsl.g, current_hsl.b));
break;
default: // Normal (case 0)
result.rgb = prev_color.rgb * (1.0 - current_color.a) + current_color.rgb;
break;
}
result.a = prev_color.a * (1.0 - current_color.a) + current_color.a;
result = clamp(result, 0.0, 1.0);
return mix(current_color, result, prev_color.a);
}
void fragment() {
vec4 col = texture(layers, vec3(UV - origins[0], 0.0));
col.a *= opacities[0];
for(int i = 1; i < textureSize(layers, 0).z; i++) // Loops through every layer
{
col = blend(blend_modes[i], texture(layers, vec3(UV - origins[i], float(i))), col, opacities[i]);
}
COLOR = col;
}

View file

@ -6,7 +6,7 @@ const CURSOR_SPEED_RATE := 6.0
var current_pixel := Vector2.ZERO
var sprite_changed_this_frame := false ## For optimization purposes
var move_preview_location := Vector2.ZERO
var move_preview_location := Vector2i.ZERO
@onready var currently_visible_frame := $CurrentlyVisibleFrame as SubViewport
@onready var current_frame_drawer := $CurrentlyVisibleFrame/CurrentFrameDrawer as Node2D
@ -34,32 +34,18 @@ func _ready() -> void:
func _draw() -> void:
var current_cels: Array = (
Global.current_project.frames[Global.current_project.current_frame].cels
)
var position_tmp := position
var scale_tmp := scale
if Global.mirror_view:
position_tmp.x = position_tmp.x + Global.current_project.size.x
scale_tmp.x = -1
draw_set_transform(position_tmp, rotation, scale_tmp)
# Draw current frame layers
for i in range(Global.current_project.layers.size()):
if current_cels[i] is GroupCel:
continue
var modulate_color := Color(1, 1, 1, current_cels[i].opacity)
if Global.current_project.layers[i].is_visible_in_hierarchy():
var selected_layers = []
if move_preview_location != Vector2.ZERO:
for cel_pos in Global.current_project.selected_cels:
if cel_pos[0] == Global.current_project.current_frame:
if Global.current_project.layers[cel_pos[1]].can_layer_get_drawn():
selected_layers.append(cel_pos[1])
if i in selected_layers:
draw_texture(current_cels[i].image_texture, move_preview_location, modulate_color)
else:
draw_texture(current_cels[i].image_texture, Vector2.ZERO, modulate_color)
# Placeholder so we can have a material here
draw_texture(
Global.current_project.frames[Global.current_project.current_frame].cels[0].image_texture,
Vector2.ZERO
)
draw_layers()
if Global.onion_skinning:
refresh_onion()
currently_visible_frame.size = Global.current_project.size
@ -133,6 +119,33 @@ func update_selected_cels_textures(project := Global.current_project) -> void:
current_cel.update_texture()
func draw_layers() -> void:
var current_cels := Global.current_project.frames[Global.current_project.current_frame].cels
var textures: Array[Image] = []
var opacities := PackedFloat32Array()
var blend_modes := PackedInt32Array()
var origins := PackedVector2Array()
# Draw current frame layers
for i in Global.current_project.layers.size():
if current_cels[i] is GroupCel:
continue
if Global.current_project.layers[i].is_visible_in_hierarchy():
var cel_image := current_cels[i].get_image()
textures.append(cel_image)
opacities.append(current_cels[i].opacity)
if [Global.current_project.current_frame, i] in Global.current_project.selected_cels:
origins.append(Vector2(move_preview_location) / Vector2(cel_image.get_size()))
else:
origins.append(Vector2.ZERO)
blend_modes.append(Global.current_project.layers[i].blend_mode)
var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures)
material.set_shader_parameter("layers", texture_array)
material.set_shader_parameter("opacities", opacities)
material.set_shader_parameter("blend_modes", blend_modes)
material.set_shader_parameter("origins", origins)
func refresh_onion() -> void:
onion_past.queue_redraw()
onion_future.queue_redraw()

View file

@ -1,6 +1,7 @@
[gd_scene load_steps=18 format=3 uid="uid://ba24iuv55m4l3"]
[gd_scene load_steps=20 format=3 uid="uid://ba24iuv55m4l3"]
[ext_resource type="Script" path="res://src/UI/Canvas/Canvas.gd" id="1"]
[ext_resource type="Shader" path="res://src/Shaders/BlendLayers.gdshader" id="1_253dh"]
[ext_resource type="Script" path="res://src/UI/Canvas/Grid.gd" id="2"]
[ext_resource type="Script" path="res://src/UI/Canvas/Indicators.gd" id="3"]
[ext_resource type="Script" path="res://src/UI/Canvas/TileMode.gd" id="4"]
@ -15,6 +16,12 @@
[ext_resource type="Script" path="res://src/UI/Canvas/CropRect.gd" id="13"]
[ext_resource type="Script" path="res://src/UI/Canvas/Gizmos3D.gd" id="14"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_6b0ox"]
shader = ExtResource("1_253dh")
shader_parameter/opacities = null
shader_parameter/blend_modes = null
shader_parameter/origins = null
[sub_resource type="CanvasItemMaterial" id="1"]
blend_mode = 4
@ -31,6 +38,7 @@ shader_parameter/stripe_direction = 0.5
shader = ExtResource("10")
[node name="Canvas" type="Node2D"]
material = SubResource("ShaderMaterial_6b0ox")
script = ExtResource("1")
[node name="CurrentlyVisibleFrame" type="SubViewport" parent="."]
@ -40,6 +48,7 @@ handle_input_locally = false
render_target_update_mode = 3
[node name="CurrentFrameDrawer" type="Node2D" parent="CurrentlyVisibleFrame"]
material = SubResource("ShaderMaterial_6b0ox")
script = ExtResource("5")
[node name="TileMode" type="Node2D" parent="."]

View file

@ -1,13 +1,12 @@
extends Node2D
enum Mode { TIMELINE, SPRITESHEET }
var mode: int = Mode.TIMELINE
var mode := Mode.TIMELINE
var h_frames := 1
var v_frames := 1
var start_sprite_sheet_frame := 1
var end_sprite_sheet_frame := 1
var sprite_frames := []
var frame_index := 0
@onready var animation_timer := $AnimationTimer as Timer
@ -19,81 +18,78 @@ func _ready() -> void:
func _draw() -> void:
var current_project := Global.current_project
var project := Global.current_project
match mode:
Mode.TIMELINE:
var modulate_color := Color.WHITE
if frame_index >= current_project.frames.size():
frame_index = current_project.current_frame
if frame_index >= project.frames.size():
frame_index = project.current_frame
if animation_timer.is_stopped():
frame_index = current_project.current_frame
var frame := current_project.frames[frame_index]
animation_timer.wait_time = frame.duration * (1.0 / current_project.fps)
var current_cels := frame.cels
# Draw current frame layers
for i in range(current_cels.size()):
var cel := current_cels[i]
if cel is GroupCel:
continue
modulate_color = Color(1, 1, 1, cel.opacity)
if (
i < current_project.layers.size()
and current_project.layers[i].is_visible_in_hierarchy()
):
draw_texture(cel.image_texture, Vector2.ZERO, modulate_color)
frame_index = project.current_frame
var frame := project.frames[frame_index]
animation_timer.wait_time = frame.duration * (1.0 / project.fps)
var texture := frame.cels[0].image_texture
draw_texture(texture, Vector2.ZERO) # Placeholder so we can have a material here
Mode.SPRITESHEET:
var texture_to_draw: ImageTexture
var target_frame: Frame = current_project.frames[current_project.current_frame]
var frame_image := Image.create(
current_project.size.x, current_project.size.y, false, Image.FORMAT_RGBA8
)
Export.blend_all_layers(frame_image, target_frame)
sprite_frames = _split_spritesheet(frame_image, h_frames, v_frames)
# limit start and end
if end_sprite_sheet_frame > sprite_frames.size():
end_sprite_sheet_frame = sprite_frames.size()
var image := project.frames[project.current_frame].cels[0].get_image()
var slices := _split_spritesheet(image, h_frames, v_frames)
# Limit start and end
if end_sprite_sheet_frame > slices.size():
end_sprite_sheet_frame = slices.size()
if start_sprite_sheet_frame < 0:
start_sprite_sheet_frame = 0
# reset frame if required
if frame_index >= end_sprite_sheet_frame:
frame_index = start_sprite_sheet_frame - 1
texture_to_draw = sprite_frames[frame_index]
draw_texture(texture_to_draw, Vector2.ZERO)
var rect := Rect2(Vector2.ZERO, texture_to_draw.get_image().get_size())
var src_rect := slices[frame_index]
var rect := Rect2(Vector2.ZERO, src_rect.size)
var texture := project.frames[project.current_frame].cels[0].image_texture
# Placeholder so we can have a material here
draw_texture_rect_region(texture, rect, src_rect)
transparent_checker.fit_rect(rect)
_draw_layers()
func _draw_layers() -> void:
var current_cels := Global.current_project.frames[frame_index].cels
var textures: Array[Image] = []
var opacities := PackedFloat32Array()
var blend_modes := PackedInt32Array()
# Draw current frame layers
for i in Global.current_project.layers.size():
if current_cels[i] is GroupCel:
continue
if Global.current_project.layers[i].is_visible_in_hierarchy():
textures.append(current_cels[i].get_image())
opacities.append(current_cels[i].opacity)
blend_modes.append(Global.current_project.layers[i].blend_mode)
var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures)
material.set_shader_parameter("layers", texture_array)
material.set_shader_parameter("opacities", opacities)
material.set_shader_parameter("blend_modes", blend_modes)
func _on_AnimationTimer_timeout() -> void:
match mode:
Mode.TIMELINE:
var current_project := Global.current_project
var project := Global.current_project
var first_frame := 0
var last_frame := current_project.frames.size() - 1
var last_frame := project.frames.size() - 1
if Global.play_only_tags:
for tag in current_project.animation_tags:
if (
current_project.current_frame + 1 >= tag.from
&& current_project.current_frame + 1 <= tag.to
):
for tag in project.animation_tags:
if project.current_frame + 1 >= tag.from && project.current_frame + 1 <= tag.to:
first_frame = tag.from - 1
last_frame = min(current_project.frames.size() - 1, tag.to - 1)
last_frame = mini(project.frames.size() - 1, tag.to - 1)
if frame_index < last_frame:
frame_index += 1
else:
frame_index = first_frame
animation_timer.wait_time = (
current_project.frames[frame_index].duration * (1.0 / current_project.fps)
)
animation_timer.wait_time = project.frames[frame_index].duration * (1.0 / project.fps)
Mode.SPRITESHEET:
frame_index += 1
animation_timer.wait_time = (1.0 / Global.current_project.fps)
animation_timer.wait_time = 1.0 / Global.current_project.fps
animation_timer.set_one_shot(true)
animation_timer.start()
queue_redraw()
@ -103,18 +99,13 @@ func _cel_changed() -> void:
queue_redraw()
func _split_spritesheet(image: Image, horiz: int, vert: int) -> Array:
var result := []
func _split_spritesheet(image: Image, horiz: int, vert: int) -> Array[Rect2]:
var result: Array[Rect2] = []
horiz = mini(horiz, image.get_size().x)
vert = mini(vert, image.get_size().y)
var frame_width := image.get_size().x / horiz
var frame_height := image.get_size().y / vert
for yy in range(vert):
for xx in range(horiz):
var cropped_image := Image.new()
var rect := Rect2(frame_width * xx, frame_height * yy, frame_width, frame_height)
cropped_image = image.get_region(rect)
cropped_image.convert(Image.FORMAT_RGBA8)
var tex := ImageTexture.create_from_image(cropped_image)
result.append(tex)
result.append(Rect2(frame_width * xx, frame_height * yy, frame_width, frame_height))
return result

View file

@ -1,8 +1,15 @@
[gd_scene load_steps=2 format=3 uid="uid://c546tskdu53j1"]
[gd_scene load_steps=4 format=3 uid="uid://c546tskdu53j1"]
[ext_resource type="Script" path="res://src/UI/Canvas/CanvasPreview.gd" id="1"]
[ext_resource type="Shader" path="res://src/Shaders/BlendLayers.gdshader" id="1_28j41"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_21d5l"]
shader = ExtResource("1_28j41")
shader_parameter/opacities = null
shader_parameter/blend_modes = null
[node name="CanvasPreview" type="Node2D"]
material = SubResource("ShaderMaterial_21d5l")
script = ExtResource("1")
[node name="AnimationTimer" type="Timer" parent="."]

View file

@ -2,15 +2,8 @@ extends Node2D
func _draw() -> void:
var current_cels: Array = (
Global.current_project.frames[Global.current_project.current_frame].cels
# Placeholder so we can have a material here
draw_texture(
Global.current_project.frames[Global.current_project.current_frame].cels[0].image_texture,
Vector2.ZERO
)
for i in range(Global.current_project.layers.size()):
if current_cels[i] is GroupCel:
continue
if (
Global.current_project.layers[i].is_visible_in_hierarchy()
and current_cels[i].opacity > 0
):
var modulate_color := Color(1, 1, 1, current_cels[i].opacity)
draw_texture(current_cels[i].image_texture, Vector2.ZERO, modulate_color)

View file

@ -907,7 +907,7 @@ func clear_selection(use_undo := false) -> void:
func _get_preview_image() -> void:
var project := Global.current_project
var blended_image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
Export.blend_selected_cels(blended_image, project.frames[project.current_frame])
DrawingAlgos.blend_selected_cels(blended_image, project.frames[project.current_frame])
if original_preview_image.is_empty():
original_preview_image = blended_image.get_region(big_bounding_rectangle)
# For non-rectangular selections

View file

@ -10,7 +10,7 @@ var param_names: PackedStringArray = []
func _about_to_popup() -> void:
Global.canvas.selection.transform_content_confirm()
var frame := Global.current_project.frames[Global.current_project.current_frame]
Export.blend_selected_cels(selected_cels, frame)
DrawingAlgos.blend_selected_cels(selected_cels, frame)
preview_image.copy_from(selected_cels)
preview_texture = ImageTexture.create_from_image(preview_image)

View file

@ -125,7 +125,7 @@ func change_mask():
var tiles := Global.current_project.tiles
var tiles_size := tiles.tile_size
var image := Image.create(tiles_size.x, tiles_size.y, false, Image.FORMAT_RGBA8)
Export.blend_all_layers(image, current_frame)
DrawingAlgos.blend_all_layers(image, current_frame)
if (
image.get_used_rect().size == Vector2i.ZERO
or not $VBoxContainer/HBoxContainer/Masking.button_pressed

View file

@ -74,7 +74,7 @@ func capture_frame() -> void:
else:
var frame := project.frames[project.current_frame]
image = Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
Export.blend_all_layers(image, frame, Vector2i.ZERO, project)
DrawingAlgos.blend_all_layers(image, frame, Vector2i.ZERO, project)
if mode == Mode.CANVAS:
if resize != 100:

View file

@ -19,6 +19,7 @@ var frame_button_node := preload("res://src/UI/Timeline/FrameButton.tscn")
@onready var tag_spacer = find_child("TagSpacer")
@onready var start_spacer = find_child("StartSpacer")
@onready var add_layer_list: MenuButton = $"%AddLayerList"
@onready var blend_modes_button := %BlendModes as OptionButton
@onready var timeline_scroll: ScrollContainer = find_child("TimelineScroll")
@onready var frame_scroll_container: Control = find_child("FrameScrollContainer")
@ -152,6 +153,21 @@ func _cel_size_changed(value: int) -> void:
tag_c.get_node("Line2D").points[3] = Vector2(tag_c.custom_minimum_size.x, 32)
func _on_blend_modes_item_selected(index: BaseLayer.BlendModes) -> void:
var current_layer := Global.current_project.layers[Global.current_project.current_layer]
var previous_index := current_layer.blend_mode
Global.current_project.undo_redo.create_action("Set Blend Mode")
Global.current_project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
Global.current_project.undo_redo.add_do_property(current_layer, "blend_mode", index)
Global.current_project.undo_redo.add_do_method(blend_modes_button.select.bind(index))
Global.current_project.undo_redo.add_do_method(Global.canvas.draw_layers)
Global.current_project.undo_redo.add_undo_property(current_layer, "blend_mode", previous_index)
Global.current_project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
Global.current_project.undo_redo.add_undo_method(blend_modes_button.select.bind(previous_index))
Global.current_project.undo_redo.add_undo_method(Global.canvas.draw_layers)
Global.current_project.undo_redo.commit_action()
func add_frame() -> void:
var project := Global.current_project
var frame_add_index := project.current_frame + 1
@ -842,21 +858,30 @@ func _on_MergeDownLayer_pressed() -> void:
for frame in project.frames:
top_cels.append(frame.cels[top_layer.index]) # Store for undo purposes
var top_image := Image.new()
top_image.copy_from(frame.cels[top_layer.index].get_image())
if frame.cels[top_layer.index].opacity < 1: # If we have layer transparency
for xx in top_image.get_size().x:
for yy in top_image.get_size().y:
var pixel_color := top_image.get_pixel(xx, yy)
var alpha := pixel_color.a * frame.cels[top_layer.index].opacity
top_image.set_pixel(
xx, yy, Color(pixel_color.r, pixel_color.g, pixel_color.b, alpha)
)
var top_image := frame.cels[top_layer.index].get_image()
var bottom_cel := frame.cels[bottom_layer.index]
var bottom_image := Image.new()
bottom_image.copy_from(bottom_cel.get_image())
bottom_image.blend_rect(top_image, Rect2i(Vector2i.ZERO, project.size), Vector2i.ZERO)
var textures: Array[Image] = []
var opacities := PackedFloat32Array()
var blend_modes := PackedInt32Array()
textures.append(bottom_cel.get_image())
opacities.append(bottom_cel.opacity)
blend_modes.append(bottom_layer.blend_mode)
textures.append(top_image)
opacities.append(frame.cels[top_layer.index].opacity)
blend_modes.append(top_layer.blend_mode)
var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures)
var params := {
"layers": texture_array,
"opacities": opacities,
"blend_modes": blend_modes,
}
var bottom_image := Image.create(
top_image.get_width(), top_image.get_height(), false, top_image.get_format()
)
var gen := ShaderImageEffect.new()
gen.generate_image(bottom_image, DrawingAlgos.blend_layers_shader, params, project.size)
if (
bottom_cel.link_set != null
and bottom_cel.link_set.size() > 1

View file

@ -371,6 +371,61 @@ size_flags_horizontal = 0
size_flags_vertical = 0
texture = ExtResource("5")
[node name="HBoxContainer" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Blend mode:"
[node name="BlendModes" type="OptionButton" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
item_count = 20
selected = 0
popup/item_0/text = "Normal"
popup/item_0/id = 0
popup/item_1/text = "Darken"
popup/item_1/id = 1
popup/item_2/text = "Multiply"
popup/item_2/id = 2
popup/item_3/text = "Color burn"
popup/item_3/id = 3
popup/item_4/text = "Linear burn"
popup/item_4/id = 4
popup/item_5/text = "Lighten"
popup/item_5/id = 5
popup/item_6/text = "Screen"
popup/item_6/id = 6
popup/item_7/text = "Color dodge"
popup/item_7/id = 7
popup/item_8/text = "Add"
popup/item_8/id = 8
popup/item_9/text = "Overlay"
popup/item_9/id = 9
popup/item_10/text = "Soft Light"
popup/item_10/id = 10
popup/item_11/text = "Hard light"
popup/item_11/id = 11
popup/item_12/text = "Difference"
popup/item_12/id = 12
popup/item_13/text = "Exclusion"
popup/item_13/id = 13
popup/item_14/text = "Subtract"
popup/item_14/id = 14
popup/item_15/text = "Divide"
popup/item_15/id = 15
popup/item_16/text = "Hue"
popup/item_16/id = 16
popup/item_17/text = "Saturation"
popup/item_17/id = 17
popup/item_18/text = "Color"
popup/item_18/id = 18
popup/item_19/text = "Luminosity"
popup/item_19/id = 19
[node name="BlendingHBox" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 10
@ -920,6 +975,7 @@ script = ExtResource("12")
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/MoveDownLayer" to="." method="change_layer_order" binds= [false]]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/CloneLayer" to="." method="_on_CloneLayer_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/MergeDownLayer" to="." method="_on_MergeDownLayer_pressed"]
[connection signal="item_selected" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/HBoxContainer/BlendModes" to="." method="_on_blend_modes_item_selected"]
[connection signal="value_changed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/BlendingHBox/OpacitySlider" to="." method="_on_OpacitySlider_value_changed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons/AddFrame" to="." method="add_frame"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons/DeleteFrame" to="." method="_on_DeleteFrame_pressed"]