mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-01-19 09:39:48 +00:00
0c1fcdea89
This commit makes Pixelorama unable to run in Godot versions prior to 3.4. I wonder if there are plans for an upload_buffer() method as well...
331 lines
12 KiB
GDScript
331 lines
12 KiB
GDScript
extends Node
|
|
|
|
# Gif exporter
|
|
const GIFExporter = preload("res://addons/gdgifexporter/exporter.gd")
|
|
const MedianCutQuantization = preload("res://addons/gdgifexporter/quantization/median_cut.gd")
|
|
|
|
enum ExportTab { FRAME = 0, SPRITESHEET = 1, ANIMATION = 2 }
|
|
var current_tab : int = ExportTab.FRAME
|
|
|
|
# Frame options
|
|
var frame_number := 0
|
|
|
|
# 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
|
|
enum Orientation { ROWS = 0, COLUMNS = 1 }
|
|
var orientation : int = Orientation.ROWS
|
|
# How many rows/columns before new line is added
|
|
var lines_count := 1
|
|
|
|
# Animation options
|
|
enum AnimationType { MULTIPLE_FILES = 0, ANIMATED = 1 }
|
|
var animation_type : int = AnimationType.MULTIPLE_FILES
|
|
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
|
|
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
|
|
enum FileFormat { PNG = 0, GIF = 1}
|
|
|
|
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():
|
|
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 fileCheck = File.new()
|
|
if fileCheck.file_exists(export_path):
|
|
# Ask user if he want's 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 he want's 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
|
|
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
|
|
export_progress_fraction = 100 / processed_images.size() # one fraction per each frame, one fraction for write to disk
|
|
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, export_dialog: Node) -> void:
|
|
exporter.add_frame(image, wait_time, MedianCutQuantization)
|
|
increase_export_progress(export_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].visible:
|
|
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)))
|