From 227095a4f710ac6afd43f0a7e8b296f188cf20be Mon Sep 17 00:00:00 2001 From: David Luevano Alvarado Date: Tue, 31 May 2022 21:11:46 -0600 Subject: add working gif maker --- src/Main.tscn | 45 +++- src/addons/GifMaker/GifDecoder.gd | 44 ++++ src/addons/GifMaker/GifRecorder.gd | 205 +++++++++++++++ src/addons/GifMaker/GifRectangle.gd | 2 + src/addons/GifMaker/GifRectangle.svg | 43 +++ src/addons/GifMaker/GifRectangle.svg.import | 35 +++ src/addons/GifMaker/LICENSE | 21 ++ src/addons/GifMaker/gif-dark.svg | 64 +++++ src/addons/GifMaker/gif-dark.svg.import | 35 +++ src/addons/GifMaker/gif.svg | 57 ++++ src/addons/GifMaker/gif.svg.import | 35 +++ src/addons/GifMaker/godot-gdgifexporter/LICENSE | 21 ++ src/addons/GifMaker/godot-gdgifexporter/README.md | 60 +++++ .../godot-gdgifexporter/gdgifexporter/LICENSE | 21 ++ .../godot-gdgifexporter/gdgifexporter/converter.gd | 59 +++++ .../godot-gdgifexporter/gdgifexporter/exporter.gd | 292 +++++++++++++++++++++ .../gdgifexporter/gif-lzw/LICENSE | 21 ++ .../gdgifexporter/gif-lzw/lsbbitpacker.gd | 31 +++ .../gdgifexporter/gif-lzw/lsbbitunpacker.gd | 41 +++ .../gdgifexporter/gif-lzw/lzw.gd | 210 +++++++++++++++ .../gdgifexporter/little_endian.gd | 5 + .../gdgifexporter/lookup_color.shader | 19 ++ .../gdgifexporter/lookup_similar.shader | 22 ++ .../gdgifexporter/quantization/median_cut.gd | 163 ++++++++++++ .../gdgifexporter/quantization/uniform.gd | 88 +++++++ .../godot-gdgifexporter/imgs/chocolate.png | Bin 0 -> 415979 bytes .../godot-gdgifexporter/imgs/chocolate.png.import | 35 +++ .../GifMaker/godot-gdgifexporter/imgs/colors.png | Bin 0 -> 54053 bytes .../godot-gdgifexporter/imgs/colors.png.import | 35 +++ .../GifMaker/godot-gdgifexporter/imgs/colors2.png | Bin 0 -> 95327 bytes .../godot-gdgifexporter/imgs/colors2.png.import | 35 +++ .../godot-gdgifexporter/imgs/green_parrot.png | Bin 0 -> 1644217 bytes .../imgs/green_parrot.png.import | 35 +++ .../godot-gdgifexporter/imgs/half_transparent.png | Bin 0 -> 372 bytes .../imgs/half_transparent.png.import | 35 +++ .../GifMaker/godot-gdgifexporter/imgs/img1.png | Bin 0 -> 212 bytes .../godot-gdgifexporter/imgs/img1.png.import | 35 +++ .../GifMaker/godot-gdgifexporter/imgs/img2.png | Bin 0 -> 208 bytes .../godot-gdgifexporter/imgs/img2.png.import | 35 +++ .../godot-gdgifexporter/imgs/one_color.png | Bin 0 -> 199 bytes .../godot-gdgifexporter/imgs/one_color.png.import | 35 +++ .../GifMaker/godot-gdgifexporter/imgs/parrots.png | Bin 0 -> 395692 bytes .../godot-gdgifexporter/imgs/parrots.png.import | 35 +++ .../godot-gdgifexporter/imgs/transparent.png | Bin 0 -> 290 bytes .../imgs/transparent.png.import | 35 +++ .../godot-gdgifexporter/imgs/two_colors.png | Bin 0 -> 282 bytes .../godot-gdgifexporter/imgs/two_colors.png.import | 35 +++ src/addons/GifMaker/plugin.cfg | 7 + src/addons/GifMaker/plugin.gd | 10 + src/docs/LICENSE.GifMaker | 21 ++ src/docs/LICENSE.gdgifexporter | 21 ++ src/project.godot | 22 ++ src/screen_recorder.gd | 13 + src/test.gif | Bin 0 -> 34428 bytes 54 files changed, 2117 insertions(+), 1 deletion(-) create mode 100644 src/addons/GifMaker/GifDecoder.gd create mode 100644 src/addons/GifMaker/GifRecorder.gd create mode 100644 src/addons/GifMaker/GifRectangle.gd create mode 100644 src/addons/GifMaker/GifRectangle.svg create mode 100644 src/addons/GifMaker/GifRectangle.svg.import create mode 100644 src/addons/GifMaker/LICENSE create mode 100644 src/addons/GifMaker/gif-dark.svg create mode 100644 src/addons/GifMaker/gif-dark.svg.import create mode 100644 src/addons/GifMaker/gif.svg create mode 100644 src/addons/GifMaker/gif.svg.import create mode 100644 src/addons/GifMaker/godot-gdgifexporter/LICENSE create mode 100644 src/addons/GifMaker/godot-gdgifexporter/README.md create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/LICENSE create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/converter.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/exporter.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/LICENSE create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitpacker.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitunpacker.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lzw.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/little_endian.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_color.shader create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_similar.shader create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/median_cut.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/uniform.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/chocolate.png create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/chocolate.png.import create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/colors.png create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/colors.png.import create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/colors2.png create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/colors2.png.import create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/green_parrot.png create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/green_parrot.png.import create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/half_transparent.png create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/half_transparent.png.import create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/img1.png create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/img1.png.import create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/img2.png create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/img2.png.import create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/one_color.png create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/one_color.png.import create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/parrots.png create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/parrots.png.import create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/transparent.png create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/transparent.png.import create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/two_colors.png create mode 100644 src/addons/GifMaker/godot-gdgifexporter/imgs/two_colors.png.import create mode 100644 src/addons/GifMaker/plugin.cfg create mode 100644 src/addons/GifMaker/plugin.gd create mode 100644 src/docs/LICENSE.GifMaker create mode 100644 src/docs/LICENSE.gdgifexporter create mode 100644 src/screen_recorder.gd create mode 100644 src/test.gif diff --git a/src/Main.tscn b/src/Main.tscn index 3a3eb6b..c0f7499 100644 --- a/src/Main.tscn +++ b/src/Main.tscn @@ -1,11 +1,54 @@ -[gd_scene load_steps=2 format=2] +[gd_scene load_steps=6 format=2] [ext_resource path="res://entities/actors/snake/scenes/Snake.tscn" type="PackedScene" id=1] +[ext_resource path="res://addons/GifMaker/GifRecorder.gd" type="Script" id=2] +[ext_resource path="res://addons/GifMaker/GifRectangle.gd" type="Script" id=3] +[ext_resource path="res://main.gd" type="Script" id=4] +[ext_resource path="res://screen_recorder.gd" type="Script" id=5] [node name="Main" type="Node2D"] +script = ExtResource( 4 ) [node name="Snake" parent="." instance=ExtResource( 1 )] [node name="Camera" type="Camera2D" parent="."] current = true zoom = Vector2( 0.5, 0.5 ) + +[node name="ScreenRecorder" type="CanvasLayer" parent="."] +script = ExtResource( 5 ) + +[node name="Control" type="Control" parent="ScreenRecorder"] +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="GifRectangle" type="ReferenceRect" parent="ScreenRecorder/Control"] +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +margin_left = -64.0 +margin_top = -64.0 +margin_right = 64.0 +margin_bottom = 64.0 +script = ExtResource( 3 ) + +[node name="GifRecorder" type="Viewport" parent="ScreenRecorder/Control"] +size = Vector2( 128, 128 ) +script = ExtResource( 2 ) +render_type = 1 +seconds = 2.0 +autostart = true +capture_node_path = NodePath("../GifRectangle") + +[node name="Label" type="Label" parent="ScreenRecorder/Control"] +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +margin_left = -20.0 +margin_top = -15.5 +margin_right = 20.0 +margin_bottom = 15.5 +text = "TEST +" diff --git a/src/addons/GifMaker/GifDecoder.gd b/src/addons/GifMaker/GifDecoder.gd new file mode 100644 index 0000000..08ccaa9 --- /dev/null +++ b/src/addons/GifMaker/GifDecoder.gd @@ -0,0 +1,44 @@ +extends Node +class_name GifDecoder + +func decode_file(path): + var file = File.new() + file.open(path, File.READ) + var bytes = file.get_buffer(file.get_len()) + file.close() + + return decode_data(bytes) + +func decode_block(block: PoolByteArray, start, end): + var string = block.subarray(start, end).get_string_from_utf8() + if string.begins_with('GIF_MAKER::'): + return str2var(string.substr('GIF_MAKER::'.length(), string.length())) + return string + +func decode_data(bytes: PoolByteArray): + var index = 0 + var start = -1 + var end = -1 + var data = [] + + for byte in bytes: + if start != -1 \ + and byte == 0x21: + end = index - 1 + + data.append(decode_block(bytes, start, end)) + start = -1 + end = -1 + + if byte == 0x21 \ + and index < bytes.size() \ + and bytes[index + 1] == 0xFE: + start = index + 2 + + index += 1 + + if end == -1 and start != -1: + end = bytes.size() - 1 + data.append(decode_block(bytes, start, end)) + + return data diff --git a/src/addons/GifMaker/GifRecorder.gd b/src/addons/GifMaker/GifRecorder.gd new file mode 100644 index 0000000..44e30ed --- /dev/null +++ b/src/addons/GifMaker/GifRecorder.gd @@ -0,0 +1,205 @@ +extends Viewport +class_name GifRecorder, 'res://addons/GifMaker/gif.svg' + +signal record_past_buffer_filled +signal encoding_progress(percentage, frames_done) +signal done_encoding + +const GIFExporter = preload('res://addons/GifMaker/godot-gdgifexporter/gdgifexporter/exporter.gd') +const MedianCut = preload('res://addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/median_cut.gd') +const Uniform = preload('res://addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/uniform.gd') + +enum RenderType { + RENDER_3D, + RENDER_2D +} + +enum RecordType { + RECORD_PAST, + RECORD +} + +enum Framerate { + FPS_100 = 1, + FPS_50 = 2, + FPS_33 = 3, + FPS_25 = 4, + FPS_20 = 5, + FPS_10 = 10, + FPS_8 = 12, + FPS_4 = 24, + FPS_2 = 48, + FPS_1 = 100 +} + +enum Quantization { + MEDIAN_CUT, + UNIFORM +} + +# TODO: Re-write get_properties_list to fancify input +# TODO: Perhaps find a way to render the viewport in editor +# TODO: Record a lil' video with an example Godot project + +export(int, 'Render 3D', 'Render 2D') var render_type = RenderType.RENDER_3D setget set_render_type +export(RecordType) var record_type = RecordType.RECORD_PAST +export var seconds = 6.28 setget set_seconds +export(Framerate) var framerate = Framerate.FPS_25 setget set_framerate +export(Quantization) var quantization = Quantization.UNIFORM +export var autostart = false + +export(NodePath) var capture_node_path +onready var capture_node = get_node(capture_node_path) + +export var preview = false +export var preview_render = false +export(NodePath) var preview_path +onready var preview_node = get_node(preview_path) + +var frame_amount = 0 +var frame_passed = 0 +var frames = [] +var exporter +var frame_timer +var thread + +func _ready(): + frame_timer = Timer.new() + add_child(frame_timer) + + assert(capture_node, 'No capture node detected, please add a Camera3D or GifRectangle to get started') + + self.framerate = framerate + self.seconds = seconds + self.render_type = render_type + + frame_timer.connect('timeout', self, 'capture') + + if autostart: + start() + +func start(): + frame_timer.start() + +func stop(): + frame_timer.stop() + +func clear(): + frames = [] + +func render(metadata = null): + stop() + + exporter = GIFExporter.new(size.x, size.y) + + if OS.can_use_threads(): + thread = Thread.new() + thread.start(self, 'encode', metadata) + while thread.is_alive(): + yield(get_tree(), 'idle_frame') + + return thread.wait_to_finish() + else: + return encode(metadata) + +func encode(metadata = null): + var frames_done = 0 + var quantization_method = \ + Uniform if quantization == Quantization.UNIFORM else MedianCut + + for frame in frames: + emit_signal('encoding_progress', float(frames_done) / frames.size(), frames_done) + frame.convert(Image.FORMAT_RGBA8) + exporter.add_frame(frame, framerate, quantization_method) + frames_done += 1 + + if preview and preview_render: + var texture = ImageTexture.new() + texture.create_from_image(frame) + preview_node.texture = texture + + if metadata: + exporter.add_comment_ext('GIF_MAKER::' + var2str(metadata)) + + exporter.add_comment_ext('🙋 Made with GifMaker by Bram Dingelstad') + + emit_signal('encoding_progress', 1.0, frames_done) + emit_signal('done_encoding') + return exporter.export_file_data() + +func render_to_file(file_path, metadata = null): + var buffer = render(metadata) + if buffer is GDScriptFunctionState: + buffer = yield(buffer, 'completed') + + var file = File.new() + file.open(file_path, File.WRITE) + file.store_buffer(buffer) + file.close() + +func capture(): + var frame = Image.new() + var screenshot + match render_type: + RenderType.RENDER_2D: + screenshot = get_tree().root.get_texture().get_data() + screenshot.flip_y() + RenderType.RENDER_3D: + screenshot = get_texture().get_data() + if not render_target_v_flip: + screenshot.flip_y() + + frame.create(size.x, size.y, false, screenshot.get_format()) + frame.blit_rect( + screenshot, + Rect2(capture_node.rect_position if render_type == RenderType.RENDER_2D else Vector2.ZERO, size), + Vector2.ZERO + ) + frames.append(frame) + + match record_type: + RecordType.RECORD_PAST: + while frames.size() > frame_amount: + frames.pop_front() + emit_signal('record_past_buffer_filled') + + if preview: + var texture = ImageTexture.new() + texture.create_from_image(frame) + preview_node.texture = texture + +func _process(delta): + render_target_update_mode = UPDATE_DISABLED if render_type == RenderType.RENDER_2D else UPDATE_ALWAYS + + match render_type: + RenderType.RENDER_2D: + size = capture_node.rect_size + + RenderType.RENDER_3D: + $Camera.transform = capture_node.transform + $Camera.fov = $Camera.fov + + # TODO: Sync more properties like culling mask, projection, etc + +func update_frame_amount(): + frame_amount = 100 / framerate * seconds + +func set_framerate(_framerate): + framerate = _framerate + if is_inside_tree(): + frame_timer.wait_time = 1.0 / (100.0 / framerate) + update_frame_amount() + +func set_seconds(_seconds): + seconds = _seconds + update_frame_amount() + +func set_render_type(_render_type): + render_type = _render_type + + match render_type: + RenderType.RENDER_3D: + var shadow_capture_node = capture_node.duplicate() + shadow_capture_node.name = 'Camera' + shadow_capture_node.script = null + add_child(shadow_capture_node) diff --git a/src/addons/GifMaker/GifRectangle.gd b/src/addons/GifMaker/GifRectangle.gd new file mode 100644 index 0000000..a75beb1 --- /dev/null +++ b/src/addons/GifMaker/GifRectangle.gd @@ -0,0 +1,2 @@ +extends ReferenceRect +class_name GifRectangle, 'res://addons/GifMaker/GifRectangle.svg' diff --git a/src/addons/GifMaker/GifRectangle.svg b/src/addons/GifMaker/GifRectangle.svg new file mode 100644 index 0000000..4f12487 --- /dev/null +++ b/src/addons/GifMaker/GifRectangle.svg @@ -0,0 +1,43 @@ + + + + + + + diff --git a/src/addons/GifMaker/GifRectangle.svg.import b/src/addons/GifMaker/GifRectangle.svg.import new file mode 100644 index 0000000..3f59db6 --- /dev/null +++ b/src/addons/GifMaker/GifRectangle.svg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/GifRectangle.svg-39b586472495007cdcfa44c2055ad072.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/GifRectangle.svg" +dest_files=[ "res://.import/GifRectangle.svg-39b586472495007cdcfa44c2055ad072.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/LICENSE b/src/addons/GifMaker/LICENSE new file mode 100644 index 0000000..3c879f4 --- /dev/null +++ b/src/addons/GifMaker/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Bram Dingelstad, Igor Santarek (jegor377) & Martin Novák (novhack) + +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/src/addons/GifMaker/gif-dark.svg b/src/addons/GifMaker/gif-dark.svg new file mode 100644 index 0000000..a215897 --- /dev/null +++ b/src/addons/GifMaker/gif-dark.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + diff --git a/src/addons/GifMaker/gif-dark.svg.import b/src/addons/GifMaker/gif-dark.svg.import new file mode 100644 index 0000000..4a9d167 --- /dev/null +++ b/src/addons/GifMaker/gif-dark.svg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/gif-dark.svg-d128ddcd15c8f58e57f1d9e5ff273637.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/gif-dark.svg" +dest_files=[ "res://.import/gif-dark.svg-d128ddcd15c8f58e57f1d9e5ff273637.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/gif.svg b/src/addons/GifMaker/gif.svg new file mode 100644 index 0000000..dbaea1e --- /dev/null +++ b/src/addons/GifMaker/gif.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + diff --git a/src/addons/GifMaker/gif.svg.import b/src/addons/GifMaker/gif.svg.import new file mode 100644 index 0000000..80e57fe --- /dev/null +++ b/src/addons/GifMaker/gif.svg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/gif.svg-9260ecf151c775bcfb933188ba2f4372.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/gif.svg" +dest_files=[ "res://.import/gif.svg-9260ecf151c775bcfb933188ba2f4372.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/godot-gdgifexporter/LICENSE b/src/addons/GifMaker/godot-gdgifexporter/LICENSE new file mode 100644 index 0000000..ca5b2ad --- /dev/null +++ b/src/addons/GifMaker/godot-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/src/addons/GifMaker/godot-gdgifexporter/README.md b/src/addons/GifMaker/godot-gdgifexporter/README.md new file mode 100644 index 0000000..8da7779 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/README.md @@ -0,0 +1,60 @@ +# Gif exporter for Godot made entirely in GDScript +This is gif exporter for godot made entirely using GDScript. This is based on [godot-gifexporter](https://github.com/novhack/godot-gifexporter). + +

+ + Mentioned in Awesome Godot + +

+ +## Example +```gdscript +extends Node2D + + +# load gif exporter module +const GIFExporter = preload("res://gdgifexporter/exporter.gd") +# load quantization module that you want to use +const MedianCutQuantization = preload("res://gdgifexporter/quantization/median_cut.gd") + + +func _ready(): + var img := Image.new() + # load your image from png file + img.load('res://image.png') + # remember to use this image format when exporting + img.convert(Image.FORMAT_RGBA8) + + # initialize exporter object with width and height of gif canvas + var exporter = GIFExporter.new(img1.get_width(), img1.get_height()) + # write image using median cut quantization method and with one second animation delay + exporter.add_frame(img, 1, MedianCutQuantization) + + # when you have exported all frames of animation you, then you can save data into file + var file: File = File.new() + # open new file with write privlige + file.open('user://result.gif', File.WRITE) + # save data stream into file + file.store_buffer(exporter.export_file_data()) + # close the file + file.close() +``` + +## Quantization methods +Addon supports two quantization methods: +- Median Cut +- Uniform (with small color adjustment) + +Both method files are stored in gdgifexporter/quantization directory. + +## Error Codes +Some methods give error codes. These are used error codes and their meaning: +- OK = 0 (Everything went okay) +- EMPTY_IMAGE = 1 (Passed image object has no data in it) +- BAD_IMAGE_FORMAT = 2 (You are using different image format than FORMAT_RGBA8) + +# Contributors +If you want to contribute to this code then go ahead! :) Huge thanks to Kinwailo and novhack. This project wouldn't work without their help! :D + +# Used external libs +- [godot-gif-lzw](https://github.com/jegor377/godot-gif-lzw) diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/LICENSE b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/LICENSE new file mode 100644 index 0000000..ca5b2ad --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/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/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/converter.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/converter.gd new file mode 100644 index 0000000..846d93c --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/converter.gd @@ -0,0 +1,59 @@ +extends Reference + +var _shader: Shader + + +func get_indexed_datas(image: Image, colors: Array) -> PoolByteArray: + _shader = preload("./lookup_color.shader") + return _convert(image, colors) + + +func get_similar_indexed_datas(image: Image, colors: Array) -> PoolByteArray: + _shader = preload("./lookup_similar.shader") + return _convert(image, colors) + + +func _convert(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 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.fill(Color8(colors[0][0], colors[0][1], colors[0][2])) + lut.lock() + for i in colors.size(): + 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)) + + VisualServer.free_rid(vp) + VisualServer.free_rid(canvas) + VisualServer.free_rid(ci_rid) + VisualServer.free_rid(mat_rid) + + image.convert(Image.FORMAT_R8) + return image.get_data() diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/exporter.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/exporter.gd new file mode 100644 index 0000000..eb33c27 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/exporter.gd @@ -0,0 +1,292 @@ +extends Reference + +enum Error { OK = 0, EMPTY_IMAGE = 1, BAD_IMAGE_FORMAT = 2 } + +var little_endian = preload("./little_endian.gd").new() +var lzw = preload("./gif-lzw/lzw.gd").new() +var converter = preload("./converter.gd") + +var last_color_table := [] +var last_transparency_index := -1 + +# File data and Header +var data := PoolByteArray([]) + + +func _init(_width: int, _height: int): + add_header() + add_logical_screen_descriptor(_width, _height) + add_application_ext("NETSCAPE", "2.0", [1, 0, 0]) + + +func export_file_data() -> PoolByteArray: + return data + PoolByteArray([0x3b]) + + +func add_header() -> void: + data += "GIF".to_ascii() + "89a".to_ascii() + + +func add_logical_screen_descriptor(width: int, height: int) -> void: + # 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) + + +func add_application_ext(app_iden: String, app_auth_code: String, _data: Array) -> void: + var extension_introducer := 0x21 + var extension_label := 0xff + + var block_size := 11 + + data.append(extension_introducer) + data.append(extension_label) + data.append(block_size) + data += app_iden.to_ascii() + data += app_auth_code.to_ascii() + data.append(_data.size()) + data += PoolByteArray(_data) + data.append(0) + +func add_comment_ext(string: String) -> void: + var extension_introducer := 0x21 + var comment_label := 0xFE + + data.append(extension_introducer) + data.append(comment_label) + data += string.to_utf8() + data.append(0) + + +# finds the image color table. Stops if the size gets larger than 256. +func find_color_table(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 find_transparency_color_index(color_table: Dictionary) -> int: + for color in color_table: + if color[3] == 0: + return color_table[color] + return -1 + + +func colors_to_codes(img: Image, col_palette: Dictionary, transp_color_index: int) -> PoolByteArray: + img.lock() + var image_data: PoolByteArray = img.get_data() + var result: PoolByteArray = PoolByteArray([]) + + for i in range(0, image_data.size(), 4): + var color: Array = [image_data[i], image_data[i + 1], image_data[i + 2], image_data[i + 3]] + + if color in col_palette: + if color[3] == 0 and transp_color_index != -1: + result.append(transp_color_index) + else: + result.append(col_palette[color]) + else: + result.append(0) + push_warning("colors_to_codes: color not found! [%d, %d, %d, %d]" % color) + + img.unlock() + return result + + +# makes sure that the color table is at least size 4. +func make_proper_size(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 color_table_to_indexes(colors: Array) -> PoolByteArray: + var result: PoolByteArray = PoolByteArray([]) + for i in range(colors.size()): + result.append(i) + return result + + +func add_frame(image: Image, delay_time: int, quantizator: Script) -> int: + # check if image is of good format + if image.get_format() != Image.FORMAT_RGBA8: + return Error.BAD_IMAGE_FORMAT + + # check if image isn't empty + if image.is_empty(): + return Error.EMPTY_IMAGE + + var found_color_table: Dictionary = find_color_table(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. + # try to find transparency color index. + transparency_color_index = find_transparency_color_index(found_color_table) + # if didn't found transparency color index but there is atleast one + # place for this color then add it artificially. + 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 = colors_to_codes( + image, found_color_table, transparency_color_index + ) + color_table = make_proper_size(found_color_table.keys()) + else: # we have to quantize the image. + var quantization_result: Array = quantizator.new().quantize(image) + image_converted_to_codes = quantization_result[0] + color_table = quantization_result[1] + # transparency index should always be as the first element of color table. + transparency_color_index = 0 if quantization_result[2] else -1 + + last_color_table = color_table + last_transparency_index = transparency_color_index + + var color_table_indexes := color_table_to_indexes(color_table) + var compressed_image_result: Array = lzw.compress_lzw( + 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] + + add_graphic_constrol_ext(delay_time, transparency_color_index) + add_image_descriptor(Vector2.ZERO, image.get_size(), color_table_bit_size(color_table)) + add_local_color_table(color_table) + add_image_data_block(lzw_min_code_size, compressed_image_data) + + return Error.OK + + +# adds frame with last color informations +func add_frame_with_lci(image: Image, delay_time: int) -> int: + # check if image is of good format + if image.get_format() != Image.FORMAT_RGBA8: + return Error.BAD_IMAGE_FORMAT + + # check if image isn't empty + if image.is_empty(): + return Error.EMPTY_IMAGE + + var image_converted_to_codes: PoolByteArray = converter.new().get_similar_indexed_datas( + image, last_color_table + ) + + var color_table_indexes := color_table_to_indexes(last_color_table) + var compressed_image_result: Array = lzw.compress_lzw( + 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] + + add_graphic_constrol_ext(delay_time, last_transparency_index) + add_image_descriptor(Vector2.ZERO, image.get_size(), color_table_bit_size(last_color_table)) + add_local_color_table(last_color_table) + add_image_data_block(lzw_min_code_size, compressed_image_data) + + return Error.OK + + +func add_graphic_constrol_ext(_delay_time: float, tci: int = -1) -> void: + var extension_introducer: int = 0x21 + var graphic_control_label: int = 0xf9 + + var block_size: int = 4 + var packed_fields: int = 0b00001000 + if tci != -1: + packed_fields = 0b00001001 + + var delay_time: int = _delay_time + var transparent_color_index: int = tci if tci != -1 else 0 + + data.append(extension_introducer) + data.append(graphic_control_label) + + data.append(block_size) + data.append(packed_fields) + data += little_endian.int_to_2bytes(delay_time) + data.append(transparent_color_index) + + data.append(0) + + +func add_image_descriptor(pos: Vector2, size: Vector2, l_color_table_size: int) -> void: + var image_separator: int = 0x2c + var packed_fields: int = 0b10000000 | (0b111 & l_color_table_size) + + data.append(image_separator) + data += little_endian.int_to_2bytes(int(pos.x)) # left pos + data += little_endian.int_to_2bytes(int(pos.y)) # top pos + data += little_endian.int_to_2bytes(int(size.x)) # width + data += little_endian.int_to_2bytes(int(size.y)) # height + data.append(packed_fields) + + +func color_table_bit_size(color_table: Array) -> int: + if color_table.size() <= 1: + return 0 + var bit_size := int(ceil(log(color_table.size()) / log(2.0))) + return bit_size - 1 + + +func add_local_color_table(color_table: Array) -> void: + for color in color_table: + data.append(color[0]) + data.append(color[1]) + data.append(color[2]) + + var size := color_table_bit_size(color_table) + var proper_size := int(pow(2, size + 1)) + + if color_table.size() != proper_size: + for i in range(proper_size - color_table.size()): + data += PoolByteArray([0, 0, 0]) + + +func add_image_data_block(lzw_min_code_size: int, _data: PoolByteArray) -> void: + data.append(lzw_min_code_size) + + var block_size_index: int = 0 + var i: int = 0 + var data_index: int = 0 + while data_index < _data.size(): + if i == 0: + data.append(0) + block_size_index = data.size() - 1 + data.append(_data[data_index]) + data[block_size_index] += 1 + data_index += 1 + i += 1 + if i == 254: + i = 0 + + if not _data.empty(): + data.append(0) diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/LICENSE b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/LICENSE new file mode 100644 index 0000000..ca5b2ad --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/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/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitpacker.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitpacker.gd new file mode 100644 index 0000000..3141d09 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitpacker.gd @@ -0,0 +1,31 @@ +extends Reference + + +class LSBLZWBitPacker: + var bit_index: int = 0 + var stream: int = 0 + + var chunks: PoolByteArray = PoolByteArray([]) + + func put_byte(): + chunks.append(stream & 0xff) + bit_index -= 8 + stream >>= 8 + + func write_bits(value: int, bits_count: int) -> void: + value &= (1 << bits_count) - 1 + value <<= bit_index + stream |= value + bit_index += bits_count + while bit_index >= 8: + self.put_byte() + + func pack() -> PoolByteArray: + if bit_index != 0: + self.put_byte() + return chunks + + func reset() -> void: + bit_index = 0 + stream = 0 + chunks = PoolByteArray([]) diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitunpacker.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitunpacker.gd new file mode 100644 index 0000000..9f8507f --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitunpacker.gd @@ -0,0 +1,41 @@ +extends Reference + + +class LSBLZWBitUnpacker: + 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/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lzw.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lzw.gd new file mode 100644 index 0000000..9140e7c --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lzw.gd @@ -0,0 +1,210 @@ +extends Reference + +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.LSBLZWBitPacker.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] + + +# gdlint: ignore=max-line-length +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.LSBLZWBitUnpacker.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/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/little_endian.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/little_endian.gd new file mode 100644 index 0000000..cb865e1 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/little_endian.gd @@ -0,0 +1,5 @@ +extends Reference + + +func int_to_2bytes(value: int) -> PoolByteArray: + return PoolByteArray([value & 255, (value >> 8) & 255]) diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_color.shader b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_color.shader new file mode 100644 index 0000000..8379108 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_color.shader @@ -0,0 +1,19 @@ +shader_type canvas_item; +render_mode unshaded; + +uniform sampler2D lut; + +void fragment() { + vec4 color = texture(TEXTURE, UV); + float index = 0.0; + if (color.a > 0.0) { + for (int i = 0; i < 256; i++) { + vec4 c = texture(lut, vec2((float(i) + 0.5) / 256.0, 0.5)); + if (c.rgb == color.rgb) { + index = float(i) / 255.0; + break; + } + } + } + COLOR = vec4(vec3(index), 1.0); +} \ No newline at end of file diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_similar.shader b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_similar.shader new file mode 100644 index 0000000..0614661 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/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/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/median_cut.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/median_cut.gd new file mode 100644 index 0000000..4610f03 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/median_cut.gd @@ -0,0 +1,163 @@ +extends Reference + +var converter = preload("../converter.gd").new() +var transparency := false + + +func longest_axis(colors: Array) -> int: + var start := [255, 255, 255] + var end := [0, 0, 0] + for color in colors: + for i in 3: + start[i] = min(color[i], start[i]) + end[i] = max(color[i], end[i]) + + var max_r = end[0] - start[0] + var max_g = end[1] - start[1] + var max_b = end[2] - start[2] + + if max_r > max_g: + if max_r > max_b: + return 0 + else: + if max_g > max_b: + return 1 + return 2 + + +func get_median(colors: Array) -> Vector3: + return colors[colors.size() >> 1] + + +func median_cut(colors: Array) -> Array: + var axis := longest_axis(colors) + + var axis_sort := [] + for color in colors: + axis_sort.append(color[axis]) + axis_sort.sort() + + var cut := axis_sort.size() >> 1 + var median: int = axis_sort[cut] + axis_sort = [] + + var left_colors := [] + var right_colors := [] + + for color in colors: + if color[axis] < median: + left_colors.append(color) + else: + right_colors.append(color) + + return [left_colors, right_colors] + + +func average_color(bucket: Array) -> Array: + var r := 0 + var g := 0 + var b := 0 + for color in bucket: + r += color[0] + g += color[1] + b += color[2] + return [r / bucket.size(), g / bucket.size(), b / bucket.size()] + + +func average_colors(buckets: Array) -> Dictionary: + var avg_colors := {} + for bucket in buckets: + if bucket.size() > 0: + avg_colors[average_color(bucket)] = avg_colors.size() + return avg_colors + + +func pixels_to_colors(image: Image) -> Array: + image.lock() + var result := [] + var data: PoolByteArray = image.get_data() + + for i in range(0, data.size(), 4): + if data[i + 3] == 0: + transparency = true + continue + result.append([data[i], data[i + 1], data[i + 2]]) + image.unlock() + return result + + +func remove_smallest_bucket(buckets: Array) -> Array: + if buckets.size() == 0: + return buckets + var i_of_smallest_bucket := 0 + for i in range(buckets.size()): + if buckets[i].size() < buckets[i_of_smallest_bucket].size(): + i_of_smallest_bucket = i + buckets.remove(i_of_smallest_bucket) + return buckets + + +func remove_empty_buckets(buckets: Array) -> Array: + if buckets.size() == 0: + return buckets + + var i := buckets.find([]) + while i != -1: + buckets.remove(i) + i = buckets.find([]) + + return buckets + + +# quantizes to gif ready codes +func quantize(image: Image) -> Array: + var pixels = pixels_to_colors(image) + if pixels.size() == 0: + return pixels + + var buckets := [pixels] + var done_buckets := [] + + # it tells how many times buckets should be divided into two + var dimensions := 8 + + for i in range(0, dimensions): + var new_buckets := [] + for bucket in buckets: + # don't median cut if bucket is smaller than 2, because + # it won't produce two new buckets. + if bucket.size() > 1: + var res := median_cut(bucket) + # sometimes when you try to median cut a bucket, the result + # is one with size equal to 0 and other with full size as the + # source bucket. Because of that it's useless to try to divide + # it further so it's better to put it into separate list and + # process only those buckets witch divide further. + if res[0].size() == 0 or res[1].size() == 0: + done_buckets += res + else: + new_buckets += res + buckets = [] + buckets = new_buckets + + var all_buckets := remove_empty_buckets(done_buckets + buckets) + + buckets = [] + done_buckets = [] + + if transparency: + if all_buckets.size() == pow(2, dimensions): + all_buckets = remove_smallest_bucket(all_buckets) + + # dictionaries are only for speed. + var color_array := average_colors(all_buckets).keys() + + # if pixel_to_colors detected that the image has transparent pixels + # then add transparency color at the beginning so it will be properly + # exported. + if transparency: + color_array = [[0, 0, 0]] + color_array + + var data: PoolByteArray = converter.get_similar_indexed_datas(image, color_array) + + return [data, color_array, transparency] diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/uniform.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/uniform.gd new file mode 100644 index 0000000..e953342 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/uniform.gd @@ -0,0 +1,88 @@ +extends Reference + +var converter = preload("../converter.gd").new() +var transparency := false + + +func how_many_divisions(colors_count: int) -> int: + return int(ceil(pow(colors_count, 1.0 / 4.0))) + + +func generate_colors(colors_count: int) -> Array: + var divisions_count: int = how_many_divisions(colors_count) + var colors: Array = [] + + for a in range(divisions_count): + for b in range(divisions_count): + for g in range(divisions_count): + for r in range(divisions_count): + colors.append( + [ + Vector3( + (255.0 / divisions_count) * r, + (255.0 / divisions_count) * g, + (255.0 / divisions_count) * b + ), + (255.0 / divisions_count) * a + ] + ) + + return colors + + +func find_nearest_color(palette_color: Vector3, image_data: PoolByteArray) -> Array: + var nearest_color = null + var nearest_alpha = null + for i in range(0, image_data.size(), 4): + var color = Vector3(image_data[i], image_data[i + 1], image_data[i + 2]) + # detect transparency + if image_data[3] == 0: + transparency = true + if ( + (nearest_color == null) + or ( + palette_color.distance_squared_to(color) + < palette_color.distance_squared_to(nearest_color) + ) + ): + nearest_color = color + nearest_alpha = image_data[i + 3] + return [nearest_color, nearest_alpha] + + +# moves every color from palette colors to the nearest found color in image +func enhance_colors(image: Image, palette_colors: Array) -> Array: + var data := image.get_data() + + for i in range(palette_colors.size()): + var nearest_color := find_nearest_color(palette_colors[i][0], data) + palette_colors[i] = nearest_color + + return palette_colors + + +func to_color_array(colors: Array) -> Array: + var result := [] + for v in colors: + result.append([v[0].x, v[0].y, v[0].z]) + return result + + +# quantizes to gif ready codes +func quantize(image: Image) -> Array: + image.lock() + + var colors: Array = generate_colors(256) + var tmp_image: Image = Image.new() + tmp_image.copy_from(image) + tmp_image.resize(32, 32) + tmp_image.lock() + colors = enhance_colors(tmp_image, colors) + tmp_image.unlock() + + image.unlock() + colors = to_color_array(colors) + + var data: PoolByteArray = converter.get_similar_indexed_datas(image, colors) + + return [data, colors, transparency] diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/chocolate.png b/src/addons/GifMaker/godot-gdgifexporter/imgs/chocolate.png new file mode 100644 index 0000000..20ddb3c Binary files /dev/null and b/src/addons/GifMaker/godot-gdgifexporter/imgs/chocolate.png differ diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/chocolate.png.import b/src/addons/GifMaker/godot-gdgifexporter/imgs/chocolate.png.import new file mode 100644 index 0000000..e9e1e9a --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/imgs/chocolate.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/chocolate.png-34e2ba81579eb1fa5f3e43edbb7d77a7.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/godot-gdgifexporter/imgs/chocolate.png" +dest_files=[ "res://.import/chocolate.png-34e2ba81579eb1fa5f3e43edbb7d77a7.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/colors.png b/src/addons/GifMaker/godot-gdgifexporter/imgs/colors.png new file mode 100644 index 0000000..ef5a146 Binary files /dev/null and b/src/addons/GifMaker/godot-gdgifexporter/imgs/colors.png differ diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/colors.png.import b/src/addons/GifMaker/godot-gdgifexporter/imgs/colors.png.import new file mode 100644 index 0000000..3915844 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/imgs/colors.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/colors.png-ce283e5c03d836da781607a180e7aa71.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/godot-gdgifexporter/imgs/colors.png" +dest_files=[ "res://.import/colors.png-ce283e5c03d836da781607a180e7aa71.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/colors2.png b/src/addons/GifMaker/godot-gdgifexporter/imgs/colors2.png new file mode 100644 index 0000000..746e0ec Binary files /dev/null and b/src/addons/GifMaker/godot-gdgifexporter/imgs/colors2.png differ diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/colors2.png.import b/src/addons/GifMaker/godot-gdgifexporter/imgs/colors2.png.import new file mode 100644 index 0000000..8d104ab --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/imgs/colors2.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/colors2.png-ce18d0c4c3718b88a1b7f3114adf8e78.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/godot-gdgifexporter/imgs/colors2.png" +dest_files=[ "res://.import/colors2.png-ce18d0c4c3718b88a1b7f3114adf8e78.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/green_parrot.png b/src/addons/GifMaker/godot-gdgifexporter/imgs/green_parrot.png new file mode 100644 index 0000000..15003a8 Binary files /dev/null and b/src/addons/GifMaker/godot-gdgifexporter/imgs/green_parrot.png differ diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/green_parrot.png.import b/src/addons/GifMaker/godot-gdgifexporter/imgs/green_parrot.png.import new file mode 100644 index 0000000..13010bd --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/imgs/green_parrot.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/green_parrot.png-d2a592da4624540d1214565b240e40bb.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/godot-gdgifexporter/imgs/green_parrot.png" +dest_files=[ "res://.import/green_parrot.png-d2a592da4624540d1214565b240e40bb.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/half_transparent.png b/src/addons/GifMaker/godot-gdgifexporter/imgs/half_transparent.png new file mode 100644 index 0000000..ce9bed6 Binary files /dev/null and b/src/addons/GifMaker/godot-gdgifexporter/imgs/half_transparent.png differ diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/half_transparent.png.import b/src/addons/GifMaker/godot-gdgifexporter/imgs/half_transparent.png.import new file mode 100644 index 0000000..cf372a0 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/imgs/half_transparent.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/half_transparent.png-e930e02c59422de3a13ef3a308fd011c.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/godot-gdgifexporter/imgs/half_transparent.png" +dest_files=[ "res://.import/half_transparent.png-e930e02c59422de3a13ef3a308fd011c.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/img1.png b/src/addons/GifMaker/godot-gdgifexporter/imgs/img1.png new file mode 100644 index 0000000..6cd5643 Binary files /dev/null and b/src/addons/GifMaker/godot-gdgifexporter/imgs/img1.png differ diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/img1.png.import b/src/addons/GifMaker/godot-gdgifexporter/imgs/img1.png.import new file mode 100644 index 0000000..6807fe2 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/imgs/img1.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/img1.png-2f74ec6ad8e8c230c1b57cd8aa285202.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/godot-gdgifexporter/imgs/img1.png" +dest_files=[ "res://.import/img1.png-2f74ec6ad8e8c230c1b57cd8aa285202.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/img2.png b/src/addons/GifMaker/godot-gdgifexporter/imgs/img2.png new file mode 100644 index 0000000..b530e31 Binary files /dev/null and b/src/addons/GifMaker/godot-gdgifexporter/imgs/img2.png differ diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/img2.png.import b/src/addons/GifMaker/godot-gdgifexporter/imgs/img2.png.import new file mode 100644 index 0000000..01c15e5 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/imgs/img2.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/img2.png-ea1dbb037cc3a919f6ebf042098e80e1.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/godot-gdgifexporter/imgs/img2.png" +dest_files=[ "res://.import/img2.png-ea1dbb037cc3a919f6ebf042098e80e1.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/one_color.png b/src/addons/GifMaker/godot-gdgifexporter/imgs/one_color.png new file mode 100644 index 0000000..4672a49 Binary files /dev/null and b/src/addons/GifMaker/godot-gdgifexporter/imgs/one_color.png differ diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/one_color.png.import b/src/addons/GifMaker/godot-gdgifexporter/imgs/one_color.png.import new file mode 100644 index 0000000..d3d76ab --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/imgs/one_color.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/one_color.png-7864815b42a9af97ff7a9d747779a813.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/godot-gdgifexporter/imgs/one_color.png" +dest_files=[ "res://.import/one_color.png-7864815b42a9af97ff7a9d747779a813.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/parrots.png b/src/addons/GifMaker/godot-gdgifexporter/imgs/parrots.png new file mode 100644 index 0000000..53cb031 Binary files /dev/null and b/src/addons/GifMaker/godot-gdgifexporter/imgs/parrots.png differ diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/parrots.png.import b/src/addons/GifMaker/godot-gdgifexporter/imgs/parrots.png.import new file mode 100644 index 0000000..3d32d1e --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/imgs/parrots.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/parrots.png-cc20b2e4597b52743a9b12b4e00aaf3c.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/godot-gdgifexporter/imgs/parrots.png" +dest_files=[ "res://.import/parrots.png-cc20b2e4597b52743a9b12b4e00aaf3c.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/transparent.png b/src/addons/GifMaker/godot-gdgifexporter/imgs/transparent.png new file mode 100644 index 0000000..ef18b14 Binary files /dev/null and b/src/addons/GifMaker/godot-gdgifexporter/imgs/transparent.png differ diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/transparent.png.import b/src/addons/GifMaker/godot-gdgifexporter/imgs/transparent.png.import new file mode 100644 index 0000000..fe0155a --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/imgs/transparent.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/transparent.png-7e6b3a2c68ee40026b550dcef0ac0554.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/godot-gdgifexporter/imgs/transparent.png" +dest_files=[ "res://.import/transparent.png-7e6b3a2c68ee40026b550dcef0ac0554.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/two_colors.png b/src/addons/GifMaker/godot-gdgifexporter/imgs/two_colors.png new file mode 100644 index 0000000..ca27ea8 Binary files /dev/null and b/src/addons/GifMaker/godot-gdgifexporter/imgs/two_colors.png differ diff --git a/src/addons/GifMaker/godot-gdgifexporter/imgs/two_colors.png.import b/src/addons/GifMaker/godot-gdgifexporter/imgs/two_colors.png.import new file mode 100644 index 0000000..38eacc4 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/imgs/two_colors.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/two_colors.png-2a9ea00d42cbfde181d536f2d6095ad8.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GifMaker/godot-gdgifexporter/imgs/two_colors.png" +dest_files=[ "res://.import/two_colors.png-2a9ea00d42cbfde181d536f2d6095ad8.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/src/addons/GifMaker/plugin.cfg b/src/addons/GifMaker/plugin.cfg new file mode 100644 index 0000000..f540664 --- /dev/null +++ b/src/addons/GifMaker/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="GifMaker" +description="A simple node that can record gameplay and turn it into an animated GIF!" +author="Bram Dingelstad" +version="1.0" +script="plugin.gd" diff --git a/src/addons/GifMaker/plugin.gd b/src/addons/GifMaker/plugin.gd new file mode 100644 index 0000000..49e712e --- /dev/null +++ b/src/addons/GifMaker/plugin.gd @@ -0,0 +1,10 @@ +tool +extends EditorPlugin + + +func _enter_tree(): + pass + + +func _exit_tree(): + pass diff --git a/src/docs/LICENSE.GifMaker b/src/docs/LICENSE.GifMaker new file mode 100644 index 0000000..3c879f4 --- /dev/null +++ b/src/docs/LICENSE.GifMaker @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Bram Dingelstad, Igor Santarek (jegor377) & Martin Novák (novhack) + +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/src/docs/LICENSE.gdgifexporter b/src/docs/LICENSE.gdgifexporter new file mode 100644 index 0000000..ca5b2ad --- /dev/null +++ b/src/docs/LICENSE.gdgifexporter @@ -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/src/project.godot b/src/project.godot index c23bed6..4146dfd 100644 --- a/src/project.godot +++ b/src/project.godot @@ -10,6 +10,21 @@ config_version=4 _global_script_classes=[ { "base": "Node", +"class": "GifDecoder", +"language": "GDScript", +"path": "res://addons/GifMaker/GifDecoder.gd" +}, { +"base": "Viewport", +"class": "GifRecorder", +"language": "GDScript", +"path": "res://addons/GifMaker/GifRecorder.gd" +}, { +"base": "ReferenceRect", +"class": "GifRectangle", +"language": "GDScript", +"path": "res://addons/GifMaker/GifRectangle.gd" +}, { +"base": "Node", "class": "Main", "language": "GDScript", "path": "res://main.gd" @@ -20,6 +35,9 @@ _global_script_classes=[ { "path": "res://entities/actors/snake/scripts/snake.gd" } ] _global_script_class_icons={ +"GifDecoder": "", +"GifRecorder": "res://addons/GifMaker/gif.svg", +"GifRectangle": "res://addons/GifMaker/GifRectangle.svg", "Main": "", "Snake": "" } @@ -41,6 +59,10 @@ gdscript/warnings/unused_argument=false gdscript/warnings/unused_signal=false gdscript/warnings/return_value_discarded=false +[editor_plugins] + +enabled=PoolStringArray( "res://addons/GifMaker/plugin.cfg" ) + [gui] common/drop_mouse_on_gui_input_disabled=true diff --git a/src/screen_recorder.gd b/src/screen_recorder.gd new file mode 100644 index 0000000..2ab77e5 --- /dev/null +++ b/src/screen_recorder.gd @@ -0,0 +1,13 @@ +extends CanvasLayer + +onready var gif_recorder: GifRecorder = $Control/GifRecorder + + +func _input(event: InputEvent) -> void: + if event is InputEventKey and event.scancode == KEY_G and event.pressed: + print("Encoding GIF.") + gif_recorder.render_to_file("res://test.gif") + yield(gif_recorder, "done_encoding") + print("Done encoding GIF.") + gif_recorder.clear() + gif_recorder.start() diff --git a/src/test.gif b/src/test.gif new file mode 100644 index 0000000..e9219c1 Binary files /dev/null and b/src/test.gif differ -- cgit v1.2.3