2020-07-31 20:26:52 +00:00
|
|
|
extends Node
|
|
|
|
|
2022-11-28 19:22:29 +00:00
|
|
|
enum ExportTab { IMAGE = 0, SPRITESHEET = 1 }
|
2021-11-25 12:48:30 +00:00
|
|
|
enum Orientation { ROWS = 0, COLUMNS = 1 }
|
|
|
|
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
|
2022-10-30 22:24:24 +00:00
|
|
|
# See file_format_string, file_format_description, and ExportDialog.gd
|
|
|
|
enum FileFormat { PNG = 0, GIF = 1, APNG = 2 }
|
2020-08-07 05:13:04 +00:00
|
|
|
|
2023-05-27 14:29:53 +00:00
|
|
|
# list of animated formats
|
|
|
|
var animated_formats := [FileFormat.GIF, FileFormat.APNG]
|
|
|
|
|
|
|
|
# A dictionary of custom exporter generators (Recieved from extensions)
|
|
|
|
var custom_exporter_generators := {}
|
|
|
|
|
2022-11-28 19:22:29 +00:00
|
|
|
var current_tab: int = ExportTab.IMAGE
|
2020-07-31 20:26:52 +00:00
|
|
|
# All frames and their layers processed/blended into images
|
2022-11-28 19:22:29 +00:00
|
|
|
var processed_images := [] # Image[]
|
|
|
|
var durations := [] # Array of floats
|
2020-07-31 20:26:52 +00:00
|
|
|
|
|
|
|
# Spritesheet options
|
2021-11-25 12:48:30 +00:00
|
|
|
var orientation: int = Orientation.ROWS
|
|
|
|
var lines_count := 1 # How many rows/columns before new line is added
|
|
|
|
|
2022-11-28 19:22:29 +00:00
|
|
|
# General options
|
|
|
|
var frame_current_tag := 0 # Export only current frame tag
|
|
|
|
var export_layers := 0
|
|
|
|
var number_of_frames := 1
|
2021-11-25 12:48:30 +00:00
|
|
|
var direction: int = AnimationDirection.FORWARD
|
2020-07-31 20:26:52 +00:00
|
|
|
var resize := 100
|
2021-11-25 12:48:30 +00:00
|
|
|
var interpolation := 0 # Image.Interpolation
|
2022-11-28 19:22:29 +00:00
|
|
|
var new_dir_for_each_frame_tag := false # we don't need to store this after export
|
2020-07-31 20:26:52 +00:00
|
|
|
|
|
|
|
# Export coroutine signal
|
2022-11-28 19:22:29 +00:00
|
|
|
var stop_export := false
|
2020-07-31 20:26:52 +00:00
|
|
|
|
2022-12-02 00:11:23 +00:00
|
|
|
var file_exists_alert := "The following files already exist. Do you wish to overwrite them?\n%s"
|
2020-07-31 20:26:52 +00:00
|
|
|
|
2020-08-07 05:13:04 +00:00
|
|
|
# Export progress variables
|
|
|
|
var export_progress_fraction := 0.0
|
|
|
|
var export_progress := 0.0
|
|
|
|
onready var gif_export_thread := Thread.new()
|
|
|
|
|
|
|
|
|
2020-08-28 15:05:49 +00:00
|
|
|
func _exit_tree() -> void:
|
2020-08-07 05:13:04 +00:00
|
|
|
if gif_export_thread.is_active():
|
|
|
|
gif_export_thread.wait_to_finish()
|
|
|
|
|
2020-07-31 20:26:52 +00:00
|
|
|
|
2023-05-27 14:29:53 +00:00
|
|
|
func add_file_format(name: String) -> int:
|
|
|
|
var id = FileFormat.size()
|
|
|
|
FileFormat.merge({name: id})
|
|
|
|
return id
|
|
|
|
|
|
|
|
|
|
|
|
func remove_file_format(id: int) -> void:
|
|
|
|
for key in Export.FileFormat.keys():
|
|
|
|
if Export.FileFormat[key] == id:
|
|
|
|
Export.FileFormat.erase(key)
|
|
|
|
return
|
|
|
|
|
|
|
|
|
2022-11-28 19:22:29 +00:00
|
|
|
func external_export(project := Global.current_project) -> void:
|
|
|
|
process_data(project)
|
|
|
|
export_processed_images(true, Global.export_dialog, project)
|
2020-08-28 15:05:49 +00:00
|
|
|
|
|
|
|
|
2022-11-28 19:22:29 +00:00
|
|
|
func process_data(project := Global.current_project) -> void:
|
|
|
|
match current_tab:
|
|
|
|
ExportTab.IMAGE:
|
|
|
|
process_animation(project)
|
|
|
|
ExportTab.SPRITESHEET:
|
|
|
|
process_spritesheet(project)
|
2020-07-31 20:26:52 +00:00
|
|
|
|
|
|
|
|
2022-11-28 19:22:29 +00:00
|
|
|
func process_spritesheet(project := Global.current_project) -> void:
|
2020-10-01 18:27:08 +00:00
|
|
|
processed_images.clear()
|
2020-07-31 20:26:52 +00:00
|
|
|
# Range of frames determined by tags
|
2022-11-28 19:22:29 +00:00
|
|
|
var frames := calculate_frames(project)
|
2020-07-31 20:26:52 +00:00
|
|
|
# Then store the size of frames for other functions
|
|
|
|
number_of_frames = frames.size()
|
|
|
|
|
|
|
|
# If rows mode selected calculate columns count and vice versa
|
2022-11-28 19:22:29 +00:00
|
|
|
var spritesheet_columns := (
|
2021-11-25 12:48:30 +00:00
|
|
|
lines_count
|
|
|
|
if orientation == Orientation.ROWS
|
|
|
|
else frames_divided_by_spritesheet_lines()
|
|
|
|
)
|
2022-11-28 19:22:29 +00:00
|
|
|
var spritesheet_rows := (
|
2021-11-25 12:48:30 +00:00
|
|
|
lines_count
|
|
|
|
if orientation == Orientation.COLUMNS
|
|
|
|
else frames_divided_by_spritesheet_lines()
|
|
|
|
)
|
2020-07-31 20:26:52 +00:00
|
|
|
|
2022-11-28 19:22:29 +00:00
|
|
|
var width := project.size.x * spritesheet_columns
|
|
|
|
var height := project.size.y * spritesheet_rows
|
2020-07-31 20:26:52 +00:00
|
|
|
|
|
|
|
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:
|
2022-11-28 19:22:29 +00:00
|
|
|
origin.x = project.size.x * vv
|
2020-07-31 20:26:52 +00:00
|
|
|
vv += 1
|
|
|
|
else:
|
|
|
|
hh += 1
|
|
|
|
origin.x = 0
|
|
|
|
vv = 1
|
2022-11-28 19:22:29 +00:00
|
|
|
origin.y = project.size.y * hh
|
2020-07-31 20:26:52 +00:00
|
|
|
else:
|
|
|
|
if hh < spritesheet_rows:
|
2022-11-28 19:22:29 +00:00
|
|
|
origin.y = project.size.y * hh
|
2020-07-31 20:26:52 +00:00
|
|
|
hh += 1
|
|
|
|
else:
|
|
|
|
vv += 1
|
|
|
|
origin.y = 0
|
|
|
|
hh = 1
|
2022-11-28 19:22:29 +00:00
|
|
|
origin.x = project.size.x * vv
|
2020-07-31 20:26:52 +00:00
|
|
|
blend_layers(whole_image, frame, origin)
|
|
|
|
|
|
|
|
processed_images.append(whole_image)
|
|
|
|
|
|
|
|
|
2022-11-28 19:22:29 +00:00
|
|
|
func process_animation(project := Global.current_project) -> void:
|
2020-07-31 20:26:52 +00:00
|
|
|
processed_images.clear()
|
2022-11-28 19:22:29 +00:00
|
|
|
durations.clear()
|
|
|
|
var frames := calculate_frames(project)
|
|
|
|
for frame in frames:
|
2020-07-31 20:26:52 +00:00
|
|
|
var image := Image.new()
|
2022-11-28 19:22:29 +00:00
|
|
|
image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
|
2020-07-31 20:26:52 +00:00
|
|
|
blend_layers(image, frame)
|
|
|
|
processed_images.append(image)
|
2022-11-28 19:22:29 +00:00
|
|
|
durations.append(frame.duration * (1.0 / project.fps))
|
2020-07-31 20:26:52 +00:00
|
|
|
|
|
|
|
|
2022-11-28 19:22:29 +00:00
|
|
|
func calculate_frames(project := Global.current_project) -> Array:
|
|
|
|
var frames := []
|
|
|
|
if frame_current_tag > 1: # Specific tag
|
|
|
|
var frame_start: int = project.animation_tags[frame_current_tag - 2].from
|
|
|
|
var frame_end: int = project.animation_tags[frame_current_tag - 2].to
|
|
|
|
frames = project.frames.slice(frame_start - 1, frame_end - 1, 1, true)
|
|
|
|
elif frame_current_tag == 1: # Selected frames
|
|
|
|
for cel in project.selected_cels:
|
|
|
|
frames.append(project.frames[cel[0]])
|
|
|
|
else: # All frames
|
|
|
|
frames = project.frames.duplicate()
|
|
|
|
|
|
|
|
if direction == AnimationDirection.BACKWARDS:
|
|
|
|
frames.invert()
|
|
|
|
elif direction == AnimationDirection.PING_PONG:
|
|
|
|
var inverted_frames := frames.duplicate()
|
|
|
|
inverted_frames.invert()
|
|
|
|
inverted_frames.remove(0)
|
|
|
|
frames.append_array(inverted_frames)
|
|
|
|
return frames
|
|
|
|
|
|
|
|
|
|
|
|
func export_processed_images(
|
|
|
|
ignore_overwrites: bool, export_dialog: ConfirmationDialog, project := Global.current_project
|
|
|
|
) -> bool:
|
2020-07-31 20:26:52 +00:00
|
|
|
# Stop export if directory path or file name are not valid
|
2022-11-28 19:22:29 +00:00
|
|
|
var dir := Directory.new()
|
2022-12-18 01:33:53 +00:00
|
|
|
if not dir.dir_exists(project.directory_path) or not project.file_name.is_valid_filename():
|
|
|
|
if not dir.dir_exists(project.directory_path) and project.file_name.is_valid_filename():
|
2022-04-19 16:29:21 +00:00
|
|
|
export_dialog.open_path_validation_alert_popup(0)
|
2022-12-18 01:33:53 +00:00
|
|
|
elif not project.file_name.is_valid_filename() and dir.dir_exists(project.directory_path):
|
2022-04-19 16:29:21 +00:00
|
|
|
export_dialog.open_path_validation_alert_popup(1)
|
|
|
|
else:
|
|
|
|
export_dialog.open_path_validation_alert_popup()
|
2020-07-31 20:26:52 +00:00
|
|
|
return false
|
|
|
|
|
2022-11-28 19:22:29 +00:00
|
|
|
var multiple_files := false
|
2022-12-18 01:33:53 +00:00
|
|
|
if current_tab == ExportTab.IMAGE and not is_single_file_format(project):
|
2022-11-28 19:22:29 +00:00
|
|
|
multiple_files = true if processed_images.size() > 1 else false
|
2020-07-31 20:26:52 +00:00
|
|
|
# Check export paths
|
2022-11-28 19:22:29 +00:00
|
|
|
var export_paths := []
|
2022-12-02 00:11:23 +00:00
|
|
|
var paths_of_existing_files := ""
|
2020-07-31 20:26:52 +00:00
|
|
|
for i in range(processed_images.size()):
|
|
|
|
stop_export = false
|
2022-11-28 19:22:29 +00:00
|
|
|
var export_path := create_export_path(multiple_files, project, i + 1)
|
2022-12-02 00:11:23 +00:00
|
|
|
# If the user wants to create a new directory for each animation tag then check
|
|
|
|
# if directories exist, and create them if not
|
2020-07-31 20:26:52 +00:00
|
|
|
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()):
|
2022-12-18 01:33:53 +00:00
|
|
|
frame_tag_directory.open(project.directory_path)
|
2020-07-31 20:26:52 +00:00
|
|
|
frame_tag_directory.make_dir(export_path.get_base_dir().get_file())
|
2022-12-02 00:11:23 +00:00
|
|
|
|
|
|
|
if not ignore_overwrites: # Check if the files already exist
|
|
|
|
var file_check: File = File.new()
|
|
|
|
if file_check.file_exists(export_path):
|
|
|
|
if not paths_of_existing_files.empty():
|
|
|
|
paths_of_existing_files += "\n"
|
|
|
|
paths_of_existing_files += export_path
|
2020-07-31 20:26:52 +00:00
|
|
|
export_paths.append(export_path)
|
|
|
|
# Only get one export path if single file animated image is exported
|
2022-12-18 01:33:53 +00:00
|
|
|
if is_single_file_format(project):
|
2020-07-31 20:26:52 +00:00
|
|
|
break
|
|
|
|
|
2022-12-02 00:11:23 +00:00
|
|
|
if not paths_of_existing_files.empty(): # If files already exist
|
|
|
|
# Ask user if they want to overwrite the files
|
|
|
|
export_dialog.open_file_exists_alert_popup(tr(file_exists_alert) % paths_of_existing_files)
|
|
|
|
# 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
|
|
|
|
|
2020-07-31 20:26:52 +00:00
|
|
|
scale_processed_images()
|
|
|
|
|
2023-05-27 14:29:53 +00:00
|
|
|
# override if a custom export is chosen
|
|
|
|
if project.file_format in custom_exporter_generators.keys():
|
|
|
|
# Divert the path to the custom exporter instead
|
|
|
|
var custom_exporter: Object = custom_exporter_generators[project.file_format][0]
|
|
|
|
if custom_exporter.has_method("override_export"):
|
|
|
|
var result := true
|
|
|
|
var details := {
|
|
|
|
"processed_images": processed_images,
|
|
|
|
"durations": durations,
|
|
|
|
"export_dialog": export_dialog,
|
|
|
|
"export_paths": export_paths,
|
|
|
|
"project": project
|
|
|
|
}
|
|
|
|
if OS.get_name() != "HTML5" and is_single_file_format(project):
|
|
|
|
if gif_export_thread.is_active():
|
|
|
|
gif_export_thread.wait_to_finish()
|
|
|
|
var error = gif_export_thread.start(custom_exporter, "override_export", details)
|
|
|
|
if error == OK:
|
|
|
|
result = gif_export_thread.wait_to_finish()
|
|
|
|
else:
|
|
|
|
result = custom_exporter.call("override_export", details)
|
|
|
|
return result
|
|
|
|
|
2022-12-18 01:33:53 +00:00
|
|
|
if is_single_file_format(project):
|
2022-12-23 18:08:46 +00:00
|
|
|
var exporter: AImgIOBaseExporter
|
2022-12-18 01:33:53 +00:00
|
|
|
if project.file_format == FileFormat.APNG:
|
2022-12-23 18:08:46 +00:00
|
|
|
exporter = AImgIOAPNGExporter.new()
|
2022-10-30 22:24:24 +00:00
|
|
|
else:
|
|
|
|
exporter = GIFAnimationExporter.new()
|
2022-11-28 19:22:29 +00:00
|
|
|
var details := {
|
|
|
|
"exporter": exporter,
|
|
|
|
"export_dialog": export_dialog,
|
|
|
|
"export_paths": export_paths,
|
|
|
|
"project": project
|
2022-10-30 22:24:24 +00:00
|
|
|
}
|
2020-08-07 08:01:27 +00:00
|
|
|
if OS.get_name() == "HTML5":
|
2022-10-30 22:24:24 +00:00
|
|
|
export_animated(details)
|
2020-08-07 08:01:27 +00:00
|
|
|
else:
|
|
|
|
if gif_export_thread.is_active():
|
|
|
|
gif_export_thread.wait_to_finish()
|
2022-10-30 22:24:24 +00:00
|
|
|
gif_export_thread.start(self, "export_animated", details)
|
2020-07-31 20:26:52 +00:00
|
|
|
else:
|
2023-04-06 23:16:52 +00:00
|
|
|
var succeeded := true
|
2020-07-31 20:26:52 +00:00
|
|
|
for i in range(processed_images.size()):
|
|
|
|
if OS.get_name() == "HTML5":
|
2021-11-25 12:48:30 +00:00
|
|
|
JavaScript.download_buffer(
|
|
|
|
processed_images[i].save_png_to_buffer(),
|
|
|
|
export_paths[i].get_file(),
|
|
|
|
"image/png"
|
|
|
|
)
|
2020-07-31 20:26:52 +00:00
|
|
|
else:
|
2023-04-06 23:09:49 +00:00
|
|
|
var err: int = processed_images[i].save_png(export_paths[i])
|
2020-07-31 20:26:52 +00:00
|
|
|
if err != OK:
|
2020-10-25 16:02:51 +00:00
|
|
|
Global.error_dialog.set_text(tr("File failed to save. Error code %s") % err)
|
|
|
|
Global.error_dialog.popup_centered()
|
|
|
|
Global.dialog_open(true)
|
2023-04-06 23:16:52 +00:00
|
|
|
succeeded = false
|
|
|
|
if succeeded:
|
|
|
|
Global.notification_label("File(s) exported")
|
2020-07-31 20:26:52 +00:00
|
|
|
|
|
|
|
# Store settings for quick export and when the dialog is opened again
|
2022-12-18 01:33:53 +00:00
|
|
|
var file_name_with_ext := project.file_name + file_format_string(project.file_format)
|
2022-11-28 19:22:29 +00:00
|
|
|
project.was_exported = true
|
|
|
|
if project.export_overwrite:
|
2022-03-25 00:00:40 +00:00
|
|
|
Global.top_menu_container.file_menu.set_item_text(
|
2022-12-18 01:33:53 +00:00
|
|
|
Global.FileMenu.EXPORT, tr("Overwrite") + " %s" % file_name_with_ext
|
2022-03-25 00:00:40 +00:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
Global.top_menu_container.file_menu.set_item_text(
|
2022-12-18 01:33:53 +00:00
|
|
|
Global.FileMenu.EXPORT, tr("Export") + " %s" % file_name_with_ext
|
2022-03-25 00:00:40 +00:00
|
|
|
)
|
2020-07-31 20:26:52 +00:00
|
|
|
return true
|
|
|
|
|
|
|
|
|
2022-10-30 22:24:24 +00:00
|
|
|
func export_animated(args: Dictionary) -> void:
|
2022-11-28 19:22:29 +00:00
|
|
|
var project: Project = args["project"]
|
2022-12-23 18:08:46 +00:00
|
|
|
var exporter: AImgIOBaseExporter = args["exporter"]
|
2022-10-30 22:24:24 +00:00
|
|
|
# This is an ExportDialog (which refers back here).
|
2022-11-28 19:22:29 +00:00
|
|
|
var export_dialog: ConfirmationDialog = args["export_dialog"]
|
2020-08-07 08:01:27 +00:00
|
|
|
|
2022-10-30 22:24:24 +00:00
|
|
|
# Export progress popup
|
|
|
|
# One fraction per each frame, one fraction for write to disk
|
2022-11-28 19:22:29 +00:00
|
|
|
export_progress_fraction = 100.0 / len(processed_images)
|
2022-10-30 22:24:24 +00:00
|
|
|
export_progress = 0.0
|
|
|
|
export_dialog.set_export_progress_bar(export_progress)
|
|
|
|
export_dialog.toggle_export_progress_popup(true)
|
|
|
|
|
2022-12-23 18:08:46 +00:00
|
|
|
# Transform into AImgIO form
|
|
|
|
var frames := []
|
|
|
|
for i in range(len(processed_images)):
|
|
|
|
var frame: AImgIOFrame = AImgIOFrame.new()
|
|
|
|
frame.content = processed_images[i]
|
|
|
|
frame.duration = durations[i]
|
|
|
|
frames.push_back(frame)
|
|
|
|
|
|
|
|
# Export and save GIF/APNG
|
2022-11-28 19:22:29 +00:00
|
|
|
var file_data := exporter.export_animation(
|
2022-12-23 18:08:46 +00:00
|
|
|
frames, project.fps, self, "increase_export_progress", [export_dialog]
|
2022-10-30 22:24:24 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
if OS.get_name() == "HTML5":
|
|
|
|
JavaScript.download_buffer(file_data, args["export_paths"][0], exporter.mime_type)
|
2020-08-07 08:01:27 +00:00
|
|
|
else:
|
|
|
|
var file: File = File.new()
|
|
|
|
file.open(args["export_paths"][0], File.WRITE)
|
2022-10-30 22:24:24 +00:00
|
|
|
file.store_buffer(file_data)
|
2020-08-07 08:01:27 +00:00
|
|
|
file.close()
|
2022-10-30 22:24:24 +00:00
|
|
|
export_dialog.toggle_export_progress_popup(false)
|
2020-08-07 05:13:04 +00:00
|
|
|
Global.notification_label("File(s) exported")
|
|
|
|
|
|
|
|
|
|
|
|
func increase_export_progress(export_dialog: Node) -> void:
|
|
|
|
export_progress += export_progress_fraction
|
|
|
|
export_dialog.set_export_progress_bar(export_progress)
|
|
|
|
|
|
|
|
|
2020-07-31 20:26:52 +00:00
|
|
|
func scale_processed_images() -> void:
|
|
|
|
for processed_image in processed_images:
|
|
|
|
if resize != 100:
|
|
|
|
processed_image.unlock()
|
2021-11-25 12:48:30 +00:00
|
|
|
processed_image.resize(
|
|
|
|
processed_image.get_size().x * resize / 100,
|
|
|
|
processed_image.get_size().y * resize / 100,
|
|
|
|
interpolation
|
|
|
|
)
|
2020-07-31 20:26:52 +00:00
|
|
|
|
|
|
|
|
2021-11-25 12:48:30 +00:00
|
|
|
func file_format_string(format_enum: int) -> String:
|
2020-07-31 20:26:52 +00:00
|
|
|
match format_enum:
|
2022-10-30 22:24:24 +00:00
|
|
|
FileFormat.PNG:
|
2021-11-25 12:48:30 +00:00
|
|
|
return ".png"
|
2022-10-30 22:24:24 +00:00
|
|
|
FileFormat.GIF:
|
2021-11-25 12:48:30 +00:00
|
|
|
return ".gif"
|
2022-10-30 22:24:24 +00:00
|
|
|
FileFormat.APNG:
|
|
|
|
return ".apng"
|
|
|
|
_:
|
2023-05-27 14:29:53 +00:00
|
|
|
# If a file format description is not found, try generating one
|
|
|
|
if custom_exporter_generators.has(format_enum):
|
|
|
|
return custom_exporter_generators[format_enum][1]
|
2022-10-30 22:24:24 +00:00
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
func file_format_description(format_enum: int) -> String:
|
|
|
|
match format_enum:
|
2023-05-27 14:29:53 +00:00
|
|
|
# these are overrides
|
|
|
|
# (if they are not given, they will generate themselves based on the enum key name)
|
2022-10-30 22:24:24 +00:00
|
|
|
FileFormat.PNG:
|
|
|
|
return "PNG Image"
|
|
|
|
FileFormat.GIF:
|
|
|
|
return "GIF Image"
|
|
|
|
FileFormat.APNG:
|
|
|
|
return "APNG Image"
|
2020-07-31 20:26:52 +00:00
|
|
|
_:
|
2023-05-27 14:29:53 +00:00
|
|
|
# If a file format description is not found, try generating one
|
|
|
|
for key in FileFormat.keys():
|
|
|
|
if FileFormat[key] == format_enum:
|
|
|
|
return str(key.capitalize())
|
2021-11-25 12:48:30 +00:00
|
|
|
return ""
|
2020-07-31 20:26:52 +00:00
|
|
|
|
|
|
|
|
2022-12-18 01:33:53 +00:00
|
|
|
func is_single_file_format(project := Global.current_project) -> bool:
|
2022-11-28 19:22:29 +00:00
|
|
|
# True when exporting to .gif and .apng (and potentially video formats in the future)
|
|
|
|
# False when exporting to .png, and other non-animated formats in the future
|
2023-05-27 14:29:53 +00:00
|
|
|
return animated_formats.has(project.file_format)
|
2022-11-28 19:22:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
func create_export_path(multifile: bool, project: Project, frame: int = 0) -> String:
|
2022-12-18 01:33:53 +00:00
|
|
|
var path := project.file_name
|
2020-07-31 20:26:52 +00:00
|
|
|
# Only append frame number when there are multiple files exported
|
|
|
|
if multifile:
|
2022-11-28 19:22:29 +00:00
|
|
|
var frame_tag_and_start_id := get_proccessed_image_animation_tag_and_start_id(
|
|
|
|
project, frame - 1
|
|
|
|
)
|
2020-07-31 20:26:52 +00:00
|
|
|
# Check if exported frame is in frame tag
|
|
|
|
if frame_tag_and_start_id != null:
|
2022-11-28 19:22:29 +00:00
|
|
|
var frame_tag: String = frame_tag_and_start_id[0]
|
|
|
|
var start_id: int = frame_tag_and_start_id[1]
|
2020-07-31 20:26:52 +00:00
|
|
|
# Remove unallowed characters in frame tag directory
|
|
|
|
var regex := RegEx.new()
|
|
|
|
regex.compile("[^a-zA-Z0-9_]+")
|
2022-11-28 19:22:29 +00:00
|
|
|
var frame_tag_dir := regex.sub(frame_tag, "", true)
|
2020-07-31 20:26:52 +00:00
|
|
|
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)
|
2022-12-18 01:33:53 +00:00
|
|
|
return project.directory_path.plus_file(frame_tag_dir).plus_file(
|
|
|
|
path + file_format_string(project.file_format)
|
2021-11-25 12:48:30 +00:00
|
|
|
)
|
2020-07-31 20:26:52 +00:00
|
|
|
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)
|
|
|
|
|
2022-12-18 01:33:53 +00:00
|
|
|
return project.directory_path.plus_file(path + file_format_string(project.file_format))
|
2020-07-31 20:26:52 +00:00
|
|
|
|
|
|
|
|
2022-11-28 19:22:29 +00:00
|
|
|
func get_proccessed_image_animation_tag_and_start_id(
|
|
|
|
project: Project, processed_image_id: int
|
|
|
|
) -> Array:
|
2020-07-31 20:26:52 +00:00
|
|
|
var result_animation_tag_and_start_id = null
|
2022-11-28 19:22:29 +00:00
|
|
|
for animation_tag in project.animation_tags:
|
2020-07-31 20:26:52 +00:00
|
|
|
# Check if processed image is in frame tag and assign frame tag and start id if yes
|
|
|
|
# Then stop
|
2021-11-25 12:48:30 +00:00
|
|
|
if (
|
|
|
|
(processed_image_id + 1) >= animation_tag.from
|
|
|
|
and (processed_image_id + 1) <= animation_tag.to
|
|
|
|
):
|
2020-07-31 20:26:52 +00:00
|
|
|
result_animation_tag_and_start_id = [animation_tag.name, animation_tag.from]
|
|
|
|
break
|
|
|
|
return result_animation_tag_and_start_id
|
|
|
|
|
|
|
|
|
2022-11-28 19:22:29 +00:00
|
|
|
func blend_layers(
|
|
|
|
image: Image, frame: Frame, origin := Vector2.ZERO, project := Global.current_project
|
|
|
|
) -> void:
|
|
|
|
if export_layers == 0:
|
|
|
|
blend_all_layers(image, frame, origin, project)
|
|
|
|
elif export_layers == 1:
|
|
|
|
blend_selected_cels(image, frame, origin, project)
|
|
|
|
else:
|
|
|
|
var layer: BaseLayer = project.layers[export_layers - 2]
|
|
|
|
var layer_image := Image.new()
|
2023-03-31 18:58:56 +00:00
|
|
|
if layer is GroupLayer:
|
2022-11-28 19:22:29 +00:00
|
|
|
layer_image.copy_from(layer.blend_children(frame, Vector2.ZERO))
|
2023-03-31 18:58:56 +00:00
|
|
|
else:
|
|
|
|
layer_image.copy_from(frame.cels[export_layers - 2].get_image())
|
2022-11-28 19:22:29 +00:00
|
|
|
image.blend_rect(layer_image, Rect2(Vector2.ZERO, project.size), origin)
|
|
|
|
|
|
|
|
|
2020-07-31 20:26:52 +00:00
|
|
|
# Blends canvas layers into passed image starting from the origin position
|
2022-11-28 19:22:29 +00:00
|
|
|
func blend_all_layers(
|
|
|
|
image: Image, frame: Frame, origin := Vector2.ZERO, project := Global.current_project
|
|
|
|
) -> void:
|
2020-07-31 20:26:52 +00:00
|
|
|
var layer_i := 0
|
|
|
|
for cel in frame.cels:
|
2023-03-31 18:58:56 +00:00
|
|
|
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
|
|
|
|
cel_image.lock()
|
|
|
|
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)
|
|
|
|
)
|
|
|
|
cel_image.unlock()
|
|
|
|
image.blend_rect(cel_image, Rect2(Vector2.ZERO, project.size), origin)
|
2020-07-31 20:26:52 +00:00
|
|
|
layer_i += 1
|
|
|
|
|
|
|
|
|
2022-07-23 23:20:58 +00:00
|
|
|
# Blends selected cels of the given frame into passed image starting from the origin position
|
2022-11-28 19:22:29 +00:00
|
|
|
func blend_selected_cels(
|
|
|
|
image: Image, frame: Frame, origin := Vector2(0, 0), project := Global.current_project
|
|
|
|
) -> void:
|
2022-07-23 23:20:58 +00:00
|
|
|
for cel_ind in frame.cels.size():
|
2022-11-28 19:22:29 +00:00
|
|
|
var test_array := [project.current_frame, cel_ind]
|
|
|
|
if not test_array in project.selected_cels:
|
2022-07-23 23:20:58 +00:00
|
|
|
continue
|
2023-03-31 18:58:56 +00:00
|
|
|
if frame.cels[cel_ind] is GroupCel:
|
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 18:59:49 +00:00
|
|
|
continue
|
2023-03-27 00:42:16 +00:00
|
|
|
if not project.layers[cel_ind].is_visible_in_hierarchy():
|
|
|
|
continue
|
2023-03-31 18:58:56 +00:00
|
|
|
var cel: BaseCel = frame.cels[cel_ind]
|
2023-03-27 00:42:16 +00:00
|
|
|
var cel_image := Image.new()
|
2023-03-31 18:58:56 +00:00
|
|
|
cel_image.copy_from(cel.get_image())
|
2023-03-27 00:42:16 +00:00
|
|
|
if cel.opacity < 1: # If we have cel transparency
|
|
|
|
cel_image.lock()
|
|
|
|
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)
|
|
|
|
)
|
|
|
|
cel_image.unlock()
|
|
|
|
image.blend_rect(cel_image, Rect2(Vector2.ZERO, project.size), origin)
|
2022-07-23 23:20:58 +00:00
|
|
|
|
|
|
|
|
2020-07-31 20:26:52 +00:00
|
|
|
func frames_divided_by_spritesheet_lines() -> int:
|
|
|
|
return int(ceil(number_of_frames / float(lines_count)))
|