diff --git a/addons/README.md b/addons/README.md new file mode 100644 index 000000000..f30b9eaf8 --- /dev/null +++ b/addons/README.md @@ -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` \ No newline at end of file diff --git a/addons/gdgifexporter/LICENSE b/addons/gdgifexporter/LICENSE new file mode 100644 index 000000000..ca5b2ad5e --- /dev/null +++ b/addons/gdgifexporter/LICENSE @@ -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. diff --git a/addons/gdgifexporter/converter.gd b/addons/gdgifexporter/converter.gd new file mode 100644 index 000000000..54c7580c8 --- /dev/null +++ b/addons/gdgifexporter/converter.gd @@ -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() diff --git a/addons/gdgifexporter/gif-lzw/LICENSE b/addons/gdgifexporter/gif-lzw/LICENSE new file mode 100644 index 000000000..ca5b2ad5e --- /dev/null +++ b/addons/gdgifexporter/gif-lzw/LICENSE @@ -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. diff --git a/addons/gdgifexporter/gif-lzw/lsbbitpacker.gd b/addons/gdgifexporter/gif-lzw/lsbbitpacker.gd new file mode 100644 index 000000000..a3434d61f97 --- /dev/null +++ b/addons/gdgifexporter/gif-lzw/lsbbitpacker.gd @@ -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([]) diff --git a/addons/gdgifexporter/gif-lzw/lsbbitunpacker.gd b/addons/gdgifexporter/gif-lzw/lsbbitunpacker.gd new file mode 100644 index 000000000..d2348dc0f --- /dev/null +++ b/addons/gdgifexporter/gif-lzw/lsbbitunpacker.gd @@ -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) diff --git a/addons/gdgifexporter/gif-lzw/lzw.gd b/addons/gdgifexporter/gif-lzw/lzw.gd new file mode 100644 index 000000000..87ed25d60 --- /dev/null +++ b/addons/gdgifexporter/gif-lzw/lzw.gd @@ -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** + # 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 + # + 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 + # + 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 diff --git a/addons/gdgifexporter/gifexporter.gd b/addons/gdgifexporter/gifexporter.gd new file mode 100644 index 000000000..a8af577dd --- /dev/null +++ b/addons/gdgifexporter/gifexporter.gd @@ -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]) diff --git a/addons/gdgifexporter/little_endian.gd b/addons/gdgifexporter/little_endian.gd new file mode 100644 index 000000000..1077f183a --- /dev/null +++ b/addons/gdgifexporter/little_endian.gd @@ -0,0 +1,5 @@ +extends Node + + +func int_to_2bytes(value: int) -> PoolByteArray: + return PoolByteArray([value & 255, (value >> 8) & 255]) diff --git a/addons/gdgifexporter/lookup_similar.shader b/addons/gdgifexporter/lookup_similar.shader new file mode 100644 index 000000000..061466192 --- /dev/null +++ b/addons/gdgifexporter/lookup_similar.shader @@ -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); +} \ No newline at end of file diff --git a/addons/gdgifexporter/quantization/median_cut.gd b/addons/gdgifexporter/quantization/median_cut.gd new file mode 100644 index 000000000..e53d90292 --- /dev/null +++ b/addons/gdgifexporter/quantization/median_cut.gd @@ -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] diff --git a/addons/godot-gifexporter b/addons/godot-gifexporter deleted file mode 160000 index 1af82adea..000000000 --- a/addons/godot-gifexporter +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1af82adeafb98a0cfc8eaad5a02efe9774242947 diff --git a/project.godot b/project.godot index 3116edc08..1a6d8495a 100644 --- a/project.godot +++ b/project.godot @@ -129,10 +129,6 @@ gdscript/warnings/return_value_discarded=false window/size/width=1280 window/size/height=720 -[editor_plugins] - -enabled=PoolStringArray( "godot-gifexporter" ) - [importer_defaults] texture={ diff --git a/src/Autoload/Export.gd b/src/Autoload/Export.gd index 0768a8da3..b8c2491d2 100644 --- a/src/Autoload/Export.gd +++ b/src/Autoload/Export.gd @@ -1,5 +1,9 @@ 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 } var current_tab : int = ExportTab.FRAME @@ -20,7 +24,6 @@ var lines_count := 1 # Animation options enum AnimationType { MULTIPLE_FILES = 0, ANIMATED = 1 } var animation_type : int = AnimationType.MULTIPLE_FILES -var background_color : Color = Color.white enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 } var direction : int = AnimationDirection.FORWARD @@ -43,7 +46,6 @@ var exported_frame_current_tag : int var exported_orientation : int var exported_lines_count : int var exported_animation_type : int -var exported_background_color : Color var exported_direction : int var exported_resize : int var exported_interpolation : int @@ -56,6 +58,16 @@ 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(): + if gif_export_thread.is_active(): + gif_export_thread.wait_to_finish() + func process_frame() -> void: var frame = Global.current_project.frames[frame_number - 1] @@ -127,18 +139,18 @@ func process_animation() -> void: 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 var dir = Directory.new() 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 # 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 && 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) # 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: @@ -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 if not was_exported or (was_exported and not ignore_overwrites): # Overwrite existing file? - file_exists_alert_popup.dialog_text = file_exists_alert % export_path - file_exists_alert_popup.popup_centered() + 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: @@ -161,29 +172,16 @@ func export_processed_images(ignore_overwrites: bool, path_validation_alert_popu return export_paths.append(export_path) # 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 # Scale images that are to export scale_processed_images() - if current_tab == ExportTab.ANIMATION && animation_type == AnimationType.ANIMATED: - var frame_delay_in_ms = Global.animation_timer.wait_time * 100 - - $GifExporter.begin_export(export_paths[0], processed_images[0].get_width(), processed_images[0].get_height(), frame_delay_in_ms, 0) - 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() + if current_tab == ExportTab.ANIMATION and animation_type == AnimationType.ANIMATED: + 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": @@ -197,10 +195,54 @@ func export_processed_images(ignore_overwrites: bool, path_validation_alert_popu was_exported = true store_export_settings() 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 +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: for processed_image in processed_images: if resize != 100: @@ -288,7 +330,6 @@ func store_export_settings() -> void: exported_orientation = orientation exported_lines_count = lines_count exported_animation_type = animation_type - exported_background_color = background_color exported_direction = direction exported_resize = resize exported_interpolation = interpolation @@ -305,7 +346,6 @@ func restore_previous_export_settings() -> void: orientation = exported_orientation lines_count = exported_lines_count animation_type = exported_animation_type - background_color = exported_background_color direction = exported_direction resize = exported_resize interpolation = exported_interpolation diff --git a/src/UI/Dialogs/ExportDialog.gd b/src/UI/Dialogs/ExportDialog.gd index 4c96f14a0..88aa7d24c 100644 --- a/src/UI/Dialogs/ExportDialog.gd +++ b/src/UI/Dialogs/ExportDialog.gd @@ -11,8 +11,12 @@ onready var popups = $Popups onready var file_exists_alert_popup = $Popups/FileExistsAlert onready var path_validation_alert_popup = $Popups/PathValidationAlert 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 frame_timer = $FrameTimer onready var frame_options = $VBoxContainer/FrameOptions 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_animation_type = $VBoxContainer/AnimationOptions/AnimationType 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 frame_timer = $FrameTimer onready var options_resize = $VBoxContainer/Options/Resize 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_file_format = $VBoxContainer/File/FileFormat -onready var animation_options_multiple_animations_directories = $VBoxContainer/AnimationOptions/MultipleAnimationsDirectories - func _ready() -> void: tabs.add_tab("Frame") @@ -52,10 +52,8 @@ func _ready() -> void: add_button("Cancel", false, "cancel") file_exists_alert_popup.add_button("Cancel Export", false, "cancel") - # Disable GIF export for unsupported platforms - if not $GifExporter.is_platform_supported(): - animation_options_animation_type.selected = Export.AnimationType.MULTIPLE_FILES - animation_options_animation_type.disabled = true + # Remove close button from export progress bar + export_progress_popup.get_close_button().hide() func show_tab() -> void: @@ -95,7 +93,6 @@ func show_tab() -> void: set_file_format_selector() Export.process_animation() animation_options_animation_type.selected = Export.animation_type - animation_options_background_color.color = Export.background_color animation_options_direction.selected = Export.direction animation_options.show() set_preview() @@ -111,7 +108,7 @@ func external_export() -> void: Export.process_spritesheet() Export.ExportTab.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() @@ -216,6 +213,26 @@ func create_frame_tag_list() -> void: 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: # If export already occured - fill the dialog with previous export settings if Export.was_exported: @@ -278,10 +295,6 @@ func _on_AnimationType_item_selected(id : int) -> void: set_preview() -func _on_BackgroundColor_color_changed(color : Color) -> void: - Export.background_color = color - - func _on_Direction_item_selected(id : int) -> void: Export.direction = id match id: @@ -303,7 +316,7 @@ func _on_Interpolation_item_selected(id: int) -> 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() diff --git a/src/UI/Dialogs/ExportDialog.tscn b/src/UI/Dialogs/ExportDialog.tscn index 05789363b..d3e2b9d07 100644 --- a/src/UI/Dialogs/ExportDialog.tscn +++ b/src/UI/Dialogs/ExportDialog.tscn @@ -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://addons/godot-gifexporter/src/GifExporter.gd" type="Script" id=2] [node name="ExportDialog" type="AcceptDialog"] margin_right = 532.0 @@ -183,31 +182,14 @@ margin_right = 516.0 margin_bottom = 52.0 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"] -margin_left = 267.0 margin_top = 5.0 -margin_right = 330.0 +margin_right = 63.0 margin_bottom = 19.0 text = "Direction:" [node name="Direction" type="OptionButton" parent="VBoxContainer/AnimationOptions/AnimatedOptions"] -margin_left = 334.0 +margin_left = 67.0 margin_right = 516.0 margin_bottom = 24.0 rect_min_size = Vector2( 100, 0 ) @@ -365,10 +347,10 @@ __meta__ = { } [node name="FileExistsAlert" type="AcceptDialog" parent="Popups"] -margin_left = 8.0 -margin_top = 180.0 -margin_right = 448.0 -margin_bottom = 280.0 +margin_left = 10.5227 +margin_top = 176.636 +margin_right = 450.523 +margin_bottom = 276.636 size_flags_horizontal = 0 size_flags_vertical = 0 window_title = "Alarm!" @@ -378,16 +360,41 @@ __meta__ = { "_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="."] __meta__ = { "_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="confirmed" from="." to="." method="_on_ExportDialog_confirmed"] [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="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="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="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"]