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:
parent
6247ab2252
commit
8de9697be0
|
@ -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] = []
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
146
src/Shaders/BlendLayers.gdshader
Normal file
146
src/Shaders/BlendLayers.gdshader
Normal 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;
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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="."]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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="."]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
Loading…
Reference in a new issue