mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-02-13 01:03:07 +00:00
Compare commits
20 commits
46272fa7e4
...
b564a8fdce
Author | SHA1 | Date | |
---|---|---|---|
|
b564a8fdce | ||
|
cbdab45ed0 | ||
|
4c7d7da5e7 | ||
|
36329efaf6 | ||
|
7c1435e95f | ||
|
ad77d98f42 | ||
|
2600180736 | ||
|
5739a8b28e | ||
|
ce738f02c2 | ||
|
b0b1361722 | ||
|
5fa97988b5 | ||
|
af703d486e | ||
|
d2892358e3 | ||
|
ec17e970e0 | ||
|
8beb79a33b | ||
|
e2971a8fe9 | ||
|
6863adf957 | ||
|
dafc2fb1d5 | ||
|
2d9a582f21 | ||
|
aa59f73e65 |
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -4,6 +4,26 @@ 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
|
||||
- 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
|
||||
- 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.
|
||||
|
||||
## [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 +35,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.
|
||||
|
|
|
@ -1865,6 +1865,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 ""
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language-Team: Japanese\n"
|
||||
"Language: ja_JP\n"
|
||||
"PO-Revision-Date: 2024-11-13 23:12\n"
|
||||
"PO-Revision-Date: 2024-11-14 02:32\n"
|
||||
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
@ -1941,7 +1941,7 @@ 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 ""
|
||||
msgstr "密度:"
|
||||
|
||||
msgid "Brush color from"
|
||||
msgstr "ブラシの色"
|
||||
|
|
BIN
assets/graphics/misc/check_plain.png
Normal file
BIN
assets/graphics/misc/check_plain.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 176 B |
34
assets/graphics/misc/check_plain.png.import
Normal file
34
assets/graphics/misc/check_plain.png.import
Normal 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
|
BIN
assets/graphics/misc/close.png
Normal file
BIN
assets/graphics/misc/close.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 153 B |
34
assets/graphics/misc/close.png.import
Normal file
34
assets/graphics/misc/close.png.import
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
18
src/Main.gd
18
src/Main.gd
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,24 @@ 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"]
|
||||
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 +161,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"]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, ")")
|
||||
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -98,6 +98,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)
|
||||
|
|
Loading…
Reference in a new issue