1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-20 10:09:48 +00:00
Pixelorama/src/Autoload/Export.gd
mrtripie 1fa34d7196
Basic Layer Groups and Timeline Refactor (#698)
* Fixed issues with Shading tool Saturation and Value not always being right in Hue Shading mode

* Shading tool hue shifting fixes and tweaks

* Bringing over changes from layer groups brach, without any changes to layer blending

* Some quick fixes to make it work again

* Fixed some of the places where GroupLayers cause errors. Cel Buttons are now hidden when groups are collapsed

* Layer drag highlighting (need to actually drop them correctly, also need to do cels)

* Added more layer hierarchy related functions, organized the function order in the Layer classes a bit

* Switched the layer type changing from string to int

* Moved layer type enum to Global

* Added get_layer_type_name(), currently used for the default layer name

* Renamed the layer get_children/is_a_parent_of functions

* changed get_layer_type_name() to get_default_name(number)

* New layer drag and dropping behavior

* Added read/write_image_data_from/to_pxo functions to Cel classes to handle saving/loading the binary image data for each cel type

* Fixed warning

* Added a line to child layers wich makes it easier to see where they are in the hierarchy

* Fixed debugger warning

* Fixed all cel types loading as PixelCels

* Fixed spacing issue with cels when collapsing groups

* Fixed bug when dropping a child layer to the bottom region of its parent group, where it would end up to far down (maybe disappearing)

* updated temporary todo comments

* Created a base scene for layer buttons and merged layer button script into one

* Prevent the case of parenting to itself in layer drag and drop, fixed static reference to LayerButton still being BaseLayerButton

* Use a base scene for CelButtons

* First bit of the refactoring work

* Several bits of refactoring

* Fixed moving cels

* Cleaned up Project.move_cel function

* Fixed project_layer_removed

* Updated change_frame_order on FrameButton. Some (not all) work on getting the layer UI updated when pressing buttons such as collapse/visible/lock

* Bug fixes. Updating layer button's buttons

* Fixed timeline selection issues when creating a new project. Some code cleanup

* tweaks

* Removed a bunch of commented out code

* Removing more commented out code

* Fixed bugs with timeline selectio. Fixed cels being placed in the reverse layer order when adding a frame

* Changed add/remove_frame to add/remove_frames (multiple support)

* Refactored copy_frames in animation timeline

* added copy function to cel classes

* added layer copy function

* simplifed copy_frames a tiny bit

* Updated TODO comments to categorize them and remove any that were already done

* Turned Project.add/remove_layer into Project.add/remove_layers (multiple support), not yet tested

* Seperated the layer cloning functionality in timeline's add_layer to its own function, since they're only used by one button, renamed to _on_Button_pressed naming scheme, added children support to the delete layer button

* some TODOs

* Added layer swapping

* Added priorities to refactor TODOs

* Simplified layer swapping code a little

* Fixed performance regression on changing project, updated TODOs

* Included _on_MergeDownLayer_pressed in timeline refactor

* Cleaned up _on_MergeDownLayer_pressed refactor

* If all frames are selected, prevent being able to remove all of them

* Fixed cel linking when cloning layers/frames. Moved the copy function from cel classes to layer classes, splitting into copy_cel and copy_all_cels

* Combined and rewrote the 2 project _toggle_layer_buttons_.. functions into 1 simpler _toggle_layer_buttons function

* Simplified _toggle_layer_buttons some more

* Added hierarchy support for move up/down layer buttons

* Added toggle_frame_buttons method to project (extracted from  _frame_changed). Called from main when setting up startup project. Removed _ from start of _toggle_layer_buttons name

* Fixed duplicate_layers parent references being to the original layers

* cleaned up project.move_layers method a bit

* TODOs

* moved the transform_content_confirm calls for the layer buttons in AnimationTimeline (Add/remove/clone) to the project layer modification functions

* animation first/last_frame tweaks and un-press play buttons when the first/last_frame are the same in _on_AnimationTimer_timeout in AnimationTimeline

* Cleaned up project_changed in ANimationTimeline a bit

* Cleaned up project_layer_added in AnimationTimeline

* Changed Layer classes get_default_name to set_name_to_default

* Cleaned up LayerButton.drop_data slightly

* Looked at some of my TODOs

* cleaned up copying cels

* Fixed CelButton linked_indicator not showing up right away when becoming linked

* Cleand up link/unlink cel menu option a little. Fixed situatoin where trying to call button_setup on cel_button that doesn't exist anymore due to undo/redo

* Fixed regression with copy_cel (linked) in when cloning a frame

* Minor cleanup, more detailed comments, updated TODOs

* more improved comments

* Made focus_mode on Cel/Layer/FrameButton NONE to fix bug where it looks like one is selected after pressing it and adding a new Layer/Frame (but its just in the focus state, not the pressed state

* Made AnimationTimeline.change_layer_order work a little more consistantly with LayerButton.drop_data, and fixed a minor bug in it

* Updated comments and TODOs

* cleanup

* removed some code that should no longer be needed

* updated comment

* removed Project's frames and layers setters _frames_changed and _layers_changed

* Made some 'for x in range(array.size())' just 'for x in array.size()'

* updated comments/TODOs

* Cel content changes intial

* Added 'content' methods to Cel classes

* Removed image var from PixelCelButton

* Reusing PixelCelButton.gd on GroupCelButton scene

* Renamed PixelCelButton.gd to CelButton.gd (as it will be used for all Cel Buttons) and deleted GroupCelButton.gd

* Hide the TransparentChecker on GroupCelButton.tscn until a preview texture is added for GroupCels

* TODOs, prevent memory leak when closing projects

* Link/unlink cel cleanup
:

* Added _project param to _init methods of Layer classes

* Added update_texture method to Cel classes (moving part from the update_texture and update_selected_cels_textures methods from Canvas.gd

* Removed a temporary check (which also fixed another bug)

* Clone child layers when cloning a layer

* Added temp dummy get_image method to GroupCel, and use get_image when copying or picking colors

* TODOs

* Made open_image_as_spritesheet_layer work after the timeline refactor (still doesn't work with groups yet though). TODO comment updates

* Added create_new_cel methods to Layer classes

* Updated TODOs and comments

* Renamed Layer class's create_empty_cel to new_empty_cel to match Project's new_emtpy_frame

* Renamed create_layer/cel_button to instantiate_layer/cel_button

* updated TODOs

* prioritized TODOs

* Fixed some warnings

* removed commented out code from previous commit

* Fixed export

* Made open_image_as_new_frame work after timeline refactor

* Fixed open_image_as_new_layer after timeline refactor

* Some linked cel fixes

* More linked cels fixes

* cleanup

* Optimized importing spreadsheet as new layer

* Fixed Scale Image crash with Groups

* Fixed onion skin with groups

* Removed blend_mode from BaseLayer for now

* Mostly fixed image effects

* Fixed resize canvas

* Fixed drag and drop not working with Cel Buttons on Group Layers

* updated TODOs

* Renamed Replace Frame (in open image) to Replace Cel

* Continued renaming Replace Frame to Replace Cel

* Made open_image_at_cels work after timeline refactor

* Added get_layer_path method to BaseLayer

* Replaced AtLayerSpinbox with AtLayerOption for Open Image as New Frame or Replace Cel

* Updated TODOs

* updated TODOs

* Comments for cel content methods

* fixed right clicking group cel button deselecting the button (even though cel is still selected

* frame/layer modification methods comments

* Removed unneeded size flags

* TODO updates

* Removed a loop that would never run from open_image_as_spritesheet_tab

* TODO update

* Combined BaseLayer.get_children_direct and get_children_recursive into a single get_children method with a bool for recursive. Added a get_child_count method

* Removed unneeded frame paramaters from _on_DeleteFrame_pressed and _on_CopyFrame_pressed

* TODO Updates

* Removed unneeded code from delete_frames

* Made delete_frames variable names more consistent with my other changes

* Continuation

* made variable names in copy_frames more consistent with rest of changes

* Update TODOs

* Removed TODOs for after this PR (moved to my notes)

* Fixed crash when pasting image on Group

* Fixed layer .visible check to be is_visible_in_hierarchy()

* Removed some drag highlight polish code that didn't work

* Removed code from Canvas update_texture and update_selected_cels_textures that was redundant

* gdformat

* gdformat

* gdlint fixes

* Fixed Cel button not having its linked indicator show when enabling new cels linked on a layer other than the current layer

* Fixed crop image and centralize image

* Added '# gdlint: ignore=max-public-methods' to the top of Project'

* Fixed dragging cels to layer of different type crash

* Formatted CelButton.gd

Co-authored-by: MrTriPie <MrTriPie>
2022-09-28 21:59:49 +03:00

434 lines
14 KiB
GDScript

extends Node
enum ExportTab { FRAME = 0, SPRITESHEET = 1, ANIMATION = 2 }
enum Orientation { ROWS = 0, COLUMNS = 1 }
enum AnimationType { MULTIPLE_FILES = 0, ANIMATED = 1 }
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
enum FileFormat { PNG = 0, GIF = 1 }
# Gif exporter
const GIFExporter = preload("res://addons/gdgifexporter/exporter.gd")
const MedianCutQuantization = preload("res://addons/gdgifexporter/quantization/median_cut.gd")
var current_tab: int = ExportTab.FRAME
# Frame options
var frame_number := 1
# All frames and their layers processed/blended into images
var processed_images = [] # Image[]
# Spritesheet options
var frame_current_tag := 0 # Export only current frame tag
var number_of_frames := 1
var orientation: int = Orientation.ROWS
var lines_count := 1 # How many rows/columns before new line is added
var animation_type: int = AnimationType.MULTIPLE_FILES
var direction: int = AnimationDirection.FORWARD
# Options
var resize := 100
var interpolation := 0 # Image.Interpolation
var new_dir_for_each_frame_tag: bool = true # you don't need to store this after export
# Export directory path and export file name
var directory_path := ""
var file_name := "untitled"
var file_format: int = FileFormat.PNG
var was_exported: bool = false
# Export coroutine signal
var stop_export = false
var file_exists_alert = "File %s already exists. Overwrite?"
# Export progress variables
var export_progress_fraction := 0.0
var export_progress := 0.0
onready var gif_export_thread := Thread.new()
func _exit_tree() -> void:
if gif_export_thread.is_active():
gif_export_thread.wait_to_finish()
func external_export() -> void:
match current_tab:
ExportTab.FRAME:
process_frame()
ExportTab.SPRITESHEET:
process_spritesheet()
ExportTab.ANIMATION:
process_animation()
export_processed_images(true, Global.export_dialog)
func process_frame() -> void:
processed_images.clear()
var frame = Global.current_project.frames[frame_number - 1]
var image := Image.new()
image.create(
Global.current_project.size.x, Global.current_project.size.y, false, Image.FORMAT_RGBA8
)
blend_layers(image, frame)
processed_images.append(image)
func process_spritesheet() -> void:
processed_images.clear()
# Range of frames determined by tags
var frames := []
if frame_current_tag > 0:
var frame_start = Global.current_project.animation_tags[frame_current_tag - 1].from
var frame_end = Global.current_project.animation_tags[frame_current_tag - 1].to
frames = Global.current_project.frames.slice(frame_start - 1, frame_end - 1, 1, true)
else:
frames = Global.current_project.frames
# Then store the size of frames for other functions
number_of_frames = frames.size()
# If rows mode selected calculate columns count and vice versa
var spritesheet_columns = (
lines_count
if orientation == Orientation.ROWS
else frames_divided_by_spritesheet_lines()
)
var spritesheet_rows = (
lines_count
if orientation == Orientation.COLUMNS
else frames_divided_by_spritesheet_lines()
)
var width = Global.current_project.size.x * spritesheet_columns
var height = Global.current_project.size.y * spritesheet_rows
var whole_image := Image.new()
whole_image.create(width, height, false, Image.FORMAT_RGBA8)
var origin := Vector2.ZERO
var hh := 0
var vv := 0
for frame in frames:
if orientation == Orientation.ROWS:
if vv < spritesheet_columns:
origin.x = Global.current_project.size.x * vv
vv += 1
else:
hh += 1
origin.x = 0
vv = 1
origin.y = Global.current_project.size.y * hh
else:
if hh < spritesheet_rows:
origin.y = Global.current_project.size.y * hh
hh += 1
else:
vv += 1
origin.y = 0
hh = 1
origin.x = Global.current_project.size.x * vv
blend_layers(whole_image, frame, origin)
processed_images.append(whole_image)
func process_animation() -> void:
processed_images.clear()
for frame in Global.current_project.frames:
var image := Image.new()
image.create(
Global.current_project.size.x, Global.current_project.size.y, false, Image.FORMAT_RGBA8
)
blend_layers(image, frame)
processed_images.append(image)
func export_processed_images(ignore_overwrites: bool, export_dialog: AcceptDialog) -> bool:
# Stop export if directory path or file name are not valid
var dir = Directory.new()
if not dir.dir_exists(directory_path) or not file_name.is_valid_filename():
if not dir.dir_exists(directory_path) and file_name.is_valid_filename():
export_dialog.open_path_validation_alert_popup(0)
elif not file_name.is_valid_filename() and dir.dir_exists(directory_path):
export_dialog.open_path_validation_alert_popup(1)
else:
export_dialog.open_path_validation_alert_popup()
return false
# Check export paths
var export_paths = []
for i in range(processed_images.size()):
stop_export = false
var multiple_files := (
true
if (
current_tab == ExportTab.ANIMATION
and animation_type == AnimationType.MULTIPLE_FILES
)
else false
)
var export_path = create_export_path(multiple_files, i + 1)
# If user want to create new directory for each animation tag then check
# if directories exist and create them if not
if multiple_files and new_dir_for_each_frame_tag:
var frame_tag_directory := Directory.new()
if not frame_tag_directory.dir_exists(export_path.get_base_dir()):
frame_tag_directory.open(directory_path)
frame_tag_directory.make_dir(export_path.get_base_dir().get_file())
# Check if the file already exists
var file_check: File = File.new()
if file_check.file_exists(export_path):
# Ask user if they want to overwrite the file
if not was_exported or (was_exported and not ignore_overwrites):
# Overwrite existing file?
export_dialog.open_file_exists_alert_popup(file_exists_alert % export_path)
# Stops the function until the user decides if they want to overwrite
yield(export_dialog, "resume_export_function")
if stop_export:
# User decided to stop export
return
export_paths.append(export_path)
# Only get one export path if single file animated image is exported
if current_tab == ExportTab.ANIMATION and animation_type == AnimationType.ANIMATED:
break
# Scale images that are to export
scale_processed_images()
if current_tab == ExportTab.ANIMATION and animation_type == AnimationType.ANIMATED:
if OS.get_name() == "HTML5":
export_gif({"export_dialog": export_dialog, "export_paths": export_paths})
else:
if gif_export_thread.is_active():
gif_export_thread.wait_to_finish()
gif_export_thread.start(
self, "export_gif", {"export_dialog": export_dialog, "export_paths": export_paths}
)
else:
for i in range(processed_images.size()):
if OS.get_name() == "HTML5":
JavaScript.download_buffer(
processed_images[i].save_png_to_buffer(),
export_paths[i].get_file(),
"image/png"
)
else:
var err = processed_images[i].save_png(export_paths[i])
if err != OK:
Global.error_dialog.set_text(tr("File failed to save. Error code %s") % err)
Global.error_dialog.popup_centered()
Global.dialog_open(true)
# Store settings for quick export and when the dialog is opened again
was_exported = true
Global.current_project.was_exported = true
if Global.current_project.export_overwrite:
Global.top_menu_container.file_menu.set_item_text(
6, tr("Overwrite") + " %s" % (file_name + Export.file_format_string(file_format))
)
else:
Global.top_menu_container.file_menu.set_item_text(
6, tr("Export") + " %s" % (file_name + file_format_string(file_format))
)
# Only show when not exporting gif - gif export finishes in thread
if not (current_tab == ExportTab.ANIMATION and animation_type == AnimationType.ANIMATED):
Global.notification_label("File(s) exported")
return true
func export_gif(args: Dictionary) -> void:
# Export progress popup
# One fraction per each frame, one fraction for write to disk
export_progress_fraction = 100 / processed_images.size()
export_progress = 0.0
args["export_dialog"].set_export_progress_bar(export_progress)
args["export_dialog"].toggle_export_progress_popup(true)
# Export and save gif
var exporter = GIFExporter.new(
processed_images[0].get_width(), processed_images[0].get_height()
)
match direction:
AnimationDirection.FORWARD:
for i in range(processed_images.size()):
write_frame_to_gif(
processed_images[i],
Global.current_project.frames[i].duration * (1 / Global.current_project.fps),
exporter,
args["export_dialog"]
)
AnimationDirection.BACKWARDS:
for i in range(processed_images.size() - 1, -1, -1):
write_frame_to_gif(
processed_images[i],
Global.current_project.frames[i].duration * (1 / Global.current_project.fps),
exporter,
args["export_dialog"]
)
AnimationDirection.PING_PONG:
export_progress_fraction = 100 / (processed_images.size() * 2)
for i in range(0, processed_images.size()):
write_frame_to_gif(
processed_images[i],
Global.current_project.frames[i].duration * (1 / Global.current_project.fps),
exporter,
args["export_dialog"]
)
for i in range(processed_images.size() - 2, 0, -1):
write_frame_to_gif(
processed_images[i],
Global.current_project.frames[i].duration * (1 / Global.current_project.fps),
exporter,
args["export_dialog"]
)
if OS.get_name() == "HTML5":
JavaScript.download_buffer(
exporter.export_file_data(), args["export_paths"][0], "image/gif"
)
else:
var file: File = File.new()
file.open(args["export_paths"][0], File.WRITE)
file.store_buffer(exporter.export_file_data())
file.close()
args["export_dialog"].toggle_export_progress_popup(false)
Global.notification_label("File(s) exported")
func write_frame_to_gif(image: Image, wait_time: float, exporter: Reference, dialog: Node) -> void:
exporter.add_frame(image, wait_time, MedianCutQuantization)
increase_export_progress(dialog)
func increase_export_progress(export_dialog: Node) -> void:
export_progress += export_progress_fraction
export_dialog.set_export_progress_bar(export_progress)
func scale_processed_images() -> void:
for processed_image in processed_images:
if resize != 100:
processed_image.unlock()
processed_image.resize(
processed_image.get_size().x * resize / 100,
processed_image.get_size().y * resize / 100,
interpolation
)
func file_format_string(format_enum: int) -> String:
match format_enum:
0: # PNG
return ".png"
1: # GIF
return ".gif"
_:
return ""
func create_export_path(multifile: bool, frame: int = 0) -> String:
var path = file_name
# Only append frame number when there are multiple files exported
if multifile:
var frame_tag_and_start_id = get_proccessed_image_animation_tag_and_start_id(frame - 1)
# Check if exported frame is in frame tag
if frame_tag_and_start_id != null:
var frame_tag = frame_tag_and_start_id[0]
var start_id = frame_tag_and_start_id[1]
# Remove unallowed characters in frame tag directory
var regex := RegEx.new()
regex.compile("[^a-zA-Z0-9_]+")
var frame_tag_dir = regex.sub(frame_tag, "", true)
if new_dir_for_each_frame_tag:
# Add frame tag if frame has one
# (frame - start_id + 1) Makes frames id to start from 1 in each frame tag directory
path += "_" + frame_tag_dir + "_" + String(frame - start_id + 1)
return directory_path.plus_file(frame_tag_dir).plus_file(
path + file_format_string(file_format)
)
else:
# Add frame tag if frame has one
# (frame - start_id + 1) Makes frames id to start from 1 in each frame tag
path += "_" + frame_tag_dir + "_" + String(frame - start_id + 1)
else:
path += "_" + String(frame)
return directory_path.plus_file(path + file_format_string(file_format))
func get_proccessed_image_animation_tag_and_start_id(processed_image_id: int) -> Array:
var result_animation_tag_and_start_id = null
for animation_tag in Global.current_project.animation_tags:
# Check if processed image is in frame tag and assign frame tag and start id if yes
# Then stop
if (
(processed_image_id + 1) >= animation_tag.from
and (processed_image_id + 1) <= animation_tag.to
):
result_animation_tag_and_start_id = [animation_tag.name, animation_tag.from]
break
return result_animation_tag_and_start_id
# Blends canvas layers into passed image starting from the origin position
func blend_layers(image: Image, frame: Frame, origin: Vector2 = Vector2(0, 0)) -> void:
image.lock()
var layer_i := 0
for cel in frame.cels:
if Global.current_project.layers[layer_i].is_visible_in_hierarchy() and cel is PixelCel:
var cel_image := Image.new()
cel_image.copy_from(cel.image)
cel_image.lock()
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, Rect2(Vector2.ZERO, Global.current_project.size), origin)
cel_image.unlock()
layer_i += 1
image.unlock()
# Blends selected cels of the given frame into passed image starting from the origin position
func blend_selected_cels(image: Image, frame: Frame, origin: Vector2 = Vector2(0, 0)) -> void:
image.lock()
var layer_i := 0
for cel_ind in frame.cels.size():
var test_array = [Global.current_project.current_frame, cel_ind]
if not test_array in Global.current_project.selected_cels:
continue
if not frame.cels[cel_ind] is PixelCel:
continue
var cel: PixelCel = frame.cels[cel_ind]
if Global.current_project.layers[layer_i].is_visible_in_hierarchy():
var cel_image := Image.new()
cel_image.copy_from(cel.image)
cel_image.lock()
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, Rect2(Vector2.ZERO, Global.current_project.size), origin)
cel_image.unlock()
layer_i += 1
image.unlock()
func frames_divided_by_spritesheet_lines() -> int:
return int(ceil(number_of_frames / float(lines_count)))