1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 17:19:50 +00:00

Compare commits

...

22 commits

Author SHA1 Message Date
Piotr Kostrzewski 619456f4cc
Merge 5bfe44a202 into 8bd31112be 2024-12-16 07:28:35 -05:00
Emmanouil Papadeas 8bd31112be [skip ci] Update CHANGELOG.md 2024-12-16 01:36:11 +02:00
Emmanouil Papadeas d980eec683 [skip ci] Update CHANGELOG.md 2024-12-16 01:32:46 +02:00
Emmanouil Papadeas 39c85c3079 Implement the ability to load custom shaders and image and layer effects
Finally expose the feature of importing custom shaders as image effects, and implement custom shader loading for layer effects as well. To load a shader, drag and drop a .gdshader file into Pixelorama and it will get copied into `user://shaders`. Then, in the Effects menu, a new "Loaded" submenu will appear, and the new shaders will also be available in the layer effects dialog. Since they are stored on a persistent location, the shaders will also be available on the next times Pixelorama will launch as well.
2024-12-16 01:18:56 +02:00
Emmanouil Papadeas 8ceeba76c0 Update Translations.pot 2024-12-15 21:14:11 +02:00
Emmanouil Papadeas 93eab6929b For PixelCel's set_content not working when passing an Image instead of ImageExtended 2024-12-15 21:05:10 +02:00
Emmanouil Papadeas 1d9b9fda1e Add color curves layer effect 2024-12-15 21:04:32 +02:00
Emmanouil Papadeas 482dbecd13 Add presets to the curve edit widget 2024-12-15 20:46:07 +02:00
Emmanouil Papadeas cf8dacf0f5 Fix curve edit tangent points
They are working properly now
2024-12-15 18:37:21 +02:00
Emmanouil Papadeas 048058bd35 Implement a color curves image effect
Massive thanks to Material Maker for the custom widget code. The color curves effect is still WIP, I need to make the tangent points visible (not yet sure why they aren't now), add some curve presets, and implement it as a layer effect as well.
2024-12-15 18:10:55 +02:00
Emmanouil Papadeas 605bff7324 Show the index of the palette swatches when color indices are visible on the canvas 2024-12-15 00:13:59 +02:00
Emmanouil Papadeas 206773c4e7 Play audio at the correct frame when switching frames while the animation is running 2024-12-14 21:08:31 +02:00
Emmanouil Papadeas b5d5c44c4b Use the linked cel rectangle for audio cels 2024-12-14 18:01:46 +02:00
Emmanouil Papadeas 8e55b91a39 Unmute audio layes when making them visible while the animation is playing 2024-12-14 17:18:52 +02:00
Emmanouil Papadeas 0fad406967 Fix extension entry enlarged thumbnail dialog not being on top 2024-12-14 16:58:43 +02:00
Emmanouil Papadeas 4b12f764b5 Add a MarginContainer in the timeline's animation buttons 2024-12-14 16:10:05 +02:00
Emmanouil Papadeas 02d1900dc2 Make brush size in draw tools not affect the draw tiles mode
We could change it again if we implement it in a non-confusing way
2024-12-14 16:05:51 +02:00
Emmanouil Papadeas 18e9e2ec56
Implement audio layers (#1149)
* Initial work on audio layers

* Load ogg audio files

* Fix playback position

* Support mp3 files

* Play audio at the appropriate position when the animation runs, and stop when the pause button is pressed

* Change audio cel textures for the cels where audio is playing

* Fix audio not playing at the appropriate position

* Don't play audio is layer is invisible

* Set the audio layer names to be the imported audio file names

* Import audio from videos

* Export videos with audio

Only works with mp3 for now

* Remove support for ogg audio files as they cannot be saved

At least until I find a way to save them. Wav files will be supported with Godot 4.4

* Fix adding/removing in-between frames breaking the visual indication of audio cels

* Minor code improvements

* Export audio in videos with custom delay

* Support frame delay

* Change the frame where the audio plays at

* Fix crashes when the audio layer has no track

* Remove unneeded cel properties for audio cels

* Pxo loading/saving

* Load audio files from the audio layer properties

* Change the audio driver to Dummy from the Preferences for performance reasons

* Clone audio layers, disable layer merge and FX buttons when an audio layer is selected

* Easily change the playback frame of an audio layer from the right click menu of cel buttons

* Update Translations.pot

* Some code improvements and documentation

* Stop audio from playing when looping, and the audio does not play at the first frame

* Update audio cel buttons when changing the audio of the layer

* Mute audio layer when hiding it mid-play

* Only plays the portion of the sound that corresponds to the specific frame so maybe we should do that as well

When the animation is not running. If it is running, play the sound properly.

* Some code changes to allow for potential negative frames placement for audio

This woud allow audio to be placed in negative frames, which essentially means that audio would start before the first frame. This is not yet supported, however, because I don't know how to make it work with FFMPEG.
2024-12-14 00:30:04 +02:00
Variable 6100bdc8df
QOL improvements to the tileset panel (#1148)
* Added a background to swatches

* Improved UI for tilemap editor

* Implemented requested changes
2024-12-13 21:30:03 +02:00
Emmanouil Papadeas 5ec316a50f Some minor UI improvements in the text tool
The text tool's caret is now blinking, and the width of the border should now change depending on the zoom value
2024-12-12 21:36:56 +02:00
Emmanouil Papadeas a7a76ff9f0 Enable always on top for more child dialogs of other dialogs
Continuation of c72a1f4b90
2024-12-10 14:36:56 +02:00
Piotr Kostrzewski 5bfe44a202
Create pixelorama pl.nsi
Polish translation of the Pixelorama installer and context menu.
2024-01-11 15:49:36 +01:00
57 changed files with 1906 additions and 213 deletions

View file

@ -6,13 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [v1.1] - Unreleased
This update has been brought to you by the contributions of:
Fayez Akhtar ([@Variable-ind](https://github.com/Variable-ind))
Fayez Akhtar ([@Variable-ind](https://github.com/Variable-ind)), Spencer Beckwith ([@spencerjbeckwith](https://github.com/spencerjbeckwith))
Built using Godot 4.3
### Added
- Tilemap layers have arrived! Tilemap layers allow artists to create tiles, and easily preview and dynamically modify them within Pixelorama. [#1146](https://github.com/Orama-Interactive/Pixelorama/pull/1146)
- Indexed mode has finally been implemented! [#1136](https://github.com/Orama-Interactive/Pixelorama/pull/1136)
- Audio layers have been added, allowing artists to easily synchronize their animations with audio. [#1149](https://github.com/Orama-Interactive/Pixelorama/pull/1149)
- Added a new text tool. Destructive only for now, meaning that once the text is confirmed, it cannot be changed later. [#1134](https://github.com/Orama-Interactive/Pixelorama/pull/1134)
- A color curves image and layer effect has been added.
- It is now possible to load custom Godot shaders as image and layer effects.
- Implemented support for multiple grids. [#1122](https://github.com/Orama-Interactive/Pixelorama/pull/1122)
### Changed
@ -21,7 +25,11 @@ Built using Godot 4.3
### Fixed
- Fixed crash when Pixelorama starts without a palette.
- Undo/redo now works again when the cursor is hovering over the timeline.
- Palette swatches now get deleted when the user removes all palettes
- The first frame is no longer exported twice when using ping-pong loop.
- Fixed pencil/eraser/shading previews turning white for a brief moment when changing image brushes, and when switchin between tools.
- Dialogs that are children of other dialogs now always appear on top, to avoid issues where they could hide behind their parents and causing confusion that made Pixelorama seem unresponsive.
- Palette swatches now get deleted when the user removes all palettes.
- The CLI's output option now works with filepaths instead of just filenames. [#1145](https://github.com/Orama-Interactive/Pixelorama/pull/1145)
- Fixed the Palettize effect and palette exporting to images storing slightly wrong color values. [77f6bcf](https://github.com/Orama-Interactive/Pixelorama/commit/77f6bcf07bd80bc042e478bb883d05900cebe436)
- Fixed some issues with the Palettize effect where the output would be different if the palette size changed and empty swatches were added, even if the colors themselves stayed the same. Initially fixed by [bd7d3b1](https://github.com/Orama-Interactive/Pixelorama/commit/bd7d3b19cc98804e9b99754153c4d553d2048ee3), but [1dcb696](https://github.com/Orama-Interactive/Pixelorama/commit/1dcb696c35121f8208bde699f87bb75deff99d13) is the proper fix.
- Fixed recorder label not updating when project is changed. [#1139](https://github.com/Orama-Interactive/Pixelorama/pull/1139)
@ -50,7 +58,7 @@ Built using Godot 4.3
- 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
- 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.

View file

@ -1157,6 +1157,27 @@ msgstr ""
msgid "Tint effect factor:"
msgstr ""
#. An image effect that adjusts the colors of the image by using curves.
msgid "Color Curves"
msgstr ""
#. Refers to a color channel, such as the red, green, blue or alpha channels.
msgid "Channel:"
msgstr ""
msgid "Red"
msgstr ""
msgid "Green"
msgstr ""
#. Refers to the value (as in HSV) of the colors of an image.
msgid "Value"
msgstr ""
msgid "Presets"
msgstr ""
msgid "Apply"
msgstr ""
@ -1773,6 +1794,10 @@ msgstr ""
msgid "If enabled, the application window can become transparent. This affects performance, so keep it off if you don't need it."
msgstr ""
#. An option found in the preferences, under the Performance section.
msgid "Use dummy audio driver"
msgstr ""
#. Found in the Preferences, under Drivers. Specifies the renderer/video driver being used.
msgid "Renderer:"
msgstr ""
@ -2203,6 +2228,10 @@ msgstr ""
msgid "Unlink Cels"
msgstr ""
#. An option found in the right click menu of an audio cel. If selected, the audio of the audio layer will start playing from this frame.
msgid "Play audio here"
msgstr ""
msgid "Properties"
msgstr ""
@ -2243,6 +2272,9 @@ msgstr ""
msgid "Tilemap"
msgstr ""
msgid "Audio"
msgstr ""
msgid "Layers"
msgstr ""
@ -2275,6 +2307,11 @@ msgstr ""
msgid "Add Tilemap Layer"
msgstr ""
#. One of the options of the create new layer button.
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Add Audio Layer"
msgstr ""
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Remove current layer"
msgstr ""
@ -2405,6 +2442,17 @@ msgstr ""
msgid "Expand/collapse group"
msgstr ""
#. Refers to the audio file of an audio layer.
msgid "Audio file:"
msgstr ""
msgid "Load file"
msgstr ""
#. An option in the audio layer properties, allows users to play the audio starting from a specific frame.
msgid "Play at frame:"
msgstr ""
msgid "Palette"
msgstr ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dfjd72smxp6ma"
path="res://.godot/imported/musical_note.png-f1be7cc6341733e6ffe2fa5b650b80c2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/misc/musical_note.png"
dest_files=["res://.godot/imported/musical_note.png-f1be7cc6341733e6ffe2fa5b650b80c2.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: 204 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bqr3n3tm8b6w2"
path="res://.godot/imported/place_tiles_disabled.png-f43e25b0863e3eedf3c6fc7ef902127f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/tileset/place_tiles_disabled.png"
dest_files=["res://.godot/imported/place_tiles_disabled.png-f43e25b0863e3eedf3c6fc7ef902127f.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: 185 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dga8nirhhgyc4"
path="res://.godot/imported/place_tiles_enabled.png-845e4dd5c3bbd38cc7bf5ee82ed05667.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/tileset/place_tiles_enabled.png"
dest_files=["res://.godot/imported/place_tiles_enabled.png-845e4dd5c3bbd38cc7bf5ee82ed05667.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

264
installer/pixelorama pl.nsi Normal file
View file

@ -0,0 +1,264 @@
; Pixelorama Installer NSIS Script
; Copyright Xenofon Konitsas (huskee) 2021
; Licensed under the MIT License
; Helper variables so that we don't change 20 instances of the version for every update
!define APPNAME "Pixelorama"
!define APPVERSION "v0.11.3"
!define COMPANYNAME "Orama Interactive"
; Include the Modern UI library
!include "MUI2.nsh"
!include "x64.nsh"
; Basic Installer Info
Name "${APPNAME} ${APPVERSION}"
OutFile "${APPNAME}_${APPVERSION}_setup.exe"
Unicode True
; Default installation folder
InstallDir "$APPDATA\${COMPANYNAME}\${APPNAME}"
; Get installation folder from registry if available
InstallDirRegKey HKCU "Software\${COMPANYNAME}\${APPNAME}" "InstallDir"
; Request application privileges for Vista and later
RequestExecutionLevel admin
; Interface Settings
!define MUI_ICON "assets\pixel-install.ico"
!define MUI_UNICON "assets\pixel-uninstall.ico"
!define MUI_WELCOMEFINISHPAGE_BITMAP "assets\wizard.bmp"
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "assets\wizard.bmp"
!define MUI_HEADERIMAGE
!define MUI_HEADERIMAGE_RIGHT
!define MUI_HEADERIMAGE_BITMAP "assets\header.bmp"
!define MUI_HEADERIMAGE_UNBITMAP "assets\header.bmp"
!define MUI_ABORTWARNING
!define MUI_COMPONENTSPAGE_SMALLDESC
!define MUI_FINISHPAGE_NOAUTOCLOSE
!define MUI_UNFINISHPAGE_NOAUTOCLOSE
!define MUI_FINISHPAGE_RUN "$INSTDIR\pixelorama.exe"
; Language selection settings
!define MUI_LANGDLL_ALLLANGUAGES
## Remember the installer language
!define MUI_LANGDLL_REGISTRY_ROOT HKCU
!define MUI_LANGDLL_REGISTRY_KEY "Software\${COMPANYNAME}\${APPNAME}"
!define MUI_LANGDLL_REGISTRY_VALUENAME "Język instalatora"
; Installer pages
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "Licencja"
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_WELCOME
!insertmacro MUI_UNPAGE_COMPONENTS
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH
; Multilingual support
!insertmacro MUI_LANGUAGE "Polski"
;@INSERT_TRANSLATIONS@
; Assign language strings to installer/uninstaller section names
LangString SecInstall ${LANG_ENGLISH} "Zainstaluj ${APPNAME}"
LangString SecStartmenu ${LANG_ENGLISH} "Utwórz skróty w menu Start (opcjonalnie)"
LangString SecDesktop ${LANG_ENGLISH} "Utwórz skrót na pulpicie (opcjonalnie)"
LangString un.SecUninstall ${LANG_ENGLISH} "Odinstaluj ${APPNAME} ${APPVERSION}"
LangString un.SecConfig ${LANG_ENGLISH} "Usuń pliki konfiguracyjne (opcjonalnie)"
; Installer sections
Section "$(SecInstall)" SecInstall ; Main install section
SectionIn RO ; Non optional section
; Set the installation folder as the output directory
SetOutPath "$INSTDIR"
; Copy all files to install directory
${If} ${RunningX64}
File "..\build\windows-64bit\pixelorama.exe"
File "..\build\windows-64bit\pixelorama.pck"
${Else}
File "..\build\windows-32bit\pixelorama.exe"
File "..\build\windows-32bit\pixelorama.pck"
${EndIf}
File "..\assets\graphics\icons\pxo.ico"
SetOutPath "$INSTDIR\pixelorama_data"
File /nonfatal /r "..\build\pixelorama_data\*"
; Store installation folder in the registry
WriteRegStr HKCU "Software\${COMPANYNAME}\${APPNAME}" "InstallDir" $INSTDIR
; Create uninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
; Create Add/Remove Programs entry
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"DisplayName" "${APPNAME}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"UninstallString" "$INSTDIR\uninstall.exe"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"DisplayIcon" "$INSTDIR\pixelorama.exe,0"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"InstallLocation" "$INSTDIR"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"Publisher" "${COMPANYNAME}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"HelpLink" "https://orama-interactive.github.io/Pixelorama-Docs"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"DisplayVersion" "${APPVERSION}"
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"NoModify" 1
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"NoRepair" 1
; Associate .pxo files with Pixelorama
WriteRegStr HKCR ".pxo" "" "Pixelorama project"
WriteRegStr HKCR ".pxo" "ContentType" "image/pixelorama"
WriteRegStr HKCR ".pxo" "PerceivedType" "document"
WriteRegStr HKCR "Pixelorama project" "" "Pixelorama project"
WriteRegStr HKCR "Pixelorama project\shell" "" "open"
WriteRegStr HKCR "Pixelorama project\DefaultIcon" "" "$INSTDIR\pxo.ico"
WriteRegStr HKCR "Pixelorama project\shell\open\command" "" '$INSTDIR\${APPNAME}.exe "%1"'
WriteRegStr HKCR "Pixelorama project\shell\edit" "" "Edytuj projekt w ${APPNAME}"
WriteRegStr HKCR "Pixelorama project\shell\edit\command" "" '$INSTDIR\${APPNAME}.exe "%1"'
SectionEnd
Section /o "$(SecStartmenu)" SecStartmenu ; Create Start Menu shortcuts
; Create folder in Start Menu\Programs and create shortcuts for app and uninstaller
CreateDirectory "$SMPROGRAMS\${COMPANYNAME}"
CreateShortCut "$SMPROGRAMS\${COMPANYNAME}\${APPNAME} ${APPVERSION}.lnk" "$INSTDIR\Pixelorama.exe"
CreateShortCut "$SMPROGRAMS\${COMPANYNAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe"
SectionEnd
Section /o "$(SecDesktop)" SecDesktop ; Create Desktop shortcut
; Create shortcut for app on desktop
CreateShortCut "$DESKTOP\${APPNAME} ${APPVERSION}.lnk" "$INSTDIR\Pixelorama.exe"
SectionEnd
; Installer functions
Function .onInit
!insertmacro MUI_LANGDLL_DISPLAY
FunctionEnd
; Uninstaller sections
Section "un.$(un.SecUninstall)" un.SecUninstall ; Main uninstall section
SectionIn RO
; Delete all files and folders created by the installer
Delete "$INSTDIR\uninstall.exe"
Delete "$INSTDIR\Pixelorama.exe"
Delete "$INSTDIR\Pixelorama.pck"
Delete "$INSTDIR\pxo.ico"
RMDir /r "$INSTDIR\pixelorama_data"
RMDir "$INSTDIR"
; Delete shortcuts
RMDir /r "$SMPROGRAMS\${COMPANYNAME}"
Delete "$DESKTOP\${APPNAME} ${APPVERSION}.lnk"
; Delete the install folder
SetOutPath "$APPDATA"
RMDir /r "${COMPANYNAME}"
; If empty, delete the application's registry key
DeleteRegKey /ifempty HKCU "Software\${COMPANYNAME}\${APPNAME}"
; Delete the Add/Remove Programs entry
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}"
; Delete the .pxo file association
DeleteRegKey HKCR "Pixelorama project"
DeleteRegKey HKCR ".pxo"
SectionEnd
Section "un.$(un.SecConfig)" un.SecConfig ; Configuration removal section
; Delete the application's settings file
Delete "$APPDATA\Godot\app_userdata\${APPNAME}\cache.ini"
SectionEnd
; Uninstaller functions
Function un.onInit
!insertmacro MUI_UNGETLANGUAGE
FunctionEnd
; Section description language strings for multilingual support
LangString DESC_SecInstall ${LANG_ENGLISH} "Instalowanie ${APPNAME} ${APPVERSION}."
LangString DESC_SecStartmenu ${LANG_ENGLISH} "Tworzenie skrótów w menu Start ${APPNAME}."
LangString DESC_SecDesktop ${LANG_ENGLISH} "Tworzenie skrótu na pulpicie dla ${APPNAME}."
LangString DESC_un.SecUninstall ${LANG_ENGLISH} "Odinstalowywanie ${APPNAME} ${APPVERSION} i usuwanie wszystkich skrótów."
LangString DESC_un.SecConfig ${LANG_ENGLISH} "Usuwanie plików konfiguracyjnych ${APPNAME}."
; Assign language strings to installer/uninstaller descriptions
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${SecInstall} $(DESC_SecInstall)
!insertmacro MUI_DESCRIPTION_TEXT ${SecStartmenu} $(DESC_SecStartmenu)
!insertmacro MUI_DESCRIPTION_TEXT ${SecDesktop} $(DESC_SecDesktop)
!insertmacro MUI_FUNCTION_DESCRIPTION_END
!insertmacro MUI_UNFUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${un.SecUninstall} $(DESC_un.SecUninstall)
!insertmacro MUI_DESCRIPTION_TEXT ${un.SecConfig} $(DESC_un.SecConfig)
!insertmacro MUI_UNFUNCTION_DESCRIPTION_END

View file

@ -27,10 +27,6 @@ config/windows_native_icon="res://assets/graphics/icons/icon.ico"
config/ExtensionsAPI_Version=5
config/Pxo_Version=4
[audio]
driver/driver="Dummy"
[autoload]
Global="*res://src/Autoload/Global.gd"
@ -689,6 +685,10 @@ adjust_brightness_contrast={
"deadzone": 0.5,
"events": []
}
color_curves={
"deadzone": 0.5,
"events": []
}
gradient={
"deadzone": 0.5,
"events": []

View file

@ -282,7 +282,7 @@ func process_animation(project := Global.current_project) -> void:
for cel in frame.cels:
var image := Image.new()
image.copy_from(cel.get_image())
var duration := frame.duration * (1.0 / project.fps)
var duration := frame.get_duration_in_seconds(project.fps)
processed_images.append(
ProcessedImage.new(image, project.frames.find(frame), duration)
)
@ -298,7 +298,7 @@ func process_animation(project := Global.current_project) -> void:
image.copy_from(crop)
if trim_images:
image = image.get_region(image.get_used_rect())
var duration := frame.duration * (1.0 / project.fps)
var duration := frame.get_duration_in_seconds(project.fps)
processed_images.append(ProcessedImage.new(image, project.frames.find(frame), duration))
@ -427,7 +427,7 @@ func export_processed_images(
if is_single_file_format(project):
if is_using_ffmpeg(project.file_format):
var video_exported := export_video(export_paths)
var video_exported := export_video(export_paths, project)
if not video_exported:
Global.popup_error(
tr("Video failed to export. Ensure that FFMPEG is installed correctly.")
@ -505,8 +505,9 @@ func export_processed_images(
## Uses FFMPEG to export a video
func export_video(export_paths: PackedStringArray) -> bool:
func export_video(export_paths: PackedStringArray, project: Project) -> bool:
DirAccess.make_dir_absolute(TEMP_PATH)
var video_duration := 0
var temp_path_real := ProjectSettings.globalize_path(TEMP_PATH)
var input_file_path := temp_path_real.path_join("input.txt")
var input_file := FileAccess.open(input_file_path, FileAccess.WRITE)
@ -516,25 +517,80 @@ func export_video(export_paths: PackedStringArray) -> bool:
processed_images[i].image.save_png(temp_file_path)
input_file.store_line("file '" + temp_file_name + "'")
input_file.store_line("duration %s" % processed_images[i].duration)
video_duration += processed_images[i].duration
input_file.close()
# ffmpeg -y -f concat -i input.txt output_path
var ffmpeg_execute: PackedStringArray = [
"-y", "-f", "concat", "-i", input_file_path, export_paths[0]
]
var output := []
var success := OS.execute(Global.ffmpeg_path, ffmpeg_execute, output, true)
print(output)
var temp_dir := DirAccess.open(TEMP_PATH)
for file in temp_dir.get_files():
temp_dir.remove(file)
DirAccess.remove_absolute(TEMP_PATH)
var success := OS.execute(Global.ffmpeg_path, ffmpeg_execute, [], true)
if success < 0 or success > 1:
var fail_text := """Video failed to export. Make sure you have FFMPEG installed
and have set the correct path in the preferences."""
Global.popup_error(tr(fail_text))
_clear_temp_folder()
return false
# Find audio layers
var ffmpeg_combine_audio: PackedStringArray = ["-y"]
var audio_layer_count := 0
var max_audio_duration := 0
var adelay_string := ""
for layer in project.get_all_audio_layers():
if layer.audio is AudioStreamMP3:
var temp_file_name := str(audio_layer_count + 1).pad_zeros(number_of_digits) + ".mp3"
var temp_file_path := temp_path_real.path_join(temp_file_name)
var temp_audio_file := FileAccess.open(temp_file_path, FileAccess.WRITE)
temp_audio_file.store_buffer(layer.audio.data)
ffmpeg_combine_audio.append("-i")
ffmpeg_combine_audio.append(temp_file_path)
var delay := floori(layer.playback_position * 1000)
# [n]adelay=delay_in_ms:all=1[na]
adelay_string += (
"[%s]adelay=%s:all=1[%sa];" % [audio_layer_count, delay, audio_layer_count]
)
audio_layer_count += 1
if layer.get_audio_length() >= max_audio_duration:
max_audio_duration = layer.get_audio_length()
if audio_layer_count > 0:
# If we have audio layers, merge them all into one file.
for i in audio_layer_count:
adelay_string += "[%sa]" % i
var amix_inputs_string := "amix=inputs=%s[a]" % audio_layer_count
var final_filter_string := adelay_string + amix_inputs_string
var audio_file_path := temp_path_real.path_join("audio.mp3")
ffmpeg_combine_audio.append_array(
PackedStringArray(
["-filter_complex", final_filter_string, "-map", '"[a]"', audio_file_path]
)
)
# ffmpeg -i input1 -i input2 ... -i inputn -filter_complex amix=inputs=n output_path
var combined_audio_success := OS.execute(Global.ffmpeg_path, ffmpeg_combine_audio, [], true)
if combined_audio_success == 0 or combined_audio_success == 1:
var copied_video := temp_path_real.path_join("video." + export_paths[0].get_extension())
# Then mix the audio file with the video.
DirAccess.copy_absolute(export_paths[0], copied_video)
# ffmpeg -y -i video_file -i input_audio -c:v copy -map 0:v:0 -map 1:a:0 video_file
var ffmpeg_final_video: PackedStringArray = [
"-y", "-i", copied_video, "-i", audio_file_path
]
if max_audio_duration > video_duration:
ffmpeg_final_video.append("-shortest")
ffmpeg_final_video.append_array(
["-c:v", "copy", "-map", "0:v:0", "-map", "1:a:0", export_paths[0]]
)
OS.execute(Global.ffmpeg_path, ffmpeg_final_video, [], true)
_clear_temp_folder()
return true
func _clear_temp_folder() -> void:
var temp_dir := DirAccess.open(TEMP_PATH)
for file in temp_dir.get_files():
temp_dir.remove(file)
DirAccess.remove_absolute(TEMP_PATH)
func export_animated(args: Dictionary) -> void:
var project: Project = args["project"]
var exporter: AImgIOBaseExporter = args["exporter"]

View file

@ -631,7 +631,7 @@ class ProjectAPI:
## Returns the current cel.
## Cel type can be checked using function [method get_class_name] inside the cel
## type can be GroupCel, PixelCel, Cel3D, or BaseCel.
## type can be GroupCel, PixelCel, Cel3D, CelTileMap, AudioCel or BaseCel.
func get_current_cel() -> BaseCel:
return current_project.get_current_cel()

View file

@ -14,7 +14,7 @@ signal cel_switched ## Emitted whenever you select a different cel.
signal project_data_changed(project: Project) ## Emitted when project data is modified.
signal font_loaded ## Emitted when a new font has been loaded, or an old one gets unloaded.
enum LayerTypes { PIXEL, GROUP, THREE_D, TILEMAP }
enum LayerTypes { PIXEL, GROUP, THREE_D, TILEMAP, AUDIO }
enum GridTypes { CARTESIAN, ISOMETRIC, ALL }
## ## Used to tell whether a color is being taken from the current theme,
## or if it is a custom color.
@ -64,13 +64,14 @@ enum EffectsMenu {
DESATURATION,
HSV,
BRIGHTNESS_SATURATION,
COLOR_CURVES,
PALETTIZE,
PIXELIZE,
POSTERIZE,
GAUSSIAN_BLUR,
GRADIENT,
GRADIENT_MAP,
SHADER
LOADED_EFFECTS
}
## Enumeration of items present in the Select Menu.
enum SelectMenu { SELECT_ALL, CLEAR_SELECTION, INVERT, TILE_MODE, MODIFY }
@ -490,6 +491,11 @@ var window_transparency := false:
return
window_transparency = value
_save_to_override_file()
var dummy_audio_driver := false:
set(value):
if value != dummy_audio_driver:
dummy_audio_driver = value
_save_to_override_file()
## Found in Preferences. The time (in minutes) after which backup is created (if enabled).
var autosave_interval := 1.0:
@ -726,6 +732,7 @@ func _init() -> void:
window_transparency = ProjectSettings.get_setting(
"display/window/per_pixel_transparency/allowed"
)
dummy_audio_driver = ProjectSettings.get_setting("audio/driver/driver") == "Dummy"
func _ready() -> void:
@ -803,6 +810,7 @@ func _initialize_keychain() -> void:
&"drop_shadow": Keychain.InputAction.new("", "Effects menu", true),
&"adjust_hsv": Keychain.InputAction.new("", "Effects menu", true),
&"adjust_brightness_contrast": Keychain.InputAction.new("", "Effects menu", true),
&"color_curves": Keychain.InputAction.new("", "Effects menu", true),
&"gaussian_blur": Keychain.InputAction.new("", "Effects menu", true),
&"gradient": Keychain.InputAction.new("", "Effects menu", true),
&"gradient_map": Keychain.InputAction.new("", "Effects menu", true),
@ -1187,3 +1195,6 @@ func _save_to_override_file() -> void:
file.store_line("[display]\n")
file.store_line("window/subwindows/embed_subwindows=%s" % single_window_mode)
file.store_line("window/per_pixel_transparency/allowed=%s" % window_transparency)
if dummy_audio_driver:
file.store_line("[audio]\n")
file.store_line('driver/driver="Dummy"')

View file

@ -3,6 +3,9 @@ extends Node
signal project_saved
signal reference_image_imported
signal shader_copied(file_path: String)
const SHADERS_DIRECTORY := "user://shaders"
var preview_dialog_tscn := preload("res://src/UI/Dialogs/ImportPreviewDialog.tscn")
var preview_dialogs := [] ## Array of preview dialogs
@ -39,12 +42,15 @@ func handle_loading_file(file: String) -> void:
elif file_ext in ["pck", "zip"]: # Godot resource pack file
Global.control.get_node("Extensions").install_extension(file)
elif file_ext == "shader" or file_ext == "gdshader": # Godot shader file
elif file_ext == "gdshader": # Godot shader file
var shader := load(file)
if not shader is Shader:
return
var file_name: String = file.get_file().get_basename()
Global.control.find_child("ShaderEffect").change_shader(shader, file_name)
var new_path := SHADERS_DIRECTORY.path_join(file.get_file())
DirAccess.copy_absolute(file, new_path)
shader_copied.emit(new_path)
elif file_ext == "mp3": # Audio file
open_audio_file(file)
else: # Image files
# Attempt to load as APNG.
@ -185,8 +191,8 @@ func handle_loading_video(file: String) -> bool:
project_size.x = temp_image.get_width()
if temp_image.get_height() > project_size.y:
project_size.y = temp_image.get_height()
DirAccess.remove_absolute(Export.TEMP_PATH)
if images_to_import.size() == 0 or project_size == Vector2i.ZERO:
DirAccess.remove_absolute(Export.TEMP_PATH)
return false # We didn't find any images, return
# If we found images, create a new project out of them
var new_project := Project.new([], file.get_basename().get_file(), project_size)
@ -196,6 +202,14 @@ func handle_loading_video(file: String) -> bool:
Global.projects.append(new_project)
Global.tabs.current_tab = Global.tabs.get_tab_count() - 1
Global.canvas.camera_zoom()
var output_audio_file := temp_path_real.path_join("audio.mp3")
# ffmpeg -y -i input_file -vn audio.mp3
var ffmpeg_execute_audio: PackedStringArray = ["-y", "-i", file, "-vn", output_audio_file]
OS.execute(Global.ffmpeg_path, ffmpeg_execute_audio, [], true)
if FileAccess.file_exists(output_audio_file):
open_audio_file(output_audio_file)
temp_dir.remove("audio.mp3")
DirAccess.remove_absolute(Export.TEMP_PATH)
return true
@ -438,6 +452,14 @@ func save_pxo_file(
zip_packer.start_file(tileset_path.path_join(str(j)))
zip_packer.write_file(tile.image.get_data())
zip_packer.close_file()
var audio_layers := project.get_all_audio_layers()
for i in audio_layers.size():
var layer := audio_layers[i]
var audio_path := "audio/%s" % i
if layer.audio is AudioStreamMP3:
zip_packer.start_file(audio_path)
zip_packer.write_file(layer.audio.data)
zip_packer.close_file()
zip_packer.close()
if temp_path != path:
@ -902,6 +924,23 @@ func set_new_imported_tab(project: Project, path: String) -> void:
Global.tabs.delete_tab(prev_project_pos)
func open_audio_file(path: String) -> void:
var audio_stream: AudioStream
var file := FileAccess.open(path, FileAccess.READ)
audio_stream = AudioStreamMP3.new()
audio_stream.data = file.get_buffer(file.get_length())
if not is_instance_valid(audio_stream):
return
var project := Global.current_project
for layer in project.layers:
if layer is AudioLayer and not is_instance_valid(layer.audio):
layer.audio = audio_stream
return
var new_layer := AudioLayer.new(project, path.get_basename().get_file())
new_layer.audio = audio_stream
Global.animation_timeline.add_layer(new_layer, project)
func update_autosave() -> void:
if not is_instance_valid(autosave_timer):
return

View file

@ -800,7 +800,10 @@ func _cel_switched() -> void:
var layer: BaseLayer = Global.current_project.layers[Global.current_project.current_layer]
var layer_type := layer.get_layer_type()
# Do not make any changes when its the same type of layer, or a group layer
if layer_type == _curr_layer_type or layer_type == Global.LayerTypes.GROUP:
if (
layer_type == _curr_layer_type
or layer_type in [Global.LayerTypes.GROUP, Global.LayerTypes.AUDIO]
):
return
_show_relevant_tools(layer_type)

View file

@ -0,0 +1,18 @@
class_name AudioCel
extends BaseCel
## A class for the properties of cels in AudioLayers.
## The term "cel" comes from "celluloid" (https://en.wikipedia.org/wiki/Cel).
func _init(_opacity := 1.0) -> void:
opacity = _opacity
image_texture = ImageTexture.new()
func get_image() -> Image:
var image := Global.current_project.new_empty_image()
return image
func get_class_name() -> String:
return "AudioCel"

View file

@ -25,7 +25,13 @@ func get_content() -> ImageExtended:
func set_content(content, texture: ImageTexture = null) -> void:
image = content
var proper_content: ImageExtended
if content is not ImageExtended:
proper_content = ImageExtended.new()
proper_content.copy_from_custom(content, image.is_indexed)
else:
proper_content = content
image = proper_content
if is_instance_valid(texture) and is_instance_valid(texture.get_image()):
image_texture = texture
if image_texture.get_image().get_size() != image.get_size():

View file

@ -11,3 +11,26 @@ var user_data := "" ## User defined data, set in the frame properties.
func _init(_cels: Array[BaseCel] = [], _duration := 1.0) -> void:
cels = _cels
duration = _duration
func get_duration_in_seconds(fps: float) -> float:
return duration * (1.0 / fps)
func position_in_seconds(project: Project, start_from := 0) -> float:
var pos := 0.0
var index := project.frames.find(self)
if index > start_from:
for i in range(start_from, index):
if i >= 0:
var frame := project.frames[i]
pos += frame.get_duration_in_seconds(project.fps)
else:
pos += 1.0 / project.fps
else:
if start_from >= project.frames.size():
return -1.0
for i in range(start_from, index, -1):
var frame := project.frames[i]
pos -= frame.get_duration_in_seconds(project.fps)
return pos

View file

@ -144,7 +144,9 @@ func set_nodes() -> void:
selection_checkbox = $VBoxContainer/OptionsContainer/SelectionCheckBox
affect_option_button = $VBoxContainer/OptionsContainer/AffectOptionButton
animate_panel = $"%AnimatePanel"
if is_instance_valid(animate_panel):
animate_panel.image_effect_node = self
if is_instance_valid(live_checkbox):
live_checkbox.button_pressed = live_preview

View file

@ -0,0 +1,74 @@
class_name AudioLayer
extends BaseLayer
## A unique type of layer which acts as an audio track for the timeline.
## Each audio layer has one audio stream, and its starting position can be
## in any point during the animation.
signal audio_changed
signal playback_frame_changed
var audio: AudioStream: ## The audio stream of the layer.
set(value):
audio = value
audio_changed.emit()
var playback_position := 0.0: ## The time in seconds where the audio stream starts playing.
get():
if playback_frame >= 0:
var frame := project.frames[playback_frame]
return frame.position_in_seconds(project)
var pos := 0.0
for i in absi(playback_frame):
pos -= 1.0 / project.fps
return pos
var playback_frame := 0: ## The frame where the audio stream starts playing.
set(value):
playback_frame = value
playback_frame_changed.emit()
func _init(_project: Project, _name := "") -> void:
project = _project
name = _name
## Returns the length of the audio stream.
func get_audio_length() -> float:
if is_instance_valid(audio):
return audio.get_length()
else:
return -1.0
## Returns the class name of the audio stream. E.g. "AudioStreamMP3".
func get_audio_type() -> String:
if not is_instance_valid(audio):
return ""
return audio.get_class()
# Overridden Methods:
func serialize() -> Dictionary:
var data := {
"name": name,
"type": get_layer_type(),
"playback_frame": playback_frame,
"audio_type": get_audio_type()
}
return data
func deserialize(dict: Dictionary) -> void:
super.deserialize(dict)
playback_frame = dict.get("playback_frame", playback_frame)
func get_layer_type() -> int:
return Global.LayerTypes.AUDIO
func new_empty_cel() -> AudioCel:
return AudioCel.new()
func set_name_to_default(number: int) -> void:
name = tr("Audio") + " %s" % number

View file

@ -232,7 +232,7 @@ func display_effects(cel: BaseCel, image_override: Image = null) -> Image:
return image
var image_size := image.get_size()
for effect in effects:
if not effect.enabled:
if not effect.enabled or not is_instance_valid(effect.shader):
continue
var shader_image_effect := ShaderImageEffect.new()
shader_image_effect.generate_image(image, effect.shader, effect.params, image_size)

View file

@ -8,6 +8,7 @@ signal serialized(dict: Dictionary)
signal about_to_deserialize(dict: Dictionary)
signal resized
signal timeline_updated
signal fps_changed
const INDEXED_MODE := Image.FORMAT_MAX + 1
@ -65,7 +66,10 @@ var brushes: Array[Image] = []
var reference_images: Array[ReferenceImage] = []
var reference_index: int = -1 # The currently selected index ReferenceImage
var vanishing_points := [] ## Array of Vanishing Points
var fps := 6.0
var fps := 6.0:
set(value):
fps = value
fps_changed.emit()
var user_data := "" ## User defined data, set in the project properties.
var x_symmetry_point: float
@ -356,6 +360,7 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces
tileset.deserialize(saved_tileset)
tilesets.append(tileset)
if dict.has("frames") and dict.has("layers"):
var audio_layers := 0
for saved_layer in dict.layers:
match int(saved_layer.get("type", Global.LayerTypes.PIXEL)):
Global.LayerTypes.PIXEL:
@ -366,6 +371,18 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces
layers.append(Layer3D.new(self))
Global.LayerTypes.TILEMAP:
layers.append(LayerTileMap.new(self, null))
Global.LayerTypes.AUDIO:
var layer := AudioLayer.new(self)
var audio_path := "audio/%s" % audio_layers
if zip_reader.file_exists(audio_path):
var audio_data := zip_reader.read_file(audio_path)
var stream: AudioStream
if saved_layer.get("audio_type", "") == "AudioStreamMP3":
stream = AudioStreamMP3.new()
stream.data = audio_data
layer.audio = stream
layers.append(layer)
audio_layers += 1
var frame_i := 0
for frame in dict.frames:
@ -390,6 +407,8 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces
var tileset := tilesets[tileset_index]
var new_cel := CelTileMap.new(tileset, image)
cels.append(new_cel)
Global.LayerTypes.AUDIO:
cels.append(AudioCel.new())
cel["pxo_version"] = pxo_version
cels[cel_i].deserialize(cel)
_deserialize_metadata(cels[cel_i], cel)
@ -640,10 +659,10 @@ func find_first_drawable_cel(frame := frames[current_frame]) -> BaseCel:
var result: BaseCel
var cel := frame.cels[0]
var i := 0
while cel is GroupCel and i < layers.size():
while (cel is GroupCel or cel is AudioCel) and i < layers.size():
cel = frame.cels[i]
i += 1
if not cel is GroupCel:
if cel is not GroupCel and cel is not AudioCel:
result = cel
return result
@ -658,6 +677,18 @@ func get_all_pixel_cels() -> Array[PixelCel]:
return cels
func get_all_audio_layers(only_valid_streams := true) -> Array[AudioLayer]:
var audio_layers: Array[AudioLayer]
for layer in layers:
if layer is AudioLayer:
if only_valid_streams:
if is_instance_valid(layer.audio):
audio_layers.append(layer)
else:
audio_layers.append(layer)
return audio_layers
## Reads data from [param cels] and appends them to [param data],
## to be used for the undo/redo system.
## It adds data such as the images of [PixelCel]s,

View file

@ -252,6 +252,17 @@ static func create_ui_for_shader_uniforms(
func(_gradient, _cc): value_changed.call(gradient_edit.texture, u_name)
)
hbox.add_child(gradient_edit)
elif u_name.begins_with("curve_"):
var curve_edit := CurveEdit.new()
curve_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
if params.has(u_name) and params[u_name] is CurveTexture:
curve_edit.curve = params[u_name].curve
else:
curve_edit.set_default_curve()
curve_edit.value_changed.connect(
func(curve: Curve): value_changed.call(CurveEdit.to_texture(curve), u_name)
)
hbox.add_child(curve_edit)
else: ## Simple texture
var file_dialog := FileDialog.new()
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE

View file

@ -64,6 +64,7 @@ func draw_palette() -> void:
var grid_index := i + grid_size.x * j
var index := convert_grid_index_to_palette_index(grid_index)
var swatch := swatches[grid_index]
swatch.color_index = index
swatch.show_left_highlight = Palettes.left_selected_color == index
swatch.show_right_highlight = Palettes.right_selected_color == index
var color = current_palette.get_color(index)

View file

@ -8,6 +8,7 @@ signal dropped(source_index: int, new_index: int)
const DEFAULT_COLOR := Color(0.0, 0.0, 0.0, 0.0)
var index := -1
var color_index := -1
var show_left_highlight := false
var show_right_highlight := false
var empty := true:
@ -48,6 +49,23 @@ func _draw() -> void:
draw_rect(
Rect2(margin - Vector2.ONE, size - margin * 2 + Vector2(2, 2)), Color.WHITE, false, 1
)
if Global.show_pixel_indices:
var font := Themes.get_font()
var str_pos := Vector2(size.x / 2, size.y - 2)
var text_color := Global.control.theme.get_color(&"font_color", &"Label")
draw_string_outline(
font,
str_pos,
str(color_index),
HORIZONTAL_ALIGNMENT_RIGHT,
-1,
size.x / 2,
1,
text_color.inverted()
)
draw_string(
font, str_pos, str(color_index), HORIZONTAL_ALIGNMENT_RIGHT, -1, size.x / 2, text_color
)
## Enables drawing of highlights which indicate selected swatches

View file

@ -181,6 +181,13 @@ var preferences: Array[Preference] = [
false,
true
),
Preference.new(
"dummy_audio_driver",
"Performance/PerformanceContainer/DummyAudioDriver",
"button_pressed",
false,
true
),
Preference.new("tablet_driver", "Drivers/DriversContainer/TabletDriver", "selected", 0)
]

View file

@ -1142,18 +1142,28 @@ mouse_default_cursor_shape = 2
button_pressed = true
text = "On"
[node name="WindowTransparencyLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer"]
[node name="WindowTransparencyLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]]
layout_mode = 2
tooltip_text = "If enabled, the application window can become transparent. This affects performance, so keep it off if you don't need it."
mouse_filter = 0
text = "Enable window transparency"
[node name="WindowTransparency" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer"]
[node name="WindowTransparency" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]]
layout_mode = 2
tooltip_text = "If enabled, the application window can become transparent. This affects performance, so keep it off if you don't need it."
mouse_default_cursor_shape = 2
text = "On"
[node name="DummyAudioDriverLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]]
layout_mode = 2
mouse_filter = 0
text = "Use dummy audio driver"
[node name="DummyAudioDriver" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]]
layout_mode = 2
mouse_default_cursor_shape = 2
text = "On"
[node name="Drivers" type="VBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide"]
visible = false
layout_mode = 2
@ -1473,6 +1483,7 @@ text = "Pixelorama must be restarted for changes to take effect."
mode = 1
title = "Open File(s)"
size = Vector2i(560, 400)
always_on_top = true
ok_button_text = "Open"
file_mode = 1
access = 2
@ -1484,6 +1495,7 @@ transient = true
[node name="EnableExtensionConfirmation" type="ConfirmationDialog" parent="."]
unique_name_in_owner = true
always_on_top = true
dialog_text = "Are you sure you want to enable this extension? Make sure to only enable extensions from sources that you trust."
dialog_autowrap = true
@ -1491,11 +1503,13 @@ dialog_autowrap = true
unique_name_in_owner = true
position = Vector2i(0, 36)
size = Vector2i(400, 100)
always_on_top = true
ok_button_text = "Delete Permanently"
dialog_text = "Are you sure you want to delete this extension?"
dialog_autowrap = true
[node name="ResetOptionsConfirmation" type="ConfirmationDialog" parent="."]
always_on_top = true
dialog_text = "Are you sure you want to reset the selected options? There will be no way to undo this."
[connection signal="about_to_popup" from="." to="." method="_on_PreferencesDialog_about_to_show"]

View file

@ -0,0 +1,67 @@
shader_type canvas_item;
// CurveTexture(s)
uniform sampler2D curve_rgb;
uniform sampler2D curve_red;
uniform sampler2D curve_green;
uniform sampler2D curve_blue;
uniform sampler2D curve_alpha;
uniform sampler2D curve_hue;
uniform sampler2D curve_sat;
uniform sampler2D curve_value;
uniform sampler2D selection : filter_nearest;
vec3 rgb2hsb(vec3 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz),
vec4(c.gb, K.xy),
step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r),
vec4(c.r, p.yzx),
step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)),
d / (q.x + e),
q.x);
}
vec3 hsb2rgb(vec3 c)
{
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void fragment() {
vec4 original_color = texture(TEXTURE, UV);
vec4 selection_color = texture(selection, UV);
vec4 col;
float red_curve_color = texture(curve_red, vec2(COLOR.r, 0.0)).r;
float green_curve_color = texture(curve_green, vec2(COLOR.g, 0.0)).r;
float blue_curve_color = texture(curve_blue, vec2(COLOR.b, 0.0)).r;
float alpha_curve_color = texture(curve_alpha, vec2(COLOR.a, 0.0)).r;
col.r = red_curve_color;
col.g = green_curve_color;
col.b = blue_curve_color;
col.a = alpha_curve_color;
vec3 hsb = rgb2hsb(col.rgb);
float hue_curve_color = texture(curve_hue, vec2(hsb.r, 0.0)).r;
float sat_curve_color = texture(curve_sat, vec2(hsb.g, 0.0)).r;
float value_curve_color = texture(curve_value, vec2(hsb.b, 0.0)).r;
hsb.r = hue_curve_color;
hsb.g = sat_curve_color;
hsb.b = value_curve_color;
col.rgb = hsb2rgb(hsb);
float rgb_curve_color_r = texture(curve_rgb, vec2(col.r, 0.0)).r;
float rgb_curve_color_g = texture(curve_rgb, vec2(col.g, 0.0)).r;
float rgb_curve_color_b = texture(curve_rgb, vec2(col.b, 0.0)).r;
col.rgb = vec3(rgb_curve_color_r, rgb_curve_color_g, rgb_curve_color_b);
vec4 output = mix(original_color.rgba, col, selection_color.a);
COLOR = output;
}

View file

@ -212,8 +212,7 @@ func update_brush() -> void:
$DensityValueSlider.visible = _brush.type not in IMAGE_BRUSHES
$ColorInterpolation.visible = _brush.type in IMAGE_BRUSHES
$RotationOptions.visible = _brush.type in IMAGE_BRUSHES
var canvas_indicators := Global.canvas.indicators
canvas_indicators.queue_redraw()
Global.canvas.indicators.queue_redraw()
func update_random_image() -> void:
@ -378,6 +377,8 @@ func _prepare_tool() -> void:
func _draw_tool(pos: Vector2) -> PackedVector2Array:
if !Global.current_project.layers[Global.current_project.current_layer].can_layer_get_drawn():
return PackedVector2Array() # empty fallback
if Tools.is_placing_tiles():
return _compute_draw_tool_pixel(pos)
match _brush.type:
Brushes.PIXEL:
return _compute_draw_tool_pixel(pos)
@ -428,9 +429,12 @@ func draw_fill_gap(start: Vector2i, end: Vector2i) -> void:
## Compute the array of coordinates that should be drawn
func _compute_draw_tool_pixel(pos: Vector2) -> PackedVector2Array:
var brush_size := _brush_size_dynamics
if Tools.is_placing_tiles():
brush_size = 1
var result := PackedVector2Array()
var start := pos - Vector2.ONE * (_brush_size_dynamics >> 1)
var end := start + Vector2.ONE * _brush_size_dynamics
var start := pos - Vector2.ONE * (brush_size >> 1)
var end := start + Vector2.ONE * brush_size
for y in range(start.y, end.y):
for x in range(start.x, end.x):
result.append(Vector2(x, y))

View file

@ -139,7 +139,7 @@ func text_to_pixels() -> void:
var text := text_edit.text
var color := tool_slot.color
var font_ascent := font.get_ascent(text_size)
var pos := Vector2(1, font_ascent + text_edit.get_theme_constant(&"line_spacing"))
var pos := Vector2(0, font_ascent + text_edit.get_theme_constant(&"line_spacing"))
pos += text_edit.position
font.draw_multiline_string(ci_rid, pos, text, horizontal_alignment, -1, text_size, -1, color)

View file

@ -0,0 +1,50 @@
extends ImageEffect
enum Channel { RGB, RED, GREEN, BLUE, ALPHA, HUE, SATURATION, VALUE }
const SHADER := preload("res://src/Shaders/Effects/ColorCurves.gdshader")
var curves: Array[Curve]
@onready var channel_option_button := %ChannelOptionButton as OptionButton
@onready var curve_edit := $VBoxContainer/CurveEdit as CurveEdit
func _ready() -> void:
super._ready()
var sm := ShaderMaterial.new()
sm.shader = SHADER
preview.set_material(sm)
for i in channel_option_button.item_count:
var curve := Curve.new()
curve.add_point(Vector2.ZERO, 0, 1, Curve.TANGENT_LINEAR)
curve.add_point(Vector2.ONE, 1, 0, Curve.TANGENT_LINEAR)
curves.append(curve)
curve_edit.curve = curves[Channel.RGB]
func commit_action(cel: Image, project := Global.current_project) -> void:
var selection_tex: ImageTexture
if selection_checkbox.button_pressed and project.has_selection:
var selection := project.selection_map.return_cropped_copy(project.size)
selection_tex = ImageTexture.create_from_image(selection)
var params := {
"curve_rgb": CurveEdit.to_texture(curves[Channel.RGB]),
"curve_red": CurveEdit.to_texture(curves[Channel.RED]),
"curve_green": CurveEdit.to_texture(curves[Channel.GREEN]),
"curve_blue": CurveEdit.to_texture(curves[Channel.BLUE]),
"curve_alpha": CurveEdit.to_texture(curves[Channel.ALPHA]),
"curve_hue": CurveEdit.to_texture(curves[Channel.HUE]),
"curve_sat": CurveEdit.to_texture(curves[Channel.SATURATION]),
"curve_value": CurveEdit.to_texture(curves[Channel.VALUE]),
"selection": selection_tex
}
if !has_been_confirmed:
for param in params:
preview.material.set_shader_parameter(param, params[param])
else:
var gen := ShaderImageEffect.new()
gen.generate_image(cel, SHADER, params, project.size)
func _on_channel_option_button_item_selected(index: int) -> void:
curve_edit.curve = curves[index]

View file

@ -0,0 +1,61 @@
[gd_scene load_steps=5 format=3 uid="uid://cthknpr74lawl"]
[ext_resource type="PackedScene" uid="uid://bybqhhayl5ay5" path="res://src/UI/Dialogs/ImageEffects/ImageEffectParent.tscn" id="1_4g7xo"]
[ext_resource type="Script" path="res://src/UI/Dialogs/ImageEffects/ColorCurvesDialog.gd" id="2_xkivc"]
[ext_resource type="Script" path="res://src/UI/Nodes/CurveEditor/CurveEdit.gd" id="3_3yyhs"]
[sub_resource type="Curve" id="Curve_gvi51"]
_data = [Vector2(0, 0), 0.0, 1.0, 0, 1, Vector2(1, 1), 1.0, 0.0, 1, 0]
point_count = 2
[node name="ColorCurvesDialog" instance=ExtResource("1_4g7xo")]
title = "Color Curves"
size = Vector2i(362, 481)
script = ExtResource("2_xkivc")
[node name="VBoxContainer" parent="." index="3"]
offset_bottom = 432.0
[node name="ShowAnimate" parent="VBoxContainer" index="0"]
visible = false
[node name="ColorOptions" type="GridContainer" parent="VBoxContainer" index="3"]
layout_mode = 2
columns = 2
[node name="ChannelLabel" type="Label" parent="VBoxContainer/ColorOptions" index="0"]
layout_mode = 2
size_flags_horizontal = 3
text = "Channel:"
[node name="ChannelOptionButton" type="OptionButton" parent="VBoxContainer/ColorOptions" index="1"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
selected = 0
item_count = 8
popup/item_0/text = "RGB"
popup/item_1/text = "Red"
popup/item_1/id = 1
popup/item_2/text = "Green"
popup/item_2/id = 2
popup/item_3/text = "Blue"
popup/item_3/id = 3
popup/item_4/text = "Alpha"
popup/item_4/id = 4
popup/item_5/text = "Hue"
popup/item_5/id = 5
popup/item_6/text = "Saturation"
popup/item_6/id = 6
popup/item_7/text = "Value"
popup/item_7/id = 7
[node name="CurveEdit" type="VBoxContainer" parent="VBoxContainer" index="4"]
custom_minimum_size = Vector2(32, 150)
layout_mode = 2
size_flags_vertical = 3
script = ExtResource("3_3yyhs")
curve = SubResource("Curve_gvi51")
[connection signal="item_selected" from="VBoxContainer/ColorOptions/ChannelOptionButton" to="." method="_on_channel_option_button_item_selected"]

View file

@ -3,8 +3,7 @@ extends ImageEffect
var shader: Shader
var params := {}
@onready var shader_loaded_label: Label = $VBoxContainer/ShaderLoadedLabel
@onready var shader_params: BoxContainer = $VBoxContainer/ShaderParams
@onready var shader_params := $VBoxContainer/ShaderParams as VBoxContainer
func _about_to_popup() -> void:
@ -17,36 +16,22 @@ func _about_to_popup() -> void:
super._about_to_popup()
func commit_action(cel: Image, project := Global.current_project) -> void:
if !shader:
return
func set_nodes() -> void:
aspect_ratio_container = $VBoxContainer/AspectRatioContainer
preview = $VBoxContainer/AspectRatioContainer/Preview
func commit_action(cel: Image, project := Global.current_project) -> void:
if not is_instance_valid(shader):
return
var gen := ShaderImageEffect.new()
gen.generate_image(cel, shader, params, project.size)
func _on_ChooseShader_pressed() -> void:
if OS.get_name() == "Web":
Html5FileExchange.load_shader()
else:
$FileDialog.popup_centered(Vector2(300, 340))
func _on_FileDialog_file_selected(path: String) -> void:
var shader_tmp = load(path)
if !shader_tmp is Shader:
return
change_shader(shader_tmp, path.get_file().get_basename())
func set_nodes() -> void:
preview = $VBoxContainer/AspectRatioContainer/Preview
func change_shader(shader_tmp: Shader, shader_name: String) -> void:
shader = shader_tmp
preview.material.shader = shader_tmp
shader_loaded_label.text = tr("Shader loaded:") + " " + shader_name
title = shader_name
params.clear()
for child in shader_params.get_children():
child.queue_free()

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=4 format=3 uid="uid://bkr47ocij684y"]
[gd_scene load_steps=4 format=3 uid="uid://b1ola6loro5m7"]
[ext_resource type="Script" path="res://src/UI/Dialogs/ImageEffects/ShaderEffect.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://3pmb60gpst7b" path="res://src/UI/Nodes/TransparentChecker.tscn" id="2"]
@ -6,6 +6,8 @@
[sub_resource type="ShaderMaterial" id="1"]
[node name="ShaderEffect" type="ConfirmationDialog"]
position = Vector2i(0, 36)
size = Vector2i(612, 350)
script = ExtResource("1")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
@ -15,17 +17,14 @@ anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -36.0
[node name="Label" type="Label" parent="VBoxContainer"]
layout_mode = 2
text = "This is an experimental feature and may not be included in the stable version"
offset_bottom = -49.0
[node name="AspectRatioContainer" type="AspectRatioContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="Preview" type="TextureRect" parent="VBoxContainer/AspectRatioContainer"]
texture_filter = 1
material = SubResource("1")
custom_minimum_size = Vector2(200, 200)
layout_mode = 2
@ -39,22 +38,5 @@ anchors_preset = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="ChooseShader" type="Button" parent="VBoxContainer"]
layout_mode = 2
mouse_default_cursor_shape = 2
text = "Choose Shader"
[node name="ShaderLoadedLabel" type="Label" parent="VBoxContainer"]
layout_mode = 2
text = "No shader loaded!"
[node name="ShaderParams" type="VBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="FileDialog" type="FileDialog" parent="." groups=["FileDialogs"]]
access = 2
filters = PackedStringArray("*gdshader; Godot Shader File")
show_hidden_files = true
[connection signal="pressed" from="VBoxContainer/ChooseShader" to="." method="_on_ChooseShader_pressed"]
[connection signal="file_selected" from="FileDialog" to="." method="_on_FileDialog_file_selected"]

View file

@ -16,6 +16,7 @@ resource_name = "Layout"
script = ExtResource("3")
root = SubResource("1")
hidden_tabs = {}
windows = {}
save_on_change = false
[node name="ManageLayouts" type="AcceptDialog"]
@ -110,6 +111,7 @@ disabled = true
text = "Delete"
[node name="LayoutSettings" type="ConfirmationDialog" parent="."]
always_on_top = true
[node name="VBoxContainer" type="VBoxContainer" parent="LayoutSettings"]
offset_right = 40.0
@ -131,12 +133,12 @@ text = "Copy from"
[node name="LayoutFrom" type="OptionButton" parent="LayoutSettings/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
item_count = 1
selected = 0
item_count = 1
popup/item_0/text = "Current layout"
popup/item_0/id = 0
[node name="DeleteConfirmation" type="ConfirmationDialog" parent="."]
always_on_top = true
dialog_text = "Are you sure you want to delete this layout?"
[connection signal="about_to_popup" from="." to="." method="_on_ManageLayouts_about_to_show"]

View file

@ -194,7 +194,7 @@ func _on_ProgressTimer_timeout() -> void:
update_progress()
func _manage_enlarded_thumbnail_close() -> void:
func _manage_enlarged_thumbnail_close() -> void:
enlarged_picture.get_parent().hide()

View file

@ -93,6 +93,7 @@ unique_name_in_owner = true
[node name="Alert" type="AcceptDialog" parent="."]
unique_name_in_owner = true
size = Vector2i(421, 106)
always_on_top = true
[node name="Text" type="Label" parent="Alert"]
anchors_preset = 15
@ -104,15 +105,16 @@ offset_right = -8.0
offset_bottom = -49.0
horizontal_alignment = 1
[node name="EnlardedThumbnail" type="Window" parent="."]
[node name="EnlargedThumbnail" type="Window" parent="."]
title = "Image"
position = Vector2i(0, 36)
size = Vector2i(440, 360)
visible = false
transient = true
exclusive = true
always_on_top = true
[node name="Enlarged" type="TextureRect" parent="EnlardedThumbnail"]
[node name="Enlarged" type="TextureRect" parent="EnlargedThumbnail"]
unique_name_in_owner = true
anchors_preset = 15
anchor_right = 1.0
@ -131,5 +133,5 @@ stretch_mode = 5
[connection signal="request_completed" from="DownloadRequest" to="." method="_on_DownloadRequest_request_completed"]
[connection signal="close_requested" from="Alert" to="." method="_manage_alert_close"]
[connection signal="focus_exited" from="Alert" to="." method="_manage_alert_close"]
[connection signal="close_requested" from="EnlardedThumbnail" to="." method="_manage_enlarded_thumbnail_close"]
[connection signal="focus_exited" from="EnlardedThumbnail" to="." method="_manage_enlarded_thumbnail_close"]
[connection signal="close_requested" from="EnlargedThumbnail" to="." method="_manage_enlarged_thumbnail_close"]
[connection signal="focus_exited" from="EnlargedThumbnail" to="." method="_manage_enlarged_thumbnail_close"]

View file

@ -12,6 +12,7 @@ size = Vector2i(760, 470)
visible = false
wrap_controls = true
exclusive = true
always_on_top = true
script = ExtResource("1_pwcwi")
[node name="TabContainer" type="TabContainer" parent="."]
@ -24,9 +25,11 @@ offset_left = 7.0
offset_top = 8.0
offset_right = -7.0
offset_bottom = -8.0
current_tab = 0
[node name="Store" type="MarginContainer" parent="TabContainer"]
layout_mode = 2
metadata/_tab_index = 0
[node name="StoreMainContainer" type="HSplitContainer" parent="TabContainer/Store"]
layout_mode = 2
@ -68,6 +71,7 @@ size_flags_horizontal = 3
[node name="Options" type="MarginContainer" parent="TabContainer"]
visible = false
layout_mode = 2
metadata/_tab_index = 1
[node name="ScrollContainer" type="ScrollContainer" parent="TabContainer/Options"]
layout_mode = 2
@ -153,6 +157,7 @@ unique_name_in_owner = true
position = Vector2i(0, 36)
size = Vector2i(511, 326)
unresizable = true
always_on_top = true
popup_window = true
[node name="Content" type="VBoxContainer" parent="Error"]
@ -200,6 +205,7 @@ autowrap_mode = 3
unique_name_in_owner = true
position = Vector2i(0, 36)
size = Vector2i(357, 110)
always_on_top = true
popup_window = true
[node name="Content" type="VBoxContainer" parent="ErrorCustom"]

View file

@ -0,0 +1,103 @@
# Code taken and modified from Material Maker, licensed under MIT
# gdlint: ignore=max-line-length
# https://github.com/RodZill4/material-maker/blob/master/material_maker/widgets/curve_edit/control_point.gd
class_name CurveEditControlPoint
extends Control
signal moved(index: int)
signal removed(index: int)
const OFFSET := Vector2(3, 3)
var moving := false
var min_x: float
var max_x: float
var min_y: float
var max_y: float
var left_slope: CurveEditTangentPoint
var right_slope: CurveEditTangentPoint
@onready var parent := get_parent() as Control
@onready var grandparent := parent.get_parent() as CurveEdit
func _ready() -> void:
gui_input.connect(_on_gui_input)
custom_minimum_size = Vector2(8, 8)
left_slope = CurveEditTangentPoint.new()
right_slope = CurveEditTangentPoint.new()
add_child(left_slope)
add_child(right_slope)
func _draw() -> void:
var color := Color.GRAY
var current_scene := get_tree().current_scene
if current_scene is Control:
var current_theme := (current_scene as Control).theme
color = current_theme.get_color("font_color", "Label")
for c: CurveEditTangentPoint in get_children():
if c.visible:
draw_line(OFFSET, c.position + OFFSET, color)
draw_rect(Rect2(0, 0, 7, 7), color)
func initialize(curve: Curve, index: int) -> void:
if not is_instance_valid(parent):
await ready
position = grandparent.transform_point(curve.get_point_position(index)) - OFFSET
var left_tangent := curve.get_point_left_tangent(index)
var right_tangent := curve.get_point_right_tangent(index)
if left_tangent != INF:
left_slope.position = (
left_slope.distance * (parent.size * Vector2(1.0, -left_tangent)).normalized()
)
if right_tangent != INF:
right_slope.position = (
right_slope.distance * (parent.size * Vector2(1.0, -right_tangent)).normalized()
)
func set_control_point_visibility(left: bool, new_visible: bool) -> void:
if not is_instance_valid(left_slope):
await ready
if left:
left_slope.visible = new_visible
else:
right_slope.visible = new_visible
func set_constraint(new_min_x: float, new_max_x: float, new_min_y: float, new_max_y: float) -> void:
min_x = new_min_x
max_x = new_max_x
min_y = new_min_y
max_y = new_max_y
func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
moving = true
else:
moving = false
grandparent.update_controls()
elif event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
removed.emit(get_index())
elif moving and event is InputEventMouseMotion:
position += event.relative
if position.x < min_x:
position.x = min_x
elif position.x > max_x:
position.x = max_x
if position.y < min_y:
position.y = min_y
elif position.y > max_y:
position.y = max_y
moved.emit(get_index())
func update_tangents() -> void:
queue_redraw()
moved.emit(get_index())

View file

@ -0,0 +1,273 @@
# Code taken and modified from Material Maker, licensed under MIT
# gdlint: ignore=max-line-length
# https://github.com/RodZill4/material-maker/blob/master/material_maker/widgets/curve_edit/curve_view.gd
# and
# gdlint: ignore=max-line-length
# https://github.com/RodZill4/material-maker/blob/master/material_maker/widgets/curve_edit/curve_editor.gd
@tool
class_name CurveEdit
extends VBoxContainer
signal value_changed(value: Curve)
@export var show_axes := true
@export var curve: Curve:
set(value):
curve = value
queue_redraw()
update_controls()
## Array of dictionaries of key [String] and value [Array] of type [CurveEdit.CurvePoint].
var presets: Array[Dictionary] = [
{"Linear": [CurvePoint.new(0.0, 0.0, 0.0, 1.0), CurvePoint.new(1.0, 1.0, 1.0, 0.0)]},
{
"Ease out":
[
CurvePoint.new(0.0, 0.0, 0.0, 4.0),
CurvePoint.new(0.292893, 0.707107, 1.0, 1.0),
CurvePoint.new(1.0, 1.0, 0.0, 0.0)
]
},
{
"Ease in out":
[
CurvePoint.new(0.0, 0.0, 0.0, 0.0),
CurvePoint.new(0.5, 0.5, 3.0, 3.0),
CurvePoint.new(1.0, 1.0, 0.0, 0.0)
]
},
{
"Ease in":
[
CurvePoint.new(0.0, 0.0, 0.0, 0.0),
CurvePoint.new(0.707107, 0.292893, 1.0, 1.0),
CurvePoint.new(1.0, 1.0, 4.0, 0.0)
]
},
{
"Sawtooth":
[
CurvePoint.new(0.0, 0.0, 0.0, 2.0),
CurvePoint.new(0.5, 1.0, 2.0, -2.0),
CurvePoint.new(1.0, 0.0, -2.0, 0.0)
]
},
{
"Bounce":
[
CurvePoint.new(0.0, 0.0, 0.0, 5.0),
CurvePoint.new(0.15, 0.65, 2.45201, 2.45201),
CurvePoint.new(0.5, 1.0, 0.0, 0.0),
CurvePoint.new(0.85, 0.65, -2.45201, -2.45201),
CurvePoint.new(1.0, 0.0, -5.0, 0.0)
]
},
{
"Bevel":
[
CurvePoint.new(0.0, 0.0, 0.0, 2.38507),
CurvePoint.new(0.292893, 0.707107, 2.34362, 0.428147),
CurvePoint.new(1.0, 1.0, 0.410866, 0.0)
]
}
]
var curve_editor := Control.new()
var hbox := HBoxContainer.new()
class CurvePoint:
var pos: Vector2
var left_tangent: float
var right_tangent: float
func _init(x: float, y: float, _left_tangent := 0.0, _right_tangent := 0.0) -> void:
pos = Vector2(x, y)
left_tangent = _left_tangent
right_tangent = _right_tangent
func _ready() -> void:
if not is_instance_valid(curve):
curve = Curve.new()
if custom_minimum_size.is_zero_approx():
custom_minimum_size = Vector2(32, 150)
curve_editor.gui_input.connect(_on_gui_input)
resized.connect(_on_resize)
curve_editor.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(curve_editor)
add_child(hbox)
var presets_button := MenuButton.new()
presets_button.text = "Presets"
presets_button.flat = false
presets_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
presets_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
presets_button.get_popup().id_pressed.connect(_on_presets_item_selected)
for preset in presets:
presets_button.get_popup().add_item(preset.keys()[0])
var invert_button := Button.new()
invert_button.text = "Invert"
invert_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
invert_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
invert_button.pressed.connect(_on_invert_button_pressed)
hbox.add_child(presets_button)
hbox.add_child(invert_button)
_on_resize.call_deferred()
func update_controls() -> void:
for c in curve_editor.get_children():
if c is CurveEditControlPoint:
c.queue_free()
for i in curve.point_count:
var p := curve.get_point_position(i)
var control_point := CurveEditControlPoint.new()
curve_editor.add_child(control_point)
control_point.initialize(curve, i)
control_point.position = transform_point(p) - control_point.OFFSET
if i == 0 or i == curve.point_count - 1:
control_point.set_constraint(
control_point.position.x,
control_point.position.x,
-control_point.OFFSET.y,
available_size().y - control_point.OFFSET.y
)
if i == 0:
control_point.set_control_point_visibility(true, false)
else:
control_point.set_control_point_visibility(false, false)
else:
var min_x := transform_point(curve.get_point_position(i - 1)).x + 1
var max_x := transform_point(curve.get_point_position(i + 1)).x - 1
control_point.set_constraint(
min_x, max_x, -control_point.OFFSET.y, available_size().y - control_point.OFFSET.y
)
control_point.moved.connect(_on_control_point_moved)
control_point.removed.connect(_on_control_point_removed)
value_changed.emit(curve)
static func to_texture(from_curve: Curve, width := 256) -> CurveTexture:
var texture := CurveTexture.new()
texture.texture_mode = CurveTexture.TEXTURE_MODE_RED
texture.curve = from_curve
texture.width = width
return texture
func set_default_curve() -> void:
if not is_instance_valid(curve):
curve = Curve.new()
_on_presets_item_selected(0)
func available_size() -> Vector2:
if curve_editor.size.is_zero_approx():
return Vector2.ONE
return curve_editor.size
func transform_point(p: Vector2) -> Vector2:
return (Vector2(0.0, 1.0) + Vector2(1.0, -1.0) * p) * available_size()
func reverse_transform_point(p: Vector2) -> Vector2:
return Vector2(0.0, 1.0) + Vector2(1.0, -1.0) * p / available_size()
func _draw() -> void:
var bg := Color.DARK_GRAY
var fg := Color.GRAY
var current_scene := get_tree().current_scene
if current_scene is Control:
var current_theme := (current_scene as Control).theme
var panel_stylebox := current_theme.get_stylebox("panel", "Panel")
if panel_stylebox is StyleBoxFlat:
bg = panel_stylebox.bg_color
fg = current_theme.get_color("font_color", "Label")
var axes_color := bg.lerp(fg, 0.25)
var curve_color := bg.lerp(fg, 0.75)
if show_axes:
for i in range(5):
var p := transform_point(0.25 * Vector2(i, i))
draw_line(Vector2(p.x, 0), Vector2(p.x, available_size().y - 1), axes_color)
draw_line(Vector2(0, p.y), Vector2(available_size().x - 1, p.y), axes_color)
var points := PackedVector2Array()
for i in range(curve.point_count - 1):
var p1 := curve.get_point_position(i)
var p2 := curve.get_point_position(i + 1)
var d := (p2.x - p1.x) / 3.0
var yac := p1.y + d * curve.get_point_right_tangent(i)
var ybc := p2.y - d * curve.get_point_left_tangent(i + 1)
var p := transform_point(p1)
if points.is_empty():
points.push_back(p)
var count := maxi(1, transform_point(p2).x - p.x / 5.0)
for tt in range(count):
var t := (tt + 1.0) / count
var omt := 1.0 - t
var omt2 := omt * omt
var omt3 := omt2 * omt
var t2 := t * t
var t3 := t2 * t
var x := p1.x + (p2.x - p1.x) * t
var y := p1.y * omt3 + yac * omt2 * t * 3.0 + ybc * omt * t2 * 3.0 + p2.y * t3
p = transform_point(Vector2(x, y))
points.push_back(p)
draw_polyline(points, curve_color)
func _on_control_point_moved(index: int) -> void:
var control_point := curve_editor.get_child(index) as CurveEditControlPoint
var new_point := reverse_transform_point(control_point.position + control_point.OFFSET)
curve.set_point_offset(index, new_point.x)
curve.set_point_value(index, new_point.y)
if is_instance_valid(control_point.left_slope):
var slope_vector := control_point.left_slope.position / available_size()
if slope_vector.x != 0:
curve.set_point_left_tangent(index, -slope_vector.y / slope_vector.x)
if is_instance_valid(control_point.right_slope):
var slope_vector := control_point.right_slope.position / available_size()
if slope_vector.x != 0:
curve.set_point_right_tangent(index, -slope_vector.y / slope_vector.x)
queue_redraw()
value_changed.emit(curve)
func _on_control_point_removed(index: int) -> void:
if index > 0 and index < curve.point_count:
curve.remove_point(index)
queue_redraw()
update_controls()
func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
var new_point_position := reverse_transform_point(get_local_mouse_position())
curve.add_point(new_point_position, 0.0, 0.0)
update_controls()
func _on_resize() -> void:
queue_redraw()
update_controls()
func _on_presets_item_selected(index: int) -> void:
curve.clear_points()
var preset_points: Array = presets[index].values()[0]
for point: CurvePoint in preset_points:
curve.add_point(point.pos, point.left_tangent, point.right_tangent)
curve = curve # Call setter
func _on_invert_button_pressed() -> void:
var copy_curve := curve.duplicate() as Curve
curve.clear_points()
for i in copy_curve.point_count:
var point := copy_curve.get_point_position(i)
point.y = 1.0 - point.y
var left_tangent := -copy_curve.get_point_left_tangent(i)
var right_tangent := -copy_curve.get_point_right_tangent(i)
curve.add_point(point, left_tangent, right_tangent)
curve = curve # Call setter

View file

@ -0,0 +1,62 @@
# Code taken and modified from Material Maker, licensed under MIT
# gdlint: ignore=max-line-length
# https://github.com/RodZill4/material-maker/blob/master/material_maker/widgets/curve_edit/slope_point.gd
class_name CurveEditTangentPoint
extends Control
const OFFSET := Vector2(0, 0)
@export var distance := 30
var moving = false
@onready var parent := get_parent() as CurveEditControlPoint
@onready var grandparent := parent.get_parent() as Control
func _ready() -> void:
gui_input.connect(_on_gui_input)
custom_minimum_size = Vector2(8, 8)
if get_index() == 0:
distance = -distance
func _draw() -> void:
var color := Color.GRAY
var current_scene := get_tree().current_scene
if current_scene is Control:
var current_theme := (current_scene as Control).theme
color = current_theme.get_color("font_color", "Label")
draw_circle(Vector2(3.0, 3.0), 3.0, color)
func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
if event.double_click:
var vector: Vector2
if get_index() == 0:
vector = (
parent.position - grandparent.get_child(parent.get_index() - 1).position
)
else:
vector = (
grandparent.get_child(parent.get_index() + 1).position - parent.position
)
vector = distance * vector.normalized()
position = vector - OFFSET
if event.is_control_or_command_pressed():
parent.get_child(1 - get_index()).position = -vector - OFFSET
parent.update_tangents()
else:
moving = true
else:
moving = false
elif moving and event is InputEventMouseMotion:
var vector := get_global_mouse_position() - parent.get_global_rect().position + OFFSET
vector *= signf(vector.x)
vector = distance * vector.normalized()
position = vector - OFFSET
if event.is_command_or_control_pressed():
parent.get_child(1 - get_index()).position = -vector - OFFSET
parent.update_tangents()

View file

@ -5,15 +5,18 @@ var font: Font:
set(value):
font = value
add_theme_font_override(&"font", font)
var _border_node := Control.new()
func _ready() -> void:
Global.camera.zoom_changed.connect(func(): _border_node.queue_redraw())
_border_node.draw.connect(_on_border_redraw)
_border_node.set_anchors_preset(Control.PRESET_FULL_RECT)
_border_node.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_border_node)
caret_blink = true
var stylebox := StyleBoxFlat.new()
stylebox.draw_center = false
stylebox.border_width_left = 1
stylebox.border_width_top = 1
stylebox.border_width_right = 1
stylebox.border_width_bottom = 1
add_theme_stylebox_override(&"normal", stylebox)
add_theme_stylebox_override(&"focus", stylebox)
add_theme_constant_override(&"line_spacing", 0)
@ -44,3 +47,9 @@ func _on_text_changed() -> void:
var string_size := font.get_string_size(max_line, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size)
size.x = font_size + string_size.x
size.y = (get_line_count() + 1) * font.get_height(font_size)
_border_node.queue_redraw()
func _on_border_redraw() -> void:
var border_width := (1.0 / Global.camera.zoom.x) * 2.0 + 1.0
_border_node.draw_rect(_border_node.get_rect(), Color.WHITE, false, border_width)

View file

@ -47,9 +47,10 @@ var button_size := 36:
button.custom_minimum_size = Vector2(button_size, button_size)
button.size = Vector2(button_size, button_size)
@onready var place_tiles: CheckBox = $VBoxContainer/PlaceTiles
@onready var transform_buttons_container: HFlowContainer = $VBoxContainer/TransformButtonsContainer
@onready var place_tiles: Button = %PlaceTiles
@onready var transform_buttons_container: HFlowContainer = %TransformButtonsContainer
@onready var tile_button_container: HFlowContainer = %TileButtonContainer
@onready var mode_buttons_container: HFlowContainer = %ModeButtonsContainer
func _ready() -> void:
@ -57,6 +58,7 @@ func _ready() -> void:
Global.cel_switched.connect(_on_cel_switched)
for child: Button in transform_buttons_container.get_children():
Global.disable_button(child, true)
update_tip()
func _gui_input(event: InputEvent) -> void:
@ -80,6 +82,15 @@ func set_tileset(tileset: TileSetCustom) -> void:
current_tileset.updated.connect(_update_tileset)
func update_tip():
var tip = %Tip
tip.get_parent().visible = true
if placing_tiles:
tip.text = "Select a tile to place it on the canvas."
else:
tip.text = "Modify tiles on the canvas."
func _on_cel_switched() -> void:
if Global.current_project.get_current_cel() is not CelTileMap:
set_tileset(null)
@ -157,8 +168,15 @@ func _clear_tile_buttons() -> void:
func _on_place_tiles_toggled(toggled_on: bool) -> void:
placing_tiles = toggled_on
transform_buttons_container.visible = placing_tiles
mode_buttons_container.visible = !placing_tiles
for child: Button in transform_buttons_container.get_children():
Global.disable_button(child, not toggled_on)
if toggled_on:
Global.change_button_texturerect(place_tiles.get_child(0), "place_tiles_enabled.png")
else:
Global.change_button_texturerect(place_tiles.get_child(0), "place_tiles_disabled.png")
update_tip()
func _on_manual_toggled(toggled_on: bool) -> void:

View file

@ -1,6 +1,7 @@
[gd_scene load_steps=22 format=3 uid="uid://bfbragmmdwfbl"]
[gd_scene load_steps=23 format=3 uid="uid://bfbragmmdwfbl"]
[ext_resource type="Script" path="res://src/UI/TilesPanel.gd" id="1_d2oc5"]
[ext_resource type="Texture2D" uid="uid://bqr3n3tm8b6w2" path="res://assets/graphics/tileset/place_tiles_disabled.png" id="2_pv2cd"]
[ext_resource type="Texture2D" uid="uid://bv7ldl8obhawm" path="res://assets/graphics/misc/icon_reload.png" id="2_r1kie"]
[ext_resource type="Texture2D" uid="uid://bpsfilx47bw3r" path="res://assets/graphics/misc/mirror_x.svg" id="3_5o62r"]
[ext_resource type="Texture2D" uid="uid://bk6iaxiyl74ih" path="res://assets/graphics/misc/mirror_y.svg" id="4_2xhnr"]
@ -66,23 +67,52 @@ script = ExtResource("1_d2oc5")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
[node name="PlaceTiles" type="CheckBox" parent="VBoxContainer"]
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/MarginContainer"]
layout_mode = 2
[node name="Buttons" type="HBoxContainer" parent="VBoxContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="PlaceTiles" type="Button" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons" groups=["UIButtons"]]
unique_name_in_owner = true
custom_minimum_size = Vector2(25, 25)
layout_mode = 2
size_flags_vertical = 0
mouse_default_cursor_shape = 2
toggle_mode = true
shortcut = SubResource("Shortcut_6ebuw")
text = "Draw tiles"
[node name="TransformButtonsContainer" type="HFlowContainer" parent="VBoxContainer"]
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons/PlaceTiles"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("2_pv2cd")
expand_mode = 1
stretch_mode = 5
[node name="VSeparator" type="VSeparator" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons"]
layout_mode = 2
[node name="RotateLeftButton" type="Button" parent="VBoxContainer/TransformButtonsContainer" groups=["UIButtons"]]
[node name="TransformButtonsContainer" type="HFlowContainer" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_horizontal = 3
[node name="RotateLeftButton" type="Button" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons/TransformButtonsContainer" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
tooltip_text = "Rotate tile left (counterclockwise)"
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_yas23")
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/TransformButtonsContainer/RotateLeftButton"]
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons/TransformButtonsContainer/RotateLeftButton"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
@ -92,14 +122,14 @@ grow_vertical = 2
texture = ExtResource("2_r1kie")
stretch_mode = 3
[node name="RotateRightButton" type="Button" parent="VBoxContainer/TransformButtonsContainer" groups=["UIButtons"]]
[node name="RotateRightButton" type="Button" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons/TransformButtonsContainer" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
tooltip_text = "Rotate tile right (clockwise)"
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_cmy2w")
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/TransformButtonsContainer/RotateRightButton"]
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons/TransformButtonsContainer/RotateRightButton"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
@ -110,14 +140,14 @@ texture = ExtResource("2_r1kie")
stretch_mode = 3
flip_h = true
[node name="FlipHorizontalButton" type="Button" parent="VBoxContainer/TransformButtonsContainer" groups=["UIButtons"]]
[node name="FlipHorizontalButton" type="Button" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons/TransformButtonsContainer" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
tooltip_text = "Flip tile horizontally"
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_ouoxo")
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/TransformButtonsContainer/FlipHorizontalButton"]
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons/TransformButtonsContainer/FlipHorizontalButton"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
@ -127,14 +157,14 @@ grow_vertical = 2
texture = ExtResource("3_5o62r")
stretch_mode = 3
[node name="FlipVerticalButton" type="Button" parent="VBoxContainer/TransformButtonsContainer" groups=["UIButtons"]]
[node name="FlipVerticalButton" type="Button" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons/TransformButtonsContainer" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
tooltip_text = "Flip tile vertically"
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_jj4yy")
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/TransformButtonsContainer/FlipVerticalButton"]
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons/TransformButtonsContainer/FlipVerticalButton"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
@ -144,17 +174,19 @@ grow_vertical = 2
texture = ExtResource("4_2xhnr")
stretch_mode = 3
[node name="ModeButtonsContainer" type="HFlowContainer" parent="VBoxContainer"]
[node name="ModeButtonsContainer" type="HFlowContainer" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="Manual" type="CheckBox" parent="VBoxContainer/ModeButtonsContainer"]
[node name="Manual" type="CheckBox" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons/ModeButtonsContainer"]
layout_mode = 2
mouse_default_cursor_shape = 2
button_group = SubResource("ButtonGroup_uxnt0")
shortcut = SubResource("Shortcut_pgg48")
text = "Manual"
[node name="Auto" type="CheckBox" parent="VBoxContainer/ModeButtonsContainer"]
[node name="Auto" type="CheckBox" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons/ModeButtonsContainer"]
layout_mode = 2
mouse_default_cursor_shape = 2
button_pressed = true
@ -162,28 +194,43 @@ button_group = SubResource("ButtonGroup_uxnt0")
shortcut = SubResource("Shortcut_a0fx5")
text = "Auto"
[node name="Stack" type="CheckBox" parent="VBoxContainer/ModeButtonsContainer"]
[node name="Stack" type="CheckBox" parent="VBoxContainer/MarginContainer/VBoxContainer/Buttons/ModeButtonsContainer"]
layout_mode = 2
mouse_default_cursor_shape = 2
button_group = SubResource("ButtonGroup_uxnt0")
shortcut = SubResource("Shortcut_ysxej")
text = "Stack"
[node name="HSeparator" type="HSeparator" parent="VBoxContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="Tip" type="Label" parent="VBoxContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(20, 0)
layout_mode = 2
autowrap_mode = 3
[node name="HSeparator2" type="HSeparator" parent="VBoxContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="TileButtonContainer" type="HFlowContainer" parent="VBoxContainer/ScrollContainer"]
unique_name_in_owner = true
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[connection signal="toggled" from="VBoxContainer/PlaceTiles" to="." method="_on_place_tiles_toggled"]
[connection signal="pressed" from="VBoxContainer/TransformButtonsContainer/RotateLeftButton" to="." method="_on_rotate_pressed" binds= [false]]
[connection signal="pressed" from="VBoxContainer/TransformButtonsContainer/RotateRightButton" to="." method="_on_rotate_pressed" binds= [true]]
[connection signal="pressed" from="VBoxContainer/TransformButtonsContainer/FlipHorizontalButton" to="." method="_on_flip_horizontal_button_pressed"]
[connection signal="pressed" from="VBoxContainer/TransformButtonsContainer/FlipVerticalButton" to="." method="_on_flip_vertical_button_pressed"]
[connection signal="toggled" from="VBoxContainer/ModeButtonsContainer/Manual" to="." method="_on_manual_toggled"]
[connection signal="toggled" from="VBoxContainer/ModeButtonsContainer/Auto" to="." method="_on_auto_toggled"]
[connection signal="toggled" from="VBoxContainer/ModeButtonsContainer/Stack" to="." method="_on_stack_toggled"]
[node name="TileButtonContainer" type="HFlowContainer" parent="VBoxContainer/ScrollContainer/MarginContainer"]
unique_name_in_owner = true
layout_mode = 2
[connection signal="toggled" from="VBoxContainer/MarginContainer/VBoxContainer/Buttons/PlaceTiles" to="." method="_on_place_tiles_toggled"]
[connection signal="pressed" from="VBoxContainer/MarginContainer/VBoxContainer/Buttons/TransformButtonsContainer/RotateLeftButton" to="." method="_on_rotate_pressed" binds= [false]]
[connection signal="pressed" from="VBoxContainer/MarginContainer/VBoxContainer/Buttons/TransformButtonsContainer/RotateRightButton" to="." method="_on_rotate_pressed" binds= [true]]
[connection signal="pressed" from="VBoxContainer/MarginContainer/VBoxContainer/Buttons/TransformButtonsContainer/FlipHorizontalButton" to="." method="_on_flip_horizontal_button_pressed"]
[connection signal="pressed" from="VBoxContainer/MarginContainer/VBoxContainer/Buttons/TransformButtonsContainer/FlipVerticalButton" to="." method="_on_flip_vertical_button_pressed"]
[connection signal="toggled" from="VBoxContainer/MarginContainer/VBoxContainer/Buttons/ModeButtonsContainer/Manual" to="." method="_on_manual_toggled"]
[connection signal="toggled" from="VBoxContainer/MarginContainer/VBoxContainer/Buttons/ModeButtonsContainer/Auto" to="." method="_on_auto_toggled"]
[connection signal="toggled" from="VBoxContainer/MarginContainer/VBoxContainer/Buttons/ModeButtonsContainer/Stack" to="." method="_on_stack_toggled"]

View file

@ -1,7 +1,14 @@
extends Panel
## Emitted when the animation starts playing.
signal animation_started(forward: bool)
## Emitted when the animation reaches the final frame and is not looping,
## or if the animation is manually paused.
## Note: This signal is not emitted if the animation is looping.
signal animation_finished
## Emitted when the animation loops, meaning when it reaches the final frame
## and the animation keeps playing.
signal animation_looped
enum LoopType { NO, CYCLE, PINGPONG }
@ -41,6 +48,7 @@ var global_layer_expand := true
@onready var move_up_layer := %MoveUpLayer as Button
@onready var move_down_layer := %MoveDownLayer as Button
@onready var merge_down_layer := %MergeDownLayer as Button
@onready var layer_fx := %LayerFX as Button
@onready var blend_modes_button := %BlendModes as OptionButton
@onready var opacity_slider := %OpacitySlider as ValueSlider
@onready var frame_scroll_container := %FrameScrollContainer as Control
@ -687,9 +695,11 @@ func _on_AnimationTimer_timeout() -> void:
animation_timer.wait_time = (
project.frames[project.current_frame].duration * (1 / fps)
)
animation_looped.emit()
animation_timer.start()
LoopType.PINGPONG:
animation_forward = false
animation_looped.emit()
_on_AnimationTimer_timeout()
else:
@ -712,9 +722,11 @@ func _on_AnimationTimer_timeout() -> void:
animation_timer.wait_time = (
project.frames[project.current_frame].duration * (1 / fps)
)
animation_looped.emit()
animation_timer.start()
LoopType.PINGPONG:
animation_forward = true
animation_looped.emit()
_on_AnimationTimer_timeout()
frame_scroll_container.ensure_control_visible(
Global.frame_hbox.get_child(project.current_frame)
@ -855,6 +867,8 @@ func _on_add_layer_list_id_pressed(id: int) -> void:
Global.LayerTypes.THREE_D:
layer = Layer3D.new(project)
SteamManager.set_achievement("ACH_3D_LAYER")
Global.LayerTypes.AUDIO:
layer = AudioLayer.new(project)
add_layer(layer, project)
@ -904,6 +918,8 @@ func _on_CloneLayer_pressed() -> void:
cl_layer = LayerTileMap.new(project, src_layer.tileset)
else:
cl_layer = src_layer.get_script().new(project)
if src_layer is AudioLayer:
cl_layer.audio = src_layer.audio
cl_layer.project = project
cl_layer.index = src_layer.index
var src_layer_data: Dictionary = src_layer.serialize()
@ -1191,10 +1207,13 @@ func _toggle_layer_buttons() -> void:
(
project.current_layer == child_count
or layer is GroupLayer
or layer is AudioLayer
or project.layers[project.current_layer - 1] is GroupLayer
or project.layers[project.current_layer - 1] is Layer3D
or project.layers[project.current_layer - 1] is AudioLayer
)
)
Global.disable_button(layer_fx, layer is AudioLayer)
func project_changed() -> void:

View file

@ -240,7 +240,7 @@ offset_left = -22.0
offset_top = -10.0
offset_bottom = 10.0
mouse_default_cursor_shape = 2
item_count = 4
item_count = 5
popup/item_0/text = "Add Pixel Layer"
popup/item_1/text = "Add Group Layer"
popup/item_1/id = 1
@ -248,6 +248,8 @@ popup/item_2/text = "Add 3D Layer"
popup/item_2/id = 2
popup/item_3/text = "Add Tilemap Layer"
popup/item_3/id = 3
popup/item_4/text = "Add Audio Layer"
popup/item_4/id = 4
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/AddLayer/AddLayerList"]
layout_mode = 0
@ -380,6 +382,7 @@ texture = ExtResource("5")
stretch_mode = 3
[node name="LayerFX" type="Button" parent="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons" groups=["UIButtons"]]
unique_name_in_owner = true
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
size_flags_horizontal = 3
@ -424,17 +427,20 @@ vertical_scroll_mode = 0
layout_mode = 2
size_flags_horizontal = 10
[node name="AnimationButtons" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools"]
[node name="MarginContainer" type="MarginContainer" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools"]
layout_mode = 2
[node name="AnimationButtons" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer"]
custom_minimum_size = Vector2(0, 24)
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 24
alignment = 2
[node name="FrameButtons" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons"]
[node name="FrameButtons" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons"]
layout_mode = 2
[node name="AddFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons" groups=["UIButtons"]]
[node name="AddFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
size_flags_horizontal = 0
@ -443,7 +449,7 @@ focus_mode = 0
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_mvoxm")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons/AddFrame"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons/AddFrame"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -455,7 +461,7 @@ offset_right = 6.0
offset_bottom = 6.0
texture = ExtResource("19")
[node name="DeleteFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons" groups=["UIButtons"]]
[node name="DeleteFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons" groups=["UIButtons"]]
unique_name_in_owner = true
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
@ -465,7 +471,7 @@ focus_mode = 0
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_o40ql")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons/DeleteFrame"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons/DeleteFrame"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -479,7 +485,7 @@ size_flags_horizontal = 0
size_flags_vertical = 0
texture = ExtResource("20")
[node name="CopyFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons" groups=["UIButtons"]]
[node name="CopyFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
size_flags_horizontal = 0
@ -488,7 +494,7 @@ focus_mode = 0
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_5g7t7")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons/CopyFrame"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons/CopyFrame"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -500,7 +506,7 @@ offset_right = 5.0
offset_bottom = 7.0
texture = ExtResource("27")
[node name="MoveFrameLeft" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons" groups=["UIButtons"]]
[node name="MoveFrameLeft" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons" groups=["UIButtons"]]
unique_name_in_owner = true
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
@ -510,7 +516,7 @@ focus_mode = 0
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_kwn1u")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons/MoveFrameLeft"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons/MoveFrameLeft"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -523,7 +529,7 @@ offset_bottom = 5.5
texture = ExtResource("8")
flip_h = true
[node name="MoveFrameRight" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons" groups=["UIButtons"]]
[node name="MoveFrameRight" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons" groups=["UIButtons"]]
unique_name_in_owner = true
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
@ -533,7 +539,7 @@ focus_mode = 0
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_m2mvi")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons/MoveFrameRight"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons/MoveFrameRight"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -545,10 +551,10 @@ offset_right = 7.5
offset_bottom = 5.5
texture = ExtResource("8")
[node name="PlaybackButtons" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons"]
[node name="PlaybackButtons" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons"]
layout_mode = 2
[node name="FirstFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons" groups=["UIButtons"]]
[node name="FirstFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
tooltip_text = "Jump to the first frame"
@ -556,7 +562,7 @@ focus_mode = 0
mouse_default_cursor_shape = 2
shortcut = SubResource("4")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons/FirstFrame"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons/FirstFrame"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -568,7 +574,7 @@ offset_right = 5.5
offset_bottom = 6.0
texture = ExtResource("21")
[node name="PreviousFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons" groups=["UIButtons"]]
[node name="PreviousFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
tooltip_text = "Go to the previous frame"
@ -576,7 +582,7 @@ focus_mode = 0
mouse_default_cursor_shape = 2
shortcut = SubResource("6")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons/PreviousFrame"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons/PreviousFrame"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -588,7 +594,7 @@ offset_right = 5.5
offset_bottom = 6.0
texture = ExtResource("23")
[node name="PlayBackwards" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons" groups=["UIButtons"]]
[node name="PlayBackwards" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons" groups=["UIButtons"]]
unique_name_in_owner = true
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
@ -598,7 +604,7 @@ mouse_default_cursor_shape = 2
toggle_mode = true
shortcut = SubResource("8")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons/PlayBackwards"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons/PlayBackwards"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -610,7 +616,7 @@ offset_right = 3.0
offset_bottom = 6.0
texture = ExtResource("24")
[node name="PlayForward" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons" groups=["UIButtons"]]
[node name="PlayForward" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons" groups=["UIButtons"]]
unique_name_in_owner = true
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
@ -621,7 +627,7 @@ mouse_default_cursor_shape = 2
toggle_mode = true
shortcut = SubResource("10")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons/PlayForward"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons/PlayForward"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -633,7 +639,7 @@ offset_right = 3.5
offset_bottom = 6.0
texture = ExtResource("22")
[node name="NextFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons" groups=["UIButtons"]]
[node name="NextFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
tooltip_text = "Go to the next frame"
@ -641,7 +647,7 @@ focus_mode = 0
mouse_default_cursor_shape = 2
shortcut = SubResource("12")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons/NextFrame"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons/NextFrame"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -653,7 +659,7 @@ offset_right = 5.5
offset_bottom = 6.0
texture = ExtResource("26")
[node name="LastFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons" groups=["UIButtons"]]
[node name="LastFrame" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
tooltip_text = "Jump to the last frame"
@ -661,7 +667,7 @@ focus_mode = 0
mouse_default_cursor_shape = 2
shortcut = SubResource("14")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons/LastFrame"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons/LastFrame"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -673,10 +679,10 @@ offset_right = 5.5
offset_bottom = 6.0
texture = ExtResource("25")
[node name="LoopButtons" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons"]
[node name="LoopButtons" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons"]
layout_mode = 2
[node name="TimelineSettingsButton" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/LoopButtons" groups=["UIButtons"]]
[node name="TimelineSettingsButton" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/LoopButtons" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
size_flags_horizontal = 0
@ -685,7 +691,7 @@ focus_mode = 0
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_tke6v")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/LoopButtons/TimelineSettingsButton"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/LoopButtons/TimelineSettingsButton"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -699,7 +705,7 @@ size_flags_horizontal = 0
size_flags_vertical = 0
texture = ExtResource("30")
[node name="OnionSkinning" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/LoopButtons" groups=["UIButtons"]]
[node name="OnionSkinning" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/LoopButtons" groups=["UIButtons"]]
unique_name_in_owner = true
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
@ -708,7 +714,7 @@ focus_mode = 0
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_pouu1")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/LoopButtons/OnionSkinning"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/LoopButtons/OnionSkinning"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -722,7 +728,7 @@ size_flags_horizontal = 0
size_flags_vertical = 0
texture = ExtResource("29")
[node name="LoopAnim" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/LoopButtons" groups=["UIButtons"]]
[node name="LoopAnim" type="Button" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/LoopButtons" groups=["UIButtons"]]
unique_name_in_owner = true
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
@ -731,7 +737,7 @@ focus_mode = 0
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_1onr8")
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/LoopButtons/LoopAnim"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/LoopButtons/LoopAnim"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
@ -743,7 +749,7 @@ offset_right = 7.0
offset_bottom = 7.0
texture = ExtResource("31")
[node name="FPSValue" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/LoopButtons" instance=ExtResource("9")]
[node name="FPSValue" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/LoopButtons" instance=ExtResource("9")]
unique_name_in_owner = true
custom_minimum_size = Vector2(72, 24)
layout_mode = 2
@ -1136,21 +1142,21 @@ color = Color(0, 0.741176, 1, 0.501961)
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/MergeDownLayer" to="." method="_on_MergeDownLayer_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/LayerFX" to="." method="_on_layer_fx_pressed"]
[connection signal="item_selected" from="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/BlendContainer/BlendModes" to="." method="_on_blend_modes_item_selected"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons/AddFrame" to="." method="add_frame"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons/DeleteFrame" to="." method="_on_DeleteFrame_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons/CopyFrame" to="." method="_on_CopyFrame_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons/MoveFrameLeft" to="." method="_on_MoveLeft_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/FrameButtons/MoveFrameRight" to="." method="_on_MoveRight_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons/FirstFrame" to="." method="_on_FirstFrame_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons/PreviousFrame" to="." method="_on_PreviousFrame_pressed"]
[connection signal="toggled" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons/PlayBackwards" to="." method="_on_PlayBackwards_toggled"]
[connection signal="toggled" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons/PlayForward" to="." method="_on_PlayForward_toggled"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons/NextFrame" to="." method="_on_NextFrame_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/PlaybackButtons/LastFrame" to="." method="_on_LastFrame_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/LoopButtons/TimelineSettingsButton" to="." method="_on_timeline_settings_button_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/LoopButtons/OnionSkinning" to="." method="_on_OnionSkinning_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/LoopButtons/LoopAnim" to="." method="_on_LoopAnim_pressed"]
[connection signal="value_changed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/AnimationButtons/LoopButtons/FPSValue" to="." method="_on_FPSValue_value_changed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons/AddFrame" to="." method="add_frame"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons/DeleteFrame" to="." method="_on_DeleteFrame_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons/CopyFrame" to="." method="_on_CopyFrame_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons/MoveFrameLeft" to="." method="_on_MoveLeft_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/FrameButtons/MoveFrameRight" to="." method="_on_MoveRight_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons/FirstFrame" to="." method="_on_FirstFrame_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons/PreviousFrame" to="." method="_on_PreviousFrame_pressed"]
[connection signal="toggled" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons/PlayBackwards" to="." method="_on_PlayBackwards_toggled"]
[connection signal="toggled" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons/PlayForward" to="." method="_on_PlayForward_toggled"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons/NextFrame" to="." method="_on_NextFrame_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/PlaybackButtons/LastFrame" to="." method="_on_LastFrame_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/LoopButtons/TimelineSettingsButton" to="." method="_on_timeline_settings_button_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/LoopButtons/OnionSkinning" to="." method="_on_OnionSkinning_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/LoopButtons/LoopAnim" to="." method="_on_LoopAnim_pressed"]
[connection signal="value_changed" from="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer/AnimationTools/MarginContainer/AnimationButtons/LoopButtons/FPSValue" to="." method="_on_FPSValue_value_changed"]
[connection signal="dragged" from="TimelineContainer/MainBodyPanel/MainBodyVBoxContainer/MarginContainer/LayerFrameHeaderHSplit" to="." method="_on_layer_frame_h_split_dragged"]
[connection signal="gui_input" from="TimelineContainer/MainBodyPanel/MainBodyVBoxContainer/MarginContainer/LayerFrameHeaderHSplit" to="." method="_on_LayerFrameSplitContainer_gui_input"]
[connection signal="pressed" from="TimelineContainer/MainBodyPanel/MainBodyVBoxContainer/MarginContainer/LayerFrameHeaderHSplit/LayerHeaderContainer/GlobalVisibilityButton" to="." method="_on_global_visibility_button_pressed"]

View file

@ -9,7 +9,7 @@ var cel: BaseCel
var _is_guide_stylebox := false
@onready var popup_menu: PopupMenu = get_node_or_null("PopupMenu")
@onready var linked: ColorRect = $Linked
@onready var linked_rect: ColorRect = $Linked
@onready var cel_texture: TextureRect = $CelTexture
@onready var transparent_checker: ColorRect = $CelTexture/TransparentChecker
@onready var properties: AcceptDialog = Global.control.find_child("CelProperties")
@ -31,6 +31,14 @@ func _ready() -> void:
popup_menu.add_item("Unlink Cels")
elif cel is GroupCel:
transparent_checker.visible = false
elif cel is AudioCel:
popup_menu.add_item("Play audio here")
_is_playing_audio()
Global.cel_switched.connect(_is_playing_audio)
Themes.theme_switched.connect(_is_playing_audio)
Global.current_project.fps_changed.connect(_is_playing_audio)
Global.current_project.layers[layer].audio_changed.connect(_is_playing_audio)
Global.current_project.layers[layer].playback_frame_changed.connect(_is_playing_audio)
func _notification(what: int) -> void:
@ -66,11 +74,12 @@ func button_setup() -> void:
var base_layer := Global.current_project.layers[layer]
tooltip_text = tr("Frame: %s, Layer: %s") % [frame + 1, base_layer.name]
if cel is not AudioCel:
cel_texture.texture = cel.image_texture
if is_instance_valid(linked):
linked.visible = cel.link_set != null
if is_instance_valid(linked_rect):
linked_rect.visible = cel.link_set != null
if cel.link_set != null:
linked.color.h = cel.link_set["hue"]
linked_rect.color.h = cel.link_set["hue"]
func _on_CelButton_pressed() -> void:
@ -129,6 +138,10 @@ func _on_PopupMenu_id_pressed(id: int) -> void:
properties.cel_indices = _get_cel_indices()
properties.popup_centered()
MenuOptions.DELETE:
var layer_class := Global.current_project.layers[layer]
if layer_class is AudioLayer:
layer_class.playback_frame = frame
else:
_delete_cel_content()
MenuOptions.LINK, MenuOptions.UNLINK:
@ -396,3 +409,29 @@ func _sort_cel_indices_by_frame(a: Array, b: Array) -> bool:
if frame_a < frame_b:
return true
return false
func _is_playing_audio() -> void:
var project := Global.current_project
var frame_class := project.frames[frame]
var layer_class := project.layers[layer] as AudioLayer
var audio_length := layer_class.get_audio_length()
var frame_pos := frame_class.position_in_seconds(project, layer_class.playback_frame)
var audio_color := Color.LIGHT_GRAY
var pressed_stylebox := Global.control.theme.get_stylebox(&"pressed", &"CelButton")
if pressed_stylebox is StyleBoxFlat:
audio_color = pressed_stylebox.border_color
var is_last_frame := frame + 1 >= project.frames.size()
if not is_last_frame:
is_last_frame = (
project.frames[frame + 1].position_in_seconds(project, layer_class.playback_frame)
>= audio_length
)
if frame_pos == 0 or (is_last_frame and frame_pos < audio_length):
cel_texture.texture = preload("res://assets/graphics/misc/musical_note.png")
cel_texture.self_modulate = audio_color
linked_rect.visible = false
else:
linked_rect.visible = frame_pos < audio_length and frame_pos > 0
linked_rect.color = audio_color
cel_texture.texture = null

View file

@ -74,7 +74,6 @@ grow_vertical = 2
[node name="PopupMenu" type="PopupMenu" parent="."]
item_count = 1
item_0/text = "Properties"
item_0/id = 0
[connection signal="pressed" from="." to="." method="_on_CelButton_pressed"]
[connection signal="id_pressed" from="PopupMenu" to="." method="_on_PopupMenu_id_pressed"]

View file

@ -15,18 +15,18 @@ func _on_visibility_changed() -> void:
Global.dialog_open(visible)
var first_cel := Global.current_project.frames[cel_indices[0][0]].cels[cel_indices[0][1]]
if visible:
if cel_indices.size() == 1:
var layer := Global.current_project.layers[cel_indices[0][1]]
frame_num.text = str(cel_indices[0][0] + 1)
layer_num.text = layer.name
else:
var first_layer := Global.current_project.layers[cel_indices[0][1]]
if cel_indices.size() == 1:
frame_num.text = str(cel_indices[0][0] + 1)
layer_num.text = first_layer.name
else:
var last_layer := Global.current_project.layers[cel_indices[-1][1]]
frame_num.text = "[%s...%s]" % [cel_indices[0][0] + 1, cel_indices[-1][0] + 1]
layer_num.text = "[%s...%s]" % [first_layer.name, last_layer.name]
opacity_slider.value = first_cel.opacity * 100.0
z_index_slider.value = first_cel.z_index
user_data_text_edit.text = first_cel.user_data
get_tree().set_group(&"VisualCels", "visible", first_layer is not AudioLayer)
else:
cel_indices = []

View file

@ -33,12 +33,12 @@ layout_mode = 2
text = "1"
horizontal_alignment = 1
[node name="OpacityLabel" type="Label" parent="GridContainer"]
[node name="OpacityLabel" type="Label" parent="GridContainer" groups=["VisualCels"]]
layout_mode = 2
size_flags_horizontal = 3
text = "Opacity:"
[node name="OpacitySlider" type="TextureProgressBar" parent="GridContainer"]
[node name="OpacitySlider" type="TextureProgressBar" parent="GridContainer" groups=["VisualCels"]]
layout_mode = 2
size_flags_horizontal = 3
focus_mode = 2
@ -52,12 +52,12 @@ stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource("1_85pb7")
[node name="ZIndexLabel" type="Label" parent="GridContainer"]
[node name="ZIndexLabel" type="Label" parent="GridContainer" groups=["VisualCels"]]
layout_mode = 2
size_flags_horizontal = 3
text = "Z-Index:"
[node name="ZIndexSlider" type="TextureProgressBar" parent="GridContainer"]
[node name="ZIndexSlider" type="TextureProgressBar" parent="GridContainer" groups=["VisualCels"]]
layout_mode = 2
size_flags_horizontal = 3
focus_mode = 2
@ -76,11 +76,13 @@ script = ExtResource("1_85pb7")
[node name="UserDataLabel" type="Label" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 0
text = "User data:"
[node name="UserDataTextEdit" type="TextEdit" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
scroll_fit_content_height = true
[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]

View file

@ -19,8 +19,9 @@ func _ready() -> void:
func _update_tooltip() -> void:
var duration := Global.current_project.frames[frame].duration
var duration_sec := duration * (1.0 / Global.current_project.fps)
var frame_class := Global.current_project.frames[frame]
var duration := frame_class.duration
var duration_sec := frame_class.get_duration_in_seconds(Global.current_project.fps)
var duration_str := str(duration_sec)
if "." in duration_str: # If its a decimal value
duration_str = "%.2f" % duration_sec # Up to 2 decimal places

View file

@ -14,7 +14,10 @@ var button_pressed := false:
main_button.button_pressed = value
get:
return main_button.button_pressed
var animation_running := false
var audio_playing_at_frame := 0
var audio_player: AudioStreamPlayer
@onready var properties: AcceptDialog = Global.control.find_child("LayerProperties")
@onready var main_button := %LayerMainButton as Button
@onready var expand_button := %ExpandButton as BaseButton
@ -31,14 +34,22 @@ var button_pressed := false:
func _ready() -> void:
main_button.layer_index = layer_index
main_button.hierarchy_depth_pixel_shift = HIERARCHY_DEPTH_PIXEL_SHIFT
Global.cel_switched.connect(func(): z_index = 1 if button_pressed else 0)
Global.cel_switched.connect(_on_cel_switched)
var layer := Global.current_project.layers[layer_index]
layer.name_changed.connect(func(): label.text = layer.name)
layer.visibility_changed.connect(update_buttons)
layer.visibility_changed.connect(_on_layer_visibility_changed)
if layer is PixelLayer:
linked_button.visible = true
elif layer is GroupLayer:
expand_button.visible = true
elif layer is AudioLayer:
audio_player = AudioStreamPlayer.new()
audio_player.stream = layer.audio
layer.audio_changed.connect(func(): audio_player.stream = layer.audio)
add_child(audio_player)
Global.animation_timeline.animation_started.connect(_on_animation_started)
Global.animation_timeline.animation_looped.connect(_on_animation_looped)
Global.animation_timeline.animation_finished.connect(_on_animation_finished)
custom_minimum_size.y = Global.animation_timeline.cel_size
label.text = layer.name
line_edit.text = layer.name
@ -56,6 +67,76 @@ func _ready() -> void:
update_buttons()
func _on_cel_switched() -> void:
z_index = 1 if button_pressed else 0
var project := Global.current_project
var layer := project.layers[layer_index]
if layer is AudioLayer:
if not is_instance_valid(audio_player):
return
if not layer.is_visible_in_hierarchy():
audio_player.stop()
return
if animation_running:
var current_frame := project.current_frame
if (
current_frame == layer.playback_frame
or (current_frame == 0 and layer.playback_frame < 0)
## True when switching cels while the animation is running
or current_frame != audio_playing_at_frame + 1
):
_play_audio(false)
audio_playing_at_frame = current_frame
else:
_play_audio(true)
func _on_layer_visibility_changed() -> void:
update_buttons()
var layer := Global.current_project.layers[layer_index]
if layer is AudioLayer:
_play_audio(not animation_running)
func _on_animation_started(_dir: bool) -> void:
animation_running = true
_play_audio(false)
func _on_animation_looped() -> void:
var layer := Global.current_project.layers[layer_index]
if layer is AudioLayer:
if layer.playback_frame > 0 or not layer.is_visible_in_hierarchy():
if is_instance_valid(audio_player):
audio_player.stop()
func _on_animation_finished() -> void:
animation_running = false
if is_instance_valid(audio_player):
audio_player.stop()
func _play_audio(single_frame: bool) -> void:
if not is_instance_valid(audio_player):
return
var project := Global.current_project
var layer := project.layers[layer_index] as AudioLayer
if not layer.is_visible_in_hierarchy():
return
var audio_length := layer.get_audio_length()
var frame := project.frames[project.current_frame]
var frame_pos := frame.position_in_seconds(project, layer.playback_frame)
if frame_pos >= 0 and frame_pos < audio_length:
audio_player.play(frame_pos)
audio_playing_at_frame = project.current_frame
if single_frame:
var timer := get_tree().create_timer(frame.get_duration_in_seconds(project.fps))
timer.timeout.connect(func(): audio_player.stop())
else:
audio_player.stop()
func update_buttons() -> void:
var layer := Global.current_project.layers[layer_index]
if layer is GroupLayer:

View file

@ -169,7 +169,6 @@ caret_blink_interval = 0.5
disable_3d = true
item_count = 2
item_0/text = "Properties"
item_0/id = 0
item_1/text = "Clipping mask"
item_1/checkable = 1
item_1/id = 1

View file

@ -20,6 +20,7 @@ var effects: Array[LayerEffect] = [
"Adjust Brightness/Contrast",
preload("res://src/Shaders/Effects/BrightnessContrast.gdshader")
),
LayerEffect.new("Color Curves", preload("res://src/Shaders/Effects/ColorCurves.gdshader")),
LayerEffect.new("Palettize", preload("res://src/Shaders/Effects/Palettize.gdshader")),
LayerEffect.new("Pixelize", preload("res://src/Shaders/Effects/Pixelize.gdshader")),
LayerEffect.new("Posterize", preload("res://src/Shaders/Effects/Posterize.gdshader")),
@ -36,6 +37,11 @@ var effects: Array[LayerEffect] = [
func _ready() -> void:
for effect in effects:
effect_list.get_popup().add_item(effect.name)
if not DirAccess.dir_exists_absolute(OpenSave.SHADERS_DIRECTORY):
DirAccess.make_dir_recursive_absolute(OpenSave.SHADERS_DIRECTORY)
for file_name in DirAccess.get_files_at(OpenSave.SHADERS_DIRECTORY):
_load_shader_file(OpenSave.SHADERS_DIRECTORY.path_join(file_name))
OpenSave.shader_copied.connect(_load_shader_file)
effect_list.get_popup().id_pressed.connect(_on_effect_list_id_pressed)
@ -48,6 +54,7 @@ func _on_about_to_popup() -> void:
var layer := Global.current_project.layers[Global.current_project.current_layer]
enabled_button.button_pressed = layer.effects_enabled
for effect in layer.effects:
if is_instance_valid(effect.shader):
_create_effect_ui(layer, effect)
@ -58,6 +65,14 @@ func _on_visibility_changed() -> void:
child.queue_free()
func _load_shader_file(file_path: String) -> void:
var file := load(file_path)
if file is Shader:
var effect_name := file_path.get_file().get_basename()
effects.append(LayerEffect.new(effect_name, file))
effect_list.get_popup().add_item(effect_name)
func _on_effect_list_id_pressed(index: int) -> void:
var layer := Global.current_project.layers[Global.current_project.current_layer]
var effect := effects[index].duplicate()

View file

@ -4,11 +4,18 @@ signal layer_property_changed
var layer_indices: PackedInt32Array
@onready var grid_container: GridContainer = $GridContainer
@onready var name_line_edit := $GridContainer/NameLineEdit as LineEdit
@onready var opacity_slider := $GridContainer/OpacitySlider as ValueSlider
@onready var blend_modes_button := $GridContainer/BlendModeOptionButton as OptionButton
@onready var play_at_frame_slider := $GridContainer/PlayAtFrameSlider as ValueSlider
@onready var user_data_text_edit := $GridContainer/UserDataTextEdit as TextEdit
@onready var tileset_option_button := $GridContainer/TilesetOptionButton as OptionButton
@onready var audio_file_dialog := $AudioFileDialog as FileDialog
func _ready() -> void:
audio_file_dialog.use_native_dialog = Global.use_native_file_dialogs
func _on_visibility_changed() -> void:
@ -23,8 +30,13 @@ func _on_visibility_changed() -> void:
opacity_slider.value = first_layer.opacity * 100.0
var blend_mode_index := blend_modes_button.get_item_index(first_layer.blend_mode)
blend_modes_button.selected = blend_mode_index
if first_layer is AudioLayer:
play_at_frame_slider.value = first_layer.playback_frame + 1
play_at_frame_slider.max_value = project.frames.size()
user_data_text_edit.text = first_layer.user_data
get_tree().set_group(&"VisualLayers", "visible", first_layer is not AudioLayer)
get_tree().set_group(&"TilemapLayers", "visible", first_layer is LayerTileMap)
get_tree().set_group(&"AudioLayers", "visible", first_layer is AudioLayer)
tileset_option_button.clear()
if first_layer is LayerTileMap:
for i in project.tilesets.size():
@ -149,3 +161,28 @@ func _on_tileset_option_button_item_selected(index: int) -> void:
project.undo_redo.add_undo_method(Global.canvas.draw_layers)
project.undo_redo.add_undo_method(func(): Global.cel_switched.emit())
project.undo_redo.commit_action()
func _on_audio_file_button_pressed() -> void:
audio_file_dialog.popup_centered()
func _on_play_at_frame_slider_value_changed(value: float) -> void:
if layer_indices.size() == 0:
return
for layer_index in layer_indices:
var layer := Global.current_project.layers[layer_index]
if layer is AudioLayer:
layer.playback_frame = value - 1
func _on_audio_file_dialog_file_selected(path: String) -> void:
var audio_stream: AudioStream
if path.get_extension() == "mp3":
var file := FileAccess.open(path, FileAccess.READ)
audio_stream = AudioStreamMP3.new()
audio_stream.data = file.get_buffer(file.get_length())
for layer_index in layer_indices:
var layer := Global.current_project.layers[layer_index]
if layer is AudioLayer:
layer.audio = audio_stream

View file

@ -5,14 +5,15 @@
[node name="LayerProperties" type="AcceptDialog"]
title = "Layer properties"
size = Vector2i(300, 208)
position = Vector2i(0, 36)
size = Vector2i(300, 270)
script = ExtResource("1_54q1t")
[node name="GridContainer" type="GridContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 292.0
offset_bottom = 159.0
offset_bottom = 221.0
columns = 2
[node name="NameLabel" type="Label" parent="GridContainer"]
@ -24,12 +25,12 @@ text = "Name:"
layout_mode = 2
size_flags_horizontal = 3
[node name="OpacityLabel" type="Label" parent="GridContainer"]
[node name="OpacityLabel" type="Label" parent="GridContainer" groups=["VisualLayers"]]
layout_mode = 2
size_flags_horizontal = 3
text = "Opacity:"
[node name="OpacitySlider" type="TextureProgressBar" parent="GridContainer"]
[node name="OpacitySlider" type="TextureProgressBar" parent="GridContainer" groups=["VisualLayers"]]
layout_mode = 2
size_flags_horizontal = 3
focus_mode = 2
@ -42,16 +43,44 @@ stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource("2_bwpwc")
[node name="BlendModeLabel" type="Label" parent="GridContainer"]
[node name="BlendModeLabel" type="Label" parent="GridContainer" groups=["VisualLayers"]]
layout_mode = 2
size_flags_horizontal = 3
text = "Blend mode:"
[node name="BlendModeOptionButton" type="OptionButton" parent="GridContainer"]
[node name="BlendModeOptionButton" type="OptionButton" parent="GridContainer" groups=["VisualLayers"]]
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
[node name="AudioFileLabel" type="Label" parent="GridContainer" groups=["AudioLayers"]]
layout_mode = 2
text = "Audio file:"
[node name="AudioFileButton" type="Button" parent="GridContainer" groups=["AudioLayers"]]
layout_mode = 2
mouse_default_cursor_shape = 2
text = "Load file"
[node name="PlayAtFrameLabel" type="Label" parent="GridContainer" groups=["AudioLayers"]]
layout_mode = 2
text = "Play at frame:"
[node name="PlayAtFrameSlider" type="TextureProgressBar" parent="GridContainer" groups=["AudioLayers"]]
layout_mode = 2
focus_mode = 2
mouse_default_cursor_shape = 2
theme_type_variation = &"ValueSlider"
min_value = 1.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("2_bwpwc")
[node name="UserDataLabel" type="Label" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
@ -73,9 +102,21 @@ text = "Tileset:"
layout_mode = 2
mouse_default_cursor_shape = 2
[node name="AudioFileDialog" type="FileDialog" parent="."]
title = "Open a File"
size = Vector2i(870, 400)
always_on_top = true
ok_button_text = "Open"
file_mode = 0
access = 2
filters = PackedStringArray("*.mp3 ; MP3 Audio")
[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
[connection signal="text_changed" from="GridContainer/NameLineEdit" to="." method="_on_name_line_edit_text_changed"]
[connection signal="value_changed" from="GridContainer/OpacitySlider" to="." method="_on_opacity_slider_value_changed"]
[connection signal="item_selected" from="GridContainer/BlendModeOptionButton" to="." method="_on_blend_mode_option_button_item_selected"]
[connection signal="pressed" from="GridContainer/AudioFileButton" to="." method="_on_audio_file_button_pressed"]
[connection signal="value_changed" from="GridContainer/PlayAtFrameSlider" to="." method="_on_play_at_frame_slider_value_changed"]
[connection signal="text_changed" from="GridContainer/UserDataTextEdit" to="." method="_on_user_data_text_edit_text_changed"]
[connection signal="item_selected" from="GridContainer/TilesetOptionButton" to="." method="_on_tileset_option_button_item_selected"]
[connection signal="file_selected" from="AudioFileDialog" to="." method="_on_audio_file_dialog_file_selected"]

View file

@ -14,6 +14,7 @@ const HEART_ICON := preload("res://assets/graphics/misc/heart.svg")
var recent_projects := []
var selected_layout := 0
var zen_mode := false
var loaded_effects_submenu: PopupMenu
# Dialogs
var new_image_dialog := Dialog.new("res://src/UI/Dialogs/CreateNewImage.tscn")
@ -33,13 +34,14 @@ var hsv_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/HSVDialog.tscn")
var adjust_brightness_saturation_dialog := Dialog.new(
"res://src/UI/Dialogs/ImageEffects/BrightnessContrastDialog.tscn"
)
var color_curves_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/ColorCurvesDialog.tscn")
var gaussian_blur_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/GaussianBlur.tscn")
var gradient_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/GradientDialog.tscn")
var gradient_map_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/GradientMapDialog.tscn")
var palettize_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/PalettizeDialog.tscn")
var pixelize_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/PixelizeDialog.tscn")
var posterize_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/Posterize.tscn")
var shader_effect_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/ShaderEffect.tscn")
var loaded_effect_dialogs: Array[Dialog] = []
var manage_layouts_dialog := Dialog.new("res://src/UI/Dialogs/ManageLayouts.tscn")
var window_opacity_dialog := Dialog.new("res://src/UI/Dialogs/WindowOpacityDialog.tscn")
var about_dialog := Dialog.new("res://src/UI/Dialogs/AboutDialog.tscn")
@ -78,21 +80,24 @@ class Dialog:
func popup(dialog_size := Vector2i.ZERO) -> void:
if not is_instance_valid(node):
instantiate_scene()
node.popup_centered(dialog_size)
var is_file_dialog := node is FileDialog
Global.dialog_open(true, is_file_dialog)
func instantiate_scene() -> void:
var scene := load(scene_path)
if not scene is PackedScene:
return
node = scene.instantiate()
if not is_instance_valid(node):
return
if is_instance_valid(node):
Global.control.get_node("Dialogs").add_child(node)
node.popup_centered(dialog_size)
var is_file_dialog := node is FileDialog
Global.dialog_open(true, is_file_dialog)
func _ready() -> void:
Global.project_switched.connect(_project_switched)
Global.cel_switched.connect(_update_current_frame_mark)
OpenSave.shader_copied.connect(_load_shader_file)
_setup_file_menu()
_setup_edit_menu()
_setup_view_menu()
@ -449,21 +454,52 @@ func _setup_effects_menu() -> void:
"Desaturation": "desaturation",
"Adjust Hue/Saturation/Value": "adjust_hsv",
"Adjust Brightness/Contrast": "adjust_brightness_contrast",
"Color Curves": "color_curves",
"Palettize": "palettize",
"Pixelize": "pixelize",
"Posterize": "posterize",
"Gaussian Blur": "gaussian_blur",
"Gradient": "gradient",
"Gradient Map": "gradient_map",
# "Shader": ""
"Loaded": ""
}
var i := 0
for item in menu_items:
if item == "Loaded":
_setup_loaded_effects_submenu()
else:
_set_menu_shortcut(menu_items[item], effects_menu, i, item)
i += 1
effects_menu.id_pressed.connect(effects_menu_id_pressed)
func _setup_loaded_effects_submenu() -> void:
if not DirAccess.dir_exists_absolute(OpenSave.SHADERS_DIRECTORY):
DirAccess.make_dir_recursive_absolute(OpenSave.SHADERS_DIRECTORY)
var shader_files := DirAccess.get_files_at(OpenSave.SHADERS_DIRECTORY)
if shader_files.size() == 0:
return
for shader_file in shader_files:
_load_shader_file(OpenSave.SHADERS_DIRECTORY.path_join(shader_file))
func _load_shader_file(file_path: String) -> void:
var file := load(file_path)
if file is not Shader:
return
var effect_name := file_path.get_file().get_basename()
if not is_instance_valid(loaded_effects_submenu):
loaded_effects_submenu = PopupMenu.new()
loaded_effects_submenu.set_name("loaded_effects_submenu")
loaded_effects_submenu.id_pressed.connect(_loaded_effects_submenu_id_pressed)
effects_menu.add_child(loaded_effects_submenu)
effects_menu.add_submenu_item("Loaded", loaded_effects_submenu.get_name())
loaded_effects_submenu.add_item(effect_name)
var effect_index := loaded_effects_submenu.item_count - 1
loaded_effects_submenu.set_item_metadata(effect_index, file)
loaded_effect_dialogs.append(Dialog.new("res://src/UI/Dialogs/ImageEffects/ShaderEffect.tscn"))
func _setup_select_menu() -> void:
# Order as in Global.SelectMenu enum
var select_menu_items := {
@ -768,6 +804,17 @@ func _snap_to_submenu_id_pressed(id: int) -> void:
snap_to_submenu.set_item_checked(id, Global.snap_to_perspective_guides)
func _loaded_effects_submenu_id_pressed(id: int) -> void:
var dialog := loaded_effect_dialogs[id]
if is_instance_valid(dialog.node):
dialog.popup()
else:
dialog.instantiate_scene()
var shader := loaded_effects_submenu.get_item_metadata(id) as Shader
dialog.node.change_shader(shader, loaded_effects_submenu.get_item_text(id))
dialog.popup()
func _panels_submenu_id_pressed(id: int) -> void:
if zen_mode:
return
@ -934,6 +981,8 @@ func effects_menu_id_pressed(id: int) -> void:
hsv_dialog.popup()
Global.EffectsMenu.BRIGHTNESS_SATURATION:
adjust_brightness_saturation_dialog.popup()
Global.EffectsMenu.COLOR_CURVES:
color_curves_dialog.popup()
Global.EffectsMenu.GAUSSIAN_BLUR:
gaussian_blur_dialog.popup()
Global.EffectsMenu.GRADIENT:
@ -946,8 +995,6 @@ func effects_menu_id_pressed(id: int) -> void:
pixelize_dialog.popup()
Global.EffectsMenu.POSTERIZE:
posterize_dialog.popup()
#Global.EffectsMenu.SHADER:
#shader_effect_dialog.popup()
_:
_handle_metadata(id, effects_menu)