1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-02-12 16:53:07 +00:00

Compare commits

...

30 commits

Author SHA1 Message Date
Emmanouil Papadeas 92abf8c8c2
Merge dbe849a104 into 763783f2f1 2024-11-15 18:59:26 +00:00
Emmanouil Papadeas dbe849a104 New translations translations.pot (Turkish) 2024-11-15 20:59:24 +02:00
Emmanouil Papadeas 990ce8bf7d New translations translations.pot (Italian) 2024-11-15 20:59:22 +02:00
Emmanouil Papadeas f70ba123ad New translations translations.pot (Greek) 2024-11-15 20:59:21 +02:00
Emmanouil Papadeas 9a097aef29 New translations translations.pot (Russian) 2024-11-15 20:59:20 +02:00
Emmanouil Papadeas 763783f2f1 Improve the UI of the tile mode offsets dialog and add an Isometric button 2024-11-15 17:59:57 +02:00
Emmanouil Papadeas e10b0d1b08 Fix crash when opening the tile mode offsets dialog 2024-11-15 17:59:25 +02:00
Emmanouil Papadeas 94735fc08b [skip ci] Update Translations.pot 2024-11-15 02:08:30 +02:00
Emmanouil Papadeas 8077262b32 [skip ci] Update CHANGELOG.md 2024-11-15 02:04:59 +02:00
Emmanouil Papadeas 0d6b140dea Add border selection, fix some missing translation strings 2024-11-15 01:41:44 +02:00
Emmanouil Papadeas dec698024c Implement selection expanding and shrinking via the Select menu 2024-11-14 17:59:53 +02:00
Emmanouil Papadeas 785d8cfc83 Hide the density slider by default
So that it doesn't appear in the shape tools, where it has no effect.
2024-11-14 16:22:53 +02:00
Emmanouil Papadeas 4c7d7da5e7 Fix regression where pressing Enter or Control would not confirm/cancel selection when a selection tool wasn't active 2024-11-14 01:39:41 +02:00
Emmanouil Papadeas 36329efaf6 Add density to the square & circle brushes
00% density means that the brush gets completely drawn, anything less leaves gaps inside the brush, acting like a spray tool.
2024-11-14 01:02:51 +02:00
Emmanouil Papadeas 7c1435e95f When using the mouse wheel over a slider, don't scroll in ScrollContainers 2024-11-13 17:32:01 +02:00
Emmanouil Papadeas ad77d98f42 Slightly optimize circle brushes by only calling the DrawingAlgos methods once while drawing
They keep getting called when size dynamics are enabled, however.
2024-11-13 02:55:15 +02:00
Emmanouil Papadeas 2600180736 Remove the Recorder from the Web version
It's not working anyway, and I'm not sure if there is a way to make it work, at least with a good and user-friendly way. If we find a way we could re-add it in the future.
2024-11-13 00:40:58 +02:00
Emmanouil Papadeas 5739a8b28e [skip ci] Update CHANGELOG.md 2024-11-12 01:46:50 +02:00
Emmanouil Papadeas ce738f02c2 Don't change brush size when resizing the timeline cels and the palette swatches 2024-11-12 00:59:01 +02:00
Emmanouil Papadeas b0b1361722 Fix layer effect slider values being rounded to the nearest integer 2024-11-12 00:47:53 +02:00
Variable 5fa97988b5
Fixed unexpected behavior of resize_selection() (#1132)
* Fixed unexpected behavior of resize_selection()

* Fix typo

---------

Co-authored-by: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com>
2024-11-10 23:12:09 +02:00
Variable af703d486e
Add a way to get autoloads through the api (#1131)
* add autoloads to api

* A name Dilemma, There are 2 autoloads for ImportApi

* add docstring
2024-11-09 23:26:14 +02:00
Emmanouil Papadeas d2892358e3 Add a set_display_scale() method to Main to avoid duplicate code 2024-11-04 18:47:29 +02:00
Emmanouil Papadeas ec17e970e0 The Recorder panel now automatically records for the current project
Making its behavior more intuitive and consistent with the other panels. This also allows for multiple projects to be recorder at the same time, something that was not previous before. Changing projects now also changes the UI accordingly, depending on whether the current project is being recorded or not.

This change also fixes a memory leak, where either the first ever project or the last recorded one, stayed forever referenced in memory by the `project` variable.

Also fixed an issue where the recorder's settings size label was not showing the correct project size.
2024-11-03 18:54:08 +02:00
Emmanouil Papadeas 8beb79a33b Fix memory leak where the project remained referenced in BaseDraw even when its tab was closed
Another memory leak remains in Recorder.gd, where the first project forever remains referenced in memory, until the user changes the project from the option button. Perhaps we should remove that option button completely and always record the current project, that also sounds like the intended behavior to me.
2024-11-03 03:36:37 +02:00
Emmanouil Papadeas e2971a8fe9 Add UI buttons for confirming and cancelling a transformation
Needed especially for users without a keyboard.
2024-10-31 23:49:58 +02:00
Emmanouil Papadeas 6863adf957 Implement support for mouse buttons to be used as menu shortcuts - fixes #1070
Also maps the mouse thumb button 1 to undo, and the mouse thumb button 2 to redo.
2024-10-30 14:25:34 +02:00
Emmanouil Papadeas dafc2fb1d5 Bump version to v1.0.5-dev 2024-10-30 13:03:51 +02:00
Variable 2d9a582f21
Added an OKHSL Lightness sorting in palette (#1126)
* added a lightness sort system

* static check

* lightness

* formatting

* more formatting

* more formatting
2024-10-26 01:31:52 +03:00
Emmanouil Papadeas aa59f73e65
[skip ci] Update CHANGELOG.md 2024-10-25 21:32:22 +03:00
46 changed files with 781 additions and 339 deletions

View file

@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). All the dates are in YYYY-MM-DD format.
<br><br>
## [v1.0.5] - Unreleased
This update has been brought to you by the contributions of:
Fayez Akhtar ([@Variable-ind](https://github.com/Variable-ind))
Built using Godot 4.3
### Added
- Add density to the square & circle brushes. 100% density means that the brush gets completely drawn. Anything less leaves gaps inside the brush, acting like a spray tool.
- Selection expanding, shrinking and borders have been added as options in the Select menu.
- Mouse buttons can now be used as menu shortcuts. [#1070](https://github.com/Orama-Interactive/Pixelorama/issues/1070)
- Added confirm and cancel buttons in the selection tool options to confirm/cancel an active transformation.
- OKHSL Lightness sorting in palettes has been implemented. [#1126](https://github.com/Orama-Interactive/Pixelorama/pull/1126)
### Changed
- The brush size no longer changes by <kbd>Control</kbd> + Mouse Wheel when resizing the timeline cels or the palette swatches.
- The Recorder panel now automatically records for the current project. This also allows for multiple projects to be recorded at the same time.
### Fixed
- Panels no longer get scrolled when using the mouse wheel over a slider.
- Fixed layer effect slider values being rounded to the nearest integer.
- Fixed memory leak where the project remained referenced by a drawing tool, even when its tab was closed.
- Fixed memory leak where the first project remained forever references in memory by the Recorder panel.
- Slightly optimize circle brushes by only calling the ellipse algorithms once while drawing
### Removed
- The Recorder panel has been removed from the Web version. It wasn't functional anyway in a way that was useful, and it's unsure if we can find a way to make it work.
## [v1.0.4] - 2024-10-25
This update has been brought to you by the contributions of:
Fayez Akhtar ([@Variable-ind](https://github.com/Variable-ind)), Mariano Semelman ([@msemelman](https://github.com/msemelman))
@ -15,10 +42,10 @@ Built using Godot 4.3
- Added a new "color replace" mode to the Shading tool, that uses the colors of the palette to apply shading. [#1107](https://github.com/Orama-Interactive/Pixelorama/pull/1107)
- Added a new Erase blend mode. [#1117](https://github.com/Orama-Interactive/Pixelorama/pull/1117)
- It is now possible to change the font, depth and line spacing of 3D text.
- Implemented the ability to change the font of the interface from the properties.
- Implemented the ability to change the font of the interface from the preferences.
- Clipping to selection during export is now possible. [#1113](https://github.com/Orama-Interactive/Pixelorama/pull/1113)
- Added a preference to share options between tools. [#1120](https://github.com/Orama-Interactive/Pixelorama/pull/1120)
- Added an option to quickly center the canvas in the View menu. Mapped to <kbd>Control + C</kbd> by default. [#1123](https://github.com/Orama-Interactive/Pixelorama/pull/1123)
- Added an option to quickly center the canvas in the View menu. Mapped to <kbd>Shift + C</kbd> by default. [#1123](https://github.com/Orama-Interactive/Pixelorama/pull/1123)
- Added hotkeys to switch between tabs. <kbd>Control+Tab</kbd> to go to the next project tab, and <kbd>Control+Shift+Tab</kbd> to go to the previous. [#1109](https://github.com/Orama-Interactive/Pixelorama/pull/1109)
- Added menus next to each of the two mirroring buttons in the Global Tool Options, that allow users to automatically move the symmetry guides to the center of the canvas, or the view center.
- A new Reset category has been added to the Preferences that lets users easily restore certain options.

View file

@ -205,6 +205,43 @@ msgstr ""
msgid "Invert"
msgstr ""
msgid "Modify"
msgstr ""
#. Found under the Select menu, in the Modify submenu. When selected, it shows a window that lets users expand the active selection.
msgid "Expand"
msgstr ""
#. Title of a window that lets users expand the active selection.
msgid "Expand Selection"
msgstr ""
#. Found under the Select menu, in the Modify submenu. When selected, it shows a window that lets users shrink the active selection.
msgid "Shrink"
msgstr ""
#. Title of a window that lets users shrink the active selection.
msgid "Shrink Selection"
msgstr ""
#. Found under the Select menu, in the Modify submenu. When selected, it shows a window that lets users create a border of the active selection.
msgid "Border"
msgstr ""
#. Title of a window that lets users create a border of the active selection.
msgid "Border Selection"
msgstr ""
#. Refers to a diamond-like shape.
msgid "Diamond"
msgstr ""
msgid "Circle"
msgstr ""
msgid "Square"
msgstr ""
msgid "Grayscale View"
msgstr ""
@ -230,19 +267,16 @@ msgstr ""
msgid "Tile Mode Offsets"
msgstr ""
msgid "X-basis x:"
#. Found under "Tile Mode Offsets". Basis is a linear algebra term. https://en.wikipedia.org/wiki/Basis_(linear_algebra)
msgid "X-basis:"
msgstr ""
msgid "X-basis y:"
#. Found under "Tile Mode Offsets". Basis is a linear algebra term. https://en.wikipedia.org/wiki/Basis_(linear_algebra)
msgid "Y-basis:"
msgstr ""
msgid "Y-basis x:"
msgstr ""
msgid "Y-basis y:"
msgstr ""
msgid "Tile Mask"
#. Found under "Tile Mode Offsets". It's a button that when pressed, enables masking for tile mode. Masking essentially limits drawing to the visible pixels of the image, thus preventing from drawing on transparent pixels.
msgid "Masking:"
msgstr ""
msgid "Reset"
@ -1865,6 +1899,10 @@ msgstr ""
msgid "Fills the drawn shape with color, instead of drawing a hollow shape"
msgstr ""
#. Found in the tool options of the Pencil, Eraser and Shading tools. It is a percentage of how dense the brush is. 100% density means that the brush gets completely drawn, anything less leaves gaps inside the brush, acting like a spray tool.
msgid "Density:"
msgstr ""
msgid "Brush color from"
msgstr ""
@ -2808,6 +2846,10 @@ msgstr ""
msgid "Sort by value"
msgstr ""
#. An option of the Sort palette button found in the palette panel. When selected, the colors of the palette are being sorted based on their OKHSL Lightness.
msgid "Sort by lightness"
msgstr ""
#. An option of the Sort palette button found in the palette panel. When selected, the colors of the palette are being sorted based on their red channel value.
msgid "Sort by red"
msgstr ""

View file

@ -10,7 +10,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Language-Team: Greek\n"
"Language: el_GR\n"
"PO-Revision-Date: 2024-11-15 17:30\n"
"PO-Revision-Date: 2024-11-15 18:59\n"
msgid "OK"
msgstr "Εντάξει"
@ -247,7 +247,7 @@ msgstr "Περίγραμμα επιλογής"
#. Refers to a diamond-like shape.
msgid "Diamond"
msgstr "Διαμάντι"
msgstr "Ρόμβος"
msgid "Circle"
msgstr "Κύκλος"

View file

@ -10,7 +10,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Language-Team: Italian\n"
"Language: it_IT\n"
"PO-Revision-Date: 2024-11-15 17:31\n"
"PO-Revision-Date: 2024-11-15 18:59\n"
msgid "OK"
msgstr "OK"
@ -282,15 +282,15 @@ msgstr "Scostamenti Modalità Piastrelle"
#. Found under "Tile Mode Offsets". Basis is a linear algebra term. https://en.wikipedia.org/wiki/Basis_(linear_algebra)
msgid "X-basis:"
msgstr ""
msgstr "Base X:"
#. Found under "Tile Mode Offsets". Basis is a linear algebra term. https://en.wikipedia.org/wiki/Basis_(linear_algebra)
msgid "Y-basis:"
msgstr ""
msgstr "Base Y:"
#. Found under "Tile Mode Offsets". It's a button that when pressed, enables masking for tile mode. Masking essentially limits drawing to the visible pixels of the image, thus preventing from drawing on transparent pixels.
msgid "Masking:"
msgstr ""
msgstr "Maschera:"
msgid "Reset"
msgstr "Azzera"

View file

@ -10,7 +10,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
"PO-Revision-Date: 2024-11-15 17:30\n"
"PO-Revision-Date: 2024-11-15 18:59\n"
msgid "OK"
msgstr "OK"
@ -246,7 +246,7 @@ msgstr ""
#. Refers to a diamond-like shape.
msgid "Diamond"
msgstr ""
msgstr "Ромб"
msgid "Circle"
msgstr "Круг"

View file

@ -10,7 +10,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
"PO-Revision-Date: 2024-11-15 17:31\n"
"PO-Revision-Date: 2024-11-15 18:59\n"
msgid "OK"
msgstr "Tamam"
@ -282,15 +282,15 @@ msgstr "Döşeme Kipi Uzaklıkları"
#. Found under "Tile Mode Offsets". Basis is a linear algebra term. https://en.wikipedia.org/wiki/Basis_(linear_algebra)
msgid "X-basis:"
msgstr ""
msgstr "X-tabanlı:"
#. Found under "Tile Mode Offsets". Basis is a linear algebra term. https://en.wikipedia.org/wiki/Basis_(linear_algebra)
msgid "Y-basis:"
msgstr ""
msgstr "Y-tabanlı:"
#. Found under "Tile Mode Offsets". It's a button that when pressed, enables masking for tile mode. Masking essentially limits drawing to the visible pixels of the image, thus preventing from drawing on transparent pixels.
msgid "Masking:"
msgstr ""
msgstr "Maskeleme:"
msgid "Reset"
msgstr "Sıfırla"

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d267xalp3p7ru"
path="res://.godot/imported/check_plain.png-6f37534ee70be1593b3b1be7b4c80f23.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/misc/check_plain.png"
dest_files=["res://.godot/imported/check_plain.png-6f37534ee70be1593b3b1be7b4c80f23.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bnc78807k1xjv"
path="res://.godot/imported/close.png-5725622e3d74d3527ee26e70390098f4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/misc/close.png"
dest_files=["res://.godot/imported/close.png-5725622e3d74d3527ee26e70390098f4.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -83,8 +83,8 @@ application/modify_resources=true
application/icon="res://assets/graphics/icons/icon.ico"
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version="1.0.4.0"
application/product_version="1.0.4.0"
application/file_version="1.0.5.0"
application/product_version="1.0.5.0"
application/company_name="Orama Interactive"
application/product_name="Pixelorama"
application/file_description="Pixelorama - Your free & open-source sprite editor"
@ -198,8 +198,8 @@ application/modify_resources=true
application/icon="res://assets/graphics/icons/icon.ico"
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version="1.0.4.0"
application/product_version="1.0.4.0"
application/file_version="1.0.5.0"
application/product_version="1.0.5.0"
application/company_name="Orama Interactive"
application/product_name="Pixelorama"
application/file_description="Pixelorama - Your free & open-source sprite editor"
@ -402,8 +402,8 @@ application/icon_interpolation=4
application/bundle_identifier="com.orama-interactive.pixelorama"
application/signature=""
application/app_category="Graphics-design"
application/short_version="1.0.4"
application/version="1.0.4"
application/short_version="1.0.5"
application/version="1.0.5"
application/copyright="Orama Interactive and contributors 2019-present"
application/copyright_localized={}
application/min_macos_version="10.12"
@ -657,7 +657,7 @@ architectures/arm64-v8a=true
architectures/x86=false
architectures/x86_64=false
version/code=1
version/name="1.0.4"
version/name="1.0.5"
package/unique_name="com.orama_interactive.pixelorama"
package/name="Pixelorama"
package/signed=true
@ -749,13 +749,13 @@ permissions/install_location_provider=false
permissions/install_packages=false
permissions/install_shortcut=false
permissions/internal_system_window=false
permissions/internet=false
permissions/internet=true
permissions/kill_background_processes=false
permissions/location_hardware=false
permissions/manage_accounts=false
permissions/manage_app_tokens=false
permissions/manage_documents=false
permissions/manage_external_storage=false
permissions/manage_external_storage=true
permissions/master_clear=false
permissions/media_content_control=false
permissions/modify_audio_settings=false

View file

@ -12,7 +12,7 @@ config_version=5
config/name="Pixelorama"
config/description="Unleash your creativity with Pixelorama, a powerful and accessible open-source pixel art multitool. Whether you want to create sprites, tiles, animations, or just express yourself in the language of pixel art, this software will realize your pixel-perfect dreams with a vast toolbox of features."
config/version="v1.0.4-stable"
config/version="v1.0.5-dev"
run/main_scene="res://src/Main.tscn"
config/use_custom_user_dir=true
config/custom_user_dir_name="pixelorama"
@ -266,12 +266,14 @@ quit={
undo={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"command_or_control_autoremap":true,"alt_pressed":false,"shift_pressed":false,"pressed":false,"keycode":90,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":8,"canceled":false,"pressed":false,"double_click":false,"script":null)
]
}
redo={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"command_or_control_autoremap":true,"alt_pressed":false,"shift_pressed":false,"pressed":false,"keycode":89,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"command_or_control_autoremap":true,"alt_pressed":false,"shift_pressed":true,"pressed":false,"keycode":90,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(138, 9),"global_position":Vector2(147, 55),"factor":1.0,"button_index":9,"canceled":false,"pressed":true,"double_click":false,"script":null)
]
}
show_grid={

View file

@ -381,6 +381,10 @@ class PanelAPI:
## Gives access to theme related functions.
class ThemeAPI:
## Returns the Themes autoload. Allows interacting with themes on a more deeper level.
func autoload() -> Themes:
return Themes
## Adds the [param theme] to [code]Edit -> Preferences -> Interface -> Themes[/code].
func add_theme(theme: Theme) -> void:
Themes.add_theme(theme)
@ -438,6 +442,10 @@ class ToolAPI:
# gdlint: ignore=constant-name
const LayerTypes := Global.LayerTypes
## Returns the Tools autoload. Allows interacting with tools on a more deeper level.
func autoload() -> Tools:
return Tools
## Adds a tool to pixelorama with name [param tool_name] (without spaces),
## display name [param display_name], tool scene [param scene], layers that the tool works
## on [param layer_types] defined by [constant LayerTypes],
@ -526,6 +534,10 @@ class SelectionAPI:
Global.canvas.selection.move_borders_start()
else:
Global.canvas.selection.transform_content_start()
if Global.canvas.selection.original_bitmap.is_empty(): # To avoid copying twice.
Global.canvas.selection.original_bitmap.copy_from(Global.current_project.selection_map)
Global.canvas.selection.big_bounding_rectangle.size = new_size
Global.canvas.selection.resize_selection()
Global.canvas.selection.move_borders_end()
@ -678,6 +690,11 @@ class ExportAPI:
# gdlint: ignore=constant-name
const ExportTab := Export.ExportTab
## Returns the Export autoload.
## Allows interacting with the export workflow on a more deeper level.
func autoload() -> Export:
return Export
## [param format_info] has keys: [code]extension[/code] and [code]description[/code]
## whose values are of type [String] e.g:[codeblock]
## format_info = {"extension": ".gif", "description": "GIF Image"}
@ -730,6 +747,15 @@ class ExportAPI:
## Gives access to adding custom import options.
class ImportAPI:
## Returns the OpenSave autoload. Contains code to handle file loading.
## It also contains code to handle project saving (.pxo)
func open_save_autoload() -> OpenSave:
return OpenSave
## Returns the Import autoload. Manages import of brushes and patterns.
func import_autoload() -> Import:
return Import
## [param import_scene] is a scene preload that will be instanced and added to "import options"
## section of pixelorama's import dialogs and will appear whenever [param import_name] is
## chosen from import menu.
@ -757,6 +783,10 @@ class ImportAPI:
## Gives access to palette related stuff.
class PaletteAPI:
## Returns the Palettes autoload. Allows interacting with palettes on a more deeper level.
func autoload() -> Palettes:
return Palettes
## Creates and adds a new [Palette] with name [param palette_name] containing [param data].
## [param data] is a [Dictionary] containing the palette information.
## An example of [code]data[/code] will be:[codeblock]

View file

@ -71,7 +71,7 @@ enum EffectsMenu {
SHADER
}
## Enumeration of items present in the Select Menu.
enum SelectMenu { SELECT_ALL, CLEAR_SELECTION, INVERT, TILE_MODE }
enum SelectMenu { SELECT_ALL, CLEAR_SELECTION, INVERT, TILE_MODE, MODIFY }
## Enumeration of items present in the Help Menu.
enum HelpMenu {
VIEW_SPLASH_SCREEN,

View file

@ -4,7 +4,7 @@ signal palette_selected(palette_name: String)
signal new_palette_created
signal new_palette_imported
enum SortOptions { NEW_PALETTE, REVERSE, HUE, SATURATION, VALUE, RED, GREEN, BLUE, ALPHA }
enum SortOptions {NEW_PALETTE, REVERSE, HUE, SATURATION, VALUE, LIGHTNESS, RED, GREEN, BLUE, ALPHA}
## Presets for creating a new palette
enum NewPalettePresetType {EMPTY, FROM_CURRENT_PALETTE, FROM_CURRENT_SPRITE, FROM_CURRENT_SELECTION}
## Color options when user creates a new palette from current sprite or selection

View file

@ -3,6 +3,7 @@ class_name Project
extends RefCounted
## A class for project properties.
signal removed
signal serialized(dict: Dictionary)
signal about_to_deserialize(dict: Dictionary)
signal resized
@ -137,6 +138,7 @@ func remove() -> void:
# Prevents memory leak (due to the layers' project reference stopping ref counting from freeing)
layers.clear()
Global.projects.erase(self)
removed.emit()
func remove_backup_file() -> void:

View file

@ -1,7 +1,8 @@
class_name SelectionMap
extends Image
var invert_shader := preload("res://src/Shaders/Effects/Invert.gdshader")
const INVERT_SHADER := preload("res://src/Shaders/Effects/Invert.gdshader")
const OUTLINE_INLINE_SHADER := preload("res://src/Shaders/Effects/OutlineInline.gdshader")
func is_pixel_selected(pixel: Vector2i, calculate_offset := true) -> bool:
@ -87,8 +88,7 @@ func clear() -> void:
func invert() -> void:
var params := {"red": true, "green": true, "blue": true, "alpha": true}
var gen := ShaderImageEffect.new()
gen.generate_image(self, invert_shader, params, get_size())
self.convert(Image.FORMAT_LA8)
gen.generate_image(self, INVERT_SHADER, params, get_size())
## Returns a copy of itself that is cropped to [param size].
@ -183,3 +183,36 @@ func resize_bitmap_values(
if new_bitmap_size != size:
crop(new_bitmap_size.x, new_bitmap_size.y)
blit_rect(smaller_image, Rect2i(Vector2i.ZERO, new_bitmap_size), dst)
func expand(width: int, brush: int) -> void:
var params := {
"color": Color(1, 1, 1, 1),
"width": width,
"brush": brush,
}
var gen := ShaderImageEffect.new()
gen.generate_image(self, OUTLINE_INLINE_SHADER, params, get_size())
func shrink(width: int, brush: int) -> void:
var params := {
"color": Color(0),
"width": width,
"brush": brush,
"inside": true,
}
var gen := ShaderImageEffect.new()
gen.generate_image(self, OUTLINE_INLINE_SHADER, params, get_size())
func border(width: int, brush: int) -> void:
var params := {
"color": Color(1, 1, 1, 1),
"width": width,
"brush": brush,
"inside": true,
"keep_border_only": true,
}
var gen := ShaderImageEffect.new()
gen.generate_image(self, OUTLINE_INLINE_SHADER, params, get_size())

View file

@ -54,7 +54,7 @@ func generate_image(img: Image, shader: Shader, params: Dictionary, size: Vector
RenderingServer.free_rid(ci_rid)
RenderingServer.free_rid(mat_rid)
RenderingServer.free_rid(texture)
viewport_texture.convert(Image.FORMAT_RGBA8)
viewport_texture.convert(img.get_format())
img.copy_from(viewport_texture)
if resized_width:
img.crop(img.get_width() - 1, img.get_height())

View file

@ -148,13 +148,13 @@ static func create_ui_for_shader_uniforms(
if u_value != "":
slider.value = int(u_value)
slider.min_value = min_value
slider.max_value = max_value
slider.step = step
if params.has(u_name):
slider.value = params[u_name]
else:
params[u_name] = slider.value
slider.min_value = min_value
slider.max_value = max_value
slider.step = step
slider.value_changed.connect(value_changed.bind(u_name))
slider.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
hbox.add_child(slider)

View file

@ -250,16 +250,10 @@ func _handle_layout_files() -> void:
func _setup_application_window_size() -> void:
if DisplayServer.get_name() == "headless":
return
var root := get_tree().root
root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_IGNORE
root.content_scale_mode = Window.CONTENT_SCALE_MODE_DISABLED
# Set a minimum window size to prevent UI elements from collapsing on each other.
root.min_size = Vector2(1024, 576)
root.content_scale_factor = Global.shrink
set_display_scale()
if Global.font_size != theme.default_font_size:
theme.default_font_size = Global.font_size
theme.set_font_size("font_size", "HeaderSmall", Global.font_size + 2)
set_custom_cursor()
if OS.get_name() == "Web":
return
@ -280,6 +274,16 @@ func _setup_application_window_size() -> void:
get_window().size = Global.config_cache.get_value("window", "size")
func set_display_scale() -> void:
var root := get_window()
root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_IGNORE
root.content_scale_mode = Window.CONTENT_SCALE_MODE_DISABLED
# Set a minimum window size to prevent UI elements from collapsing on each other.
root.min_size = Vector2(1024, 576)
root.content_scale_factor = Global.shrink
set_custom_cursor()
func set_custom_cursor() -> void:
if Global.native_cursors:
return

View file

@ -287,6 +287,38 @@ func sort(option: Palettes.SortOptions) -> void:
sort_method = func(a: PaletteColor, b: PaletteColor): return a.color.s < b.color.s
Palettes.SortOptions.VALUE:
sort_method = func(a: PaletteColor, b: PaletteColor): return a.color.v < b.color.v
Palettes.SortOptions.LIGHTNESS:
# Code inspired from:
# gdlint: ignore=max-line-length
# https://github.com/bottosson/bottosson.github.io/blob/master/misc/colorpicker/colorconversion.js#L519
sort_method = func(a: PaletteColor, b: PaletteColor):
# function that returns OKHSL lightness
var lum: Callable = func(c: Color):
var l = 0.4122214708 * (c.r) + 0.5363325363 * (c.g) + 0.0514459929 * (c.b)
var m = 0.2119034982 * (c.r) + 0.6806995451 * (c.g) + 0.1073969566 * (c.b)
var s = 0.0883024619 * (c.r) + 0.2817188376 * (c.g) + 0.6299787005 * (c.b)
var l_cr = pow(l, 1 / 3.0)
var m_cr = pow(m, 1 / 3.0)
var s_cr = pow(s, 1 / 3.0)
var oklab_l = 0.2104542553 * l_cr + 0.7936177850 * m_cr - 0.0040720468 * s_cr
# calculating toe
var k_1 = 0.206
var k_2 = 0.03
var k_3 = (1 + k_1) / (1 + k_2)
return (
0.5
* (
k_3 * oklab_l
- k_1
+ sqrt(
(
(k_3 * oklab_l - k_1) * (k_3 * oklab_l - k_1)
+ 4 * k_2 * k_3 * oklab_l
)
)
)
)
return lum.call(a.color.srgb_to_linear()) < lum.call(b.color.srgb_to_linear())
Palettes.SortOptions.RED:
sort_method = func(a: PaletteColor, b: PaletteColor): return a.color.r < b.color.r
Palettes.SortOptions.GREEN:

View file

@ -49,6 +49,8 @@ func _ready() -> void:
sort_button_popup.add_item("Sort by saturation", Palettes.SortOptions.SATURATION)
sort_button_popup.add_item("Sort by value", Palettes.SortOptions.VALUE)
sort_button_popup.add_separator()
sort_button_popup.add_item("Sort by lightness", Palettes.SortOptions.LIGHTNESS)
sort_button_popup.add_separator()
sort_button_popup.add_item("Sort by red", Palettes.SortOptions.RED)
sort_button_popup.add_item("Sort by green", Palettes.SortOptions.GREEN)
sort_button_popup.add_item("Sort by blue", Palettes.SortOptions.BLUE)

View file

@ -90,3 +90,4 @@ func _on_PaletteScroll_gui_input(event: InputEvent) -> void:
return
resize_grid()
set_sliders(palette_grid.current_palette, palette_grid.grid_window_origin + scroll_vector)
get_window().set_input_as_handled()

View file

@ -413,12 +413,7 @@ func _on_List_item_selected(index: int) -> void:
func _on_shrink_apply_button_pressed() -> void:
var root := get_tree().root
root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_IGNORE
root.content_scale_mode = Window.CONTENT_SCALE_MODE_DISABLED
root.min_size = Vector2(1024, 576)
root.content_scale_factor = Global.shrink
Global.control.set_custom_cursor()
Global.control.set_display_scale()
hide()
popup_centered(Vector2(600, 400))
Global.dialog_open(true)

View file

@ -4,9 +4,10 @@ render_mode unshaded;
uniform vec4 color : source_color = vec4(1.0);
uniform float width : hint_range(0, 10, 1) = 1.0;
// uniform_data pattern type:: OptionButton [Diamond||Circle||Square]
uniform int pattern : hint_range(0, 2) = 0;
// uniform_data brush type:: OptionButton [Diamond||Circle||Square]
uniform int brush : hint_range(0, 2) = 0;
uniform bool inside = false;
uniform bool keep_border_only = false;
uniform sampler2D selection : filter_nearest;
bool is_zero_approx(float num) {
@ -17,11 +18,11 @@ bool has_contrary_neighbour(vec2 uv, vec2 texture_pixel_size, sampler2D tex) {
for (float i = -ceil(width); i <= ceil(width); i++) {
float offset;
if (pattern == 0) {
if (brush == 0) {
offset = width - abs(i);
} else if (pattern == 1) {
} else if (brush == 1) {
offset = floor(sqrt(pow(width + 0.5, 2) - i * i));
} else if (pattern == 2) {
} else if (brush == 2) {
offset = width;
}
@ -44,7 +45,15 @@ void fragment() {
if ((output.a > 0.0) == inside && has_contrary_neighbour(UV, TEXTURE_PIXEL_SIZE, TEXTURE)) {
output.rgb = inside ? mix(output.rgb, color.rgb, color.a) : color.rgb;
output.a += (1.0 - output.a) * color.a;
if (is_zero_approx(color.a)) {
output.a = color.a;
}
else {
output.a += (1.0 - output.a) * color.a;
}
}
else if (keep_border_only) {
output.a = 0.0;
}
COLOR = mix(original_color, output, selection_color.a);

View file

@ -1,8 +1,11 @@
extends BaseTool
const IMAGE_BRUSHES := [Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM]
var _brush := Brushes.get_default_brush()
var _brush_size := 1
var _brush_size_dynamics := 1
var _brush_density := 100
var _brush_flip_x := false
var _brush_flip_y := false
var _brush_rotate_90 := false
@ -89,6 +92,12 @@ func _reset_dynamics() -> void:
save_config()
func _on_density_value_slider_value_changed(value: int) -> void:
_brush_density = value
update_config()
save_config()
func _on_InterpolateFactor_value_changed(value: float) -> void:
_brush_interpolate = int(value)
update_config()
@ -111,6 +120,7 @@ func get_config() -> Dictionary:
"brush_type": _brush.type,
"brush_index": _brush.index,
"brush_size": _brush_size,
"brush_density": _brush_density,
"brush_interpolate": _brush_interpolate,
"brush_flip_x": _brush_flip_x,
"brush_flip_y": _brush_flip_y,
@ -128,6 +138,7 @@ func set_config(config: Dictionary) -> void:
_brush_size_dynamics = _brush_size
if Tools.dynamics_size != Tools.Dynamics.NONE:
_brush_size_dynamics = Tools.brush_size_min
_brush_density = config.get("brush_density", _brush_density)
_brush_interpolate = config.get("brush_interpolate", _brush_interpolate)
_brush_flip_x = config.get("brush_flip_x", _brush_flip_x)
_brush_flip_y = config.get("brush_flip_y", _brush_flip_y)
@ -177,11 +188,13 @@ func update_brush() -> void:
_brush_texture = ImageTexture.create_from_image(_brush_image)
update_mirror_brush()
_stroke_dimensions = _brush_image.get_size()
_circle_tool_shortcut = []
_indicator = _create_brush_indicator()
_polylines = _create_polylines(_indicator)
$Brush/Type/Texture.texture = _brush_texture
$ColorInterpolation.visible = _brush.type in [Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM]
$RotationOptions.visible = _brush.type in [Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM]
$DensityValueSlider.visible = _brush.type not in IMAGE_BRUSHES
$ColorInterpolation.visible = _brush.type in IMAGE_BRUSHES
$RotationOptions.visible = _brush.type in IMAGE_BRUSHES
func update_random_image() -> void:
@ -273,6 +286,9 @@ func draw_tool(pos: Vector2i) -> void:
func draw_end(pos: Vector2i) -> void:
super.draw_end(pos)
_stroke_project = null
_stroke_images = []
_circle_tool_shortcut = []
_brush_size_dynamics = _brush_size
if Tools.dynamics_size != Tools.Dynamics.NONE:
_brush_size_dynamics = Tools.brush_size_min
@ -311,10 +327,6 @@ func _prepare_tool() -> void:
# This may prevent a few tests when setting pixels
_is_mask_size_zero = _mask.size() == 0
match _brush.type:
Brushes.CIRCLE:
_prepare_circle_tool(false)
Brushes.FILLED_CIRCLE:
_prepare_circle_tool(true)
Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM:
# save _brush_image for safe keeping
_brush_image = _create_blended_brush_image(_orignal_brush_image)
@ -324,19 +336,6 @@ func _prepare_tool() -> void:
_stroke_dimensions = _brush_image.get_size()
func _prepare_circle_tool(fill: bool) -> void:
var circle_tool_map := _create_circle_indicator(_brush_size_dynamics, fill)
# Go through that BitMap and build an Array of the "displacement" from the center of the bits
# that are true.
var diameter := _brush_size_dynamics * 2 + 1
for n in range(0, diameter):
for m in range(0, diameter):
if circle_tool_map.get_bitv(Vector2i(m, n)):
_circle_tool_shortcut.append(
Vector2i(m - _brush_size_dynamics, n - _brush_size_dynamics)
)
## Make sure to always have invoked _prepare_tool() before this. This computes the coordinates to be
## drawn if it can (except for the generic brush, when it's actually drawing them)
func _draw_tool(pos: Vector2) -> PackedVector2Array:
@ -505,7 +504,7 @@ func draw_indicator(left: bool) -> void:
func draw_indicator_at(pos: Vector2i, offset: Vector2i, color: Color) -> void:
var canvas: Node2D = Global.canvas.indicators
if _brush.type in [Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM] and not _draw_line:
if _brush.type in IMAGE_BRUSHES and not _draw_line:
pos -= _brush_image.get_size() / 2
pos -= offset
canvas.draw_texture(_brush_texture, pos)
@ -535,6 +534,8 @@ func _set_pixel(pos: Vector2i, ignore_mirroring := false) -> void:
func _set_pixel_no_cache(pos: Vector2i, ignore_mirroring := false) -> void:
if randi() % 100 >= _brush_density:
return
pos = _stroke_project.tiles.get_canon_position(pos)
if Global.current_project.has_selection:
pos = Global.current_project.selection_map.get_canon_position(pos)
@ -619,10 +620,24 @@ func _create_pixel_indicator(brush_size: int) -> BitMap:
func _create_circle_indicator(brush_size: int, fill := false) -> BitMap:
_circle_tool_shortcut = []
if Tools.dynamics_size != Tools.Dynamics.NONE:
_circle_tool_shortcut = []
var brush_size_v2 := Vector2i(brush_size, brush_size)
var diameter := brush_size_v2 * 2 + Vector2i.ONE
return _fill_bitmap_with_points(_compute_draw_tool_circle(brush_size_v2, fill), diameter)
var diameter_v2 := brush_size_v2 * 2 + Vector2i.ONE
var circle_tool_map := _fill_bitmap_with_points(
_compute_draw_tool_circle(brush_size_v2, fill), diameter_v2
)
if _circle_tool_shortcut.is_empty():
# Go through that BitMap and build an Array of the "displacement"
# from the center of the bits that are true.
var diameter := _brush_size_dynamics * 2 + 1
for n in range(0, diameter):
for m in range(0, diameter):
if circle_tool_map.get_bitv(Vector2i(m, n)):
_circle_tool_shortcut.append(
Vector2i(m - _brush_size_dynamics, n - _brush_size_dynamics)
)
return circle_tool_map
func _create_line_indicator(indicator: BitMap, start: Vector2i, end: Vector2i) -> BitMap:

View file

@ -1,9 +1,10 @@
[gd_scene load_steps=8 format=3 uid="uid://ubyatap3sylf"]
[gd_scene load_steps=9 format=3 uid="uid://ubyatap3sylf"]
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/ValueSlider.tscn" id="1"]
[ext_resource type="PackedScene" uid="uid://ctfgfelg0sho8" path="res://src/Tools/BaseTool.tscn" id="2"]
[ext_resource type="Script" path="res://src/Tools/BaseDraw.gd" id="3"]
[ext_resource type="Script" path="res://src/UI/Nodes/CollapsibleContainer.gd" id="3_76bek"]
[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="5_kdxku"]
[sub_resource type="ButtonGroup" id="ButtonGroup_7u3x0"]
resource_name = "rotate"
@ -130,7 +131,25 @@ suffix = "px"
global_increment_action = "brush_size_increment"
global_decrement_action = "brush_size_decrement"
[node name="ColorInterpolation" parent="." index="4" instance=ExtResource("1")]
[node name="DensityValueSlider" type="TextureProgressBar" parent="." index="4"]
visible = false
custom_minimum_size = Vector2(0, 24)
layout_mode = 2
focus_mode = 2
mouse_default_cursor_shape = 2
theme_type_variation = &"ValueSlider"
min_value = 1.0
value = 100.0
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource("5_kdxku")
prefix = "Density:"
suffix = "%"
[node name="ColorInterpolation" parent="." index="5" instance=ExtResource("1")]
visible = false
layout_mode = 2
tooltip_text = "0: Color from the brush itself, 100: the currently selected color"
@ -143,4 +162,5 @@ prefix = "Brush color from:"
[connection signal="toggled" from="RotationOptions/GridContainer/Rotate/Rotate270" to="." method="_on_rotate_270_toggled"]
[connection signal="pressed" from="Brush/Type" to="." method="_on_BrushType_pressed"]
[connection signal="value_changed" from="Brush/BrushSize" to="." method="_on_BrushSize_value_changed"]
[connection signal="value_changed" from="DensityValueSlider" to="." method="_on_density_value_slider_value_changed"]
[connection signal="value_changed" from="ColorInterpolation" to="." method="_on_InterpolateFactor_value_changed"]

View file

@ -22,7 +22,8 @@ var _intersect := false ## Shift + Ctrl + Mouse Click
var _content_transformation_check := false
var _skip_slider_logic := false
@onready var selection_node: Node2D = Global.canvas.selection
@onready var selection_node := Global.canvas.selection
@onready var confirm_buttons := $ConfirmButtons as HBoxContainer
@onready var position_sliders := $Position as ValueSliderV2
@onready var size_sliders := $Size as ValueSliderV2
@onready var timer := $Timer as Timer
@ -30,11 +31,17 @@ var _skip_slider_logic := false
func _ready() -> void:
super._ready()
set_confirm_buttons_visibility()
set_spinbox_values()
refresh_options()
selection_node.is_moving_content_changed.connect(set_confirm_buttons_visibility)
## Ensure all items are added when we are selecting an option (bad things will happen otherwise)
func set_confirm_buttons_visibility() -> void:
confirm_buttons.visible = selection_node.is_moving_content
## Ensure all items are added when we are selecting an option.
func refresh_options() -> void:
$Modes.clear()
$Modes.add_item("Replace selection")
@ -203,6 +210,16 @@ func apply_selection(_position: Vector2i) -> void:
_intersect = true
func _on_confirm_button_pressed() -> void:
if selection_node.is_moving_content:
selection_node.transform_content_confirm()
func _on_cancel_button_pressed() -> void:
if selection_node.is_moving_content:
selection_node.transform_content_cancel()
func _on_Modes_item_selected(index: int) -> void:
_mode_selected = index
save_config()

View file

@ -1,34 +1,85 @@
[gd_scene load_steps=4 format=3 uid="uid://bd62qfjn380wf"]
[gd_scene load_steps=10 format=3 uid="uid://bd62qfjn380wf"]
[ext_resource type="PackedScene" uid="uid://ctfgfelg0sho8" path="res://src/Tools/BaseTool.tscn" id="1"]
[ext_resource type="Script" path="res://src/Tools/BaseSelectionTool.gd" id="2"]
[ext_resource type="Texture2D" uid="uid://d267xalp3p7ru" path="res://assets/graphics/misc/check_plain.png" id="3_mtv71"]
[ext_resource type="PackedScene" path="res://src/UI/Nodes/ValueSliderV2.tscn" id="4"]
[ext_resource type="Texture2D" uid="uid://bnc78807k1xjv" path="res://assets/graphics/misc/close.png" id="4_ad04n"]
[sub_resource type="InputEventAction" id="InputEventAction_gfv4x"]
action = &"transformation_confirm"
[sub_resource type="Shortcut" id="Shortcut_5gq73"]
events = [SubResource("InputEventAction_gfv4x")]
[sub_resource type="InputEventAction" id="InputEventAction_nadbx"]
action = &"transformation_cancel"
[sub_resource type="Shortcut" id="Shortcut_04tjd"]
events = [SubResource("InputEventAction_nadbx")]
[node name="ToolOptions" instance=ExtResource("1")]
script = ExtResource("2")
[node name="ModeLabel" type="Label" parent="." index="2"]
[node name="ConfirmButtons" type="HBoxContainer" parent="." index="2"]
layout_mode = 2
[node name="ConfirmButton" type="Button" parent="ConfirmButtons" index="0"]
custom_minimum_size = Vector2(0, 26)
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_5gq73")
[node name="TextureRect" type="TextureRect" parent="ConfirmButtons/ConfirmButton" index="0" groups=["UIButtons"]]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("3_mtv71")
stretch_mode = 3
[node name="CancelButton" type="Button" parent="ConfirmButtons" index="1"]
custom_minimum_size = Vector2(0, 26)
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_04tjd")
[node name="TextureRect" type="TextureRect" parent="ConfirmButtons/CancelButton" index="0" groups=["UIButtons"]]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("4_ad04n")
stretch_mode = 3
[node name="ModeLabel" type="Label" parent="." index="3"]
layout_mode = 2
text = "Mode:"
[node name="Modes" type="OptionButton" parent="." index="3"]
[node name="Modes" type="OptionButton" parent="." index="4"]
layout_mode = 2
mouse_default_cursor_shape = 2
[node name="PositionLabel" type="Label" parent="." index="4"]
[node name="PositionLabel" type="Label" parent="." index="5"]
layout_mode = 2
text = "Position:"
[node name="Position" parent="." index="5" instance=ExtResource("4")]
[node name="Position" parent="." index="6" instance=ExtResource("4")]
layout_mode = 2
allow_greater = true
allow_lesser = true
[node name="SizeLabel" type="Label" parent="." index="6"]
[node name="SizeLabel" type="Label" parent="." index="7"]
layout_mode = 2
text = "Size:"
[node name="Size" parent="." index="7" instance=ExtResource("4")]
[node name="Size" parent="." index="8" instance=ExtResource("4")]
layout_mode = 2
value = Vector2(1, 1)
min_value = Vector2(1, 1)
@ -37,10 +88,12 @@ show_ratio = true
prefix_x = "Width:"
prefix_y = "Height:"
[node name="Timer" type="Timer" parent="." index="8"]
[node name="Timer" type="Timer" parent="." index="9"]
wait_time = 0.2
one_shot = true
[connection signal="pressed" from="ConfirmButtons/ConfirmButton" to="." method="_on_confirm_button_pressed"]
[connection signal="pressed" from="ConfirmButtons/CancelButton" to="." method="_on_cancel_button_pressed"]
[connection signal="item_selected" from="Modes" to="." method="_on_Modes_item_selected"]
[connection signal="value_changed" from="Position" to="." method="_on_Position_value_changed"]
[connection signal="ratio_toggled" from="Size" to="." method="_on_Size_ratio_toggled"]

View file

@ -92,8 +92,8 @@ func draw_move(pos_i: Vector2i) -> void:
func draw_end(pos: Vector2i) -> void:
pos = snap_position(pos)
super.draw_end(pos)
if _picking_color:
super.draw_end(pos)
return
if _draw_line:
@ -105,6 +105,7 @@ func draw_end(pos: Vector2i) -> void:
draw_fill_gap(_line_start, _line_end)
_draw_line = false
super.draw_end(pos)
commit_undo()
SteamManager.set_achievement("ACH_ERASE_PIXEL")
cursor_text = ""

View file

@ -164,8 +164,8 @@ func draw_move(pos_i: Vector2i) -> void:
func draw_end(pos: Vector2i) -> void:
pos = snap_position(pos)
super.draw_end(pos)
if _picking_color:
super.draw_end(pos)
return
if _draw_line:
@ -194,6 +194,7 @@ func draw_end(pos: Vector2i) -> void:
draw_tool(v)
_fill_inside_rect = Rect2i()
super.draw_end(pos)
commit_undo()
cursor_text = ""
update_random_image()

View file

@ -291,8 +291,8 @@ func draw_move(pos_i: Vector2i) -> void:
func draw_end(pos: Vector2i) -> void:
pos = snap_position(pos)
super.draw_end(pos)
if _picking_color:
super.draw_end(pos)
return
if _draw_line:
@ -304,6 +304,7 @@ func draw_end(pos: Vector2i) -> void:
draw_fill_gap(_line_start, _line_end)
_draw_line = false
super.draw_end(pos)
commit_undo()
cursor_text = ""
update_random_image()

View file

@ -17,7 +17,7 @@ var layer_metadata_texture := ImageTexture.new()
@onready var tile_mode := $TileMode as Node2D
@onready var pixel_grid := $PixelGrid as Node2D
@onready var grid := $Grid as Node2D
@onready var selection := $Selection as Node2D
@onready var selection := $Selection as SelectionNode
@onready var onion_past := $OnionPast as Node2D
@onready var onion_future := $OnionFuture as Node2D
@onready var crop_rect := $CropRect as CropRect

View file

@ -1,5 +1,8 @@
class_name SelectionNode
extends Node2D
signal is_moving_content_changed
enum SelectionOperation { ADD, SUBTRACT, INTERSECT }
const KEY_MOVE_ACTION_NAMES: PackedStringArray = [&"ui_up", &"ui_down", &"ui_left", &"ui_right"]
const CLIPBOARD_FILE_PATH := "user://clipboard.txt"
@ -7,7 +10,10 @@ const CLIPBOARD_FILE_PATH := "user://clipboard.txt"
# flags (additional properties of selection that can be toggled)
var flag_tilemode := false
var is_moving_content := false
var is_moving_content := false:
set(value):
is_moving_content = value
is_moving_content_changed.emit()
var arrow_key_move := false
var is_pasting := false
var big_bounding_rectangle := Rect2i():
@ -101,11 +107,10 @@ func _input(event: InputEvent) -> void:
if Global.mirror_view:
image_current_pixel.x = Global.current_project.size.x - image_current_pixel.x
if is_moving_content:
if Input.is_action_just_pressed("transformation_confirm"):
if Input.is_action_just_pressed(&"transformation_confirm"):
transform_content_confirm()
elif Input.is_action_just_pressed("transformation_cancel"):
elif Input.is_action_just_pressed(&"transformation_cancel"):
transform_content_cancel()
if not project.layers[project.current_layer].can_layer_get_drawn():
return
if event is InputEventKey:

View file

@ -3,7 +3,7 @@ extends Node2D
var tiles: Tiles
var draw_center := false
@onready var canvas := get_parent() as Canvas
@onready var canvas := Global.canvas
func _draw() -> void:

View file

@ -31,7 +31,7 @@ func commit_action(cel: Image, project := Global.current_project) -> void:
var params := {
"color": color,
"width": anim_thickness,
"pattern": pattern,
"brush": pattern,
"inside": inside_image,
"selection": selection_tex
}

View file

@ -51,16 +51,15 @@ size_flags_horizontal = 3
[node name="PatternLabel" type="Label" parent="VBoxContainer/OutlineOptions" index="4"]
layout_mode = 2
size_flags_horizontal = 3
text = "Pattern:"
text = "Brush:"
[node name="PatternOptionButton" type="OptionButton" parent="VBoxContainer/OutlineOptions" index="5"]
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
item_count = 3
selected = 0
item_count = 3
popup/item_0/text = "Diamond"
popup/item_0/id = 0
popup/item_1/text = "Circle"
popup/item_1/id = 1
popup/item_2/text = "Square"

View file

@ -0,0 +1,43 @@
extends ConfirmationDialog
enum Types { EXPAND, SHRINK, BORDER }
@export var type := Types.EXPAND:
set(value):
type = value
if type == Types.EXPAND:
title = "Expand Selection"
elif type == Types.SHRINK:
title = "Shrink Selection"
else:
title = "Border Selection"
@onready var width_slider: ValueSlider = $GridContainer/WidthSlider
@onready var brush_option_button: OptionButton = $GridContainer/BrushOptionButton
@onready var selection_node := Global.canvas.selection
func _on_visibility_changed() -> void:
if not visible:
Global.dialog_open(false)
func _on_confirmed() -> void:
var project := Global.current_project
if !project.has_selection:
return
selection_node.transform_content_confirm()
var undo_data_tmp := selection_node.get_undo_data(false)
var width: int = width_slider.value
var brush := brush_option_button.selected
project.selection_map.crop(project.size.x, project.size.y)
if type == Types.EXPAND:
project.selection_map.expand(width, brush)
elif type == Types.SHRINK:
project.selection_map.shrink(width, brush)
else:
project.selection_map.border(width, brush)
selection_node.big_bounding_rectangle = project.selection_map.get_used_rect()
project.selection_offset = Vector2.ZERO
selection_node.commit_undo("Modify Selection", undo_data_tmp)
selection_node.queue_redraw()

View file

@ -0,0 +1,60 @@
[gd_scene load_steps=3 format=3 uid="uid://wcbpnsm7gptu"]
[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="1_3jelw"]
[ext_resource type="Script" path="res://src/UI/Dialogs/ModifySelection.gd" id="1_w6rs7"]
[node name="ModifySelection" type="ConfirmationDialog"]
title = "Expand selection"
position = Vector2i(0, 36)
size = Vector2i(260, 130)
script = ExtResource("1_w6rs7")
[node name="GridContainer" type="GridContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 252.0
offset_bottom = 81.0
columns = 2
[node name="WidthLabel" type="Label" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Width:"
[node name="WidthSlider" type="TextureProgressBar" parent="GridContainer"]
custom_minimum_size = Vector2(0, 24)
layout_mode = 2
size_flags_horizontal = 3
focus_mode = 2
mouse_default_cursor_shape = 2
theme_type_variation = &"ValueSlider"
min_value = 1.0
max_value = 25.0
value = 1.0
allow_greater = true
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource("1_3jelw")
[node name="BrushLabel" type="Label" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Brush:"
[node name="BrushOptionButton" type="OptionButton" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
selected = 0
item_count = 3
popup/item_0/text = "Diamond"
popup/item_1/text = "Circle"
popup/item_1/id = 1
popup/item_2/text = "Square"
popup/item_2/id = 2
[connection signal="confirmed" from="." to="." method="_on_confirmed"]
[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]

View file

@ -1,11 +1,12 @@
extends ConfirmationDialog
@onready var x_basis_x_spinbox: SpinBox = $VBoxContainer/HBoxContainer/OptionsContainer/XBasisX
@onready var x_basis_y_spinbox: SpinBox = $VBoxContainer/HBoxContainer/OptionsContainer/XBasisY
@onready var y_basis_x_spinbox: SpinBox = $VBoxContainer/HBoxContainer/OptionsContainer/YBasisX
@onready var y_basis_y_spinbox: SpinBox = $VBoxContainer/HBoxContainer/OptionsContainer/YBasisY
@onready var x_basis_label: Label = $VBoxContainer/OptionsContainer/XBasisLabel
@onready var x_basis: ValueSliderV2 = $VBoxContainer/OptionsContainer/XBasis
@onready var y_basis_label: Label = $VBoxContainer/OptionsContainer/YBasisLabel
@onready var y_basis: ValueSliderV2 = $VBoxContainer/OptionsContainer/YBasis
@onready var preview_rect: Control = $VBoxContainer/AspectRatioContainer/Preview
@onready var tile_mode: Node2D = $VBoxContainer/AspectRatioContainer/Preview/TileMode
@onready var masking: CheckButton = $VBoxContainer/OptionsContainer/Masking
func _ready() -> void:
@ -37,35 +38,25 @@ func _on_TileModeOffsetsDialog_about_to_show() -> void:
tile_mode.tiles.mode = Global.current_project.tiles.mode
tile_mode.tiles.x_basis = Global.current_project.tiles.x_basis
tile_mode.tiles.y_basis = Global.current_project.tiles.y_basis
x_basis_x_spinbox.value = tile_mode.tiles.x_basis.x
x_basis_y_spinbox.value = tile_mode.tiles.x_basis.y
y_basis_x_spinbox.value = tile_mode.tiles.y_basis.x
y_basis_y_spinbox.value = tile_mode.tiles.y_basis.y
x_basis.value = tile_mode.tiles.x_basis
y_basis.value = tile_mode.tiles.y_basis
_show_options()
if Global.current_project.tiles.mode == Tiles.MODE.X_AXIS:
y_basis_x_spinbox.visible = false
y_basis_y_spinbox.visible = false
$VBoxContainer/HBoxContainer/OptionsContainer/YBasisXLabel.visible = false
$VBoxContainer/HBoxContainer/OptionsContainer/YBasisYLabel.visible = false
y_basis.visible = false
y_basis_label.visible = false
elif Global.current_project.tiles.mode == Tiles.MODE.Y_AXIS:
x_basis_x_spinbox.visible = false
x_basis_y_spinbox.visible = false
$VBoxContainer/HBoxContainer/OptionsContainer/XBasisXLabel.visible = false
$VBoxContainer/HBoxContainer/OptionsContainer/XBasisYLabel.visible = false
x_basis.visible = false
x_basis_label.visible = false
update_preview()
func _show_options() -> void:
x_basis_x_spinbox.visible = true
x_basis_y_spinbox.visible = true
y_basis_x_spinbox.visible = true
y_basis_y_spinbox.visible = true
$VBoxContainer/HBoxContainer/OptionsContainer/YBasisXLabel.visible = true
$VBoxContainer/HBoxContainer/OptionsContainer/YBasisYLabel.visible = true
$VBoxContainer/HBoxContainer/OptionsContainer/XBasisXLabel.visible = true
$VBoxContainer/HBoxContainer/OptionsContainer/XBasisYLabel.visible = true
x_basis.visible = true
y_basis.visible = true
x_basis_label.visible = true
y_basis_label.visible = true
func _on_TileModeOffsetsDialog_confirmed() -> void:
@ -75,23 +66,13 @@ func _on_TileModeOffsetsDialog_confirmed() -> void:
Global.transparent_checker.update_rect()
func _on_XBasisX_value_changed(value: int) -> void:
tile_mode.tiles.x_basis.x = value
func _on_x_basis_value_changed(value: Vector2) -> void:
tile_mode.tiles.x_basis = value
update_preview()
func _on_XBasisY_value_changed(value: int) -> void:
tile_mode.tiles.x_basis.y = value
update_preview()
func _on_YBasisX_value_changed(value: int) -> void:
tile_mode.tiles.y_basis.x = value
update_preview()
func _on_YBasisY_value_changed(value: int) -> void:
tile_mode.tiles.y_basis.y = value
func _on_y_basis_value_changed(value: Vector2) -> void:
tile_mode.tiles.y_basis = value
update_preview()
@ -122,10 +103,17 @@ func _on_TileModeOffsetsDialog_size_changed() -> void:
func _on_Reset_pressed() -> void:
tile_mode.tiles.x_basis = Vector2i(Global.current_project.size.x, 0)
tile_mode.tiles.y_basis = Vector2i(0, Global.current_project.size.y)
x_basis_x_spinbox.value = Global.current_project.size.x
x_basis_y_spinbox.value = 0
y_basis_x_spinbox.value = 0
y_basis_y_spinbox.value = Global.current_project.size.y
x_basis.value = tile_mode.tiles.x_basis
y_basis.value = tile_mode.tiles.y_basis
update_preview()
func _on_isometric_pressed() -> void:
tile_mode.tiles.x_basis = Global.current_project.size / 2
tile_mode.tiles.x_basis.y *= -1
tile_mode.tiles.y_basis = Global.current_project.size / 2
x_basis.value = tile_mode.tiles.x_basis
y_basis.value = tile_mode.tiles.y_basis
update_preview()
@ -138,10 +126,7 @@ func change_mask() -> void:
var tiles_size := tiles.tile_size
var image := Image.create(tiles_size.x, tiles_size.y, false, Image.FORMAT_RGBA8)
DrawingAlgos.blend_layers(image, current_frame)
if (
image.get_used_rect().size == Vector2i.ZERO
or not $VBoxContainer/HBoxContainer/Masking.button_pressed
):
if image.get_used_rect().size == Vector2i.ZERO or not masking.button_pressed:
tiles.reset_mask()
else:
load_mask(image)

View file

@ -1,7 +1,8 @@
[gd_scene load_steps=5 format=3 uid="uid://c0nuukjakmai2"]
[gd_scene load_steps=6 format=3 uid="uid://c0nuukjakmai2"]
[ext_resource type="PackedScene" uid="uid://3pmb60gpst7b" path="res://src/UI/Nodes/TransparentChecker.tscn" id="1"]
[ext_resource type="Script" path="res://src/UI/Canvas/TileMode.gd" id="2"]
[ext_resource type="PackedScene" path="res://src/UI/Nodes/ValueSliderV2.tscn" id="2_ul2eq"]
[ext_resource type="Script" path="res://src/UI/Dialogs/TileModeOffsetsDialog.gd" id="3"]
[sub_resource type="CanvasItemMaterial" id="1"]
@ -10,83 +11,72 @@ blend_mode = 4
[node name="TileModeOffsetsDialog" type="ConfirmationDialog"]
canvas_item_default_texture_filter = 0
title = "Tile Mode Offsets"
position = Vector2i(0, 36)
size = Vector2i(298, 536)
script = ExtResource("3")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 293.0
offset_bottom = 386.0
offset_right = 290.0
offset_bottom = 487.0
[node name="TileModeOffsets" type="Label" parent="VBoxContainer"]
layout_mode = 2
text = "Tile Mode Offsets"
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="OptionsContainer" type="GridContainer" parent="VBoxContainer/HBoxContainer"]
[node name="OptionsContainer" type="GridContainer" parent="VBoxContainer"]
layout_mode = 2
theme_override_constants/h_separation = 2
theme_override_constants/v_separation = 4
columns = 2
[node name="XBasisXLabel" type="Label" parent="VBoxContainer/HBoxContainer/OptionsContainer"]
[node name="XBasisLabel" type="Label" parent="VBoxContainer/OptionsContainer"]
layout_mode = 2
text = "X-basis x:"
size_flags_horizontal = 3
text = "X-basis:"
[node name="XBasisX" type="SpinBox" parent="VBoxContainer/HBoxContainer/OptionsContainer"]
[node name="XBasis" parent="VBoxContainer/OptionsContainer" instance=ExtResource("2_ul2eq")]
layout_mode = 2
mouse_default_cursor_shape = 2
min_value = -16384.0
max_value = 16384.0
suffix = "px"
size_flags_horizontal = 3
allow_greater = true
allow_lesser = true
suffix_x = "px"
suffix_y = "px"
[node name="XBasisYLabel" type="Label" parent="VBoxContainer/HBoxContainer/OptionsContainer"]
[node name="YBasisLabel" type="Label" parent="VBoxContainer/OptionsContainer"]
layout_mode = 2
text = "X-basis y:"
size_flags_horizontal = 3
text = "Y-basis:"
[node name="XBasisY" type="SpinBox" parent="VBoxContainer/HBoxContainer/OptionsContainer"]
[node name="YBasis" parent="VBoxContainer/OptionsContainer" instance=ExtResource("2_ul2eq")]
layout_mode = 2
mouse_default_cursor_shape = 2
min_value = -16384.0
max_value = 16384.0
suffix = "px"
size_flags_horizontal = 3
allow_greater = true
allow_lesser = true
suffix_x = "px"
suffix_y = "px"
[node name="YBasisXLabel" type="Label" parent="VBoxContainer/HBoxContainer/OptionsContainer"]
[node name="MaskingLabel" type="Label" parent="VBoxContainer/OptionsContainer"]
layout_mode = 2
text = "Y-basis x:"
text = "Masking:"
[node name="YBasisX" type="SpinBox" parent="VBoxContainer/HBoxContainer/OptionsContainer"]
layout_mode = 2
mouse_default_cursor_shape = 2
min_value = -16384.0
max_value = 16384.0
suffix = "px"
[node name="YBasisYLabel" type="Label" parent="VBoxContainer/HBoxContainer/OptionsContainer"]
layout_mode = 2
text = "Y-basis y:"
[node name="YBasisY" type="SpinBox" parent="VBoxContainer/HBoxContainer/OptionsContainer"]
layout_mode = 2
mouse_default_cursor_shape = 2
min_value = -16384.0
max_value = 16384.0
suffix = "px"
[node name="Reset" type="Button" parent="VBoxContainer/HBoxContainer/OptionsContainer"]
layout_mode = 2
mouse_default_cursor_shape = 2
text = "Reset"
[node name="VSeparator" type="VSeparator" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="Masking" type="CheckButton" parent="VBoxContainer/HBoxContainer"]
[node name="Masking" type="CheckButton" parent="VBoxContainer/OptionsContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 0
text = "Masking"
[node name="Reset" type="Button" parent="VBoxContainer/OptionsContainer"]
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
text = "Default"
[node name="Isometric" type="Button" parent="VBoxContainer/OptionsContainer"]
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
text = "Isometric"
[node name="AspectRatioContainer" type="AspectRatioContainer" parent="VBoxContainer"]
layout_mode = 2
@ -111,9 +101,8 @@ anchor_bottom = 1.0
[connection signal="confirmed" from="." to="." method="_on_TileModeOffsetsDialog_confirmed"]
[connection signal="size_changed" from="." to="." method="_on_TileModeOffsetsDialog_size_changed"]
[connection signal="visibility_changed" from="." to="." method="_on_TileModeOffsetsDialog_visibility_changed"]
[connection signal="value_changed" from="VBoxContainer/HBoxContainer/OptionsContainer/XBasisX" to="." method="_on_XBasisX_value_changed"]
[connection signal="value_changed" from="VBoxContainer/HBoxContainer/OptionsContainer/XBasisY" to="." method="_on_XBasisY_value_changed"]
[connection signal="value_changed" from="VBoxContainer/HBoxContainer/OptionsContainer/YBasisX" to="." method="_on_YBasisX_value_changed"]
[connection signal="value_changed" from="VBoxContainer/HBoxContainer/OptionsContainer/YBasisY" to="." method="_on_YBasisY_value_changed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/OptionsContainer/Reset" to="." method="_on_Reset_pressed"]
[connection signal="toggled" from="VBoxContainer/HBoxContainer/Masking" to="." method="_on_Masking_toggled"]
[connection signal="value_changed" from="VBoxContainer/OptionsContainer/XBasis" to="." method="_on_x_basis_value_changed"]
[connection signal="value_changed" from="VBoxContainer/OptionsContainer/YBasis" to="." method="_on_y_basis_value_changed"]
[connection signal="toggled" from="VBoxContainer/OptionsContainer/Masking" to="." method="_on_Masking_toggled"]
[connection signal="pressed" from="VBoxContainer/OptionsContainer/Reset" to="." method="_on_Reset_pressed"]
[connection signal="pressed" from="VBoxContainer/OptionsContainer/Isometric" to="." method="_on_isometric_pressed"]

View file

@ -80,15 +80,21 @@ func _notification(what: int) -> void:
_reset_display(false)
func _input(event: InputEvent) -> void:
func _unhandled_input(event: InputEvent) -> void:
if not editable or not is_visible_in_tree():
return
if event.is_action_pressed(global_increment_action, true):
if (
not global_increment_action.is_empty()
and event.is_action_pressed(global_increment_action, true)
):
if snap_by_default:
value += step if event.ctrl_pressed else snap_step
else:
value += snap_step if event.ctrl_pressed else step
elif event.is_action_pressed(global_decrement_action, true):
elif (
not global_decrement_action.is_empty()
and event.is_action_pressed(global_decrement_action, true)
):
if snap_by_default:
value -= step if event.ctrl_pressed else snap_step
else:
@ -108,11 +114,13 @@ func _gui_input(event: InputEvent) -> void:
value += step if event.ctrl_pressed else snap_step
else:
value += snap_step if event.ctrl_pressed else step
get_viewport().set_input_as_handled()
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
if snap_by_default:
value -= step if event.ctrl_pressed else snap_step
else:
value -= snap_step if event.ctrl_pressed else step
get_viewport().set_input_as_handled()
elif state == HELD:
if event.is_action_released("left_mouse"):
state = TYPING

View file

@ -1,17 +1,17 @@
class_name RecorderPanel
extends PanelContainer
signal frame_saved
enum Mode { CANVAS, PIXELORAMA }
var mode := Mode.CANVAS
var chosen_dir := ""
var chosen_dir := "":
set(value):
chosen_dir = value
if chosen_dir.ends_with("/"): # Remove end back-slashes if present
chosen_dir[-1] = ""
var recorded_projects := {} ## [Dictionary] of [Project] and [Recorder].
var save_dir := ""
var project: Project
var cache: Array[Image] = [] ## Images stored during recording
var frame_captured := 0 ## Used to visualize frames captured
var skip_amount := 1 ## Number of "do" actions after which a frame can be captured
var current_frame_no := 0 ## Used to compare with skip_amount to see if it can be captured
var skip_amount := 1 ## Number of "do" actions after which a frame can be captured.
var resize_percent := 100
var _path_dialog: FileDialog:
get:
@ -28,7 +28,6 @@ var _path_dialog: FileDialog:
return _path_dialog
@onready var captured_label := %CapturedLabel as Label
@onready var project_list := $"%TargetProjectOption" as OptionButton
@onready var start_button := $"%Start" as Button
@onready var size_label := $"%Size" as Label
@onready var path_field := $"%Path" as LineEdit
@ -36,125 +35,99 @@ var _path_dialog: FileDialog:
@onready var options_container := %OptionsContainer as VBoxContainer
class Recorder:
var project: Project
var recorder_panel: RecorderPanel
var actions_done := -1
var frames_captured := 0
var save_directory := ""
func _init(_project: Project, _recorder_panel: RecorderPanel) -> void:
project = _project
recorder_panel = _recorder_panel
# Create a new directory based on time
var time_dict := Time.get_time_dict_from_system()
var folder := str(
project.name, time_dict.hour, "_", time_dict.minute, "_", time_dict.second
)
var dir := DirAccess.open(recorder_panel.chosen_dir)
save_directory = recorder_panel.chosen_dir.path_join(folder)
dir.make_dir_recursive(save_directory)
project.removed.connect(recorder_panel.finalize_recording.bind(project))
project.undo_redo.version_changed.connect(capture_frame)
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
# Needed so that the project won't be forever remained in memory because of bind().
project.removed.disconnect(recorder_panel.finalize_recording)
func capture_frame() -> void:
actions_done += 1
if actions_done % recorder_panel.skip_amount != 0:
return
var image: Image
if recorder_panel.mode == RecorderPanel.Mode.PIXELORAMA:
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)
DrawingAlgos.blend_layers(image, frame, Vector2i.ZERO, project)
if recorder_panel.resize_percent != 100:
var resize := recorder_panel.resize_percent / 100
var new_width := image.get_width() * resize
var new_height := image.get_height() * resize
image.resize(new_width, new_height, Image.INTERPOLATE_NEAREST)
var save_file := str(project.name, "_", frames_captured, ".png")
image.save_png(save_directory.path_join(save_file))
frames_captured += 1
recorder_panel.captured_label.text = str("Saved: ", frames_captured)
func _ready() -> void:
refresh_projects_list()
project = Global.current_project
frame_saved.connect(_on_frame_saved)
if OS.get_name() == "Web":
ExtensionsApi.panel.remove_node_from_tab.call_deferred(self)
return
Global.project_switched.connect(_on_project_switched)
# Make a recordings folder if there isn't one
chosen_dir = Global.home_data_directory.path_join("Recordings")
DirAccess.make_dir_recursive_absolute(chosen_dir)
path_field.text = chosen_dir
size_label.text = str("(", project.size.x, "×", project.size.y, ")")
func _on_project_switched() -> void:
if recorded_projects.has(Global.current_project):
initialize_recording()
start_button.set_pressed_no_signal(true)
Global.change_button_texturerect(start_button.get_child(0), "stop.png")
else:
finalize_recording()
start_button.set_pressed_no_signal(false)
Global.change_button_texturerect(start_button.get_child(0), "start.png")
func initialize_recording() -> void:
connect_undo() # connect to detect changes in project
cache.clear() # clear the cache array to store new images
frame_captured = 0
current_frame_no = skip_amount - 1
# disable some options that are not required during recording
project_list.visible = false
captured_label.visible = true
for child in options_container.get_children():
if !child.is_in_group("visible during recording"):
child.visible = false
save_dir = chosen_dir
# Remove end back-slashes if present
if save_dir.ends_with("/"):
save_dir[-1] = ""
# Create a new directory based on time
var time_dict := Time.get_time_dict_from_system()
var folder := str(project.name, time_dict.hour, "_", time_dict.minute, "_", time_dict.second)
var dir := DirAccess.open(save_dir)
save_dir = save_dir.path_join(folder)
dir.make_dir_recursive(save_dir)
capture_frame() # capture first frame
$Timer.start()
func capture_frame() -> void:
current_frame_no += 1
if current_frame_no != skip_amount:
return
current_frame_no = 0
var image: Image
if mode == Mode.PIXELORAMA:
image = get_tree().root.get_viewport().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)
DrawingAlgos.blend_layers(image, frame, Vector2i.ZERO, project)
if mode == Mode.CANVAS:
if resize_percent != 100:
var resize := resize_percent / 100
image.resize(
image.get_width() * resize, image.get_height() * resize, Image.INTERPOLATE_NEAREST
)
cache.append(image)
func _on_Timer_timeout() -> void:
# Saves frames little by little during recording
if cache.size() > 0:
save_frame(cache[0])
cache.remove_at(0)
func save_frame(img: Image) -> void:
var save_file := str(project.name, "_", frame_captured, ".png")
img.save_png(save_dir.path_join(save_file))
frame_saved.emit()
func _on_frame_saved() -> void:
frame_captured += 1
captured_label.text = str("Saved: ", frame_captured)
func finalize_recording() -> void:
$Timer.stop()
for img in cache:
save_frame(img)
cache.clear()
disconnect_undo()
project_list.visible = true
captured_label.visible = false
for child in options_container.get_children():
child.visible = true
if mode == Mode.PIXELORAMA:
size_label.get_parent().visible = false
func disconnect_undo() -> void:
project.undo_redo.version_changed.disconnect(capture_frame)
func connect_undo() -> void:
project.undo_redo.version_changed.connect(capture_frame)
func _on_TargetProjectOption_item_selected(index: int) -> void:
project = Global.projects[index]
func _on_TargetProjectOption_pressed() -> void:
refresh_projects_list()
func refresh_projects_list() -> void:
project_list.clear()
for proj in Global.projects:
project_list.add_item(proj.name)
func finalize_recording(project := Global.current_project) -> void:
if recorded_projects.has(project):
recorded_projects.erase(project)
if project == Global.current_project:
captured_label.visible = false
for child in options_container.get_children():
child.visible = true
if mode == Mode.PIXELORAMA:
size_label.get_parent().visible = false
func _on_Start_toggled(button_pressed: bool) -> void:
if button_pressed:
recorded_projects[Global.current_project] = Recorder.new(Global.current_project, self)
initialize_recording()
Global.change_button_texturerect(start_button.get_child(0), "stop.png")
else:
@ -163,7 +136,8 @@ func _on_Start_toggled(button_pressed: bool) -> void:
func _on_Settings_pressed() -> void:
options_dialog.popup_on_parent(Rect2(position, options_dialog.size))
_on_SpinBox_value_changed(resize_percent)
options_dialog.popup_on_parent(Rect2i(position, options_dialog.size))
func _on_SkipAmount_value_changed(value: float) -> void:
@ -181,7 +155,7 @@ func _on_Mode_toggled(button_pressed: bool) -> void:
func _on_SpinBox_value_changed(value: float) -> void:
resize_percent = value
var new_size: Vector2 = project.size * (resize_percent / 100.0)
var new_size: Vector2 = Global.current_project.size * (resize_percent / 100.0)
size_label.text = str("(", new_size.x, "×", new_size.y, ")")

View file

@ -31,13 +31,6 @@ unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="TargetProjectOption" type="OptionButton" parent="ScrollContainer/CenterContainer/GridContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
tooltip_text = "Choose project"
clip_text = true
[node name="Start" type="Button" parent="ScrollContainer/CenterContainer/GridContainer" groups=["UIButtons"]]
unique_name_in_owner = true
custom_minimum_size = Vector2(32, 32)
@ -143,6 +136,8 @@ text = "Capture frame every"
[node name="SkipAmount" type="SpinBox" parent="OptionsDialog/PanelContainer/OptionsContainer/ActionGap"]
layout_mode = 2
size_flags_horizontal = 3
min_value = 1.0
value = 1.0
suffix = "actions"
[node name="ModeHeader" type="HBoxContainer" parent="OptionsDialog/PanelContainer/OptionsContainer" groups=["visible during recording"]]
@ -223,10 +218,6 @@ editable = false
layout_mode = 2
text = "Choose"
[node name="Timer" type="Timer" parent="."]
[connection signal="item_selected" from="ScrollContainer/CenterContainer/GridContainer/TargetProjectOption" to="." method="_on_TargetProjectOption_item_selected"]
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/TargetProjectOption" to="." method="_on_TargetProjectOption_pressed"]
[connection signal="toggled" from="ScrollContainer/CenterContainer/GridContainer/Start" to="." method="_on_Start_toggled"]
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/Settings" to="." method="_on_Settings_pressed"]
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/OpenFolder" to="." method="_on_open_folder_pressed"]
@ -234,4 +225,3 @@ text = "Choose"
[connection signal="toggled" from="OptionsDialog/PanelContainer/OptionsContainer/ModeType/Mode" to="." method="_on_Mode_toggled"]
[connection signal="value_changed" from="OptionsDialog/PanelContainer/OptionsContainer/OutputScale/Resize" to="." method="_on_SpinBox_value_changed"]
[connection signal="pressed" from="OptionsDialog/PanelContainer/OptionsContainer/PathContainer/Choose" to="." method="_on_Choose_pressed"]
[connection signal="timeout" from="Timer" to="." method="_on_Timer_timeout"]

View file

@ -141,6 +141,7 @@ func _input(event: InputEvent) -> void:
if timeline_rect.has_point(mouse_pos):
if Input.is_key_pressed(KEY_CTRL):
cel_size += (2 * int(event.is_action("zoom_in")) - 2 * int(event.is_action("zoom_out")))
get_viewport().set_input_as_handled()
func reset_settings() -> void:

View file

@ -17,6 +17,7 @@ var zen_mode := false
var new_image_dialog := Dialog.new("res://src/UI/Dialogs/CreateNewImage.tscn")
var project_properties_dialog := Dialog.new("res://src/UI/Dialogs/ProjectProperties.tscn")
var preferences_dialog := Dialog.new("res://src/Preferences/PreferencesDialog.tscn")
var modify_selection := Dialog.new("res://src/UI/Dialogs/ModifySelection.tscn")
var offset_image_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/OffsetImage.tscn")
var scale_image_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/ScaleImage.tscn")
var resize_canvas_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/ResizeCanvas.tscn")
@ -54,6 +55,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 snap_to_submenu := PopupMenu.new()
@onready var panels_submenu := PopupMenu.new()
@onready var layouts_submenu := PopupMenu.new()
@ -98,6 +100,19 @@ func _ready() -> void:
_setup_help_menu()
func _input(event: InputEvent) -> void:
# Workaround for https://github.com/Orama-Interactive/Pixelorama/issues/1070
if event is InputEventMouseButton and event.pressed:
file_menu.activate_item_by_event(event)
edit_menu.activate_item_by_event(event)
select_menu.activate_item_by_event(event)
image_menu.activate_item_by_event(event)
effects_menu.activate_item_by_event(event)
view_menu.activate_item_by_event(event)
window_menu.activate_item_by_event(event)
help_menu.activate_item_by_event(event)
func _notification(what: int) -> void:
if what == NOTIFICATION_TRANSLATION_CHANGED and Global.current_project != null:
_update_file_menu_buttons(Global.current_project)
@ -427,17 +442,30 @@ func _setup_select_menu() -> void:
"All": "select_all",
"Clear": "clear_selection",
"Invert": "invert_selection",
"Tile Mode": ""
"Tile Mode": "",
"Modify": ""
}
for i in select_menu_items.size():
var item: String = select_menu_items.keys()[i]
if item == "Tile Mode":
select_menu.add_check_item(item, i)
elif item == "Modify":
_setup_selection_modify_submenu(item)
else:
_set_menu_shortcut(select_menu_items[item], select_menu, i, item)
select_menu.id_pressed.connect(select_menu_id_pressed)
func _setup_selection_modify_submenu(item: String) -> void:
selection_modify_submenu.set_name("selection_modify_submenu")
selection_modify_submenu.add_item("Expand")
selection_modify_submenu.add_item("Shrink")
selection_modify_submenu.add_item("Border")
selection_modify_submenu.id_pressed.connect(_selection_modify_submenu_id_pressed)
select_menu.add_child(selection_modify_submenu)
select_menu.add_submenu_item(item, selection_modify_submenu.get_name())
func _setup_help_menu() -> void:
# Order as in Global.HelpMenu enum
var help_menu_items := {
@ -654,6 +682,11 @@ func _tile_mode_submenu_id_pressed(id: Tiles.MODE) -> void:
get_tree().current_scene.tile_mode_offsets_dialog.change_mask()
func _selection_modify_submenu_id_pressed(id: int) -> void:
modify_selection.popup()
modify_selection.node.type = id
func _snap_to_submenu_id_pressed(id: int) -> void:
if id == 0:
Global.snap_to_rectangular_grid_boundary = !Global.snap_to_rectangular_grid_boundary