From 2d28136449994f28c1d83a8f64919d80686d1069 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:41:37 +0200 Subject: [PATCH] Implement indexed mode (#1136) * Create a custom PixeloramaImage class, initial support for indexed mode * Convert opened projects and images to indexed mode * Use shaders for RGB to Indexed conversion and vice versa * Add `is_indexed` variable in PixeloramaImage * Basic undo/redo support for indexed mode when drawing * Make image effects respect indexed mode * Move code from image effects to ShaderImageEffect instead * Bucket tool works with indexed mode * Move and selection tools works with indexed mode * Brushes respect indexed mode * Add color_mode variable and some helper methods in Project Replace hard-coded cases of Image.FORMAT_RGBA8 with `Project.get_image_format()` just in case we want to add more formats in the future * Add a helper new_empty_image() method to Project * Set new images to indexed if the project is indexed * Change color modes from the Image menu * Fix open image to replace cel * Load/save indices in pxo files * Merging layers works with indexed mode * Layer effects respect indexed mode * Add an `other_image` parameter to `PixeloramaImage.add_data_to_dictionary()` * Scale image works with indexed mode * Resizing works with indexed mode * Fix non-shader rotation not working with indexed mode * Minor refactor of PixeloramaImage's set_pixelv_custom() * Make the text tool work with indexed mode * Remove print from PixeloramaImage * Rename "PixeloramaImage" to "ImageExtended" * Add docstrings in ImageExtended * Set color mode from the create new image dialog * Update Translations.pot * Show the color mode in the project properties dialog --- Translations/Translations.pot | 12 ++ src/Autoload/DrawingAlgos.gd | 125 ++++++++----- src/Autoload/Export.gd | 8 +- src/Autoload/Global.gd | 14 +- src/Autoload/OpenSave.gd | 83 ++++++--- src/Classes/Cels/GroupCel.gd | 4 +- src/Classes/Cels/PixelCel.gd | 8 +- src/Classes/Drawers.gd | 10 +- src/Classes/ImageEffect.gd | 6 +- src/Classes/ImageExtended.gd | 176 ++++++++++++++++++ src/Classes/Layers/BaseLayer.gd | 11 +- src/Classes/Layers/GroupLayer.gd | 8 +- src/Classes/Layers/PixelLayer.gd | 12 +- src/Classes/Project.gd | 67 ++++++- src/Classes/ShaderImageEffect.gd | 6 +- src/Shaders/Effects/Palettize.gdshader | 14 +- src/Shaders/FindPaletteColorIndex.gdshaderinc | 14 ++ src/Shaders/IndexedToRGB.gdshader | 28 +++ src/Shaders/SetIndices.gdshader | 18 ++ src/Tools/BaseDraw.gd | 6 +- src/Tools/BaseTool.gd | 6 +- src/Tools/DesignTools/Bucket.gd | 20 +- src/Tools/DesignTools/CurveTool.gd | 2 +- src/Tools/DesignTools/Eraser.gd | 1 + src/Tools/DesignTools/Pencil.gd | 2 + src/Tools/UtilityTools/Move.gd | 23 ++- src/Tools/UtilityTools/Text.gd | 6 +- src/UI/Canvas/Selection.gd | 33 ++-- src/UI/Dialogs/CreateNewImage.gd | 9 +- src/UI/Dialogs/CreateNewImage.tscn | 148 ++++++++------- src/UI/Dialogs/ImageEffects/RotateImage.gd | 4 +- src/UI/Dialogs/ImageEffects/ScaleImage.gd | 2 +- src/UI/Dialogs/ImportTagDialog.gd | 6 +- src/UI/Dialogs/ProjectProperties.gd | 7 + src/UI/Dialogs/ProjectProperties.tscn | 14 +- src/UI/Recorder/Recorder.gd | 2 +- src/UI/Timeline/AnimationTimeline.gd | 30 +-- .../LayerEffects/LayerEffectsSettings.gd | 14 +- src/UI/TopMenuContainer/TopMenuContainer.gd | 59 +++++- 39 files changed, 750 insertions(+), 268 deletions(-) create mode 100644 src/Classes/ImageExtended.gd create mode 100644 src/Shaders/FindPaletteColorIndex.gdshaderinc create mode 100644 src/Shaders/IndexedToRGB.gdshader create mode 100644 src/Shaders/SetIndices.gdshader diff --git a/Translations/Translations.pot b/Translations/Translations.pot index b311c9baf..e88a43a1f 100644 --- a/Translations/Translations.pot +++ b/Translations/Translations.pot @@ -156,6 +156,18 @@ msgstr "" msgid "Percentage" msgstr "" +#. Found in the create new image dialog. Allows users to change the color mode of the new project, such as RGBA or indexed mode. +msgid "Color mode:" +msgstr "" + +#. Found in the image menu. A submenu that allows users to change the color mode of the project, such as RGBA or indexed mode. +msgid "Color Mode" +msgstr "" + +#. Found in the image menu, under the "Color Mode" submenu. Refers to the indexed color mode. See this wikipedia page for more information: https://en.wikipedia.org/wiki/Indexed_color +msgid "Indexed" +msgstr "" + #. Found in the image menu. Sets the size of the project to be the same as the size of the active selection. msgid "Crop to Selection" msgstr "" diff --git a/src/Autoload/DrawingAlgos.gd b/src/Autoload/DrawingAlgos.gd index d3b89d733..6a668fea8 100644 --- a/src/Autoload/DrawingAlgos.gd +++ b/src/Autoload/DrawingAlgos.gd @@ -217,7 +217,7 @@ func get_ellipse_points_filled(pos: Vector2i, size: Vector2i, thickness := 1) -> func scale_3x(sprite: Image, tol := 0.196078) -> Image: var scaled := Image.create( - sprite.get_width() * 3, sprite.get_height() * 3, false, Image.FORMAT_RGBA8 + sprite.get_width() * 3, sprite.get_height() * 3, sprite.has_mipmaps(), sprite.get_format() ) var width_minus_one := sprite.get_width() - 1 var height_minus_one := sprite.get_height() - 1 @@ -509,6 +509,8 @@ func similar_colors(c1: Color, c2: Color, tol := 0.392157) -> bool: func center(indices: Array) -> void: var project := Global.current_project Global.canvas.selection.transform_content_confirm() + var redo_data := {} + var undo_data := {} project.undos += 1 project.undo_redo.create_action("Center Frames") for frame in indices: @@ -528,15 +530,20 @@ func center(indices: Array) -> void: for cel in project.frames[frame].cels: if not cel is PixelCel: continue - var sprite := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) - sprite.blend_rect(cel.image, used_rect, offset) - Global.undo_redo_compress_images({cel.image: sprite.data}, {cel.image: cel.image.data}) + var cel_image := (cel as PixelCel).get_image() + var tmp_centered := project.new_empty_image() + tmp_centered.blend_rect(cel.image, used_rect, offset) + var centered := ImageExtended.new() + centered.copy_from_custom(tmp_centered, cel_image.is_indexed) + centered.add_data_to_dictionary(redo_data, cel_image) + cel_image.add_data_to_dictionary(undo_data) + Global.undo_redo_compress_images(redo_data, undo_data) project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true)) project.undo_redo.add_do_method(Global.undo_or_redo.bind(false)) project.undo_redo.commit_action() -func scale_image(width: int, height: int, interpolation: int) -> void: +func scale_project(width: int, height: int, interpolation: int) -> void: var redo_data := {} var undo_data := {} for f in Global.current_project.frames: @@ -544,30 +551,47 @@ func scale_image(width: int, height: int, interpolation: int) -> void: var cel := f.cels[i] if not cel is PixelCel: continue - var sprite := Image.new() - sprite.copy_from(cel.get_image()) - if interpolation == Interpolation.SCALE3X: - var times := Vector2i( - ceili(width / (3.0 * sprite.get_width())), - ceili(height / (3.0 * sprite.get_height())) - ) - for _j in range(maxi(times.x, times.y)): - sprite.copy_from(scale_3x(sprite)) - sprite.resize(width, height, Image.INTERPOLATE_NEAREST) - elif interpolation == Interpolation.CLEANEDGE: - var gen := ShaderImageEffect.new() - gen.generate_image(sprite, clean_edge_shader, {}, Vector2i(width, height)) - elif interpolation == Interpolation.OMNISCALE and omniscale_shader: - var gen := ShaderImageEffect.new() - gen.generate_image(sprite, omniscale_shader, {}, Vector2i(width, height)) - else: - sprite.resize(width, height, interpolation) - redo_data[cel.image] = sprite.data - undo_data[cel.image] = cel.image.data + var cel_image := (cel as PixelCel).get_image() + var sprite := _resize_image(cel_image, width, height, interpolation) as ImageExtended + sprite.add_data_to_dictionary(redo_data, cel_image) + cel_image.add_data_to_dictionary(undo_data) general_do_and_undo_scale(width, height, redo_data, undo_data) +func _resize_image( + image: Image, width: int, height: int, interpolation: Image.Interpolation +) -> Image: + var new_image: Image + if image is ImageExtended: + new_image = ImageExtended.new() + new_image.is_indexed = image.is_indexed + new_image.copy_from(image) + new_image.select_palette("", false) + else: + new_image = Image.new() + new_image.copy_from(image) + if interpolation == Interpolation.SCALE3X: + var times := Vector2i( + ceili(width / (3.0 * new_image.get_width())), + ceili(height / (3.0 * new_image.get_height())) + ) + for _j in range(maxi(times.x, times.y)): + new_image.copy_from(scale_3x(new_image)) + new_image.resize(width, height, Image.INTERPOLATE_NEAREST) + elif interpolation == Interpolation.CLEANEDGE: + var gen := ShaderImageEffect.new() + gen.generate_image(new_image, clean_edge_shader, {}, Vector2i(width, height), false) + elif interpolation == Interpolation.OMNISCALE and omniscale_shader: + var gen := ShaderImageEffect.new() + gen.generate_image(new_image, omniscale_shader, {}, Vector2i(width, height), false) + else: + new_image.resize(width, height, interpolation) + if new_image is ImageExtended: + new_image.on_size_changed() + return new_image + + ## Sets the size of the project to be the same as the size of the active selection. func crop_to_selection() -> void: if not Global.current_project.has_selection: @@ -577,13 +601,13 @@ func crop_to_selection() -> void: Global.canvas.selection.transform_content_confirm() var rect: Rect2i = Global.canvas.selection.big_bounding_rectangle # Loop through all the cels to crop them - for f in Global.current_project.frames: - for cel in f.cels: - if not cel is PixelCel: - continue - var sprite := cel.get_image().get_region(rect) - redo_data[cel.image] = sprite.data - undo_data[cel.image] = cel.image.data + for cel in Global.current_project.get_all_pixel_cels(): + var cel_image := cel.get_image() + var tmp_cropped := cel_image.get_region(rect) + var cropped := ImageExtended.new() + cropped.copy_from_custom(tmp_cropped, cel_image.is_indexed) + cropped.add_data_to_dictionary(redo_data, cel_image) + cel_image.add_data_to_dictionary(undo_data) general_do_and_undo_scale(rect.size.x, rect.size.y, redo_data, undo_data) @@ -615,13 +639,13 @@ func crop_to_content() -> void: var redo_data := {} var undo_data := {} # Loop through all the cels to trim them - for f in Global.current_project.frames: - for cel in f.cels: - if not cel is PixelCel: - continue - var sprite := cel.get_image().get_region(used_rect) - redo_data[cel.image] = sprite.data - undo_data[cel.image] = cel.image.data + for cel in Global.current_project.get_all_pixel_cels(): + var cel_image := cel.get_image() + var tmp_cropped := cel_image.get_region(used_rect) + var cropped := ImageExtended.new() + cropped.copy_from_custom(tmp_cropped, cel_image.is_indexed) + cropped.add_data_to_dictionary(redo_data, cel_image) + cel_image.add_data_to_dictionary(undo_data) general_do_and_undo_scale(width, height, redo_data, undo_data) @@ -629,18 +653,17 @@ func crop_to_content() -> void: func resize_canvas(width: int, height: int, offset_x: int, offset_y: int) -> void: var redo_data := {} var undo_data := {} - for f in Global.current_project.frames: - for cel in f.cels: - if not cel is PixelCel: - continue - var sprite := Image.create(width, height, false, Image.FORMAT_RGBA8) - sprite.blend_rect( - cel.get_image(), - Rect2i(Vector2i.ZERO, Global.current_project.size), - Vector2i(offset_x, offset_y) - ) - redo_data[cel.image] = sprite.data - undo_data[cel.image] = cel.image.data + for cel in Global.current_project.get_all_pixel_cels(): + var cel_image := cel.get_image() + var resized := ImageExtended.create_custom( + width, height, cel_image.has_mipmaps(), cel_image.get_format(), cel_image.is_indexed + ) + resized.blend_rect( + cel_image, Rect2i(Vector2i.ZERO, cel_image.get_size()), Vector2i(offset_x, offset_y) + ) + resized.convert_rgb_to_indexed() + resized.add_data_to_dictionary(redo_data, cel_image) + cel_image.add_data_to_dictionary(undo_data) general_do_and_undo_scale(width, height, redo_data, undo_data) diff --git a/src/Autoload/Export.gd b/src/Autoload/Export.gd index ecd1105af..221dd4959 100644 --- a/src/Autoload/Export.gd +++ b/src/Autoload/Export.gd @@ -161,7 +161,7 @@ func cache_blended_frames(project := Global.current_project) -> void: blended_frames.clear() var frames := _calculate_frames(project) for frame in frames: - var image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + var image := project.new_empty_image() _blend_layers(image, frame) blended_frames[frame] = image @@ -208,7 +208,7 @@ func process_spritesheet(project := Global.current_project) -> void: spritesheet_columns = temp var width := project.size.x * spritesheet_columns var height := project.size.y * spritesheet_rows - var whole_image := Image.create(width, height, false, Image.FORMAT_RGBA8) + var whole_image := Image.create(width, height, false, project.get_image_format()) var origin := Vector2i.ZERO var hh := 0 var vv := 0 @@ -287,10 +287,10 @@ func process_animation(project := Global.current_project) -> void: ProcessedImage.new(image, project.frames.find(frame), duration) ) else: - var image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + var image := project.new_empty_image() image.copy_from(blended_frames[frame]) if erase_unselected_area and project.has_selection: - var crop := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + var crop := project.new_empty_image() var selection_image = project.selection_map.return_cropped_copy(project.size) crop.blit_rect_mask( image, selection_image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index d76ed92a6..30e8450e1 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -46,6 +46,7 @@ enum WindowMenu { WINDOW_OPACITY, PANELS, LAYOUTS, MOVABLE_PANELS, ZEN_MODE, FUL ## Enumeration of items present in the Image Menu. enum ImageMenu { PROJECT_PROPERTIES, + COLOR_MODE, RESIZE_CANVAS, SCALE_IMAGE, CROP_TO_SELECTION, @@ -1113,8 +1114,17 @@ func undo_redo_compress_images( func undo_redo_draw_op( image: Image, new_size: Vector2i, compressed_image_data: PackedByteArray, buffer_size: int ) -> void: - var decompressed := compressed_image_data.decompress(buffer_size) - image.set_data(new_size.x, new_size.y, image.has_mipmaps(), image.get_format(), decompressed) + if image is ImageExtended and image.is_indexed: + # If using indexed mode, + # just convert the indices to RGB instead of setting the image data directly. + if image.get_size() != new_size: + image.crop(new_size.x, new_size.y) + image.convert_indexed_to_rgb() + else: + var decompressed := compressed_image_data.decompress(buffer_size) + image.set_data( + new_size.x, new_size.y, image.has_mipmaps(), image.get_format(), decompressed + ) ## This method is used to write project setting overrides to the override.cfg file, located diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index 466c9275e..dc9f8f3c2 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -150,7 +150,7 @@ func handle_loading_aimg(path: String, frames: Array) -> void: if not frames_agree: frame.duration = aimg_frame.duration * project.fps var content := aimg_frame.content - content.convert(Image.FORMAT_RGBA8) + content.convert(project.get_image_format()) frame.cels.append(PixelCel.new(content, 1)) project.frames.append(frame) @@ -389,18 +389,23 @@ func save_pxo_file( var frame_index := 1 for frame in project.frames: if not autosave and include_blended: - var blended := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + var blended := project.new_empty_image() DrawingAlgos.blend_layers(blended, frame, Vector2i.ZERO, project) zip_packer.start_file("image_data/final_images/%s" % frame_index) zip_packer.write_file(blended.get_data()) zip_packer.close_file() var cel_index := 1 for cel in frame.cels: - var cel_image := cel.get_image() + var cel_image := cel.get_image() as ImageExtended if is_instance_valid(cel_image) and cel is PixelCel: zip_packer.start_file("image_data/frames/%s/layer_%s" % [frame_index, cel_index]) zip_packer.write_file(cel_image.get_data()) zip_packer.close_file() + zip_packer.start_file( + "image_data/frames/%s/indices_layer_%s" % [frame_index, cel_index] + ) + zip_packer.write_file(cel_image.indices_image.get_data()) + zip_packer.close_file() cel_index += 1 frame_index += 1 var brush_index := 0 @@ -457,12 +462,13 @@ func save_pxo_file( func open_image_as_new_tab(path: String, image: Image) -> void: var project := Project.new([], path.get_file(), image.get_size()) - project.layers.append(PixelLayer.new(project)) + var layer := PixelLayer.new(project) + project.layers.append(layer) Global.projects.append(project) var frame := Frame.new() - image.convert(Image.FORMAT_RGBA8) - frame.cels.append(PixelCel.new(image, 1)) + image.convert(project.get_image_format()) + frame.cels.append(layer.new_cel_from_image(image)) project.frames.append(frame) set_new_imported_tab(project, path) @@ -475,15 +481,18 @@ func open_image_as_spritesheet_tab_smart( frame_size = image.get_size() sliced_rects.append(Rect2i(Vector2i.ZERO, frame_size)) var project := Project.new([], path.get_file(), frame_size) - project.layers.append(PixelLayer.new(project)) + var layer := PixelLayer.new(project) + project.layers.append(layer) Global.projects.append(project) for rect in sliced_rects: var offset: Vector2 = (0.5 * (frame_size - rect.size)).floor() var frame := Frame.new() - var cropped_image := Image.create(frame_size.x, frame_size.y, false, Image.FORMAT_RGBA8) - image.convert(Image.FORMAT_RGBA8) + var cropped_image := Image.create( + frame_size.x, frame_size.y, false, project.get_image_format() + ) + image.convert(project.get_image_format()) cropped_image.blit_rect(image, rect, offset) - frame.cels.append(PixelCel.new(cropped_image, 1)) + frame.cels.append(layer.new_cel_from_image(cropped_image)) project.frames.append(frame) set_new_imported_tab(project, path) @@ -494,7 +503,8 @@ func open_image_as_spritesheet_tab(path: String, image: Image, horiz: int, vert: var frame_width := image.get_size().x / horiz var frame_height := image.get_size().y / vert var project := Project.new([], path.get_file(), Vector2(frame_width, frame_height)) - project.layers.append(PixelLayer.new(project)) + var layer := PixelLayer.new(project) + project.layers.append(layer) Global.projects.append(project) for yy in range(vert): for xx in range(horiz): @@ -503,8 +513,8 @@ func open_image_as_spritesheet_tab(path: String, image: Image, horiz: int, vert: Rect2i(frame_width * xx, frame_height * yy, frame_width, frame_height) ) project.size = cropped_image.get_size() - cropped_image.convert(Image.FORMAT_RGBA8) - frame.cels.append(PixelCel.new(cropped_image, 1)) + cropped_image.convert(project.get_image_format()) + frame.cels.append(layer.new_cel_from_image(cropped_image)) project.frames.append(frame) set_new_imported_tab(project, path) @@ -562,12 +572,12 @@ func open_image_as_spritesheet_layer_smart( if f >= start_frame and f < (start_frame + sliced_rects.size()): # Slice spritesheet var offset: Vector2 = (0.5 * (frame_size - sliced_rects[f - start_frame].size)).floor() - image.convert(Image.FORMAT_RGBA8) + image.convert(project.get_image_format()) var cropped_image := Image.create( - project_width, project_height, false, Image.FORMAT_RGBA8 + project_width, project_height, false, project.get_image_format() ) cropped_image.blit_rect(image, sliced_rects[f - start_frame], offset) - cels.append(PixelCel.new(cropped_image)) + cels.append(layer.new_cel_from_image(cropped_image)) else: cels.append(layer.new_empty_cel()) @@ -644,16 +654,16 @@ func open_image_as_spritesheet_layer( # Slice spritesheet var xx := (f - start_frame) % horizontal var yy := (f - start_frame) / horizontal - image.convert(Image.FORMAT_RGBA8) + image.convert(project.get_image_format()) var cropped_image := Image.create( - project_width, project_height, false, Image.FORMAT_RGBA8 + project_width, project_height, false, project.get_image_format() ) cropped_image.blit_rect( image, Rect2i(frame_width * xx, frame_height * yy, frame_width, frame_height), Vector2i.ZERO ) - cels.append(PixelCel.new(cropped_image)) + cels.append(layer.new_cel_from_image(cropped_image)) else: cels.append(layer.new_empty_cel()) @@ -687,12 +697,18 @@ func open_image_at_cel(image: Image, layer_index := 0, frame_index := 0) -> void var cel := project.frames[frame_index].cels[layer_index] if not cel is PixelCel: return - image.convert(Image.FORMAT_RGBA8) - var cel_image := Image.create(project_width, project_height, false, Image.FORMAT_RGBA8) - cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO) - Global.undo_redo_compress_images( - {cel.image: cel_image.data}, {cel.image: cel.image.data}, project + image.convert(project.get_image_format()) + var cel_image := (cel as PixelCel).get_image() + var new_cel_image := ImageExtended.create_custom( + project_width, project_height, false, project.get_image_format(), cel_image.is_indexed ) + new_cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO) + new_cel_image.convert_rgb_to_indexed() + var redo_data := {} + new_cel_image.add_data_to_dictionary(redo_data, cel_image) + var undo_data := {} + cel_image.add_data_to_dictionary(undo_data) + Global.undo_redo_compress_images(redo_data, undo_data, project) project.undo_redo.add_do_property(project, "selected_cels", []) project.undo_redo.add_do_method(project.change_cel.bind(frame_index, layer_index)) @@ -716,11 +732,14 @@ func open_image_as_new_frame( var frame := Frame.new() for i in project.layers.size(): - if i == layer_index: - image.convert(Image.FORMAT_RGBA8) - var cel_image := Image.create(project_width, project_height, false, Image.FORMAT_RGBA8) + var layer := project.layers[i] + if i == layer_index and layer is PixelLayer: + image.convert(project.get_image_format()) + var cel_image := Image.create( + project_width, project_height, false, project.get_image_format() + ) cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO) - frame.cels.append(PixelCel.new(cel_image, 1)) + frame.cels.append(layer.new_cel_from_image(cel_image)) else: frame.cels.append(project.layers[i].new_empty_cel()) if not undo: @@ -753,10 +772,12 @@ func open_image_as_new_layer(image: Image, file_name: String, frame_index := 0) Global.current_project.undo_redo.create_action("Add Layer") for i in project.frames.size(): if i == frame_index: - image.convert(Image.FORMAT_RGBA8) - var cel_image := Image.create(project_width, project_height, false, Image.FORMAT_RGBA8) + image.convert(project.get_image_format()) + var cel_image := Image.create( + project_width, project_height, false, project.get_image_format() + ) cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO) - cels.append(PixelCel.new(cel_image, 1)) + cels.append(layer.new_cel_from_image(cel_image)) else: cels.append(layer.new_empty_cel()) diff --git a/src/Classes/Cels/GroupCel.gd b/src/Classes/Cels/GroupCel.gd index 08f1bcf2a..cff393fe5 100644 --- a/src/Classes/Cels/GroupCel.gd +++ b/src/Classes/Cels/GroupCel.gd @@ -10,9 +10,7 @@ func _init(_opacity := 1.0) -> void: func get_image() -> Image: - var image := Image.create( - Global.current_project.size.x, Global.current_project.size.y, false, Image.FORMAT_RGBA8 - ) + var image := Global.current_project.new_empty_image() return image diff --git a/src/Classes/Cels/PixelCel.gd b/src/Classes/Cels/PixelCel.gd index cab1a2ab1..7048313e8 100644 --- a/src/Classes/Cels/PixelCel.gd +++ b/src/Classes/Cels/PixelCel.gd @@ -4,17 +4,17 @@ extends BaseCel ## The term "cel" comes from "celluloid" (https://en.wikipedia.org/wiki/Cel). ## This variable is where the image data of the cel are. -var image: Image: +var image: ImageExtended: set = image_changed -func _init(_image := Image.new(), _opacity := 1.0) -> void: +func _init(_image: ImageExtended, _opacity := 1.0) -> void: image_texture = ImageTexture.new() image = _image # Set image and call setter opacity = _opacity -func image_changed(value: Image) -> void: +func image_changed(value: ImageExtended) -> void: image = value if not image.is_empty() and is_instance_valid(image_texture): image_texture.set_image(image) @@ -48,7 +48,7 @@ func copy_content(): return copy_image -func get_image() -> Image: +func get_image() -> ImageExtended: return image diff --git a/src/Classes/Drawers.gd b/src/Classes/Drawers.gd index e0ad0c600..b0f606816 100644 --- a/src/Classes/Drawers.gd +++ b/src/Classes/Drawers.gd @@ -27,12 +27,12 @@ class ColorOp: class SimpleDrawer: - func set_pixel(image: Image, position: Vector2i, color: Color, op: ColorOp) -> void: + func set_pixel(image: ImageExtended, position: Vector2i, color: Color, op: ColorOp) -> void: var color_old := image.get_pixelv(position) var color_str := color.to_html() var color_new := op.process(Color(color_str), color_old) if not color_new.is_equal_approx(color_old): - image.set_pixelv(position, color_new) + image.set_pixelv_custom(position, color_new) class PixelPerfectDrawer: @@ -43,11 +43,11 @@ class PixelPerfectDrawer: func reset() -> void: last_pixels = [null, null] - func set_pixel(image: Image, position: Vector2i, color: Color, op: ColorOp) -> void: + func set_pixel(image: ImageExtended, position: Vector2i, color: Color, op: ColorOp) -> void: var color_old := image.get_pixelv(position) var color_str := color.to_html() last_pixels.push_back([position, color_old]) - image.set_pixelv(position, op.process(Color(color_str), color_old)) + image.set_pixelv_custom(position, op.process(Color(color_str), color_old)) var corner = last_pixels.pop_front() var neighbour = last_pixels[0] @@ -56,7 +56,7 @@ class PixelPerfectDrawer: return if position - corner[0] in CORNERS and position - neighbour[0] in NEIGHBOURS: - image.set_pixel(neighbour[0].x, neighbour[0].y, neighbour[1]) + image.set_pixel_custom(neighbour[0].x, neighbour[0].y, neighbour[1]) last_pixels[0] = corner diff --git a/src/Classes/ImageEffect.gd b/src/Classes/ImageEffect.gd index 1567d416f..f50aef337 100644 --- a/src/Classes/ImageEffect.gd +++ b/src/Classes/ImageEffect.gd @@ -170,12 +170,12 @@ func _get_undo_data(project: Project) -> Dictionary: var data := {} var images := _get_selected_draw_images(project) for image in images: - data[image] = image.data + image.add_data_to_dictionary(data) return data -func _get_selected_draw_images(project: Project) -> Array[Image]: - var images: Array[Image] = [] +func _get_selected_draw_images(project: Project) -> Array[ImageExtended]: + var images: Array[ImageExtended] = [] if affect == SELECTED_CELS: for cel_index in project.selected_cels: var cel: BaseCel = project.frames[cel_index[0]].cels[cel_index[1]] diff --git a/src/Classes/ImageExtended.gd b/src/Classes/ImageExtended.gd new file mode 100644 index 000000000..8dd82cd64 --- /dev/null +++ b/src/Classes/ImageExtended.gd @@ -0,0 +1,176 @@ +class_name ImageExtended +extends Image + +## A custom [Image] class that implements support for indexed mode. +## Before implementing indexed mode, we just used the [Image] class. +## In indexed mode, each pixel is assigned to a number that references a palette color. +## This essentially means that the colors of the image are restricted to a specific palette, +## and they will automatically get updated when you make changes to that palette, or when +## you switch to a different one. + +const TRANSPARENT := Color(0) +const SET_INDICES := preload("res://src/Shaders/SetIndices.gdshader") +const INDEXED_TO_RGB := preload("res://src/Shaders/IndexedToRGB.gdshader") + +## If [code]true[/code], the image uses indexed mode. +var is_indexed := false +## The [Palette] the image is currently using for indexed mode. +var current_palette := Palettes.current_palette +## An [Image] that contains the index of each pixel of the main image for indexed mode. +## The indices are stored in the red channel of this image, by diving each index by 255. +## This means that there can be a maximum index size of 255. 0 means that the pixel is transparent. +var indices_image := Image.create_empty(1, 1, false, Image.FORMAT_R8) +## A [PackedColorArray] containing all of the colors of the [member current_palette]. +var palette := PackedColorArray() + + +func _init() -> void: + indices_image.fill(TRANSPARENT) + Palettes.palette_selected.connect(select_palette) + + +## Equivalent of [method Image.create_empty], but returns [ImageExtended] instead. +## If [param _is_indexed] is [code]true[/code], the image that is being returned uses indexed mode. +static func create_custom( + width: int, height: int, mipmaps: bool, format: Image.Format, _is_indexed := false +) -> ImageExtended: + var new_image := ImageExtended.new() + new_image.crop(width, height) + if mipmaps: + new_image.generate_mipmaps() + new_image.convert(format) + new_image.fill(TRANSPARENT) + new_image.is_indexed = _is_indexed + if new_image.is_indexed: + new_image.resize_indices() + new_image.select_palette("", false) + return new_image + + +## Equivalent of [method Image.copy_from], but also handles the logic necessary for indexed mode. +## If [param _is_indexed] is [code]true[/code], the image is set to be using indexed mode. +func copy_from_custom(image: Image, indexed := is_indexed) -> void: + is_indexed = indexed + copy_from(image) + if is_indexed: + resize_indices() + select_palette("", false) + convert_rgb_to_indexed() + + +## Selects a new palette to use in indexed mode. +func select_palette(_name: String, convert_to_rgb := true) -> void: + current_palette = Palettes.current_palette + if not is_instance_valid(current_palette) or not is_indexed: + return + update_palette() + if not current_palette.data_changed.is_connected(update_palette): + current_palette.data_changed.connect(update_palette) + if not current_palette.data_changed.is_connected(convert_indexed_to_rgb): + current_palette.data_changed.connect(convert_indexed_to_rgb) + if convert_to_rgb: + convert_indexed_to_rgb() + + +## Updates [member palette] to contain the colors of [member current_palette]. +func update_palette() -> void: + if palette.size() != current_palette.colors.size(): + palette.resize(current_palette.colors.size()) + for i in current_palette.colors: + palette[i] = current_palette.colors[i].color + + +## Displays the actual RGBA values of each pixel in the image from indexed mode. +func convert_indexed_to_rgb() -> void: + if not is_indexed: + return + var palette_image := Palettes.current_palette.convert_to_image() + var palette_texture := ImageTexture.create_from_image(palette_image) + var shader_image_effect := ShaderImageEffect.new() + var indices_texture := ImageTexture.create_from_image(indices_image) + var params := {"palette_texture": palette_texture, "indices_texture": indices_texture} + shader_image_effect.generate_image(self, INDEXED_TO_RGB, params, get_size(), false) + Global.canvas.queue_redraw() + + +## Automatically maps each color of the image's pixel to the closest color of the palette, +## by finding the palette color's index and storing it in [member indices_image]. +func convert_rgb_to_indexed() -> void: + if not is_indexed: + return + var palette_image := Palettes.current_palette.convert_to_image() + var palette_texture := ImageTexture.create_from_image(palette_image) + var params := { + "palette_texture": palette_texture, "rgb_texture": ImageTexture.create_from_image(self) + } + var shader_image_effect := ShaderImageEffect.new() + shader_image_effect.generate_image( + indices_image, SET_INDICES, params, indices_image.get_size(), false + ) + convert_indexed_to_rgb() + + +## Resizes indices and calls [method convert_rgb_to_indexed] when the image's size changes +## and indexed mode is enabled. +func on_size_changed() -> void: + if is_indexed: + resize_indices() + convert_rgb_to_indexed() + + +## Resizes [indices_image] to the image's size. +func resize_indices() -> void: + indices_image.crop(get_width(), get_height()) + + +## Equivalent of [method Image.set_pixel_custom], +## but also handles the logic necessary for indexed mode. +func set_pixel_custom(x: int, y: int, color: Color) -> void: + set_pixelv_custom(Vector2i(x, y), color) + + +## Equivalent of [method Image.set_pixelv_custom], +## but also handles the logic necessary for indexed mode. +func set_pixelv_custom(point: Vector2i, color: Color) -> void: + var new_color := color + if is_indexed: + var color_to_fill := TRANSPARENT + var color_index := 0 + if not color.is_equal_approx(TRANSPARENT): + if palette.has(color): + color_index = palette.find(color) + else: # Find the most similar color + var smaller_distance := color_distance(color, palette[0]) + for i in palette.size(): + var swatch := palette[i] + if is_zero_approx(swatch.a): # Skip transparent colors + continue + var dist := color_distance(color, swatch) + if dist < smaller_distance: + smaller_distance = dist + color_index = i + indices_image.set_pixelv(point, Color((color_index + 1) / 255.0, 0, 0, 0)) + color_to_fill = palette[color_index] + new_color = color_to_fill + else: + indices_image.set_pixelv(point, TRANSPARENT) + new_color = TRANSPARENT + set_pixelv(point, new_color) + + +## Finds the distance between colors [param c1] and [param c2]. +func color_distance(c1: Color, c2: Color) -> float: + var v1 := Vector4(c1.r, c1.g, c1.b, c1.a) + var v2 := Vector4(c2.r, c2.g, c2.b, c2.a) + return v2.distance_to(v1) + + +## Adds image data to a [param dict] [Dictionary]. Used for undo/redo. +func add_data_to_dictionary(dict: Dictionary, other_image: ImageExtended = null) -> void: + # The order matters! Setting self's data first would make undo/redo appear to work incorrectly. + if is_instance_valid(other_image): + dict[other_image.indices_image] = indices_image.data + dict[other_image] = data + else: + dict[indices_image] = indices_image.data + dict[self] = data diff --git a/src/Classes/Layers/BaseLayer.gd b/src/Classes/Layers/BaseLayer.gd index bd67c1d50..bdc08e227 100644 --- a/src/Classes/Layers/BaseLayer.gd +++ b/src/Classes/Layers/BaseLayer.gd @@ -218,11 +218,16 @@ func link_cel(cel: BaseCel, link_set = null) -> void: ## This method is not destructive as it does NOT change the data of the image, ## it just returns a copy. func display_effects(cel: BaseCel, image_override: Image = null) -> Image: - var image := Image.new() + var image := ImageExtended.new() if is_instance_valid(image_override): - image.copy_from(image_override) + if image_override is ImageExtended: + image.is_indexed = image_override.is_indexed + image.copy_from_custom(image_override) else: - image.copy_from(cel.get_image()) + var cel_image := cel.get_image() + if cel_image is ImageExtended: + image.is_indexed = cel_image.is_indexed + image.copy_from_custom(cel_image) if not effects_enabled: return image var image_size := image.get_size() diff --git a/src/Classes/Layers/GroupLayer.gd b/src/Classes/Layers/GroupLayer.gd index 6ce3f3410..bcd2a38cd 100644 --- a/src/Classes/Layers/GroupLayer.gd +++ b/src/Classes/Layers/GroupLayer.gd @@ -13,7 +13,9 @@ func _init(_project: Project, _name := "") -> void: ## Blends all of the images of children layer of the group layer into a single image. func blend_children(frame: Frame, origin := Vector2i.ZERO, apply_effects := true) -> Image: - var image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + var image := ImageExtended.create_custom( + project.size.x, project.size.y, false, project.get_image_format(), project.is_indexed() + ) var children := get_children(false) if children.size() <= 0: return image @@ -66,7 +68,7 @@ func blend_children(frame: Frame, origin := Vector2i.ZERO, apply_effects := true func _include_child_in_blending( - image: Image, + image: ImageExtended, layer: BaseLayer, frame: Frame, textures: Array[Image], @@ -100,7 +102,7 @@ func _include_child_in_blending( ## Gets called recursively if the child group has children groups of its own, ## and they are also set to pass through mode. func _blend_child_group( - image: Image, + image: ImageExtended, layer: BaseLayer, frame: Frame, textures: Array[Image], diff --git a/src/Classes/Layers/PixelLayer.gd b/src/Classes/Layers/PixelLayer.gd index c0230d59a..0faca2ca0 100644 --- a/src/Classes/Layers/PixelLayer.gd +++ b/src/Classes/Layers/PixelLayer.gd @@ -28,9 +28,19 @@ func get_layer_type() -> int: func new_empty_cel() -> BaseCel: - var image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + var format := project.get_image_format() + var is_indexed := project.is_indexed() + var image := ImageExtended.create_custom( + project.size.x, project.size.y, false, format, is_indexed + ) return PixelCel.new(image) +func new_cel_from_image(image: Image) -> PixelCel: + var pixelorama_image := ImageExtended.new() + pixelorama_image.copy_from_custom(image, project.is_indexed()) + return PixelCel.new(pixelorama_image) + + func can_layer_get_drawn() -> bool: return is_visible_in_hierarchy() && !is_locked_in_hierarchy() diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index d4eb544cb..eec345466 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -9,6 +9,8 @@ signal about_to_deserialize(dict: Dictionary) signal resized signal timeline_updated +const INDEXED_MODE := Image.FORMAT_MAX + 1 + var name := "": set(value): name = value @@ -21,6 +23,17 @@ var undo_redo := UndoRedo.new() var tiles: Tiles var undos := 0 ## The number of times we added undo properties var can_undo := true +var color_mode: int = Image.FORMAT_RGBA8: + set(value): + if color_mode != value: + color_mode = value + for cel in get_all_pixel_cels(): + var image := cel.get_image() + image.is_indexed = is_indexed() + if image.is_indexed: + image.resize_indices() + image.select_palette("", false) + image.convert_rgb_to_indexed() var fill_color := Color(0) var has_changed := false: set(value): @@ -176,11 +189,26 @@ func new_empty_frame() -> Frame: return frame +## Returns a new [Image] of size [member size] and format [method get_image_format]. +func new_empty_image() -> Image: + return Image.create(size.x, size.y, false, get_image_format()) + + ## Returns the currently selected [BaseCel]. func get_current_cel() -> BaseCel: return frames[current_frame].cels[current_layer] +func get_image_format() -> Image.Format: + if color_mode == INDEXED_MODE: + return Image.FORMAT_RGBA8 + return color_mode + + +func is_indexed() -> bool: + return color_mode == INDEXED_MODE + + func selection_map_changed() -> void: var image_texture: ImageTexture has_selection = !selection_map.is_invisible() @@ -255,6 +283,7 @@ func serialize() -> Dictionary: "pxo_version": ProjectSettings.get_setting("application/config/Pxo_Version"), "size_x": size.x, "size_y": size.y, + "color_mode": color_mode, "tile_mode_x_basis_x": tiles.x_basis.x, "tile_mode_x_basis_y": tiles.x_basis.y, "tile_mode_y_basis_x": tiles.y_basis.x, @@ -288,6 +317,7 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces size.y = dict.size_y tiles.tile_size = size selection_map.crop(size.x, size.y) + color_mode = dict.get("color_mode", color_mode) if dict.has("tile_mode_x_basis_x") and dict.has("tile_mode_x_basis_y"): tiles.x_basis.x = dict.tile_mode_x_basis_x tiles.x_basis.y = dict.tile_mode_x_basis_y @@ -311,20 +341,33 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces for cel in frame.cels: match int(dict.layers[cel_i].get("type", Global.LayerTypes.PIXEL)): Global.LayerTypes.PIXEL: - var image := Image.new() + var image: Image + var indices_data := PackedByteArray() if is_instance_valid(zip_reader): # For pxo files saved in 1.0+ - var image_data := zip_reader.read_file( - "image_data/frames/%s/layer_%s" % [frame_i + 1, cel_i + 1] - ) + var path := "image_data/frames/%s/layer_%s" % [frame_i + 1, cel_i + 1] + var image_data := zip_reader.read_file(path) image = Image.create_from_data( - size.x, size.y, false, Image.FORMAT_RGBA8, image_data + size.x, size.y, false, get_image_format(), image_data ) + var indices_path := ( + "image_data/frames/%s/indices_layer_%s" % [frame_i + 1, cel_i + 1] + ) + if zip_reader.file_exists(indices_path): + indices_data = zip_reader.read_file(indices_path) elif is_instance_valid(file): # For pxo files saved in 0.x var buffer := file.get_buffer(size.x * size.y * 4) image = Image.create_from_data( - size.x, size.y, false, Image.FORMAT_RGBA8, buffer + size.x, size.y, false, get_image_format(), buffer ) - cels.append(PixelCel.new(image)) + var pixelorama_image := ImageExtended.new() + pixelorama_image.is_indexed = is_indexed() + if not indices_data.is_empty() and is_indexed(): + pixelorama_image.indices_image = Image.create_from_data( + size.x, size.y, false, Image.FORMAT_R8, indices_data + ) + pixelorama_image.copy_from(image) + pixelorama_image.select_palette("", true) + cels.append(PixelCel.new(pixelorama_image)) Global.LayerTypes.GROUP: cels.append(GroupCel.new()) Global.LayerTypes.THREE_D: @@ -559,6 +602,16 @@ func find_first_drawable_cel(frame := frames[current_frame]) -> BaseCel: return result +## Returns an [Array] of type [PixelCel] containing all of the pixel cels of the project. +func get_all_pixel_cels() -> Array[PixelCel]: + var cels: Array[PixelCel] + for frame in frames: + for cel in frame.cels: + if cel is PixelCel: + cels.append(cel) + return cels + + ## Re-order layers to take each cel's z-index into account. If all z-indexes are 0, ## then the order of drawing is the same as the order of the layers itself. func order_layers(frame_index := current_frame) -> void: diff --git a/src/Classes/ShaderImageEffect.gd b/src/Classes/ShaderImageEffect.gd index 3a38b75e7..4ec43313f 100644 --- a/src/Classes/ShaderImageEffect.gd +++ b/src/Classes/ShaderImageEffect.gd @@ -5,7 +5,9 @@ extends RefCounted signal done -func generate_image(img: Image, shader: Shader, params: Dictionary, size: Vector2i) -> void: +func generate_image( + img: Image, shader: Shader, params: Dictionary, size: Vector2i, respect_indexed := true +) -> void: # duplicate shader before modifying code to avoid affecting original resource var resized_width := false var resized_height := false @@ -60,4 +62,6 @@ func generate_image(img: Image, shader: Shader, params: Dictionary, size: Vector img.crop(img.get_width() - 1, img.get_height()) if resized_height: img.crop(img.get_width(), img.get_height() - 1) + if img is ImageExtended and respect_indexed: + img.convert_rgb_to_indexed() done.emit() diff --git a/src/Shaders/Effects/Palettize.gdshader b/src/Shaders/Effects/Palettize.gdshader index 1c20c260a..fc07b37fa 100644 --- a/src/Shaders/Effects/Palettize.gdshader +++ b/src/Shaders/Effects/Palettize.gdshader @@ -3,6 +3,7 @@ shader_type canvas_item; render_mode unshaded; +#include "res://src/Shaders/FindPaletteColorIndex.gdshaderinc" uniform sampler2D palette_texture : filter_nearest; uniform sampler2D selection : filter_nearest; @@ -10,18 +11,9 @@ vec4 swap_color(vec4 color) { if (color.a <= 0.01) { return color; } - int color_index = 0; + int n_of_colors = textureSize(palette_texture, 0).x; - float smaller_distance = distance(color, texture(palette_texture, vec2(0.0))); - for (int i = 0; i <= n_of_colors; i++) { - vec2 uv = vec2(float(i) / float(n_of_colors), 0.0); - vec4 palette_color = texture(palette_texture, uv); - float dist = distance(color, palette_color); - if (dist < smaller_distance) { - smaller_distance = dist; - color_index = i; - } - } + int color_index = find_index(color, n_of_colors, palette_texture); return texture(palette_texture, vec2(float(color_index) / float(n_of_colors), 0.0)); } diff --git a/src/Shaders/FindPaletteColorIndex.gdshaderinc b/src/Shaders/FindPaletteColorIndex.gdshaderinc new file mode 100644 index 000000000..e515fe749 --- /dev/null +++ b/src/Shaders/FindPaletteColorIndex.gdshaderinc @@ -0,0 +1,14 @@ +int find_index(vec4 color, int n_of_colors, sampler2D palette_texture) { + int color_index = 0; + float smaller_distance = distance(color, texture(palette_texture, vec2(0.0))); + for (int i = 0; i <= n_of_colors; i++) { + vec2 uv = vec2(float(i) / float(n_of_colors), 0.0); + vec4 palette_color = texture(palette_texture, uv); + float dist = distance(color, palette_color); + if (dist < smaller_distance) { + smaller_distance = dist; + color_index = i; + } + } + return color_index; +} diff --git a/src/Shaders/IndexedToRGB.gdshader b/src/Shaders/IndexedToRGB.gdshader new file mode 100644 index 000000000..b86caa1a8 --- /dev/null +++ b/src/Shaders/IndexedToRGB.gdshader @@ -0,0 +1,28 @@ +shader_type canvas_item; +render_mode unshaded; + +const float EPSILON = 0.0001; + +uniform sampler2D palette_texture : filter_nearest; +uniform sampler2D indices_texture : filter_nearest; + +void fragment() { + float index = texture(indices_texture, UV).r; + if (index <= EPSILON) { // If index is zero, make it transparent + COLOR = vec4(0.0); + } + else { + float n_of_colors = float(textureSize(palette_texture, 0).x); + index -= 1.0 / 255.0; + float index_normalized = ((index * 255.0)) / n_of_colors; + if (index_normalized + EPSILON < 1.0) { + COLOR = texture(palette_texture, vec2(index_normalized + EPSILON, 0.0)); + } + else { + // If index is bigger than the size of the palette, make it transparent. + // This happens when switching to a palette, where the previous palette was bigger + // than the newer one, and the current index is out of bounds of the new one. + COLOR = vec4(0.0); + } + } +} diff --git a/src/Shaders/SetIndices.gdshader b/src/Shaders/SetIndices.gdshader new file mode 100644 index 000000000..ffbbbb6a8 --- /dev/null +++ b/src/Shaders/SetIndices.gdshader @@ -0,0 +1,18 @@ +shader_type canvas_item; +render_mode unshaded; + +#include "res://src/Shaders/FindPaletteColorIndex.gdshaderinc" +uniform sampler2D rgb_texture : filter_nearest; +uniform sampler2D palette_texture : filter_nearest; + + +void fragment() { + vec4 color = texture(rgb_texture, UV); + if (color.a <= 0.01) { + COLOR.r = 0.0; + } + else { + int color_index = find_index(color, textureSize(palette_texture, 0).x, palette_texture); + COLOR.r = float(color_index + 1) / 255.0; + } +} diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index 613051b57..a045aeb20 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -35,7 +35,7 @@ var _line_polylines := [] # Memorize some stuff when doing brush strokes var _stroke_project: Project -var _stroke_images: Array[Image] = [] +var _stroke_images: Array[ImageExtended] = [] var _is_mask_size_zero := true var _circle_tool_shortcut: Array[Vector2i] @@ -730,8 +730,8 @@ func _get_undo_data() -> Dictionary: for cel in cels: if not cel is PixelCel: continue - var image := cel.get_image() - data[image] = image.data + var image := (cel as PixelCel).get_image() + image.add_data_to_dictionary(data) return data diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index c3d43aabb..75eb53106 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -299,12 +299,12 @@ func _get_draw_rect() -> Rect2i: return Rect2i(Vector2i.ZERO, Global.current_project.size) -func _get_draw_image() -> Image: +func _get_draw_image() -> ImageExtended: return Global.current_project.get_current_cel().get_image() -func _get_selected_draw_images() -> Array[Image]: - var images: Array[Image] = [] +func _get_selected_draw_images() -> Array[ImageExtended]: + var images: Array[ImageExtended] = [] var project := Global.current_project for cel_index in project.selected_cels: var cel: BaseCel = project.frames[cel_index[0]].cels[cel_index[1]] diff --git a/src/Tools/DesignTools/Bucket.gd b/src/Tools/DesignTools/Bucket.gd index 2635ab356..5a96594bd 100644 --- a/src/Tools/DesignTools/Bucket.gd +++ b/src/Tools/DesignTools/Bucket.gd @@ -220,7 +220,7 @@ func fill_in_color(pos: Vector2i) -> void: if project.has_selection: selection = project.selection_map.return_cropped_copy(project.size) else: - selection = Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + selection = project.new_empty_image() selection.fill(Color(1, 1, 1, 1)) selection_tex = ImageTexture.create_from_image(selection) @@ -263,7 +263,7 @@ func fill_in_selection() -> void: var images := _get_selected_draw_images() if _fill_with == FillWith.COLOR or _pattern == null: if project.has_selection: - var filler := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + var filler := project.new_empty_image() filler.fill(tool_slot.color) var rect: Rect2i = Global.canvas.selection.big_bounding_rectangle var selection_map_copy := project.selection_map.return_cropped_copy(project.size) @@ -284,7 +284,7 @@ func fill_in_selection() -> void: if project.has_selection: selection = project.selection_map.return_cropped_copy(project.size) else: - selection = Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + selection = project.new_empty_image() selection.fill(Color(1, 1, 1, 1)) selection_tex = ImageTexture.create_from_image(selection) @@ -461,7 +461,7 @@ func _compute_segments_for_image( done = false -func _color_segments(image: Image) -> void: +func _color_segments(image: ImageExtended) -> void: if _fill_with == FillWith.COLOR or _pattern == null: # This is needed to ensure that the color used to fill is not wrong, due to float # rounding issues. @@ -472,7 +472,7 @@ func _color_segments(image: Image) -> void: var p := _allegro_image_segments[c] for px in range(p.left_position, p.right_position + 1): # We don't have to check again whether the point being processed is within the bounds - image.set_pixel(px, p.y, color) + image.set_pixel_custom(px, p.y, color) else: # shortcircuit tests for patternfills var pattern_size := _pattern.image.get_size() @@ -484,11 +484,11 @@ func _color_segments(image: Image) -> void: _set_pixel_pattern(image, px, p.y, pattern_size) -func _set_pixel_pattern(image: Image, x: int, y: int, pattern_size: Vector2i) -> void: +func _set_pixel_pattern(image: ImageExtended, x: int, y: int, pattern_size: Vector2i) -> void: var px := (x + _offset_x) % pattern_size.x var py := (y + _offset_y) % pattern_size.y var pc := _pattern.image.get_pixel(px, py) - image.set_pixel(x, y, pc) + image.set_pixel_custom(x, y, pc) func commit_undo() -> void: @@ -514,12 +514,12 @@ func _get_undo_data() -> Dictionary: if Global.animation_timeline.animation_timer.is_stopped(): var images := _get_selected_draw_images() for image in images: - data[image] = image.data + image.add_data_to_dictionary(data) else: for frame in Global.current_project.frames: var cel := frame.cels[Global.current_project.current_layer] if not cel is PixelCel: continue - var image := cel.get_image() - data[image] = image.data + var image := (cel as PixelCel).get_image() + image.add_data_to_dictionary(data) return data diff --git a/src/Tools/DesignTools/CurveTool.gd b/src/Tools/DesignTools/CurveTool.gd index 9219c2bbc..c02a2bb06 100644 --- a/src/Tools/DesignTools/CurveTool.gd +++ b/src/Tools/DesignTools/CurveTool.gd @@ -203,7 +203,7 @@ func _draw_shape() -> void: commit_undo() -func _draw_pixel(point: Vector2i, images: Array[Image]) -> void: +func _draw_pixel(point: Vector2i, images: Array[ImageExtended]) -> void: if Global.current_project.can_pixel_get_drawn(point): for image in images: _drawer.set_pixel(image, point, tool_slot.color) diff --git a/src/Tools/DesignTools/Eraser.gd b/src/Tools/DesignTools/Eraser.gd index f2e99ac45..8f9d15b9f 100644 --- a/src/Tools/DesignTools/Eraser.gd +++ b/src/Tools/DesignTools/Eraser.gd @@ -122,6 +122,7 @@ func _draw_brush_image(image: Image, src_rect: Rect2i, dst: Vector2i) -> void: var images := _get_selected_draw_images() for draw_image in images: draw_image.blit_rect_mask(_clear_image, image, src_rect, dst) + draw_image.convert_rgb_to_indexed() else: for xx in image.get_size().x: for yy in image.get_size().y: diff --git a/src/Tools/DesignTools/Pencil.gd b/src/Tools/DesignTools/Pencil.gd index 269503553..520ab865a 100644 --- a/src/Tools/DesignTools/Pencil.gd +++ b/src/Tools/DesignTools/Pencil.gd @@ -211,6 +211,7 @@ func _draw_brush_image(brush_image: Image, src_rect: Rect2i, dst: Vector2i) -> v draw_image.blit_rect_mask(brush_image, mask, src_rect, dst) else: draw_image.blit_rect(brush_image, src_rect, dst) + draw_image.convert_rgb_to_indexed() else: for draw_image in images: if Tools.alpha_locked: @@ -218,3 +219,4 @@ func _draw_brush_image(brush_image: Image, src_rect: Rect2i, dst: Vector2i) -> v draw_image.blend_rect_mask(brush_image, mask, src_rect, dst) else: draw_image.blend_rect(brush_image, src_rect, dst) + draw_image.convert_rgb_to_indexed() diff --git a/src/Tools/UtilityTools/Move.gd b/src/Tools/UtilityTools/Move.gd index 0520dcc6c..76a53e523 100644 --- a/src/Tools/UtilityTools/Move.gd +++ b/src/Tools/UtilityTools/Move.gd @@ -8,7 +8,7 @@ var _content_transformation_check := false var _snap_to_grid := false ## Mouse Click + Ctrl var _undo_data := {} -@onready var selection_node: Node2D = Global.canvas.selection +@onready var selection_node := Global.canvas.selection func _input(event: InputEvent) -> void: @@ -78,19 +78,15 @@ func draw_end(pos: Vector2i) -> void: and _content_transformation_check == selection_node.is_moving_content ): pos = _snap_position(pos) - var project := Global.current_project - - if project.has_selection: + if Global.current_project.has_selection: selection_node.move_borders_end() else: var pixel_diff := pos - _start_pos Global.canvas.move_preview_location = Vector2i.ZERO var images := _get_selected_draw_images() for image in images: - var image_copy := Image.new() - image_copy.copy_from(image) - image.fill(Color(0, 0, 0, 0)) - image.blit_rect(image_copy, Rect2i(Vector2i.ZERO, project.size), pixel_diff) + _move_image(image, pixel_diff) + _move_image(image.indices_image, pixel_diff) _commit_undo("Draw") _start_pos = Vector2.INF @@ -99,6 +95,13 @@ func draw_end(pos: Vector2i) -> void: Global.canvas.measurements.update_measurement(Global.MeasurementMode.NONE) +func _move_image(image: Image, pixel_diff: Vector2i) -> void: + var image_copy := Image.new() + image_copy.copy_from(image) + image.fill(Color(0, 0, 0, 0)) + image.blit_rect(image_copy, Rect2i(Vector2i.ZERO, image.get_size()), pixel_diff) + + func _snap_position(pos: Vector2) -> Vector2: if Input.is_action_pressed("transform_snap_axis"): var angle := pos.angle_to_point(_start_pos) @@ -155,6 +158,6 @@ func _get_undo_data() -> Dictionary: for cel in cels: if not cel is PixelCel: continue - var image: Image = cel.image - data[image] = image.data + var image := (cel as PixelCel).get_image() + image.add_data_to_dictionary(data) return data diff --git a/src/Tools/UtilityTools/Text.gd b/src/Tools/UtilityTools/Text.gd index 966f6ea57..3a96b4189 100644 --- a/src/Tools/UtilityTools/Text.gd +++ b/src/Tools/UtilityTools/Text.gd @@ -149,12 +149,14 @@ func text_to_pixels() -> void: RenderingServer.free_rid(canvas) RenderingServer.free_rid(ci_rid) RenderingServer.free_rid(texture) - viewport_texture.convert(Image.FORMAT_RGBA8) + viewport_texture.convert(image.get_format()) text_edit.queue_free() text_edit = null if not viewport_texture.is_empty(): image.copy_from(viewport_texture) + if image is ImageExtended: + image.convert_rgb_to_indexed() commit_undo("Draw", undo_data) @@ -179,7 +181,7 @@ func _get_undo_data() -> Dictionary: var data := {} var images := _get_selected_draw_images() for image in images: - data[image] = image.data + image.add_data_to_dictionary(data) return data diff --git a/src/UI/Canvas/Selection.gd b/src/UI/Canvas/Selection.gd index 3efe8268e..2149ad90c 100644 --- a/src/UI/Canvas/Selection.gd +++ b/src/UI/Canvas/Selection.gd @@ -516,6 +516,7 @@ func transform_content_confirm() -> void: Rect2i(Vector2i.ZERO, project.selection_map.get_size()), big_bounding_rectangle.position ) + cel_image.convert_rgb_to_indexed() project.selection_map.move_bitmap_values(project) commit_undo("Move Selection", undo_data) @@ -605,13 +606,13 @@ func get_undo_data(undo_image: bool) -> Dictionary: if undo_image: var images := _get_selected_draw_images() for image in images: - data[image] = image.data + image.add_data_to_dictionary(data) return data -func _get_selected_draw_cels() -> Array[BaseCel]: - var cels: Array[BaseCel] = [] +func _get_selected_draw_cels() -> Array[PixelCel]: + var cels: Array[PixelCel] = [] var project := Global.current_project for cel_index in project.selected_cels: var cel: BaseCel = project.frames[cel_index[0]].cels[cel_index[1]] @@ -622,8 +623,8 @@ func _get_selected_draw_cels() -> Array[BaseCel]: return cels -func _get_selected_draw_images() -> Array[Image]: - var images: Array[Image] = [] +func _get_selected_draw_images() -> Array[ImageExtended]: + var images: Array[ImageExtended] = [] var project := Global.current_project for cel_index in project.selected_cels: var cel: BaseCel = project.frames[cel_index[0]].cels[cel_index[1]] @@ -794,14 +795,14 @@ func delete(selected_cels := true) -> void: return var undo_data_tmp := get_undo_data(true) - var images: Array[Image] + var images: Array[ImageExtended] if selected_cels: images = _get_selected_draw_images() else: images = [project.get_current_cel().get_image()] if project.has_selection: - var blank := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + var blank := project.new_empty_image() var selection_map_copy := project.selection_map.return_cropped_copy(project.size) for image in images: image.blit_rect_mask( @@ -870,13 +871,16 @@ func _project_switched() -> void: func _get_preview_image() -> void: var project := Global.current_project - var blended_image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + var blended_image := project.new_empty_image() DrawingAlgos.blend_layers( blended_image, project.frames[project.current_frame], Vector2i.ZERO, project, true ) if original_preview_image.is_empty(): original_preview_image = Image.create( - big_bounding_rectangle.size.x, big_bounding_rectangle.size.y, false, Image.FORMAT_RGBA8 + big_bounding_rectangle.size.x, + big_bounding_rectangle.size.y, + false, + project.get_image_format() ) var selection_map_copy := project.selection_map.return_cropped_copy(project.size) original_preview_image.blit_rect_mask( @@ -892,11 +896,11 @@ func _get_preview_image() -> void: var clear_image := Image.create( original_preview_image.get_width(), original_preview_image.get_height(), - false, - Image.FORMAT_RGBA8 + original_preview_image.has_mipmaps(), + original_preview_image.get_format() ) for cel in _get_selected_draw_cels(): - var cel_image: Image = cel.get_image() + var cel_image := cel.get_image() cel.transformed_content = _get_selected_image(cel_image) cel_image.blit_rect_mask( clear_image, @@ -911,7 +915,10 @@ func _get_preview_image() -> void: func _get_selected_image(cel_image: Image) -> Image: var project := Global.current_project var image := Image.create( - big_bounding_rectangle.size.x, big_bounding_rectangle.size.y, false, Image.FORMAT_RGBA8 + big_bounding_rectangle.size.x, + big_bounding_rectangle.size.y, + false, + project.get_image_format() ) var selection_map_copy := project.selection_map.return_cropped_copy(project.size) image.blit_rect_mask(cel_image, selection_map_copy, big_bounding_rectangle, Vector2i.ZERO) diff --git a/src/UI/Dialogs/CreateNewImage.gd b/src/UI/Dialogs/CreateNewImage.gd index f97aa3440..0cd37b121 100644 --- a/src/UI/Dialogs/CreateNewImage.gd +++ b/src/UI/Dialogs/CreateNewImage.gd @@ -52,7 +52,9 @@ var templates: Array[Template] = [ @onready var height_value := %HeightValue as SpinBox @onready var portrait_button := %PortraitButton as Button @onready var landscape_button := %LandscapeButton as Button +@onready var name_input := $VBoxContainer/FillColorContainer/NameInput as LineEdit @onready var fill_color_node := %FillColor as ColorPickerButton +@onready var color_mode := $VBoxContainer/FillColorContainer/ColorMode as OptionButton @onready var recent_templates_list := %RecentTemplates as ItemList @@ -123,13 +125,14 @@ func _on_CreateNewImage_confirmed() -> void: if recent_sizes.size() > 10: recent_sizes.resize(10) Global.config_cache.set_value("templates", "recent_sizes", recent_sizes) - var fill_color: Color = fill_color_node.color - - var proj_name: String = $VBoxContainer/ProjectName/NameInput.text + var fill_color := fill_color_node.color + var proj_name := name_input.text if !proj_name.is_valid_filename(): proj_name = tr("untitled") var new_project := Project.new([], proj_name, image_size) + if color_mode.selected == 1: + new_project.color_mode = Project.INDEXED_MODE new_project.layers.append(PixelLayer.new(new_project)) new_project.fill_color = fill_color new_project.frames.append(new_project.new_empty_frame()) diff --git a/src/UI/Dialogs/CreateNewImage.tscn b/src/UI/Dialogs/CreateNewImage.tscn index 2bdad0d6d..a8c4a987f 100644 --- a/src/UI/Dialogs/CreateNewImage.tscn +++ b/src/UI/Dialogs/CreateNewImage.tscn @@ -9,7 +9,8 @@ [node name="CreateNewImage" type="ConfirmationDialog"] title = "New..." -size = Vector2i(384, 330) +position = Vector2i(0, 36) +size = Vector2i(434, 330) script = ExtResource("1") [node name="VBoxContainer" type="VBoxContainer" parent="."] @@ -22,19 +23,6 @@ offset_right = -8.0 offset_bottom = -49.0 size_flags_horizontal = 0 -[node name="ProjectName" type="HBoxContainer" parent="VBoxContainer"] -layout_mode = 2 - -[node name="NameLabel" type="Label" parent="VBoxContainer/ProjectName"] -custom_minimum_size = Vector2(100, 0) -layout_mode = 2 -text = "Project Name:" - -[node name="NameInput" type="LineEdit" parent="VBoxContainer/ProjectName"] -layout_mode = 2 -size_flags_horizontal = 3 -placeholder_text = "Enter name... (Default \"untitled\")" - [node name="ImageSize" type="Label" parent="VBoxContainer"] layout_mode = 2 text = "Image Size" @@ -42,49 +30,48 @@ text = "Image Size" [node name="HSeparator" type="HSeparator" parent="VBoxContainer"] layout_mode = 2 -[node name="VBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +[node name="SizeContainer" type="HBoxContainer" parent="VBoxContainer"] layout_mode = 2 size_flags_vertical = 3 -[node name="Templates" type="VBoxContainer" parent="VBoxContainer/VBoxContainer"] +[node name="SizeOptions" type="VBoxContainer" parent="VBoxContainer/SizeContainer"] layout_mode = 2 size_flags_horizontal = 3 -[node name="TemplatesContainer" type="HBoxContainer" parent="VBoxContainer/VBoxContainer/Templates"] +[node name="TemplatesContainer" type="HBoxContainer" parent="VBoxContainer/SizeContainer/SizeOptions"] layout_mode = 2 -[node name="TemplatesLabel" type="Label" parent="VBoxContainer/VBoxContainer/Templates/TemplatesContainer"] +[node name="TemplatesLabel" type="Label" parent="VBoxContainer/SizeContainer/SizeOptions/TemplatesContainer"] custom_minimum_size = Vector2(100, 0) layout_mode = 2 text = "Templates:" -[node name="TemplatesOptions" type="OptionButton" parent="VBoxContainer/VBoxContainer/Templates/TemplatesContainer"] +[node name="TemplatesOptions" type="OptionButton" parent="VBoxContainer/SizeContainer/SizeOptions/TemplatesContainer"] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 mouse_default_cursor_shape = 2 toggle_mode = false -item_count = 1 selected = 0 +item_count = 1 popup/item_0/text = "Default" -popup/item_0/id = 0 -[node name="SizeContainer" type="HBoxContainer" parent="VBoxContainer/VBoxContainer/Templates"] +[node name="WidthHeightContainer" type="HBoxContainer" parent="VBoxContainer/SizeContainer/SizeOptions"] layout_mode = 2 -[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/VBoxContainer/Templates/SizeContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/SizeContainer/SizeOptions/WidthHeightContainer"] layout_mode = 2 size_flags_horizontal = 3 -[node name="WidthContainer" type="HBoxContainer" parent="VBoxContainer/VBoxContainer/Templates/SizeContainer/VBoxContainer"] +[node name="WidthContainer" type="HBoxContainer" parent="VBoxContainer/SizeContainer/SizeOptions/WidthHeightContainer/VBoxContainer"] layout_mode = 2 -[node name="WidthLabel" type="Label" parent="VBoxContainer/VBoxContainer/Templates/SizeContainer/VBoxContainer/WidthContainer"] +[node name="WidthLabel" type="Label" parent="VBoxContainer/SizeContainer/SizeOptions/WidthHeightContainer/VBoxContainer/WidthContainer"] custom_minimum_size = Vector2(100, 0) layout_mode = 2 text = "Width:" -[node name="WidthValue" type="SpinBox" parent="VBoxContainer/VBoxContainer/Templates/SizeContainer/VBoxContainer/WidthContainer"] +[node name="WidthValue" type="SpinBox" parent="VBoxContainer/SizeContainer/SizeOptions/WidthHeightContainer/VBoxContainer/WidthContainer"] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 @@ -94,15 +81,15 @@ max_value = 16384.0 value = 64.0 suffix = "px" -[node name="HeightContainer" type="HBoxContainer" parent="VBoxContainer/VBoxContainer/Templates/SizeContainer/VBoxContainer"] +[node name="HeightContainer" type="HBoxContainer" parent="VBoxContainer/SizeContainer/SizeOptions/WidthHeightContainer/VBoxContainer"] layout_mode = 2 -[node name="HeightLabel" type="Label" parent="VBoxContainer/VBoxContainer/Templates/SizeContainer/VBoxContainer/HeightContainer"] +[node name="HeightLabel" type="Label" parent="VBoxContainer/SizeContainer/SizeOptions/WidthHeightContainer/VBoxContainer/HeightContainer"] custom_minimum_size = Vector2(100, 0) layout_mode = 2 text = "Height:" -[node name="HeightValue" type="SpinBox" parent="VBoxContainer/VBoxContainer/Templates/SizeContainer/VBoxContainer/HeightContainer"] +[node name="HeightValue" type="SpinBox" parent="VBoxContainer/SizeContainer/SizeOptions/WidthHeightContainer/VBoxContainer/HeightContainer"] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 @@ -112,11 +99,11 @@ max_value = 16384.0 value = 64.0 suffix = "px" -[node name="TextureRect" type="TextureRect" parent="VBoxContainer/VBoxContainer/Templates/SizeContainer" groups=["UIButtons"]] +[node name="TextureRect" type="TextureRect" parent="VBoxContainer/SizeContainer/SizeOptions/WidthHeightContainer" groups=["UIButtons"]] layout_mode = 2 texture = ExtResource("6") -[node name="AspectRatioButton" type="TextureButton" parent="VBoxContainer/VBoxContainer/Templates/SizeContainer/TextureRect" groups=["UIButtons"]] +[node name="AspectRatioButton" type="TextureButton" parent="VBoxContainer/SizeContainer/SizeOptions/WidthHeightContainer/TextureRect" groups=["UIButtons"]] unique_name_in_owner = true layout_mode = 0 anchor_left = 0.5 @@ -133,31 +120,10 @@ toggle_mode = true texture_normal = ExtResource("4") texture_pressed = ExtResource("5") -[node name="VSeparator" type="VSeparator" parent="VBoxContainer/VBoxContainer"] +[node name="SizeButtonsContainer" type="HBoxContainer" parent="VBoxContainer/SizeContainer/SizeOptions"] layout_mode = 2 -[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/VBoxContainer"] -clip_contents = true -custom_minimum_size = Vector2(150, 0) -layout_mode = 2 -focus_mode = 2 -mouse_filter = 0 - -[node name="Label" type="Label" parent="VBoxContainer/VBoxContainer/VBoxContainer"] -layout_mode = 2 -text = "Recent:" - -[node name="RecentTemplates" type="ItemList" parent="VBoxContainer/VBoxContainer/VBoxContainer"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 -allow_reselect = true - -[node name="SizeButtonsContainer" type="HBoxContainer" parent="VBoxContainer"] -layout_mode = 2 - -[node name="PortraitButton" type="Button" parent="VBoxContainer/SizeButtonsContainer" groups=["UIButtons"]] +[node name="PortraitButton" type="Button" parent="VBoxContainer/SizeContainer/SizeOptions/SizeButtonsContainer" groups=["UIButtons"]] unique_name_in_owner = true custom_minimum_size = Vector2(24, 24) layout_mode = 2 @@ -166,7 +132,7 @@ focus_mode = 0 mouse_default_cursor_shape = 2 toggle_mode = true -[node name="TextureRect" type="TextureRect" parent="VBoxContainer/SizeButtonsContainer/PortraitButton"] +[node name="TextureRect" type="TextureRect" parent="VBoxContainer/SizeContainer/SizeOptions/SizeButtonsContainer/PortraitButton"] layout_mode = 0 anchor_left = 0.5 anchor_top = 0.5 @@ -178,7 +144,7 @@ offset_right = 8.0 offset_bottom = 8.0 texture = ExtResource("2") -[node name="LandscapeButton" type="Button" parent="VBoxContainer/SizeButtonsContainer" groups=["UIButtons"]] +[node name="LandscapeButton" type="Button" parent="VBoxContainer/SizeContainer/SizeOptions/SizeButtonsContainer" groups=["UIButtons"]] unique_name_in_owner = true custom_minimum_size = Vector2(24, 24) layout_mode = 2 @@ -187,7 +153,7 @@ focus_mode = 0 mouse_default_cursor_shape = 2 toggle_mode = true -[node name="TextureRect" type="TextureRect" parent="VBoxContainer/SizeButtonsContainer/LandscapeButton"] +[node name="TextureRect" type="TextureRect" parent="VBoxContainer/SizeContainer/SizeOptions/SizeButtonsContainer/LandscapeButton"] layout_mode = 0 anchor_left = 0.5 anchor_top = 0.5 @@ -199,12 +165,49 @@ offset_right = 8.0 offset_bottom = 8.0 texture = ExtResource("3") -[node name="FillColorContainer" type="HBoxContainer" parent="VBoxContainer"] +[node name="VSeparator" type="VSeparator" parent="VBoxContainer/SizeContainer"] layout_mode = 2 +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/SizeContainer"] +clip_contents = true +custom_minimum_size = Vector2(150, 0) +layout_mode = 2 +focus_mode = 2 +mouse_filter = 0 + +[node name="Label" type="Label" parent="VBoxContainer/SizeContainer/VBoxContainer"] +layout_mode = 2 +text = "Recent:" + +[node name="RecentTemplates" type="ItemList" parent="VBoxContainer/SizeContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +allow_reselect = true + +[node name="HSeparator2" type="HSeparator" parent="VBoxContainer"] +layout_mode = 2 + +[node name="FillColorContainer" type="GridContainer" parent="VBoxContainer"] +layout_mode = 2 +columns = 2 + +[node name="NameLabel" type="Label" parent="VBoxContainer/FillColorContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +size_flags_horizontal = 3 +text = "Project Name:" + +[node name="NameInput" type="LineEdit" parent="VBoxContainer/FillColorContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Enter name... (Default \"untitled\")" + [node name="FillColorLabel" type="Label" parent="VBoxContainer/FillColorContainer"] custom_minimum_size = Vector2(100, 0) layout_mode = 2 +size_flags_horizontal = 3 text = "Fill with color:" [node name="FillColor" type="ColorPickerButton" parent="VBoxContainer/FillColorContainer"] @@ -215,13 +218,28 @@ size_flags_horizontal = 3 mouse_default_cursor_shape = 2 color = Color(0, 0, 0, 0) +[node name="ColorModeLabel" type="Label" parent="VBoxContainer/FillColorContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +size_flags_horizontal = 3 +text = "Color mode:" + +[node name="ColorMode" type="OptionButton" parent="VBoxContainer/FillColorContainer"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +selected = 0 +item_count = 2 +popup/item_0/text = "RGBA" +popup/item_1/text = "Indexed" +popup/item_1/id = 1 + [connection signal="about_to_popup" from="." to="." method="_on_CreateNewImage_about_to_show"] [connection signal="confirmed" from="." to="." method="_on_CreateNewImage_confirmed"] [connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] -[connection signal="item_selected" from="VBoxContainer/VBoxContainer/Templates/TemplatesContainer/TemplatesOptions" to="." method="_on_TemplatesOptions_item_selected"] -[connection signal="value_changed" from="VBoxContainer/VBoxContainer/Templates/SizeContainer/VBoxContainer/WidthContainer/WidthValue" to="." method="_on_SizeValue_value_changed"] -[connection signal="value_changed" from="VBoxContainer/VBoxContainer/Templates/SizeContainer/VBoxContainer/HeightContainer/HeightValue" to="." method="_on_SizeValue_value_changed"] -[connection signal="toggled" from="VBoxContainer/VBoxContainer/Templates/SizeContainer/TextureRect/AspectRatioButton" to="." method="_on_AspectRatioButton_toggled"] -[connection signal="item_selected" from="VBoxContainer/VBoxContainer/VBoxContainer/RecentTemplates" to="." method="_on_RecentTemplates_item_selected"] -[connection signal="toggled" from="VBoxContainer/SizeButtonsContainer/PortraitButton" to="." method="_on_PortraitButton_toggled"] -[connection signal="toggled" from="VBoxContainer/SizeButtonsContainer/LandscapeButton" to="." method="_on_LandscapeButton_toggled"] +[connection signal="item_selected" from="VBoxContainer/SizeContainer/SizeOptions/TemplatesContainer/TemplatesOptions" to="." method="_on_TemplatesOptions_item_selected"] +[connection signal="value_changed" from="VBoxContainer/SizeContainer/SizeOptions/WidthHeightContainer/VBoxContainer/WidthContainer/WidthValue" to="." method="_on_SizeValue_value_changed"] +[connection signal="value_changed" from="VBoxContainer/SizeContainer/SizeOptions/WidthHeightContainer/VBoxContainer/HeightContainer/HeightValue" to="." method="_on_SizeValue_value_changed"] +[connection signal="toggled" from="VBoxContainer/SizeContainer/SizeOptions/WidthHeightContainer/TextureRect/AspectRatioButton" to="." method="_on_AspectRatioButton_toggled"] +[connection signal="toggled" from="VBoxContainer/SizeContainer/SizeOptions/SizeButtonsContainer/PortraitButton" to="." method="_on_PortraitButton_toggled"] +[connection signal="toggled" from="VBoxContainer/SizeContainer/SizeOptions/SizeButtonsContainer/LandscapeButton" to="." method="_on_LandscapeButton_toggled"] +[connection signal="item_selected" from="VBoxContainer/SizeContainer/VBoxContainer/RecentTemplates" to="." method="_on_RecentTemplates_item_selected"] diff --git a/src/UI/Dialogs/ImageEffects/RotateImage.gd b/src/UI/Dialogs/ImageEffects/RotateImage.gd index 07dc142f4..9a5712e1d 100644 --- a/src/UI/Dialogs/ImageEffects/RotateImage.gd +++ b/src/UI/Dialogs/ImageEffects/RotateImage.gd @@ -89,7 +89,7 @@ func commit_action(cel: Image, project := Global.current_project) -> void: selection_tex = ImageTexture.create_from_image(selection) if !_type_is_shader(): - var blank := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + var blank := project.new_empty_image() cel.blit_rect_mask( blank, selection, Rect2i(Vector2i.ZERO, cel.get_size()), Vector2i.ZERO ) @@ -136,6 +136,8 @@ func commit_action(cel: Image, project := Global.current_project) -> void: cel.blend_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO) else: cel.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO) + if cel is ImageExtended: + cel.convert_rgb_to_indexed() func _type_is_shader() -> bool: diff --git a/src/UI/Dialogs/ImageEffects/ScaleImage.gd b/src/UI/Dialogs/ImageEffects/ScaleImage.gd index a59276a21..b8c94ab39 100644 --- a/src/UI/Dialogs/ImageEffects/ScaleImage.gd +++ b/src/UI/Dialogs/ImageEffects/ScaleImage.gd @@ -34,7 +34,7 @@ func _on_ScaleImage_confirmed() -> void: var width: int = width_value.value var height: int = height_value.value var interpolation: int = interpolation_type.selected - DrawingAlgos.scale_image(width, height, interpolation) + DrawingAlgos.scale_project(width, height, interpolation) func _on_visibility_changed() -> void: diff --git a/src/UI/Dialogs/ImportTagDialog.gd b/src/UI/Dialogs/ImportTagDialog.gd index caf0ae45d..dcb2cccf7 100644 --- a/src/UI/Dialogs/ImportTagDialog.gd +++ b/src/UI/Dialogs/ImportTagDialog.gd @@ -22,7 +22,7 @@ func refresh_list() -> void: animation_tags_list.clear() get_ok_button().disabled = true for tag: AnimationTag in from_project.animation_tags: - var img = Image.create(from_project.size.x, from_project.size.y, true, Image.FORMAT_RGBA8) + var img := from_project.new_empty_image() DrawingAlgos.blend_layers( img, from_project.frames[tag.from - 1], Vector2i.ZERO, from_project ) @@ -186,9 +186,7 @@ func add_animation(indices: Array, destination: int, from_tag: AnimationTag = nu # add more types here if they have a copy_content() method if src_cel is PixelCel: var src_img = src_cel.copy_content() - var copy := Image.create( - project.size.x, project.size.y, false, Image.FORMAT_RGBA8 - ) + var copy := project.new_empty_image() copy.blit_rect( src_img, Rect2(Vector2.ZERO, src_img.get_size()), Vector2.ZERO ) diff --git a/src/UI/Dialogs/ProjectProperties.gd b/src/UI/Dialogs/ProjectProperties.gd index 2b2c4f13c..9380e6229 100644 --- a/src/UI/Dialogs/ProjectProperties.gd +++ b/src/UI/Dialogs/ProjectProperties.gd @@ -1,6 +1,7 @@ extends AcceptDialog @onready var size_value_label := $GridContainer/SizeValueLabel as Label +@onready var color_mode_value_label := $GridContainer/ColorModeValueLabel as Label @onready var frames_value_label := $GridContainer/FramesValueLabel as Label @onready var layers_value_label := $GridContainer/LayersValueLabel as Label @onready var name_line_edit := $GridContainer/NameLineEdit as LineEdit @@ -10,6 +11,12 @@ extends AcceptDialog func _on_visibility_changed() -> void: Global.dialog_open(visible) size_value_label.text = str(Global.current_project.size) + if Global.current_project.get_image_format() == Image.FORMAT_RGBA8: + color_mode_value_label.text = "RGBA8" + else: + color_mode_value_label.text = str(Global.current_project.get_image_format()) + if Global.current_project.is_indexed(): + color_mode_value_label.text += " (%s)" % tr("Indexed") frames_value_label.text = str(Global.current_project.frames.size()) layers_value_label.text = str(Global.current_project.layers.size()) name_line_edit.text = Global.current_project.name diff --git a/src/UI/Dialogs/ProjectProperties.tscn b/src/UI/Dialogs/ProjectProperties.tscn index 8585b0438..72c90b7c6 100644 --- a/src/UI/Dialogs/ProjectProperties.tscn +++ b/src/UI/Dialogs/ProjectProperties.tscn @@ -24,6 +24,16 @@ layout_mode = 2 size_flags_horizontal = 3 text = "64x64" +[node name="ColorModeLabel" type="Label" parent="GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Color mode:" + +[node name="ColorModeValueLabel" type="Label" parent="GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "RGBA8" + [node name="FramesLabel" type="Label" parent="GridContainer"] layout_mode = 2 size_flags_horizontal = 3 @@ -32,7 +42,7 @@ text = "Frames:" [node name="FramesValueLabel" type="Label" parent="GridContainer"] layout_mode = 2 size_flags_horizontal = 3 -text = "64x64" +text = "1" [node name="LayersLabel" type="Label" parent="GridContainer"] layout_mode = 2 @@ -42,7 +52,7 @@ text = "Layers:" [node name="LayersValueLabel" type="Label" parent="GridContainer"] layout_mode = 2 size_flags_horizontal = 3 -text = "64x64" +text = "1" [node name="NameLabel" type="Label" parent="GridContainer"] layout_mode = 2 diff --git a/src/UI/Recorder/Recorder.gd b/src/UI/Recorder/Recorder.gd index 9009c2bba..caffce80b 100644 --- a/src/UI/Recorder/Recorder.gd +++ b/src/UI/Recorder/Recorder.gd @@ -70,7 +70,7 @@ class Recorder: image = recorder_panel.get_window().get_texture().get_image() else: var frame := project.frames[project.current_frame] - image = Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + image = project.new_empty_image() DrawingAlgos.blend_layers(image, frame, Vector2i.ZERO, project) if recorder_panel.resize_percent != 100: diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index 490f4fb2a..af8c1c5fb 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -1045,9 +1045,10 @@ func _on_MergeDownLayer_pressed() -> void: top_cels.append(top_cel) # Store for undo purposes var top_image := top_layer.display_effects(top_cel) - var bottom_cel := frame.cels[bottom_layer.index] + var bottom_cel := frame.cels[bottom_layer.index] as PixelCel + var bottom_image := bottom_cel.get_image() var textures: Array[Image] = [] - textures.append(bottom_cel.get_image()) + textures.append(bottom_image) textures.append(top_image) var metadata_image := Image.create(2, 4, false, Image.FORMAT_R8) DrawingAlgos.set_layer_metadata_image(bottom_layer, bottom_cel, metadata_image, 0) @@ -1058,12 +1059,17 @@ func _on_MergeDownLayer_pressed() -> void: var params := { "layers": texture_array, "metadata": ImageTexture.create_from_image(metadata_image) } - var bottom_image := Image.create( - top_image.get_width(), top_image.get_height(), false, top_image.get_format() + var new_bottom_image := ImageExtended.create_custom( + top_image.get_width(), + top_image.get_height(), + top_image.has_mipmaps(), + top_image.get_format(), + project.is_indexed() ) + # Merge the image itself. var gen := ShaderImageEffect.new() - gen.generate_image(bottom_image, DrawingAlgos.blend_layers_shader, params, project.size) - + gen.generate_image(new_bottom_image, DrawingAlgos.blend_layers_shader, params, project.size) + new_bottom_image.convert_rgb_to_indexed() if ( bottom_cel.link_set != null and bottom_cel.link_set.size() > 1 @@ -1074,14 +1080,14 @@ func _on_MergeDownLayer_pressed() -> void: project.undo_redo.add_undo_method( bottom_layer.link_cel.bind(bottom_cel, bottom_cel.link_set) ) - project.undo_redo.add_do_property(bottom_cel, "image", bottom_image) + project.undo_redo.add_do_property(bottom_cel, "image", new_bottom_image) project.undo_redo.add_undo_property(bottom_cel, "image", bottom_cel.image) else: - Global.undo_redo_compress_images( - {bottom_cel.image: bottom_image.data}, - {bottom_cel.image: bottom_cel.image.data}, - project - ) + var redo_data := {} + var undo_data := {} + new_bottom_image.add_data_to_dictionary(redo_data, bottom_image) + bottom_image.add_data_to_dictionary(undo_data) + Global.undo_redo_compress_images(redo_data, undo_data, project) project.undo_redo.add_do_method(project.remove_layers.bind([top_layer.index])) project.undo_redo.add_undo_method( diff --git a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd index 3e035037b..09c9ff5b4 100644 --- a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd +++ b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd @@ -154,13 +154,19 @@ func _apply_effect(layer: BaseLayer, effect: LayerEffect) -> void: var undo_data := {} for frame in Global.current_project.frames: var cel := frame.cels[layer.index] - var new_image := Image.new() - new_image.copy_from(cel.get_image()) + var new_image := ImageExtended.new() + var cel_image := cel.get_image() + if cel_image is ImageExtended: + new_image.is_indexed = cel_image.is_indexed + new_image.copy_from_custom(cel_image) var image_size := new_image.get_size() var shader_image_effect := ShaderImageEffect.new() shader_image_effect.generate_image(new_image, effect.shader, effect.params, image_size) - redo_data[cel.image] = new_image.data - undo_data[cel.image] = cel.image.data + if cel_image is ImageExtended: + redo_data[cel_image.indices_image] = new_image.indices_image.data + undo_data[cel_image.indices_image] = cel_image.indices_image.data + redo_data[cel_image] = new_image.data + undo_data[cel_image] = cel_image.data Global.current_project.undos += 1 Global.current_project.undo_redo.create_action("Apply layer effect") Global.undo_redo_compress_images(redo_data, undo_data) diff --git a/src/UI/TopMenuContainer/TopMenuContainer.gd b/src/UI/TopMenuContainer/TopMenuContainer.gd index e026886e4..41ce71277 100644 --- a/src/UI/TopMenuContainer/TopMenuContainer.gd +++ b/src/UI/TopMenuContainer/TopMenuContainer.gd @@ -1,5 +1,7 @@ extends Panel +enum ColorModes { RGBA, INDEXED } + const DOCS_URL := "https://www.oramainteractive.com/Pixelorama-Docs/" const ISSUES_URL := "https://github.com/Orama-Interactive/Pixelorama/issues" const SUPPORT_URL := "https://www.patreon.com/OramaInteractive" @@ -56,6 +58,7 @@ var about_dialog := Dialog.new("res://src/UI/Dialogs/AboutDialog.tscn") @onready var greyscale_vision: ColorRect = main_ui.find_child("GreyscaleVision") @onready var tile_mode_submenu := PopupMenu.new() @onready var selection_modify_submenu := PopupMenu.new() +@onready var color_mode_submenu := PopupMenu.new() @onready var snap_to_submenu := PopupMenu.new() @onready var panels_submenu := PopupMenu.new() @onready var layouts_submenu := PopupMenu.new() @@ -124,6 +127,7 @@ func _project_switched() -> void: _update_file_menu_buttons(project) for j in Tiles.MODE.values(): tile_mode_submenu.set_item_checked(j, j == project.tiles.mode) + _check_color_mode_submenu_item(project) _update_current_frame_mark() @@ -396,19 +400,34 @@ func _setup_image_menu() -> void: # Order as in Global.ImageMenu enum var image_menu_items := { "Project Properties": "project_properties", + "Color Mode": "", "Resize Canvas": "resize_canvas", "Scale Image": "scale_image", "Crop to Selection": "crop_to_selection", "Crop to Content": "crop_to_content", } - var i := 0 - for item in image_menu_items: - _set_menu_shortcut(image_menu_items[item], image_menu, i, item) - i += 1 + for i in image_menu_items.size(): + var item: String = image_menu_items.keys()[i] + if item == "Color Mode": + _setup_color_mode_submenu(item) + else: + _set_menu_shortcut(image_menu_items[item], image_menu, i, item) image_menu.set_item_disabled(Global.ImageMenu.CROP_TO_SELECTION, true) image_menu.id_pressed.connect(image_menu_id_pressed) +func _setup_color_mode_submenu(item: String) -> void: + color_mode_submenu.set_name("color_mode_submenu") + color_mode_submenu.add_radio_check_item("RGBA", ColorModes.RGBA) + color_mode_submenu.set_item_checked(ColorModes.RGBA, true) + color_mode_submenu.add_radio_check_item("Indexed", ColorModes.INDEXED) + color_mode_submenu.hide_on_checkable_item_selection = false + + color_mode_submenu.id_pressed.connect(_color_mode_submenu_id_pressed) + image_menu.add_child(color_mode_submenu) + image_menu.add_submenu_item(item, color_mode_submenu.get_name()) + + func _setup_effects_menu() -> void: # Order as in Global.EffectMenu enum var menu_items := { @@ -687,6 +706,38 @@ func _selection_modify_submenu_id_pressed(id: int) -> void: modify_selection.node.type = id +func _color_mode_submenu_id_pressed(id: ColorModes) -> void: + var project := Global.current_project + var old_color_mode := project.color_mode + var redo_data := {} + var undo_data := {} + for cel in project.get_all_pixel_cels(): + cel.get_image().add_data_to_dictionary(undo_data) + # Change the color mode directly before undo/redo in order to affect the images, + # so we can store them as redo data. + if id == ColorModes.RGBA: + project.color_mode = Image.FORMAT_RGBA8 + else: + project.color_mode = Project.INDEXED_MODE + for cel in project.get_all_pixel_cels(): + cel.get_image().add_data_to_dictionary(redo_data) + project.undo_redo.create_action("Change color mode") + project.undos += 1 + project.undo_redo.add_do_property(project, "color_mode", project.color_mode) + project.undo_redo.add_undo_property(project, "color_mode", old_color_mode) + Global.undo_redo_compress_images(redo_data, undo_data, project) + project.undo_redo.add_do_method(_check_color_mode_submenu_item.bind(project)) + project.undo_redo.add_undo_method(_check_color_mode_submenu_item.bind(project)) + project.undo_redo.add_do_method(Global.undo_or_redo.bind(false)) + project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true)) + project.undo_redo.commit_action() + + +func _check_color_mode_submenu_item(project: Project) -> void: + color_mode_submenu.set_item_checked(ColorModes.RGBA, project.color_mode == Image.FORMAT_RGBA8) + color_mode_submenu.set_item_checked(ColorModes.INDEXED, project.is_indexed()) + + func _snap_to_submenu_id_pressed(id: int) -> void: if id == 0: Global.snap_to_rectangular_grid_boundary = !Global.snap_to_rectangular_grid_boundary