mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-01-18 09:09:47 +00:00
Replace godot-gifexporter with godot-gdgifexporter (#295)
Add exporting in a separate thread and a progress bar Remove background color option from gif export
This commit is contained in:
parent
e4aa17b01c
commit
f3bce3857a
10
addons/README.md
Normal file
10
addons/README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Addons
|
||||||
|
|
||||||
|
## gdgifexporter
|
||||||
|
|
||||||
|
- Upstream: https://github.com/jegor377/godot-gdgifexporter
|
||||||
|
- Version: git (9cdc448922717f069dd12e0377c1d9fc09d30f9f, 2020)
|
||||||
|
- License: MIT
|
||||||
|
|
||||||
|
Files extracted from source:
|
||||||
|
- `gdgifexporter/quantization/enhanced_uniform_quantization.gd`
|
21
addons/gdgifexporter/LICENSE
Normal file
21
addons/gdgifexporter/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Igor Santarek
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
39
addons/gdgifexporter/converter.gd
Normal file
39
addons/gdgifexporter/converter.gd
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
func setup(image: Image, colors: Array) -> PoolByteArray:
|
||||||
|
var vp = VisualServer.viewport_create()
|
||||||
|
var canvas = VisualServer.canvas_create()
|
||||||
|
VisualServer.viewport_attach_canvas(vp, canvas)
|
||||||
|
VisualServer.viewport_set_size(vp, image.get_width(), image.get_height())
|
||||||
|
VisualServer.viewport_set_disable_3d(vp, true)
|
||||||
|
VisualServer.viewport_set_usage(vp, VisualServer.VIEWPORT_USAGE_2D)
|
||||||
|
VisualServer.viewport_set_hdr(vp, true)
|
||||||
|
VisualServer.viewport_set_active(vp, true)
|
||||||
|
|
||||||
|
var ci_rid = VisualServer.canvas_item_create()
|
||||||
|
VisualServer.viewport_set_canvas_transform(vp, canvas, Transform())
|
||||||
|
VisualServer.canvas_item_set_parent(ci_rid, canvas)
|
||||||
|
var texture = ImageTexture.new()
|
||||||
|
texture.create_from_image(image)
|
||||||
|
VisualServer.canvas_item_add_texture_rect(ci_rid, Rect2(Vector2(0, 0), image.get_size()), texture)
|
||||||
|
|
||||||
|
var shader = preload("./lookup_similar.shader")
|
||||||
|
var mat_rid = VisualServer.material_create()
|
||||||
|
VisualServer.material_set_shader(mat_rid, shader.get_rid())
|
||||||
|
var lut = Image.new()
|
||||||
|
lut.create(256, 1, false, Image.FORMAT_RGB8)
|
||||||
|
lut.lock()
|
||||||
|
for i in 256:
|
||||||
|
lut.set_pixel(i, 0, Color8(colors[i][0], colors[i][1], colors[i][2]))
|
||||||
|
var lut_tex = ImageTexture.new()
|
||||||
|
lut_tex.create_from_image(lut)
|
||||||
|
VisualServer.material_set_param(mat_rid, "lut", lut_tex)
|
||||||
|
VisualServer.canvas_item_set_material(ci_rid, mat_rid)
|
||||||
|
|
||||||
|
VisualServer.viewport_set_update_mode(vp, VisualServer.VIEWPORT_UPDATE_ONCE)
|
||||||
|
VisualServer.viewport_set_vflip(vp, true)
|
||||||
|
VisualServer.force_draw(false)
|
||||||
|
image = VisualServer.texture_get_data(VisualServer.viewport_get_texture(vp))
|
||||||
|
image.convert(Image.FORMAT_R8)
|
||||||
|
return image.get_data()
|
21
addons/gdgifexporter/gif-lzw/LICENSE
Normal file
21
addons/gdgifexporter/gif-lzw/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Igor Santarek
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
38
addons/gdgifexporter/gif-lzw/lsbbitpacker.gd
Normal file
38
addons/gdgifexporter/gif-lzw/lsbbitpacker.gd
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
class LSB_LZWBitPacker:
|
||||||
|
var bit_index: int = 0
|
||||||
|
var byte: int = 0
|
||||||
|
|
||||||
|
var chunks: PoolByteArray = PoolByteArray([])
|
||||||
|
|
||||||
|
func get_bit(value: int, index: int) -> int:
|
||||||
|
return (value >> index) & 1
|
||||||
|
|
||||||
|
func set_bit(value: int, index: int) -> int:
|
||||||
|
return value | (1 << index)
|
||||||
|
|
||||||
|
func put_byte():
|
||||||
|
chunks.append(byte)
|
||||||
|
bit_index = 0
|
||||||
|
byte = 0
|
||||||
|
|
||||||
|
func write_bits(value: int, bits_count: int) -> void:
|
||||||
|
for i in range(bits_count):
|
||||||
|
if self.get_bit(value, i) == 1:
|
||||||
|
byte = self.set_bit(byte, bit_index)
|
||||||
|
|
||||||
|
bit_index += 1
|
||||||
|
if bit_index == 8:
|
||||||
|
self.put_byte()
|
||||||
|
|
||||||
|
func pack() -> PoolByteArray:
|
||||||
|
if bit_index != 0:
|
||||||
|
self.put_byte()
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
func reset() -> void:
|
||||||
|
bit_index = 0
|
||||||
|
byte = 0
|
||||||
|
chunks = PoolByteArray([])
|
41
addons/gdgifexporter/gif-lzw/lsbbitunpacker.gd
Normal file
41
addons/gdgifexporter/gif-lzw/lsbbitunpacker.gd
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
class LSB_LZWBitUnpacker:
|
||||||
|
var chunk_stream: PoolByteArray
|
||||||
|
var bit_index: int = 0
|
||||||
|
var byte: int
|
||||||
|
var byte_index: int = 0
|
||||||
|
|
||||||
|
func _init(_chunk_stream: PoolByteArray):
|
||||||
|
chunk_stream = _chunk_stream
|
||||||
|
self.get_byte()
|
||||||
|
|
||||||
|
func get_bit(value: int, index: int) -> int:
|
||||||
|
return (value >> index) & 1
|
||||||
|
|
||||||
|
func set_bit(value: int, index: int) -> int:
|
||||||
|
return value | (1 << index)
|
||||||
|
|
||||||
|
func get_byte():
|
||||||
|
byte = chunk_stream[byte_index]
|
||||||
|
byte_index += 1
|
||||||
|
bit_index = 0
|
||||||
|
|
||||||
|
func read_bits(bits_count: int) -> int:
|
||||||
|
var result: int = 0
|
||||||
|
var result_bit_index: int = 0
|
||||||
|
|
||||||
|
for _i in range(bits_count):
|
||||||
|
if self.get_bit(byte, bit_index) == 1:
|
||||||
|
result = self.set_bit(result, result_bit_index)
|
||||||
|
result_bit_index += 1
|
||||||
|
bit_index += 1
|
||||||
|
|
||||||
|
if bit_index == 8:
|
||||||
|
self.get_byte()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
func remove_bits(bits_count: int) -> void:
|
||||||
|
self.read_bits(bits_count)
|
202
addons/gdgifexporter/gif-lzw/lzw.gd
Normal file
202
addons/gdgifexporter/gif-lzw/lzw.gd
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
var lsbbitpacker = preload('./lsbbitpacker.gd')
|
||||||
|
var lsbbitunpacker = preload('./lsbbitunpacker.gd')
|
||||||
|
|
||||||
|
class CodeEntry:
|
||||||
|
var sequence: PoolByteArray
|
||||||
|
var raw_array: Array
|
||||||
|
|
||||||
|
func _init(_sequence):
|
||||||
|
raw_array = _sequence
|
||||||
|
sequence = _sequence
|
||||||
|
|
||||||
|
func add(other):
|
||||||
|
return CodeEntry.new(self.raw_array + other.raw_array)
|
||||||
|
|
||||||
|
func to_string():
|
||||||
|
var result: String = ''
|
||||||
|
for element in self.sequence:
|
||||||
|
result += str(element) + ', '
|
||||||
|
return result.substr(0, result.length() - 2)
|
||||||
|
|
||||||
|
class CodeTable:
|
||||||
|
var entries: Dictionary = {}
|
||||||
|
var counter: int = 0
|
||||||
|
var lookup: Dictionary = {}
|
||||||
|
|
||||||
|
func add(entry) -> int:
|
||||||
|
self.entries[self.counter] = entry
|
||||||
|
self.lookup[entry.raw_array] = self.counter
|
||||||
|
counter += 1
|
||||||
|
return counter
|
||||||
|
|
||||||
|
func find(entry) -> int:
|
||||||
|
return self.lookup.get(entry.raw_array, -1)
|
||||||
|
|
||||||
|
func has(entry) -> bool:
|
||||||
|
return self.find(entry) != -1
|
||||||
|
|
||||||
|
func get(index) -> CodeEntry:
|
||||||
|
return self.entries.get(index, null)
|
||||||
|
|
||||||
|
func to_string() -> String:
|
||||||
|
var result: String = 'CodeTable:\n'
|
||||||
|
for id in self.entries:
|
||||||
|
result += str(id) + ': ' + self.entries[id].to_string() + '\n'
|
||||||
|
result += 'Counter: ' + str(self.counter) + '\n'
|
||||||
|
return result
|
||||||
|
|
||||||
|
func log2(value: float) -> float:
|
||||||
|
return log(value) / log(2.0)
|
||||||
|
|
||||||
|
func get_bits_number_for(value: int) -> int:
|
||||||
|
if value == 0:
|
||||||
|
return 1
|
||||||
|
return int(ceil(log2(value + 1)))
|
||||||
|
|
||||||
|
func initialize_color_code_table(colors: PoolByteArray) -> CodeTable:
|
||||||
|
var result_code_table: CodeTable = CodeTable.new()
|
||||||
|
for color_id in colors:
|
||||||
|
# warning-ignore:return_value_discarded
|
||||||
|
result_code_table.add(CodeEntry.new([color_id]))
|
||||||
|
# move counter to the first available compression code index
|
||||||
|
var last_color_index: int = colors.size() - 1
|
||||||
|
var clear_code_index: int = pow(2, get_bits_number_for(last_color_index))
|
||||||
|
result_code_table.counter = clear_code_index + 2
|
||||||
|
return result_code_table
|
||||||
|
|
||||||
|
# compression and decompression done with source:
|
||||||
|
# http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp
|
||||||
|
|
||||||
|
func compress_lzw(image: PoolByteArray, colors: PoolByteArray) -> Array:
|
||||||
|
# Initialize code table
|
||||||
|
var code_table: CodeTable = initialize_color_code_table(colors)
|
||||||
|
# Clear Code index is 2**<code size>
|
||||||
|
# <code size> is the amount of bits needed to write down all colors
|
||||||
|
# from color table. We use last color index because we can write
|
||||||
|
# all colors (for example 16 colors) with indexes from 0 to 15.
|
||||||
|
# Number 15 is in binary 0b1111, so we'll need 4 bits to write all
|
||||||
|
# colors down.
|
||||||
|
var last_color_index: int = colors.size() - 1
|
||||||
|
var clear_code_index: int = pow(2, get_bits_number_for(last_color_index))
|
||||||
|
var index_stream: PoolByteArray = image
|
||||||
|
var current_code_size: int = get_bits_number_for(clear_code_index)
|
||||||
|
var binary_code_stream = lsbbitpacker.LSB_LZWBitPacker.new()
|
||||||
|
|
||||||
|
# initialize with Clear Code
|
||||||
|
binary_code_stream.write_bits(clear_code_index, current_code_size)
|
||||||
|
|
||||||
|
# Read first index from index stream.
|
||||||
|
var index_buffer: CodeEntry = CodeEntry.new([index_stream[0]])
|
||||||
|
var data_index: int = 1
|
||||||
|
# <LOOP POINT>
|
||||||
|
while data_index < index_stream.size():
|
||||||
|
# Get the next index from the index stream.
|
||||||
|
var K: CodeEntry = CodeEntry.new([index_stream[data_index]])
|
||||||
|
data_index += 1
|
||||||
|
# Is index buffer + K in our code table?
|
||||||
|
var new_index_buffer: CodeEntry = index_buffer.add(K)
|
||||||
|
if code_table.has(new_index_buffer): # if YES
|
||||||
|
# Add K to the end of the index buffer
|
||||||
|
index_buffer = new_index_buffer
|
||||||
|
else: # if NO
|
||||||
|
# Add a row for index buffer + K into our code table
|
||||||
|
binary_code_stream.write_bits(code_table.find(index_buffer), current_code_size)
|
||||||
|
|
||||||
|
# We don't want to add new code to code table if we've exceeded 4095
|
||||||
|
# index.
|
||||||
|
var last_entry_index: int = code_table.counter - 1
|
||||||
|
if last_entry_index != 4095:
|
||||||
|
# Output the code for just the index buffer to our code stream
|
||||||
|
# warning-ignore:return_value_discarded
|
||||||
|
code_table.add(new_index_buffer)
|
||||||
|
else:
|
||||||
|
# if we exceeded 4095 index (code table is full), we should
|
||||||
|
# output Clear Code and reset everything.
|
||||||
|
binary_code_stream.write_bits(clear_code_index, current_code_size)
|
||||||
|
code_table = initialize_color_code_table(colors)
|
||||||
|
# get_bits_number_for(clear_code_index) is the same as
|
||||||
|
# LZW code size + 1
|
||||||
|
current_code_size = get_bits_number_for(clear_code_index)
|
||||||
|
|
||||||
|
# Detect when you have to save new codes in bigger bits boxes
|
||||||
|
# change current code size when it happens because we want to save
|
||||||
|
# flexible code sized codes
|
||||||
|
var new_code_size_candidate: int = get_bits_number_for(code_table.counter - 1)
|
||||||
|
if new_code_size_candidate > current_code_size:
|
||||||
|
current_code_size = new_code_size_candidate
|
||||||
|
|
||||||
|
# Index buffer is set to K
|
||||||
|
index_buffer = K
|
||||||
|
# Output code for contents of index buffer
|
||||||
|
binary_code_stream.write_bits(code_table.find(index_buffer), current_code_size)
|
||||||
|
|
||||||
|
# output end with End Of Information Code
|
||||||
|
binary_code_stream.write_bits(clear_code_index + 1, current_code_size)
|
||||||
|
|
||||||
|
var min_code_size: int = get_bits_number_for(clear_code_index) - 1
|
||||||
|
|
||||||
|
return [binary_code_stream.pack(), min_code_size]
|
||||||
|
|
||||||
|
func decompress_lzw(code_stream_data: PoolByteArray, min_code_size: int, colors: PoolByteArray) -> PoolByteArray:
|
||||||
|
var code_table: CodeTable = initialize_color_code_table(colors)
|
||||||
|
var index_stream: PoolByteArray = PoolByteArray([])
|
||||||
|
var binary_code_stream = lsbbitunpacker.LSB_LZWBitUnpacker.new(code_stream_data)
|
||||||
|
var current_code_size: int = min_code_size + 1
|
||||||
|
var clear_code_index: int = pow(2, min_code_size)
|
||||||
|
|
||||||
|
# CODE is an index of code table, {CODE} is sequence inside
|
||||||
|
# code table with index CODE. The same goes for PREVCODE.
|
||||||
|
|
||||||
|
# Remove first Clear Code from stream. We don't need it.
|
||||||
|
binary_code_stream.remove_bits(current_code_size)
|
||||||
|
|
||||||
|
# let CODE be the first code in the code stream
|
||||||
|
var code: int = binary_code_stream.read_bits(current_code_size)
|
||||||
|
# output {CODE} to index stream
|
||||||
|
index_stream.append_array(code_table.get(code).sequence)
|
||||||
|
# set PREVCODE = CODE
|
||||||
|
var prevcode: int = code
|
||||||
|
# <LOOP POINT>
|
||||||
|
while true:
|
||||||
|
# let CODE be the next code in the code stream
|
||||||
|
code = binary_code_stream.read_bits(current_code_size)
|
||||||
|
# Detect Clear Code. When detected reset everything and get next code.
|
||||||
|
if code == clear_code_index:
|
||||||
|
code_table = initialize_color_code_table(colors)
|
||||||
|
current_code_size = min_code_size + 1
|
||||||
|
code = binary_code_stream.read_bits(current_code_size)
|
||||||
|
elif code == clear_code_index + 1: # Stop when detected EOI Code.
|
||||||
|
break
|
||||||
|
# is CODE in the code table?
|
||||||
|
var code_entry: CodeEntry = code_table.get(code)
|
||||||
|
if code_entry != null: # if YES
|
||||||
|
# output {CODE} to index stream
|
||||||
|
index_stream.append_array(code_entry.sequence)
|
||||||
|
# let K be the first index in {CODE}
|
||||||
|
var K: CodeEntry = CodeEntry.new([code_entry.sequence[0]])
|
||||||
|
# warning-ignore:return_value_discarded
|
||||||
|
# add {PREVCODE} + K to the code table
|
||||||
|
code_table.add(code_table.get(prevcode).add(K))
|
||||||
|
# set PREVCODE = CODE
|
||||||
|
prevcode = code
|
||||||
|
else: # if NO
|
||||||
|
# let K be the first index of {PREVCODE}
|
||||||
|
var prevcode_entry: CodeEntry = code_table.get(prevcode)
|
||||||
|
var K: CodeEntry = CodeEntry.new([prevcode_entry.sequence[0]])
|
||||||
|
# output {PREVCODE} + K to index stream
|
||||||
|
index_stream.append_array(prevcode_entry.add(K).sequence)
|
||||||
|
# add {PREVCODE} + K to code table
|
||||||
|
# warning-ignore:return_value_discarded
|
||||||
|
code_table.add(prevcode_entry.add(K))
|
||||||
|
# set PREVCODE = CODE
|
||||||
|
prevcode = code
|
||||||
|
|
||||||
|
# Detect when we should increase current code size and increase it.
|
||||||
|
var new_code_size_candidate: int = get_bits_number_for(code_table.counter)
|
||||||
|
if new_code_size_candidate > current_code_size:
|
||||||
|
current_code_size = new_code_size_candidate
|
||||||
|
|
||||||
|
return index_stream
|
392
addons/gdgifexporter/gifexporter.gd
Normal file
392
addons/gdgifexporter/gifexporter.gd
Normal file
|
@ -0,0 +1,392 @@
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
var little_endian = preload('./little_endian.gd').new()
|
||||||
|
var lzw = preload('./gif-lzw/lzw.gd').new()
|
||||||
|
var used_proc_count: int = 4
|
||||||
|
|
||||||
|
|
||||||
|
class GraphicControlExtension:
|
||||||
|
var extension_introducer: int = 0x21
|
||||||
|
var graphic_control_label: int = 0xf9
|
||||||
|
|
||||||
|
var block_size: int = 4
|
||||||
|
var packed_fields: int = 0b00001000
|
||||||
|
var delay_time: int = 0
|
||||||
|
var transparent_color_index: int = 0
|
||||||
|
|
||||||
|
func _init(_delay_time: int,
|
||||||
|
use_transparency: bool = false,
|
||||||
|
_transparent_color_index: int = 0):
|
||||||
|
delay_time = _delay_time
|
||||||
|
transparent_color_index = _transparent_color_index
|
||||||
|
if use_transparency:
|
||||||
|
packed_fields = 0b00001001
|
||||||
|
|
||||||
|
func to_bytes() -> PoolByteArray:
|
||||||
|
var little_endian = preload('./little_endian.gd').new()
|
||||||
|
var result: PoolByteArray = PoolByteArray([])
|
||||||
|
|
||||||
|
result.append(extension_introducer)
|
||||||
|
result.append(graphic_control_label)
|
||||||
|
|
||||||
|
result.append(block_size)
|
||||||
|
result.append(packed_fields)
|
||||||
|
result += little_endian.int_to_2bytes(delay_time)
|
||||||
|
result.append(transparent_color_index)
|
||||||
|
|
||||||
|
result.append(0)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
class ImageDescriptor:
|
||||||
|
var image_separator: int = 0x2c
|
||||||
|
var image_left_position: int = 0
|
||||||
|
var image_top_position: int = 0
|
||||||
|
var image_width: int
|
||||||
|
var image_height: int
|
||||||
|
var packed_fields: int = 0b10000000
|
||||||
|
|
||||||
|
func _init(_image_left_position: int,
|
||||||
|
_image_top_position: int,
|
||||||
|
_image_width: int,
|
||||||
|
_image_height: int,
|
||||||
|
size_of_local_color_table: int):
|
||||||
|
image_left_position = _image_left_position
|
||||||
|
image_top_position = _image_top_position
|
||||||
|
image_width = _image_width
|
||||||
|
image_height = _image_height
|
||||||
|
packed_fields = packed_fields | (0b111 & size_of_local_color_table)
|
||||||
|
|
||||||
|
func to_bytes() -> PoolByteArray:
|
||||||
|
var little_endian = preload('./little_endian.gd').new()
|
||||||
|
var result: PoolByteArray = PoolByteArray([])
|
||||||
|
|
||||||
|
result.append(image_separator)
|
||||||
|
result += little_endian.int_to_2bytes(image_left_position)
|
||||||
|
result += little_endian.int_to_2bytes(image_top_position)
|
||||||
|
result += little_endian.int_to_2bytes(image_width)
|
||||||
|
result += little_endian.int_to_2bytes(image_height)
|
||||||
|
result.append(packed_fields)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
class LocalColorTable:
|
||||||
|
var colors: Array = []
|
||||||
|
|
||||||
|
func log2(value: float) -> float:
|
||||||
|
return log(value) / log(2.0)
|
||||||
|
|
||||||
|
func get_size() -> int:
|
||||||
|
if colors.size() <= 1:
|
||||||
|
return 0
|
||||||
|
return int(ceil(log2(colors.size()) - 1))
|
||||||
|
|
||||||
|
func to_bytes() -> PoolByteArray:
|
||||||
|
var result: PoolByteArray = PoolByteArray([])
|
||||||
|
|
||||||
|
for v in colors:
|
||||||
|
result.append(v[0])
|
||||||
|
result.append(v[1])
|
||||||
|
result.append(v[2])
|
||||||
|
|
||||||
|
if colors.size() != int(pow(2, get_size() + 1)):
|
||||||
|
for i in range(int(pow(2, get_size() + 1)) - colors.size()):
|
||||||
|
result += PoolByteArray([0, 0, 0])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
class ApplicationExtension:
|
||||||
|
var extension_introducer: int = 0x21
|
||||||
|
var extension_label: int = 0xff
|
||||||
|
|
||||||
|
var block_size: int = 11
|
||||||
|
var application_identifier: PoolByteArray
|
||||||
|
var appl_authentication_code: PoolByteArray
|
||||||
|
|
||||||
|
var application_data: PoolByteArray
|
||||||
|
|
||||||
|
func _init(_application_identifier: String,
|
||||||
|
_appl_authentication_code: String):
|
||||||
|
application_identifier = _application_identifier.to_ascii()
|
||||||
|
appl_authentication_code = _appl_authentication_code.to_ascii()
|
||||||
|
|
||||||
|
func to_bytes() -> PoolByteArray:
|
||||||
|
var result: PoolByteArray = PoolByteArray([])
|
||||||
|
|
||||||
|
result.append(extension_introducer)
|
||||||
|
result.append(extension_label)
|
||||||
|
result.append(block_size)
|
||||||
|
result += application_identifier
|
||||||
|
result += appl_authentication_code
|
||||||
|
|
||||||
|
result.append(application_data.size())
|
||||||
|
result += application_data
|
||||||
|
|
||||||
|
result.append(0)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
class ImageData:
|
||||||
|
var lzw_minimum_code_size: int
|
||||||
|
var image_data: PoolByteArray
|
||||||
|
|
||||||
|
func to_bytes() -> PoolByteArray:
|
||||||
|
var result: PoolByteArray = PoolByteArray([])
|
||||||
|
result.append(lzw_minimum_code_size)
|
||||||
|
|
||||||
|
var block_size_index: int = 0
|
||||||
|
var i: int = 0
|
||||||
|
var data_index: int = 0
|
||||||
|
while data_index < image_data.size():
|
||||||
|
if i == 0:
|
||||||
|
result.append(0)
|
||||||
|
block_size_index = result.size() - 1
|
||||||
|
result.append(image_data[data_index])
|
||||||
|
result[block_size_index] += 1
|
||||||
|
data_index += 1
|
||||||
|
i += 1
|
||||||
|
if i == 254:
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
if not image_data.empty():
|
||||||
|
result.append(0)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
class ConvertedImage:
|
||||||
|
var image_converted_to_codes: PoolByteArray
|
||||||
|
var color_table: Array
|
||||||
|
var transparency_color_index: int
|
||||||
|
var width: int
|
||||||
|
var height: int
|
||||||
|
|
||||||
|
class ConvertionResult:
|
||||||
|
var converted_image: ConvertedImage = ConvertedImage.new()
|
||||||
|
var error: int = Error.OK
|
||||||
|
|
||||||
|
func with_error_code(_error: int) -> ConvertionResult:
|
||||||
|
error = _error
|
||||||
|
return self
|
||||||
|
|
||||||
|
class ThreadWriteFrameResult:
|
||||||
|
var frame_data: PoolByteArray = PoolByteArray([])
|
||||||
|
var error: int = Error.OK
|
||||||
|
|
||||||
|
func with_error_code(_error: int) -> ThreadWriteFrameResult:
|
||||||
|
error = _error
|
||||||
|
return self
|
||||||
|
|
||||||
|
enum Error {
|
||||||
|
OK = 0,
|
||||||
|
EMPTY_IMAGE = 1,
|
||||||
|
BAD_IMAGE_FORMAT = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# File data and Header
|
||||||
|
var data: PoolByteArray = 'GIF'.to_ascii() + '89a'.to_ascii()
|
||||||
|
|
||||||
|
func _init(_width: int, _height: int):
|
||||||
|
# Logical Screen Descriptor
|
||||||
|
var width: int = _width
|
||||||
|
var height: int = _height
|
||||||
|
# not Global Color Table Flag
|
||||||
|
# Color Resolution = 8 bits
|
||||||
|
# Sort Flag = 0, not sorted.
|
||||||
|
# Size of Global Color Table set to 0
|
||||||
|
# because we'll use only Local Tables
|
||||||
|
var packed_fields: int = 0b01110000
|
||||||
|
var background_color_index: int = 0
|
||||||
|
var pixel_aspect_ratio: int = 0
|
||||||
|
|
||||||
|
data += little_endian.int_to_2bytes(width)
|
||||||
|
data += little_endian.int_to_2bytes(height)
|
||||||
|
data.append(packed_fields)
|
||||||
|
data.append(background_color_index)
|
||||||
|
data.append(pixel_aspect_ratio)
|
||||||
|
|
||||||
|
var application_extension: ApplicationExtension = ApplicationExtension.new(
|
||||||
|
"NETSCAPE",
|
||||||
|
"2.0")
|
||||||
|
application_extension.application_data = PoolByteArray([1, 0, 0])
|
||||||
|
data += application_extension.to_bytes()
|
||||||
|
|
||||||
|
func calc_delay_time(frame_delay: float) -> int:
|
||||||
|
return int(ceil(frame_delay / 0.01))
|
||||||
|
|
||||||
|
func color_table_to_indexes(colors: Array) -> PoolByteArray:
|
||||||
|
var result: PoolByteArray = PoolByteArray([])
|
||||||
|
for i in range(colors.size()):
|
||||||
|
result.append(i)
|
||||||
|
return result
|
||||||
|
|
||||||
|
func find_color_table_if_has_less_than_256_colors(image: Image) -> Dictionary:
|
||||||
|
image.lock()
|
||||||
|
var result: Dictionary = {}
|
||||||
|
var image_data: PoolByteArray = image.get_data()
|
||||||
|
|
||||||
|
for i in range(0, image_data.size(), 4):
|
||||||
|
var color: Array = [int(image_data[i]), int(image_data[i + 1]), int(image_data[i + 2]), int(image_data[i + 3])]
|
||||||
|
if not color in result:
|
||||||
|
result[color] = result.size()
|
||||||
|
if result.size() > 256:
|
||||||
|
break
|
||||||
|
|
||||||
|
image.unlock()
|
||||||
|
return result
|
||||||
|
|
||||||
|
func change_colors_to_codes(image: Image,
|
||||||
|
color_palette: Dictionary,
|
||||||
|
transparency_color_index: int) -> PoolByteArray:
|
||||||
|
image.lock()
|
||||||
|
var image_data: PoolByteArray = image.get_data()
|
||||||
|
var result: PoolByteArray = PoolByteArray([])
|
||||||
|
|
||||||
|
for i in range(0, image_data.size(), 4):
|
||||||
|
var color: Array = [int(image_data[i]), int(image_data[i + 1]), int(image_data[i + 2]), int(image_data[i + 3])]
|
||||||
|
if color in color_palette:
|
||||||
|
if color[3] == 0 and transparency_color_index != -1:
|
||||||
|
result.append(transparency_color_index)
|
||||||
|
else:
|
||||||
|
result.append(color_palette[color])
|
||||||
|
else:
|
||||||
|
result.append(0)
|
||||||
|
push_warning('change_colors_to_codes: color not found! [%d, %d, %d, %d]' % color)
|
||||||
|
|
||||||
|
image.unlock()
|
||||||
|
return result
|
||||||
|
|
||||||
|
func sum_color(color: Array) -> int:
|
||||||
|
return color[0] + color[1] + color[2] + color[3]
|
||||||
|
|
||||||
|
func find_transparency_color_index(color_table: Dictionary) -> int:
|
||||||
|
for color in color_table:
|
||||||
|
if sum_color(color) == 0:
|
||||||
|
return color_table[color]
|
||||||
|
return -1
|
||||||
|
|
||||||
|
func find_transparency_color_index_for_quantized_image(color_table: Array) -> int:
|
||||||
|
for i in range(color_table.size()):
|
||||||
|
if sum_color(color_table[i]) == 0:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
||||||
|
func make_sure_color_table_is_at_least_size_4(color_table: Array) -> Array:
|
||||||
|
var result := [] + color_table
|
||||||
|
if color_table.size() < 4:
|
||||||
|
for i in range(4 - color_table.size()):
|
||||||
|
result.append([0, 0, 0, 0])
|
||||||
|
return result
|
||||||
|
|
||||||
|
func convert_image(image: Image, quantizator) -> ConvertionResult:
|
||||||
|
var result := ConvertionResult.new()
|
||||||
|
|
||||||
|
# check if image is of good format
|
||||||
|
if image.get_format() != Image.FORMAT_RGBA8:
|
||||||
|
return result.with_error_code(Error.BAD_IMAGE_FORMAT)
|
||||||
|
|
||||||
|
# check if image isn't empty
|
||||||
|
if image.is_empty():
|
||||||
|
return result.with_error_code(Error.EMPTY_IMAGE)
|
||||||
|
|
||||||
|
var found_color_table: Dictionary = find_color_table_if_has_less_than_256_colors(
|
||||||
|
image)
|
||||||
|
|
||||||
|
var image_converted_to_codes: PoolByteArray
|
||||||
|
var transparency_color_index: int = -1
|
||||||
|
var color_table: Array
|
||||||
|
if found_color_table.size() <= 256: # we don't need to quantize the image.
|
||||||
|
# exporter images always try to include transparency because I'm lazy.
|
||||||
|
transparency_color_index = find_transparency_color_index(found_color_table)
|
||||||
|
if transparency_color_index == -1 and found_color_table.size() <= 255:
|
||||||
|
found_color_table[[0, 0, 0, 0]] = found_color_table.size()
|
||||||
|
transparency_color_index = found_color_table.size() - 1
|
||||||
|
image_converted_to_codes = change_colors_to_codes(
|
||||||
|
image, found_color_table, transparency_color_index)
|
||||||
|
color_table = make_sure_color_table_is_at_least_size_4(found_color_table.keys())
|
||||||
|
else: # we have to quantize the image.
|
||||||
|
var quantization_result: Array = quantizator.quantize_and_convert_to_codes(image)
|
||||||
|
image_converted_to_codes = quantization_result[0]
|
||||||
|
color_table = quantization_result[1]
|
||||||
|
# don't find transparency index if the quantization algorithm
|
||||||
|
# provides it as third return value
|
||||||
|
if quantization_result.size() == 3:
|
||||||
|
transparency_color_index = 0 if quantization_result[2] else -1
|
||||||
|
else:
|
||||||
|
transparency_color_index = find_transparency_color_index_for_quantized_image(quantization_result[1])
|
||||||
|
|
||||||
|
result.converted_image.image_converted_to_codes = image_converted_to_codes
|
||||||
|
result.converted_image.color_table = color_table
|
||||||
|
result.converted_image.transparency_color_index = transparency_color_index
|
||||||
|
result.converted_image.width = image.get_width()
|
||||||
|
result.converted_image.height = image.get_height()
|
||||||
|
|
||||||
|
return result.with_error_code(Error.OK)
|
||||||
|
|
||||||
|
func write_frame(image: Image, frame_delay: float, quantizator) -> int:
|
||||||
|
var converted_image_result := convert_image(image, quantizator)
|
||||||
|
if converted_image_result.error != Error.OK:
|
||||||
|
return converted_image_result.error
|
||||||
|
|
||||||
|
var converted_image := converted_image_result.converted_image
|
||||||
|
return write_frame_from_conv_image(converted_image, frame_delay)
|
||||||
|
|
||||||
|
func write_frame_from_conv_image(converted_image: ConvertedImage,
|
||||||
|
frame_delay: float) -> int:
|
||||||
|
var delay_time := calc_delay_time(frame_delay)
|
||||||
|
|
||||||
|
var color_table_indexes = color_table_to_indexes(converted_image.color_table)
|
||||||
|
var compressed_image_result: Array = lzw.compress_lzw(
|
||||||
|
converted_image.image_converted_to_codes, color_table_indexes)
|
||||||
|
var compressed_image_data: PoolByteArray = compressed_image_result[0]
|
||||||
|
var lzw_min_code_size: int = compressed_image_result[1]
|
||||||
|
|
||||||
|
var table_image_data_block: ImageData = ImageData.new()
|
||||||
|
table_image_data_block.lzw_minimum_code_size = lzw_min_code_size
|
||||||
|
table_image_data_block.image_data = compressed_image_data
|
||||||
|
|
||||||
|
var local_color_table: LocalColorTable = LocalColorTable.new()
|
||||||
|
local_color_table.colors = converted_image.color_table
|
||||||
|
|
||||||
|
var image_descriptor: ImageDescriptor = ImageDescriptor.new(0, 0,
|
||||||
|
converted_image.width,
|
||||||
|
converted_image.height,
|
||||||
|
local_color_table.get_size())
|
||||||
|
|
||||||
|
var graphic_control_extension: GraphicControlExtension
|
||||||
|
if converted_image.transparency_color_index != -1:
|
||||||
|
graphic_control_extension = GraphicControlExtension.new(
|
||||||
|
delay_time, true, converted_image.transparency_color_index)
|
||||||
|
else:
|
||||||
|
graphic_control_extension = GraphicControlExtension.new(
|
||||||
|
delay_time, false, 0)
|
||||||
|
|
||||||
|
data += graphic_control_extension.to_bytes()
|
||||||
|
data += image_descriptor.to_bytes()
|
||||||
|
data += local_color_table.to_bytes()
|
||||||
|
data += table_image_data_block.to_bytes()
|
||||||
|
|
||||||
|
return Error.OK
|
||||||
|
|
||||||
|
func scale_conv_image(converted_image: ConvertedImage, scale_factor: int) -> ConvertedImage:
|
||||||
|
var result = ConvertedImage.new()
|
||||||
|
|
||||||
|
result.image_converted_to_codes = PoolByteArray([])
|
||||||
|
result.color_table = converted_image.color_table.duplicate()
|
||||||
|
result.transparency_color_index = converted_image.transparency_color_index
|
||||||
|
result.width = converted_image.width * scale_factor
|
||||||
|
result.height = converted_image.height * scale_factor
|
||||||
|
|
||||||
|
for y in range(converted_image.height):
|
||||||
|
var row := PoolByteArray([])
|
||||||
|
for x in range(converted_image.width):
|
||||||
|
for i in range(scale_factor):
|
||||||
|
row.append(converted_image.image_converted_to_codes[(y * converted_image.width) + x])
|
||||||
|
for i in range(scale_factor):
|
||||||
|
result.image_converted_to_codes += row
|
||||||
|
row = PoolByteArray([])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
func export_file_data() -> PoolByteArray:
|
||||||
|
return data + PoolByteArray([0x3b])
|
5
addons/gdgifexporter/little_endian.gd
Normal file
5
addons/gdgifexporter/little_endian.gd
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
func int_to_2bytes(value: int) -> PoolByteArray:
|
||||||
|
return PoolByteArray([value & 255, (value >> 8) & 255])
|
22
addons/gdgifexporter/lookup_similar.shader
Normal file
22
addons/gdgifexporter/lookup_similar.shader
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
shader_type canvas_item;
|
||||||
|
render_mode unshaded;
|
||||||
|
|
||||||
|
uniform sampler2D lut;
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
vec4 color = texture(TEXTURE, UV);
|
||||||
|
vec4 similar = texture(lut, vec2(0.5 / 256.0, 0.5));
|
||||||
|
float index = 0.0;
|
||||||
|
if (color.a > 0.0) {
|
||||||
|
float dist = distance(color.xyz, similar.xyz);
|
||||||
|
for (int i = 1; i < 256; i++) {
|
||||||
|
vec4 c = texture(lut, vec2((float(i) + 0.5) / 256.0, 0.5));
|
||||||
|
float d = distance(color.xyz, c.xyz);
|
||||||
|
if (d < dist) {
|
||||||
|
dist = d;
|
||||||
|
index = float(i) / 255.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
COLOR = vec4(vec3(index), 1.0);
|
||||||
|
}
|
148
addons/gdgifexporter/quantization/median_cut.gd
Normal file
148
addons/gdgifexporter/quantization/median_cut.gd
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
var converter = preload('../converter.gd').new()
|
||||||
|
var color_table: Dictionary = {}
|
||||||
|
var transparency: bool = false
|
||||||
|
var tree: TreeNode
|
||||||
|
var leaf: Array = []
|
||||||
|
|
||||||
|
|
||||||
|
class TreeNode:
|
||||||
|
var colors: Array
|
||||||
|
var average_color: Array
|
||||||
|
var axis: int
|
||||||
|
var median: int
|
||||||
|
var parent: TreeNode
|
||||||
|
var left: TreeNode
|
||||||
|
var right: TreeNode
|
||||||
|
|
||||||
|
|
||||||
|
func _init(_parent: TreeNode, _colors: Array):
|
||||||
|
self.parent = _parent
|
||||||
|
self.colors = _colors
|
||||||
|
|
||||||
|
|
||||||
|
func median_cut() -> void:
|
||||||
|
var start: Array = [255, 255, 255]
|
||||||
|
var end: Array = [0, 0, 0]
|
||||||
|
var delta: Array = [0, 0, 0]
|
||||||
|
|
||||||
|
for color in colors:
|
||||||
|
for i in 3:
|
||||||
|
if color[i] < start[i]:
|
||||||
|
start[i] = color[i]
|
||||||
|
if color[i] > end[i]:
|
||||||
|
end[i] = color[i]
|
||||||
|
for i in 3:
|
||||||
|
delta[i] = end[i] - start[i]
|
||||||
|
axis = 0
|
||||||
|
if delta[1] > delta[0]:
|
||||||
|
axis = 1
|
||||||
|
if delta[2] > delta[axis]:
|
||||||
|
axis = 2
|
||||||
|
|
||||||
|
var axis_sort: Array = []
|
||||||
|
for i in colors.size():
|
||||||
|
axis_sort.append(colors[i][axis])
|
||||||
|
axis_sort.sort()
|
||||||
|
var cut = colors.size() >> 1
|
||||||
|
median = axis_sort[cut]
|
||||||
|
|
||||||
|
var left_colors: Array = []
|
||||||
|
var right_colors: Array = []
|
||||||
|
for color in colors:
|
||||||
|
if color[axis] < median:
|
||||||
|
left_colors.append(color)
|
||||||
|
else:
|
||||||
|
right_colors.append(color)
|
||||||
|
left = TreeNode.new(self, left_colors)
|
||||||
|
right = TreeNode.new(self, right_colors)
|
||||||
|
colors = []
|
||||||
|
|
||||||
|
|
||||||
|
func calculate_average_color(color_table: Dictionary) -> void:
|
||||||
|
average_color = [0, 0, 0]
|
||||||
|
var total: int = 0
|
||||||
|
for color in colors:
|
||||||
|
var weight = color_table[color]
|
||||||
|
for i in 3:
|
||||||
|
average_color[i] += color[i] * weight
|
||||||
|
total += weight
|
||||||
|
for i in 3:
|
||||||
|
average_color[i] /= total
|
||||||
|
|
||||||
|
|
||||||
|
func fill_color_table(image: Image) -> void:
|
||||||
|
image.lock()
|
||||||
|
var data: PoolByteArray = image.get_data()
|
||||||
|
|
||||||
|
for i in range(0, data.size(), 4):
|
||||||
|
if data[i + 3] == 0:
|
||||||
|
transparency = true
|
||||||
|
continue
|
||||||
|
var color: Array = [data[i], data[i + 1], data[i + 2]]
|
||||||
|
var count = color_table.get(color, 0)
|
||||||
|
color_table[color] = count + 1
|
||||||
|
image.unlock()
|
||||||
|
|
||||||
|
|
||||||
|
func convert_image(image: Image, colors: Array) -> PoolByteArray:
|
||||||
|
image.lock()
|
||||||
|
var data: PoolByteArray = image.get_data()
|
||||||
|
var nearest_lookup: Dictionary = {}
|
||||||
|
var result: PoolByteArray = PoolByteArray()
|
||||||
|
|
||||||
|
for i in colors.size():
|
||||||
|
colors[i] = Vector3(colors[i][0], colors[i][1], colors[i][2])
|
||||||
|
|
||||||
|
for i in range(0, data.size(), 4):
|
||||||
|
if data[i + 3] == 0:
|
||||||
|
result.append(0)
|
||||||
|
continue
|
||||||
|
var current: Vector3 = Vector3(data[i], data[i + 1], data[i + 2])
|
||||||
|
var nearest_index: int = 0 + int(transparency)
|
||||||
|
if current in nearest_lookup:
|
||||||
|
nearest_index = nearest_lookup[current]
|
||||||
|
else:
|
||||||
|
var nearest_distance: float = current.distance_squared_to(colors[nearest_index])
|
||||||
|
for j in range(1 + int(transparency), colors.size()):
|
||||||
|
var distance: float = current.distance_squared_to(colors[j])
|
||||||
|
if distance < nearest_distance:
|
||||||
|
nearest_index = j
|
||||||
|
nearest_distance = distance
|
||||||
|
nearest_lookup[current] = nearest_index
|
||||||
|
result.append(nearest_index)
|
||||||
|
|
||||||
|
image.unlock()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func quantize_and_convert_to_codes(image: Image) -> Array:
|
||||||
|
color_table.clear()
|
||||||
|
transparency = false
|
||||||
|
fill_color_table(image)
|
||||||
|
|
||||||
|
tree = TreeNode.new(null, color_table.keys())
|
||||||
|
leaf = [tree]
|
||||||
|
var num = 254 if transparency else 255
|
||||||
|
while leaf.size() <= num:
|
||||||
|
var node = leaf.pop_front()
|
||||||
|
if node.colors.size() > 1:
|
||||||
|
node.median_cut()
|
||||||
|
leaf.append(node.left)
|
||||||
|
leaf.append(node.right)
|
||||||
|
if leaf.size() <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
var color_quantized: Dictionary = {}
|
||||||
|
for node in leaf:
|
||||||
|
node.calculate_average_color(color_table)
|
||||||
|
color_quantized[node.average_color] = color_quantized.size()
|
||||||
|
|
||||||
|
var color_array: Array = color_quantized.keys()
|
||||||
|
if transparency:
|
||||||
|
color_array.push_front([0, 0, 0])
|
||||||
|
|
||||||
|
var data: PoolByteArray = converter.setup(image, color_array)
|
||||||
|
return [data, color_array, transparency]
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 1af82adeafb98a0cfc8eaad5a02efe9774242947
|
|
|
@ -129,10 +129,6 @@ gdscript/warnings/return_value_discarded=false
|
||||||
window/size/width=1280
|
window/size/width=1280
|
||||||
window/size/height=720
|
window/size/height=720
|
||||||
|
|
||||||
[editor_plugins]
|
|
||||||
|
|
||||||
enabled=PoolStringArray( "godot-gifexporter" )
|
|
||||||
|
|
||||||
[importer_defaults]
|
[importer_defaults]
|
||||||
|
|
||||||
texture={
|
texture={
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
|
# Gif exporter
|
||||||
|
const gifexporter = preload("res://addons/gdgifexporter/gifexporter.gd")
|
||||||
|
var quantization = preload("res://addons/gdgifexporter/quantization/median_cut.gd").new()
|
||||||
|
|
||||||
enum ExportTab { FRAME = 0, SPRITESHEET = 1, ANIMATION = 2 }
|
enum ExportTab { FRAME = 0, SPRITESHEET = 1, ANIMATION = 2 }
|
||||||
var current_tab : int = ExportTab.FRAME
|
var current_tab : int = ExportTab.FRAME
|
||||||
|
|
||||||
|
@ -20,7 +24,6 @@ var lines_count := 1
|
||||||
# Animation options
|
# Animation options
|
||||||
enum AnimationType { MULTIPLE_FILES = 0, ANIMATED = 1 }
|
enum AnimationType { MULTIPLE_FILES = 0, ANIMATED = 1 }
|
||||||
var animation_type : int = AnimationType.MULTIPLE_FILES
|
var animation_type : int = AnimationType.MULTIPLE_FILES
|
||||||
var background_color : Color = Color.white
|
|
||||||
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
|
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
|
||||||
var direction : int = AnimationDirection.FORWARD
|
var direction : int = AnimationDirection.FORWARD
|
||||||
|
|
||||||
|
@ -43,7 +46,6 @@ var exported_frame_current_tag : int
|
||||||
var exported_orientation : int
|
var exported_orientation : int
|
||||||
var exported_lines_count : int
|
var exported_lines_count : int
|
||||||
var exported_animation_type : int
|
var exported_animation_type : int
|
||||||
var exported_background_color : Color
|
|
||||||
var exported_direction : int
|
var exported_direction : int
|
||||||
var exported_resize : int
|
var exported_resize : int
|
||||||
var exported_interpolation : int
|
var exported_interpolation : int
|
||||||
|
@ -56,6 +58,16 @@ var stop_export = false
|
||||||
|
|
||||||
var file_exists_alert = "File %s already exists. Overwrite?"
|
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():
|
||||||
|
if gif_export_thread.is_active():
|
||||||
|
gif_export_thread.wait_to_finish()
|
||||||
|
|
||||||
|
|
||||||
func process_frame() -> void:
|
func process_frame() -> void:
|
||||||
var frame = Global.current_project.frames[frame_number - 1]
|
var frame = Global.current_project.frames[frame_number - 1]
|
||||||
|
@ -127,18 +139,18 @@ func process_animation() -> void:
|
||||||
processed_images.append(image)
|
processed_images.append(image)
|
||||||
|
|
||||||
|
|
||||||
func export_processed_images(ignore_overwrites: bool, path_validation_alert_popup: AcceptDialog, file_exists_alert_popup: AcceptDialog, export_dialog: AcceptDialog ) -> bool:
|
func export_processed_images(ignore_overwrites: bool, export_dialog: AcceptDialog ) -> bool:
|
||||||
# Stop export if directory path or file name are not valid
|
# Stop export if directory path or file name are not valid
|
||||||
var dir = Directory.new()
|
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) or not file_name.is_valid_filename():
|
||||||
path_validation_alert_popup.popup_centered()
|
export_dialog.open_path_validation_alert_popup()
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# Check export paths
|
# Check export paths
|
||||||
var export_paths = []
|
var export_paths = []
|
||||||
for i in range(processed_images.size()):
|
for i in range(processed_images.size()):
|
||||||
stop_export = false
|
stop_export = false
|
||||||
var multiple_files := true if (current_tab == ExportTab.ANIMATION && animation_type == AnimationType.MULTIPLE_FILES) else 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)
|
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 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:
|
if multiple_files and new_dir_for_each_frame_tag:
|
||||||
|
@ -152,8 +164,7 @@ func export_processed_images(ignore_overwrites: bool, path_validation_alert_popu
|
||||||
# Ask user if he want's to overwrite the file
|
# Ask user if he want's to overwrite the file
|
||||||
if not was_exported or (was_exported and not ignore_overwrites):
|
if not was_exported or (was_exported and not ignore_overwrites):
|
||||||
# Overwrite existing file?
|
# Overwrite existing file?
|
||||||
file_exists_alert_popup.dialog_text = file_exists_alert % export_path
|
export_dialog.open_file_exists_alert_popup(file_exists_alert % export_path)
|
||||||
file_exists_alert_popup.popup_centered()
|
|
||||||
# Stops the function until the user decides if he want's to overwrite
|
# Stops the function until the user decides if he want's to overwrite
|
||||||
yield(export_dialog, "resume_export_function")
|
yield(export_dialog, "resume_export_function")
|
||||||
if stop_export:
|
if stop_export:
|
||||||
|
@ -161,29 +172,16 @@ func export_processed_images(ignore_overwrites: bool, path_validation_alert_popu
|
||||||
return
|
return
|
||||||
export_paths.append(export_path)
|
export_paths.append(export_path)
|
||||||
# Only get one export path if single file animated image is exported
|
# Only get one export path if single file animated image is exported
|
||||||
if current_tab == ExportTab.ANIMATION && animation_type == AnimationType.ANIMATED:
|
if current_tab == ExportTab.ANIMATION and animation_type == AnimationType.ANIMATED:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Scale images that are to export
|
# Scale images that are to export
|
||||||
scale_processed_images()
|
scale_processed_images()
|
||||||
|
|
||||||
if current_tab == ExportTab.ANIMATION && animation_type == AnimationType.ANIMATED:
|
if current_tab == ExportTab.ANIMATION and animation_type == AnimationType.ANIMATED:
|
||||||
var frame_delay_in_ms = Global.animation_timer.wait_time * 100
|
if gif_export_thread.is_active():
|
||||||
|
gif_export_thread.wait_to_finish()
|
||||||
$GifExporter.begin_export(export_paths[0], processed_images[0].get_width(), processed_images[0].get_height(), frame_delay_in_ms, 0)
|
gif_export_thread.start(self, "export_gif", {"export_dialog": export_dialog, "export_paths": export_paths})
|
||||||
match direction:
|
|
||||||
AnimationDirection.FORWARD:
|
|
||||||
for i in range(processed_images.size()):
|
|
||||||
$GifExporter.write_frame(processed_images[i], background_color, frame_delay_in_ms)
|
|
||||||
AnimationDirection.BACKWARDS:
|
|
||||||
for i in range(processed_images.size() - 1, -1, -1):
|
|
||||||
$GifExporter.write_frame(processed_images[i], background_color, frame_delay_in_ms)
|
|
||||||
AnimationDirection.PING_PONG:
|
|
||||||
for i in range(0, processed_images.size()):
|
|
||||||
$GifExporter.write_frame(processed_images[i], background_color, frame_delay_in_ms)
|
|
||||||
for i in range(processed_images.size() - 2, 0, -1):
|
|
||||||
$GifExporter.write_frame(processed_images[i], background_color, frame_delay_in_ms)
|
|
||||||
$GifExporter.end_export()
|
|
||||||
else:
|
else:
|
||||||
for i in range(processed_images.size()):
|
for i in range(processed_images.size()):
|
||||||
if OS.get_name() == "HTML5":
|
if OS.get_name() == "HTML5":
|
||||||
|
@ -197,10 +195,54 @@ func export_processed_images(ignore_overwrites: bool, path_validation_alert_popu
|
||||||
was_exported = true
|
was_exported = true
|
||||||
store_export_settings()
|
store_export_settings()
|
||||||
Global.file_menu.get_popup().set_item_text(5, tr("Export") + " %s" % (file_name + file_format_string(file_format)))
|
Global.file_menu.get_popup().set_item_text(5, tr("Export") + " %s" % (file_name + file_format_string(file_format)))
|
||||||
Global.notification_label("File(s) exported")
|
|
||||||
|
# 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
|
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.animation_timer.wait_time, exporter, args["export_dialog"])
|
||||||
|
AnimationDirection.BACKWARDS:
|
||||||
|
for i in range(processed_images.size() - 1, -1, -1):
|
||||||
|
write_frame_to_gif(processed_images[i], Global.animation_timer.wait_time, 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.animation_timer.wait_time, exporter, args["export_dialog"])
|
||||||
|
for i in range(processed_images.size() - 2, 0, -1):
|
||||||
|
write_frame_to_gif(processed_images[i], Global.animation_timer.wait_time, exporter, args["export_dialog"])
|
||||||
|
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: Node, export_dialog: Node) -> void:
|
||||||
|
exporter.write_frame(image, wait_time, quantization)
|
||||||
|
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:
|
func scale_processed_images() -> void:
|
||||||
for processed_image in processed_images:
|
for processed_image in processed_images:
|
||||||
if resize != 100:
|
if resize != 100:
|
||||||
|
@ -288,7 +330,6 @@ func store_export_settings() -> void:
|
||||||
exported_orientation = orientation
|
exported_orientation = orientation
|
||||||
exported_lines_count = lines_count
|
exported_lines_count = lines_count
|
||||||
exported_animation_type = animation_type
|
exported_animation_type = animation_type
|
||||||
exported_background_color = background_color
|
|
||||||
exported_direction = direction
|
exported_direction = direction
|
||||||
exported_resize = resize
|
exported_resize = resize
|
||||||
exported_interpolation = interpolation
|
exported_interpolation = interpolation
|
||||||
|
@ -305,7 +346,6 @@ func restore_previous_export_settings() -> void:
|
||||||
orientation = exported_orientation
|
orientation = exported_orientation
|
||||||
lines_count = exported_lines_count
|
lines_count = exported_lines_count
|
||||||
animation_type = exported_animation_type
|
animation_type = exported_animation_type
|
||||||
background_color = exported_background_color
|
|
||||||
direction = exported_direction
|
direction = exported_direction
|
||||||
resize = exported_resize
|
resize = exported_resize
|
||||||
interpolation = exported_interpolation
|
interpolation = exported_interpolation
|
||||||
|
|
|
@ -11,8 +11,12 @@ onready var popups = $Popups
|
||||||
onready var file_exists_alert_popup = $Popups/FileExistsAlert
|
onready var file_exists_alert_popup = $Popups/FileExistsAlert
|
||||||
onready var path_validation_alert_popup = $Popups/PathValidationAlert
|
onready var path_validation_alert_popup = $Popups/PathValidationAlert
|
||||||
onready var path_dialog_popup = $Popups/PathDialog
|
onready var path_dialog_popup = $Popups/PathDialog
|
||||||
|
onready var export_progress_popup = $Popups/ExportProgressBar
|
||||||
|
onready var export_progress_bar = $Popups/ExportProgressBar/MarginContainer/ProgressBar
|
||||||
|
|
||||||
|
onready var animation_options_multiple_animations_directories = $VBoxContainer/AnimationOptions/MultipleAnimationsDirectories
|
||||||
onready var previews = $VBoxContainer/PreviewScroll/Previews
|
onready var previews = $VBoxContainer/PreviewScroll/Previews
|
||||||
|
onready var frame_timer = $FrameTimer
|
||||||
|
|
||||||
onready var frame_options = $VBoxContainer/FrameOptions
|
onready var frame_options = $VBoxContainer/FrameOptions
|
||||||
onready var frame_options_frame_number = $VBoxContainer/FrameOptions/FrameNumber/FrameNumber
|
onready var frame_options_frame_number = $VBoxContainer/FrameOptions/FrameNumber/FrameNumber
|
||||||
|
@ -26,10 +30,8 @@ onready var spritesheet_options_lines_count_label = $VBoxContainer/SpritesheetOp
|
||||||
onready var animation_options = $VBoxContainer/AnimationOptions
|
onready var animation_options = $VBoxContainer/AnimationOptions
|
||||||
onready var animation_options_animation_type = $VBoxContainer/AnimationOptions/AnimationType
|
onready var animation_options_animation_type = $VBoxContainer/AnimationOptions/AnimationType
|
||||||
onready var animation_options_animation_options = $VBoxContainer/AnimationOptions/AnimatedOptions
|
onready var animation_options_animation_options = $VBoxContainer/AnimationOptions/AnimatedOptions
|
||||||
onready var animation_options_background_color = $VBoxContainer/AnimationOptions/AnimatedOptions/BackgroundColor
|
|
||||||
onready var animation_options_direction = $VBoxContainer/AnimationOptions/AnimatedOptions/Direction
|
onready var animation_options_direction = $VBoxContainer/AnimationOptions/AnimatedOptions/Direction
|
||||||
|
|
||||||
onready var frame_timer = $FrameTimer
|
|
||||||
|
|
||||||
onready var options_resize = $VBoxContainer/Options/Resize
|
onready var options_resize = $VBoxContainer/Options/Resize
|
||||||
onready var options_interpolation = $VBoxContainer/Options/Interpolation
|
onready var options_interpolation = $VBoxContainer/Options/Interpolation
|
||||||
|
@ -38,8 +40,6 @@ onready var path_line_edit = $VBoxContainer/Path/PathLineEdit
|
||||||
onready var file_line_edit = $VBoxContainer/File/FileLineEdit
|
onready var file_line_edit = $VBoxContainer/File/FileLineEdit
|
||||||
onready var file_file_format = $VBoxContainer/File/FileFormat
|
onready var file_file_format = $VBoxContainer/File/FileFormat
|
||||||
|
|
||||||
onready var animation_options_multiple_animations_directories = $VBoxContainer/AnimationOptions/MultipleAnimationsDirectories
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
tabs.add_tab("Frame")
|
tabs.add_tab("Frame")
|
||||||
|
@ -52,10 +52,8 @@ func _ready() -> void:
|
||||||
add_button("Cancel", false, "cancel")
|
add_button("Cancel", false, "cancel")
|
||||||
file_exists_alert_popup.add_button("Cancel Export", false, "cancel")
|
file_exists_alert_popup.add_button("Cancel Export", false, "cancel")
|
||||||
|
|
||||||
# Disable GIF export for unsupported platforms
|
# Remove close button from export progress bar
|
||||||
if not $GifExporter.is_platform_supported():
|
export_progress_popup.get_close_button().hide()
|
||||||
animation_options_animation_type.selected = Export.AnimationType.MULTIPLE_FILES
|
|
||||||
animation_options_animation_type.disabled = true
|
|
||||||
|
|
||||||
|
|
||||||
func show_tab() -> void:
|
func show_tab() -> void:
|
||||||
|
@ -95,7 +93,6 @@ func show_tab() -> void:
|
||||||
set_file_format_selector()
|
set_file_format_selector()
|
||||||
Export.process_animation()
|
Export.process_animation()
|
||||||
animation_options_animation_type.selected = Export.animation_type
|
animation_options_animation_type.selected = Export.animation_type
|
||||||
animation_options_background_color.color = Export.background_color
|
|
||||||
animation_options_direction.selected = Export.direction
|
animation_options_direction.selected = Export.direction
|
||||||
animation_options.show()
|
animation_options.show()
|
||||||
set_preview()
|
set_preview()
|
||||||
|
@ -111,7 +108,7 @@ func external_export() -> void:
|
||||||
Export.process_spritesheet()
|
Export.process_spritesheet()
|
||||||
Export.ExportTab.ANIMATION:
|
Export.ExportTab.ANIMATION:
|
||||||
Export.process_animation()
|
Export.process_animation()
|
||||||
if Export.export_processed_images(true, path_validation_alert_popup, file_exists_alert_popup, self):
|
if Export.export_processed_images(true, self):
|
||||||
hide()
|
hide()
|
||||||
|
|
||||||
|
|
||||||
|
@ -216,6 +213,26 @@ func create_frame_tag_list() -> void:
|
||||||
spritesheet_options_frames.add_item(item.name)
|
spritesheet_options_frames.add_item(item.name)
|
||||||
|
|
||||||
|
|
||||||
|
func open_path_validation_alert_popup() -> void:
|
||||||
|
path_validation_alert_popup.popup_centered()
|
||||||
|
|
||||||
|
|
||||||
|
func open_file_exists_alert_popup(dialog_text: String) -> void:
|
||||||
|
file_exists_alert_popup.dialog_text = dialog_text
|
||||||
|
file_exists_alert_popup.popup_centered()
|
||||||
|
|
||||||
|
|
||||||
|
func toggle_export_progress_popup(open: bool) -> void:
|
||||||
|
if open:
|
||||||
|
export_progress_popup.popup_centered()
|
||||||
|
else:
|
||||||
|
export_progress_popup.hide()
|
||||||
|
|
||||||
|
|
||||||
|
func set_export_progress_bar(value: float) -> void:
|
||||||
|
export_progress_bar.value = value
|
||||||
|
|
||||||
|
|
||||||
func _on_ExportDialog_about_to_show() -> void:
|
func _on_ExportDialog_about_to_show() -> void:
|
||||||
# If export already occured - fill the dialog with previous export settings
|
# If export already occured - fill the dialog with previous export settings
|
||||||
if Export.was_exported:
|
if Export.was_exported:
|
||||||
|
@ -278,10 +295,6 @@ func _on_AnimationType_item_selected(id : int) -> void:
|
||||||
set_preview()
|
set_preview()
|
||||||
|
|
||||||
|
|
||||||
func _on_BackgroundColor_color_changed(color : Color) -> void:
|
|
||||||
Export.background_color = color
|
|
||||||
|
|
||||||
|
|
||||||
func _on_Direction_item_selected(id : int) -> void:
|
func _on_Direction_item_selected(id : int) -> void:
|
||||||
Export.direction = id
|
Export.direction = id
|
||||||
match id:
|
match id:
|
||||||
|
@ -303,7 +316,7 @@ func _on_Interpolation_item_selected(id: int) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_ExportDialog_confirmed() -> void:
|
func _on_ExportDialog_confirmed() -> void:
|
||||||
if Export.export_processed_images(false, path_validation_alert_popup, file_exists_alert_popup, self):
|
if Export.export_processed_images(false, self):
|
||||||
hide()
|
hide()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
[gd_scene load_steps=3 format=2]
|
[gd_scene load_steps=2 format=2]
|
||||||
|
|
||||||
[ext_resource path="res://src/UI/Dialogs/ExportDialog.gd" type="Script" id=1]
|
[ext_resource path="res://src/UI/Dialogs/ExportDialog.gd" type="Script" id=1]
|
||||||
[ext_resource path="res://addons/godot-gifexporter/src/GifExporter.gd" type="Script" id=2]
|
|
||||||
|
|
||||||
[node name="ExportDialog" type="AcceptDialog"]
|
[node name="ExportDialog" type="AcceptDialog"]
|
||||||
margin_right = 532.0
|
margin_right = 532.0
|
||||||
|
@ -183,31 +182,14 @@ margin_right = 516.0
|
||||||
margin_bottom = 52.0
|
margin_bottom = 52.0
|
||||||
rect_min_size = Vector2( 0, 24 )
|
rect_min_size = Vector2( 0, 24 )
|
||||||
|
|
||||||
[node name="BackgroundColorLabel" type="Label" parent="VBoxContainer/AnimationOptions/AnimatedOptions"]
|
|
||||||
margin_top = 5.0
|
|
||||||
margin_right = 78.0
|
|
||||||
margin_bottom = 19.0
|
|
||||||
text = "Background:"
|
|
||||||
valign = 1
|
|
||||||
|
|
||||||
[node name="BackgroundColor" type="ColorPickerButton" parent="VBoxContainer/AnimationOptions/AnimatedOptions"]
|
|
||||||
margin_left = 82.0
|
|
||||||
margin_right = 263.0
|
|
||||||
margin_bottom = 24.0
|
|
||||||
mouse_default_cursor_shape = 2
|
|
||||||
size_flags_horizontal = 7
|
|
||||||
color = Color( 1, 1, 1, 1 )
|
|
||||||
edit_alpha = false
|
|
||||||
|
|
||||||
[node name="DirectionLabel" type="Label" parent="VBoxContainer/AnimationOptions/AnimatedOptions"]
|
[node name="DirectionLabel" type="Label" parent="VBoxContainer/AnimationOptions/AnimatedOptions"]
|
||||||
margin_left = 267.0
|
|
||||||
margin_top = 5.0
|
margin_top = 5.0
|
||||||
margin_right = 330.0
|
margin_right = 63.0
|
||||||
margin_bottom = 19.0
|
margin_bottom = 19.0
|
||||||
text = "Direction:"
|
text = "Direction:"
|
||||||
|
|
||||||
[node name="Direction" type="OptionButton" parent="VBoxContainer/AnimationOptions/AnimatedOptions"]
|
[node name="Direction" type="OptionButton" parent="VBoxContainer/AnimationOptions/AnimatedOptions"]
|
||||||
margin_left = 334.0
|
margin_left = 67.0
|
||||||
margin_right = 516.0
|
margin_right = 516.0
|
||||||
margin_bottom = 24.0
|
margin_bottom = 24.0
|
||||||
rect_min_size = Vector2( 100, 0 )
|
rect_min_size = Vector2( 100, 0 )
|
||||||
|
@ -365,10 +347,10 @@ __meta__ = {
|
||||||
}
|
}
|
||||||
|
|
||||||
[node name="FileExistsAlert" type="AcceptDialog" parent="Popups"]
|
[node name="FileExistsAlert" type="AcceptDialog" parent="Popups"]
|
||||||
margin_left = 8.0
|
margin_left = 10.5227
|
||||||
margin_top = 180.0
|
margin_top = 176.636
|
||||||
margin_right = 448.0
|
margin_right = 450.523
|
||||||
margin_bottom = 280.0
|
margin_bottom = 276.636
|
||||||
size_flags_horizontal = 0
|
size_flags_horizontal = 0
|
||||||
size_flags_vertical = 0
|
size_flags_vertical = 0
|
||||||
window_title = "Alarm!"
|
window_title = "Alarm!"
|
||||||
|
@ -378,16 +360,41 @@ __meta__ = {
|
||||||
"_edit_use_anchors_": false
|
"_edit_use_anchors_": false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[node name="ExportProgressBar" type="WindowDialog" parent="Popups"]
|
||||||
|
margin_left = 63.0
|
||||||
|
margin_top = 215.0
|
||||||
|
margin_right = 402.0
|
||||||
|
margin_bottom = 256.0
|
||||||
|
popup_exclusive = true
|
||||||
|
window_title = "Exporting in progress..."
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_group_": true,
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="MarginContainer" type="MarginContainer" parent="Popups/ExportProgressBar"]
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
margin_left = 5.0
|
||||||
|
margin_top = 5.0
|
||||||
|
margin_right = -5.0
|
||||||
|
margin_bottom = -5.0
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="ProgressBar" type="ProgressBar" parent="Popups/ExportProgressBar/MarginContainer"]
|
||||||
|
margin_right = 329.0
|
||||||
|
margin_bottom = 14.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
[node name="FrameTimer" type="Timer" parent="."]
|
[node name="FrameTimer" type="Timer" parent="."]
|
||||||
__meta__ = {
|
__meta__ = {
|
||||||
"_editor_description_": "Timer to advance animation frames in animation preview."
|
"_editor_description_": "Timer to advance animation frames in animation preview."
|
||||||
}
|
}
|
||||||
|
|
||||||
[node name="GifExporter" type="Node" parent="."]
|
|
||||||
script = ExtResource( 2 )
|
|
||||||
__meta__ = {
|
|
||||||
"_editor_description_": ""
|
|
||||||
}
|
|
||||||
[connection signal="about_to_show" from="." to="." method="_on_ExportDialog_about_to_show"]
|
[connection signal="about_to_show" from="." to="." method="_on_ExportDialog_about_to_show"]
|
||||||
[connection signal="confirmed" from="." to="." method="_on_ExportDialog_confirmed"]
|
[connection signal="confirmed" from="." to="." method="_on_ExportDialog_confirmed"]
|
||||||
[connection signal="custom_action" from="." to="." method="_on_ExportDialog_custom_action"]
|
[connection signal="custom_action" from="." to="." method="_on_ExportDialog_custom_action"]
|
||||||
|
@ -399,7 +406,6 @@ __meta__ = {
|
||||||
[connection signal="value_changed" from="VBoxContainer/SpritesheetOptions/Orientation/LinesCount" to="." method="_on_LinesCount_value_changed"]
|
[connection signal="value_changed" from="VBoxContainer/SpritesheetOptions/Orientation/LinesCount" to="." method="_on_LinesCount_value_changed"]
|
||||||
[connection signal="item_selected" from="VBoxContainer/AnimationOptions/AnimationType" to="." method="_on_AnimationType_item_selected"]
|
[connection signal="item_selected" from="VBoxContainer/AnimationOptions/AnimationType" to="." method="_on_AnimationType_item_selected"]
|
||||||
[connection signal="toggled" from="VBoxContainer/AnimationOptions/MultipleAnimationsDirectories" to="." method="_on_MultipleAnimationsDirectories_toggled"]
|
[connection signal="toggled" from="VBoxContainer/AnimationOptions/MultipleAnimationsDirectories" to="." method="_on_MultipleAnimationsDirectories_toggled"]
|
||||||
[connection signal="color_changed" from="VBoxContainer/AnimationOptions/AnimatedOptions/BackgroundColor" to="." method="_on_BackgroundColor_color_changed"]
|
|
||||||
[connection signal="item_selected" from="VBoxContainer/AnimationOptions/AnimatedOptions/Direction" to="." method="_on_Direction_item_selected"]
|
[connection signal="item_selected" from="VBoxContainer/AnimationOptions/AnimatedOptions/Direction" to="." method="_on_Direction_item_selected"]
|
||||||
[connection signal="value_changed" from="VBoxContainer/Options/Resize" to="." method="_on_Resize_value_changed"]
|
[connection signal="value_changed" from="VBoxContainer/Options/Resize" to="." method="_on_Resize_value_changed"]
|
||||||
[connection signal="item_selected" from="VBoxContainer/Options/Interpolation" to="." method="_on_Interpolation_item_selected"]
|
[connection signal="item_selected" from="VBoxContainer/Options/Interpolation" to="." method="_on_Interpolation_item_selected"]
|
||||||
|
|
Loading…
Reference in a new issue