diff --git a/.vscode/settings.json b/.vscode/settings.json index e920013..0c89e4d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,6 +18,8 @@ "sprite", "update", "on_message", - "final" + "final", + "editor", + "html5" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f3f0f..cb805cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,4 +33,11 @@ Initial resease! - The `animation_state` table now contains `animation_id`, instead of `animation` table data. It will be better to log or `pprint` the animation state. - Rename file `panthera_system` to `panthera_internal`. - Add support for `is_editor_only` timeline key property -- Add support for `easing_custom` timeline key property \ No newline at end of file +- Add support for `easing_custom` timeline key property + + +## Version v4 + +- Add Defold Editor scripts to create and edit Panthera animations directly from the Defold Editor + - Panthera Editor should be started before using the scripts. +- Add time overflow handling for more precise animation playback. \ No newline at end of file diff --git a/README.md b/README.md index 8cded56..2ccf7a4 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ https://github.com/Insality/defold-tweener/archive/refs/tags/3.zip **[Panthera Runtime](https://github.com/Insality/panthera)** ``` -https://github.com/Insality/panthera/archive/refs/tags/runtime.3.zip +https://github.com/Insality/panthera/archive/refs/tags/runtime.4.zip ``` After that, select `Project ▸ Fetch Libraries` to update [library dependencies]((https://defold.com/manuals/libraries/#setting-up-library-dependencies)). This happens automatically whenever you open a project so you will only need to do this if the dependencies change without re-opening the project. @@ -47,11 +47,13 @@ After that, select `Project ▸ Fetch Libraries` to update [library dependencies | Platform | Library Size | | ---------------- | ------------ | -| HTML5 | **11.51 KB** | -| Desktop / Mobile | **19.53 KB** | +| HTML5 | **12.42 KB** | +| Desktop / Mobile | **21.35 KB** | -### Hot Reloading Animations for Development +### Hot Reloading Animations for Development [Optional] + +> **Note:** Hot reloading is designed for use in development environments only. Hot reloading only works for animations from JSON files. If you using a lua table for animations, hot reloading will not work. Panthera Runtime supports hot reloading of animations for a more efficient development workflow. This feature allows animations to be reloaded automatically without restarting your Defold game, facilitating rapid iteration on animation assets. @@ -81,9 +83,6 @@ window.set_listener(function(_, event) end) ``` -> **Note:** Hot reloading is designed for use in development environments only. Hot reloading only works for animations from JSON files. If you using a lua table for animations, hot reloading will not work. - - ## API Reference ### Quick API Reference @@ -120,9 +119,10 @@ Load and play a animation file using the GO adapter. ```lua local panthera = require("panthera.panthera") +local animation = require("path.to.panthera_animation") function init(self) - self.animation = panthera.create_go("/animations/animation.json") + self.animation = panthera.create_go(animation) panthera.play(self.animation, "run", { is_loop = true }) end ``` @@ -134,9 +134,10 @@ Load and play a animation file using the GUI adapter. ```lua local panthera = require("panthera.panthera") +local animation = require("path.to.panthera_animation") function init(self) - self.animation = panthera.create_gui("/animations/animation.json") + self.animation = panthera.create_gui(animation) panthera.play(self.animation, "fade_in") end ``` @@ -151,6 +152,7 @@ Check if an animation is currently playing and retrieve the current animation ID local panthera = require("panthera.panthera") function init(self) + -- You can use JSON instead of Lua tables, but it should be accessible with sys.load_resource() self.animation = panthera.create_gui("/animations/animation.json") local is_playing = panthera.is_playing(self.animation) local animation_id = panthera.get_latest_animation_id(self.animation) diff --git a/docs_editor/getting_started.md b/docs_editor/getting_started.md index b671dcc..7340be9 100644 --- a/docs_editor/getting_started.md +++ b/docs_editor/getting_started.md @@ -25,6 +25,7 @@ Quickly dive into creating animations with **Panthera Editor 2.0** using this st - [Export Animation Data](#export-animation-data) * [How to Find Animation File](#how-to-find-animation-file) - [Import Defold GUI Layout](#import-defold-gui-layout) +- [Create Animations from Defold Editor](#create-animations-from-defold-editor) - [Working with Node Properties](#working-with-node-properties) * [Copy and Paste Properties](#copy-and-paste-properties) * [Discarding Changes](#discarding-changes) @@ -40,6 +41,7 @@ Quickly dive into creating animations with **Panthera Editor 2.0** using this st - [Working with Nested Animations](#working-with-nested-animations) * [Add Nested Animation](#add-nested-animation) * [Cyclic References](#cyclic-references) +- [Workflow Example](#workflow-example) - [Adjust Gizmo Settings](#adjust-gizmo-settings) * [Scene Gizmo Settings](#scene-gizmo-settings) * [Timeline Gizmo Settings](#timeline-gizmo-settings) @@ -86,6 +88,8 @@ Contains the information, latest news and quick access buttons to leave feedback --- List of all your projects. Here you can open, delete, or create a new project. Projects are sorted by the last modified date. After creation you can rename the project by right click -> Rename. This rename is not affecting the saved file name and can be used for better navigation. +To create first animation project, click on the "Plus" button and select "New Animation". As file extension use `.lua` or `.json`. + > Project Tabs --- All currently opened projects are displayed here. You can switch between them by clicking on the tab. @@ -138,6 +142,16 @@ Displays the properties of the selected node. You can view the properties here. Contains all the images in the atlas. You can add new images here. +## Interface adjustments + +You can change the UI scale by pressing `Ctrl` + `Shift` + `-` to scale down and `Ctrl` + `Shift` + `+` to scale up. + +https://github.com/user-attachments/assets/f6f94120-56c1-4abb-a5fc-acdedbe6127c + +You can adjust the width of the Node panel and Timeline panel by dragging the splitter between them. + +https://github.com/user-attachments/assets/ef0e9d38-eb39-4001-83de-5bdbaf9cc47d + # Create a New Project https://github.com/Insality/panthera/assets/3294627/cf59240b-2279-4791-843f-3ea6ebcbc813 @@ -277,22 +291,38 @@ Panthera Editor used a JSON file for animation data. This file serves a dual pur The file will be opened in the file explorer window. -# Import Defold GUI Layout +# Import Defold Layout https://github.com/Insality/panthera/assets/3294627/ed082b26-cfaf-4567-93ac-41d2169b2444 -You can import the Defold GUI layout to the Panthera Editor. The animation file should be placed inside your Defold project folder to correct reloading in the future. +You can import the Defold GUI/Collection/GO layout to the Panthera Editor. The animation file should be placed inside your Defold project folder to correct reloading in the future (it uses relative path's from `game.project` file). 1. Open animation project. 2. Click on the plus icon in the Nodes panel. 3. Select "Bind Defold File". 4. Choose the `.gui` file from your Defold project. -The GUI layout will be imported and displayed in the Editor View. The file state is changed to linked. The GUI will be reloaded automatically when the project is opened, or manually by clicking the "Reload Binded File" button. +The layout will be imported and displayed in the Editor View. The file state is changed to linked. The file will be reloaded automatically when the project is opened, or manually by clicking the "Reload Binded File" button. The layout nodes can't be modified. But you can animate them. Nodes layout data will be not stored in the animation file. Only the animation data will be stored. +# Create Animations from Defold Editor + +> Panthera Runtime v4 is required for this feature. + +You can create and open animations directly from Defold Editor. Prerequisites: + +- The Panthera Editor should be opened. +- The Panthera Runtime library should be included in your Defold project. + +To create new animation from Defold Editor, press right click on the `.gui`, `.go` or `.collection` file in the Defold Editor and select "Create Panthera Animation". The Panthera Editor will be opened with the new animation project. The new file will be created in the same folder as the `.gui`, `.go` or `.collection` file. The name will be `{file_name}_panthera.lua`. + +https://github.com/user-attachments/assets/b39445d1-ebe8-4f02-ac54-418e952d9b84 + +To open Panthera animation (both in json or lua formars) from Defold Editor, press right click on the Panthera animation file and select "Open Panthera Animation". The Panthera Editor will be opened with the selected animation project. + +https://github.com/user-attachments/assets/5e649807-f030-4c81-8264-a0e54191da2a # Working with Node Properties @@ -410,6 +440,13 @@ You can add a nested animation to the scene. Nested animations can be created in In the current version, the cyclic references are not protected. The cyclic references can cause the infinite loop in the animation playback. Be careful with it. +# Workflow Example + +Here is a 4 minutes of making simple appear/disappear animations in Panthera Editor. + +https://github.com/user-attachments/assets/18615ed3-3b09-47c3-a677-411ffa7d6600 + + # Adjust Gizmo Settings https://github.com/Insality/panthera/assets/3294627/53b1de58-84eb-4a20-800f-c4bcf13cc78b diff --git a/docs_editor/hotkeys.md b/docs_editor/hotkeys.md index 8b9025c..f135ff9 100644 --- a/docs_editor/hotkeys.md +++ b/docs_editor/hotkeys.md @@ -31,6 +31,7 @@ | `Shift + A` | Move animation time backward on time step | | `Shift + D` | Move animation time forward on time step | | `Shift + Enter` | Commit all changes in animation of selected nodes | +| `U` | Reset all changed properties of selected nodes. If no changes - reset all properties to initial values | | `Ctrl` + `C` | Copy selected nodes or animation keys | | `Ctrl` + `V` | **Layout Mode**: Paste copied nodes | | `Ctrl` + `V` | **Animation Mode**: Paste copied keys | @@ -41,7 +42,8 @@ | `Ctrl` + `N` | Create new box node | | `Ctrl` + `A` | **Layout Mode** Select all nodes | | `Ctrl` + `A` | **Animation Mode** Select all keys | -| `Ctrl` + `E` | Hide/Show Node. Hidden node is non-selectable from Editor View | +| `Ctrl` + `E` | Hide/Show selected nodes. Hidden node is non-selectable from Editor View | +| `Ctrl` + `Shift` + `E` | Hide/Show not selected nodes. | | `Arrows (Up, Down, Left, Right)` | Move selected nodes +1px | | `Shift` + `Arrows (Up, Down, Left, Right)` | Move selected nodes +10px | | `Ctrl` + `Arrows (Up, Down, Left, Right)` | Change selected nodes size +1px | @@ -56,7 +58,7 @@ | `Alt` + `Arrow Up` | Move selected nodes up in hierarcy | | `Alt` + `Arrow Down` | Move selected nodes down in hierarcy | | `Right Click` | Show context menu of element | -| `Ctrl` + `Left Click` | Select Animation with keeping nodes selection | +| `Ctrl` + `Left Click` | Select Animation with keeping current nodes selection | ## Properties View @@ -70,6 +72,17 @@ | `Mouse Hover` + `0-9` | Set opacity of color picker widget to [10% .. 100%] | | `Mouse Hover` + `Left Shift` + `0` | Set opacity of color picker widget 0% | +## Node List View + +| Key | Description | +| --- | --- | +| `Alt` + `Left Click` | (_on arrow icon_) Fold/Unfold node and all children | +| `Shift` + `[` | Select parent node | +| `Shift` + `]` | Select child node | +| `Shift` + `P` | Select next node | +| `Shift` + `Alt` + `P` | Select previous node | + + ## Timeline View | Key | Description | @@ -84,6 +97,7 @@ | `Hold Shift` + `Drag` | Add or remove keys from selection | | `Left Click` | Select key (or key behind selected if multiple selection) | + ## Transform Gizmo | Key | Description | diff --git a/docs_editor/media/convert_videos.sh b/docs_editor/media/convert_videos.sh index 4cfb8c8..1c669d0 100644 --- a/docs_editor/media/convert_videos.sh +++ b/docs_editor/media/convert_videos.sh @@ -21,7 +21,7 @@ for input_file in "$input_dir"/*.mov; do output_file="$output_dir/$base_name.mp4" # Run FFmpeg command to convert the video - ffmpeg -i "$input_file" -vf "scale=1280:-1" -c:v libx264 -preset slow -crf 22 -an "$output_file" + ffmpeg -i "$input_file" -vf "scale=1280:-2" -c:v libx264 -preset slow -crf 22 -an "$output_file" # Check if the conversion was successful if [ $? -eq 0 ]; then diff --git a/docs_editor/media/video/create_animation_defold.mp4 b/docs_editor/media/video/create_animation_defold.mp4 new file mode 100644 index 0000000..a664501 Binary files /dev/null and b/docs_editor/media/video/create_animation_defold.mp4 differ diff --git a/docs_editor/media/video/edit_animation_defold.mp4 b/docs_editor/media/video/edit_animation_defold.mp4 new file mode 100644 index 0000000..9207d52 Binary files /dev/null and b/docs_editor/media/video/edit_animation_defold.mp4 differ diff --git a/docs_editor/media/video/panels_adjust.mp4 b/docs_editor/media/video/panels_adjust.mp4 new file mode 100644 index 0000000..262cea3 Binary files /dev/null and b/docs_editor/media/video/panels_adjust.mp4 differ diff --git a/docs_editor/media/video/ui_scale.mp4 b/docs_editor/media/video/ui_scale.mp4 new file mode 100644 index 0000000..758ddba Binary files /dev/null and b/docs_editor/media/video/ui_scale.mp4 differ diff --git a/docs_editor/media/video/window_animation_create.mp4 b/docs_editor/media/video/window_animation_create.mp4 new file mode 100644 index 0000000..f768ba7 Binary files /dev/null and b/docs_editor/media/video/window_animation_create.mp4 differ diff --git a/example/example_druid_component/druid_component.gui b/example/example_druid_component/druid_component.gui index 7ff5bf5..b3d6476 100644 --- a/example/example_druid_component/druid_component.gui +++ b/example/example_druid_component/druid_component.gui @@ -52,7 +52,7 @@ nodes { yanchor: YANCHOR_NONE pivot: PIVOT_CENTER adjust_mode: ADJUST_MODE_FIT - layer: "" + layer: "gui" inherit_alpha: true slice9 { x: 16.0 @@ -111,7 +111,7 @@ nodes { pivot: PIVOT_CENTER adjust_mode: ADJUST_MODE_FIT parent: "root" - layer: "" + layer: "gui" inherit_alpha: true slice9 { x: 0.0 @@ -184,7 +184,7 @@ nodes { adjust_mode: ADJUST_MODE_FIT line_break: false parent: "root" - layer: "" + layer: "text" inherit_alpha: true alpha: 1.0 outline_alpha: 0.0 @@ -197,6 +197,12 @@ nodes { visible: true material: "" } +layers { + name: "gui" +} +layers { + name: "text" +} material: "/builtins/materials/gui.material" adjust_reference: ADJUST_REFERENCE_PARENT max_nodes: 512 diff --git a/example/example_druid_component/druid_component_animation.lua b/example/example_druid_component/druid_component_animation.lua index 23efa56..e82ba1c 100644 --- a/example/example_druid_component/druid_component_animation.lua +++ b/example/example_druid_component/druid_component_animation.lua @@ -1,148 +1,241 @@ return { - type = "animation_editor", - format = "json", - version = 1, data = { nodes = { }, + metadata = { + gizmo_steps = { + }, + layers = { + }, + fps = 60, + gui_path = "/example/example_druid_component/druid_component.gui", + settings = { + font_size = 40, + }, + }, animations = { { + animation_id = "default", duration = 2.2, animation_keys = { { - property_id = "position_y", - easing = "outsine", duration = 0.4, + end_value = -42, key_type = "tween", + property_id = "position_y", node_id = "panthera", - end_value = -42, + easing = "outsine", }, { - property_id = "scale_x", - easing = "outback", duration = 0.42, + start_value = 1, key_type = "tween", + property_id = "scale_x", + easing = "outback", node_id = "text", end_value = 1.3, - start_value = 1, }, { - property_id = "scale_y", - easing = "outback", duration = 0.42, + start_value = 1, key_type = "tween", + property_id = "scale_y", + easing = "outback", node_id = "text", end_value = 1.3, - start_value = 1, }, { - property_id = "position_y", - easing = "outback", duration = 0.42, + start_value = 80, key_type = "tween", + property_id = "position_y", + easing = "outback", node_id = "text", end_value = 45, - start_value = 80, }, { - property_id = "position_x", - easing = "outsine", duration = 0.8, + end_value = 75, key_type = "tween", + property_id = "position_x", node_id = "panthera", - end_value = 75, + easing = "outsine", }, { - property_id = "text", + data = "Just", easing = "linear", - node_id = "text", - key_type = "trigger", start_time = 0.22, - data = "Just", + property_id = "text", start_data = "Hello", + node_id = "text", + key_type = "trigger", }, { - property_id = "position_y", - easing = "outsine", duration = 1.6, + start_value = -42, key_type = "tween", start_time = 0.4, + property_id = "position_y", node_id = "panthera", - start_value = -42, + easing = "outsine", }, { - property_id = "scale_x", - easing = "outsine", duration = 0.45, - node_id = "text", + start_value = 1.3, key_type = "tween", start_time = 0.42, + property_id = "scale_x", + easing = "outsine", + node_id = "text", end_value = 1, - start_value = 1.3, }, { - property_id = "scale_y", - easing = "outsine", duration = 0.45, - node_id = "text", + start_value = 1.3, key_type = "tween", start_time = 0.42, + property_id = "scale_y", + easing = "outsine", + node_id = "text", end_value = 1, - start_value = 1.3, }, { - property_id = "position_y", - easing = "outsine", duration = 0.45, - node_id = "text", + start_value = 45, key_type = "tween", start_time = 0.42, - end_value = 75, - start_value = 45, + property_id = "position_y", + easing = "outsine", + node_id = "text", + end_value = 80, + }, + { + duration = 1.4, + end_value = 100, + key_type = "event", + start_time = 0.72, + property_id = "event", + event_id = "print", + easing = "linear", }, { - property_id = "position_x", - easing = "outsine", duration = 0.6, - node_id = "panthera", + start_value = 75, key_type = "tween", start_time = 0.8, + property_id = "position_x", + easing = "outsine", + node_id = "panthera", end_value = -65, - start_value = 75, }, { - property_id = "text", + duration = 1.36, + key_type = "animation", + start_time = 0.84, easing = "linear", - node_id = "text", - key_type = "trigger", - start_time = 0.87, + property_id = "rotate", + data = "rotate", + }, + { data = "Example", + easing = "linear", + start_time = 0.87, + property_id = "text", start_data = "Just", + node_id = "text", + key_type = "trigger", }, { - property_id = "position_x", - easing = "outsine", duration = 0.6, + start_value = -65, key_type = "tween", start_time = 1.4, + property_id = "position_x", node_id = "panthera", - start_value = -65, + easing = "outsine", + }, + { + end_value = 0.389, + key_type = "tween", + start_time = 2.1, + property_id = "color_b", + easing = "outsine", + node_id = "root", + start_value = 0.879, + }, + { + end_value = 0.389, + key_type = "tween", + start_time = 2.1, + property_id = "color_g", + easing = "outsine", + node_id = "root", + start_value = 0.924, + }, + { + end_value = 0.925, + key_type = "tween", + start_time = 2.1, + property_id = "color_r", + easing = "outsine", + node_id = "root", + start_value = 0.754, + }, + { + duration = 0.1, + start_value = 0.925, + key_type = "tween", + start_time = 2.1, + property_id = "color_r", + easing = "outsine", + node_id = "root", + end_value = 0.754, + }, + { + duration = 0.1, + start_value = 0.389, + key_type = "tween", + start_time = 2.1, + property_id = "color_b", + easing = "outsine", + node_id = "root", + end_value = 0.879, + }, + { + duration = 0.1, + start_value = 0.389, + key_type = "tween", + start_time = 2.1, + property_id = "color_g", + easing = "outsine", + node_id = "root", + end_value = 0.924, }, }, - animation_id = "default", - }, - }, - metadata = { - gui_path = "/example/example_druid_component/druid_component.gui", - fps = 60, - settings = { - font_size = 40, - }, - layers = { }, - gizmo_steps = { - time = 0.1, + { + animation_id = "rotate", + duration = 1, + animation_keys = { + { + property_id = "rotation_z", + node_id = "panthera", + key_type = "tween", + easing = "outsine", + }, + { + duration = 1, + end_value = -360, + key_type = "tween", + property_id = "rotation_z", + node_id = "panthera", + easing = "outsine", + }, + }, }, }, }, + format = "json", + type = "animation_editor", + version = 1, } \ No newline at end of file diff --git a/example/example_druid_component/exampel_druid_component.gui_script b/example/example_druid_component/exampel_druid_component.gui_script index 75e13a5..72de961 100644 --- a/example/example_druid_component/exampel_druid_component.gui_script +++ b/example/example_druid_component/exampel_druid_component.gui_script @@ -13,8 +13,30 @@ function init(self) local druid_component_cloned = self.druid:new(druid_component, "druid_component_template", nodes) gui.set_position(druid_component_cloned.root, vmath.vector3(700, 340, 0)) + local nodes = gui.clone_tree(prefab) + local druid_component_cloned2 = self.druid:new(druid_component, "druid_component_template", nodes) + gui.set_position(druid_component_cloned2.root, vmath.vector3(260, 140, 0)) + + local nodes = gui.clone_tree(prefab) + local druid_component_cloned3 = self.druid:new(druid_component, "druid_component_template", nodes) + gui.set_position(druid_component_cloned3.root, vmath.vector3(700, 140, 0)) + + local nodes = gui.clone_tree(prefab) + local druid_component_cloned4 = self.druid:new(druid_component, "druid_component_template", nodes) + gui.set_position(druid_component_cloned4.root, vmath.vector3(260, 540, 0)) + + local nodes = gui.clone_tree(prefab) + local druid_component_cloned5 = self.druid:new(druid_component, "druid_component_template", nodes) + gui.set_position(druid_component_cloned5.root, vmath.vector3(700, 540, 0)) + druid_component_on_scene:run_animation() druid_component_cloned:run_animation() + druid_component_cloned2:run_animation() + druid_component_cloned3:run_animation() + druid_component_cloned4:run_animation() + druid_component_cloned5:run_animation() + + --profiler.enable_ui(true) end diff --git a/example/example_druid_component/example_druid_component.gui b/example/example_druid_component/example_druid_component.gui index 1f95206..3e2b353 100644 --- a/example/example_druid_component/example_druid_component.gui +++ b/example/example_druid_component/example_druid_component.gui @@ -145,7 +145,7 @@ nodes { pivot: PIVOT_CENTER adjust_mode: ADJUST_MODE_FIT parent: "druid_component_template" - layer: "" + layer: "gui" inherit_alpha: true slice9 { x: 16.0 @@ -204,7 +204,7 @@ nodes { pivot: PIVOT_CENTER adjust_mode: ADJUST_MODE_FIT parent: "druid_component_template/root" - layer: "" + layer: "gui" inherit_alpha: true slice9 { x: 0.0 @@ -277,7 +277,7 @@ nodes { adjust_mode: ADJUST_MODE_FIT line_break: false parent: "druid_component_template/root" - layer: "" + layer: "text" inherit_alpha: true alpha: 1.0 outline_alpha: 0.0 @@ -290,6 +290,12 @@ nodes { visible: true material: "" } +layers { + name: "gui" +} +layers { + name: "text" +} material: "/builtins/materials/gui.material" adjust_reference: ADJUST_REFERENCE_PARENT max_nodes: 512 diff --git a/panthera/adapters/adapter_go.lua b/panthera/adapters/adapter_go.lua index a0267f0..5482071 100644 --- a/panthera/adapters/adapter_go.lua +++ b/panthera/adapters/adapter_go.lua @@ -1,24 +1,24 @@ local PROPERTY_TO_TWEEN_PROPERTY = { - ["position_x"] = "position.x", - ["position_y"] = "position.y", - ["position_z"] = "position.z", - ["rotation_x"] = "euler.x", - ["rotation_y"] = "euler.y", - ["rotation_z"] = "euler.z", - ["scale_x"] = "scale.x", - ["scale_y"] = "scale.y", - ["scale_z"] = "scale.z", - ["size_x"] = "size.x", - ["size_y"] = "size.y", - ["size_z"] = "size.z", - ["color_r"] = "color.x", - ["color_g"] = "color.y", - ["color_b"] = "color.z", - ["color_a"] = "color.w", - ["slice9_left"] = "slice.x", - ["slice9_top"] = "slice.y", - ["slice9_right"] = "slice.z", - ["slice9_bottom"] = "slice.w" + ["position_x"] = hash("position.x"), + ["position_y"] = hash("position.y"), + ["position_z"] = hash("position.z"), + ["rotation_x"] = hash("euler.x"), + ["rotation_y"] = hash("euler.y"), + ["rotation_z"] = hash("euler.z"), + ["scale_x"] = hash("scale.x"), + ["scale_y"] = hash("scale.y"), + ["scale_z"] = hash("scale.z"), + ["size_x"] = hash("size.x"), + ["size_y"] = hash("size.y"), + ["size_z"] = hash("size.z"), + ["color_r"] = hash("color.x"), + ["color_g"] = hash("color.y"), + ["color_b"] = hash("color.z"), + ["color_a"] = hash("color.w"), + ["slice9_left"] = hash("slice.x"), + ["slice9_top"] = hash("slice.y"), + ["slice9_right"] = hash("slice.z"), + ["slice9_bottom"] = hash("slice.w"), } local PROPERTY_TO_TRIGGER_PROPERTY = { @@ -161,7 +161,6 @@ local function get_trigger_property_id(node, property_id) end if defold_property_id == "texture" then local texture_name = go.get(node, "animation") - pprint(texture_name) local splitted = split(texture_name, "/") return splitted[#splitted] end @@ -197,7 +196,7 @@ local function set_node_property(node, property_id, value) stop_tween(node, property_id) defold_property_id = PROPERTY_TO_TWEEN_PROPERTY[property_id] if not defold_property_id then - print("Unknown property_id: " .. property_id, debug.traceback()) + print("Unknown property_id: ", property_id, debug.traceback()) return false end @@ -218,7 +217,7 @@ local function get_node_property(node, property_id) local defold_number_property_id = PROPERTY_TO_TWEEN_PROPERTY[property_id] if not defold_number_property_id then - print("Unknown property_id: " .. property_id, debug.traceback()) + print("Unknown property_id: ", property_id, debug.traceback()) return nil end diff --git a/panthera/adapters/adapter_gui.lua b/panthera/adapters/adapter_gui.lua index 3a401a4..dc6e8ae 100644 --- a/panthera/adapters/adapter_gui.lua +++ b/panthera/adapters/adapter_gui.lua @@ -1,40 +1,47 @@ ---@diagnostic disable: undefined-field, return-type-mismatch + +-- Localize Defold functions +local gui_animate = gui.animate +local gui_set = gui.set +local gui_get = gui.get +local gui_get_node = gui.get_node + -- In Defold 1.2.180+ gui.set and gui.get functions were added. Rotation was changed to Euler -local IS_DEFOLD_180 = (gui.set and gui.get) +local IS_DEFOLD_180 = gui_set and gui_get local PROPERTY_TO_DEFOLD_TWEEN_PROPERTY = { - ["position_x"] = "position.x", - ["position_y"] = "position.y", - ["position_z"] = "position.z", - ["rotation_x"] = IS_DEFOLD_180 and "euler.x" or "rotation.x", - ["rotation_y"] = IS_DEFOLD_180 and "euler.y" or "rotation.y", - ["rotation_z"] = IS_DEFOLD_180 and "euler.z" or "rotation.z", - ["scale_x"] = "scale.x", - ["scale_y"] = "scale.y", - ["scale_z"] = "scale.z", - ["size_x"] = "size.x", - ["size_y"] = "size.y", - ["size_z"] = "size.z", - ["color_r"] = "color.x", - ["color_g"] = "color.y", - ["color_b"] = "color.z", - ["color_a"] = "color.w", - ["outline_r"] = "outline.x", - ["outline_g"] = "outline.y", - ["outline_b"] = "outline.z", - ["outline_a"] = "outline.w", - ["shadow_r"] = "shadow.x", - ["shadow_g"] = "shadow.y", - ["shadow_b"] = "shadow.z", - ["shadow_a"] = "shadow.w", - ["slice9_left"] = "slice9.x", - ["slice9_top"] = "slice9.y", - ["slice9_right"] = "slice9.z", - ["slice9_bottom"] = "slice9.w", - ["inner_radius"] = "inner_radius", - ["fill_angle"] = "fill_angle", - ["text_tracking"] = "tracking", - ["text_leading"] = "leading", + ["position_x"] = hash("position.x"), + ["position_y"] = hash("position.y"), + ["position_z"] = hash("position.z"), + ["rotation_x"] = IS_DEFOLD_180 and hash("euler.x") or hash("rotation.x"), + ["rotation_y"] = IS_DEFOLD_180 and hash("euler.y") or hash("rotation.y"), + ["rotation_z"] = IS_DEFOLD_180 and hash("euler.z") or hash("rotation.z"), + ["scale_x"] = hash("scale.x"), + ["scale_y"] = hash("scale.y"), + ["scale_z"] = hash("scale.z"), + ["size_x"] = hash("size.x"), + ["size_y"] = hash("size.y"), + ["size_z"] = hash("size.z"), + ["color_r"] = hash("color.x"), + ["color_g"] = hash("color.y"), + ["color_b"] = hash("color.z"), + ["color_a"] = hash("color.w"), + ["outline_r"] = hash("outline.x"), + ["outline_g"] = hash("outline.y"), + ["outline_b"] = hash("outline.z"), + ["outline_a"] = hash("outline.w"), + ["shadow_r"] = hash("shadow.x"), + ["shadow_g"] = hash("shadow.y"), + ["shadow_b"] = hash("shadow.z"), + ["shadow_a"] = hash("shadow.w"), + ["slice9_left"] = hash("slice9.x"), + ["slice9_top"] = hash("slice9.y"), + ["slice9_right"] = hash("slice9.z"), + ["slice9_bottom"] = hash("slice9.w"), + ["inner_radius"] = hash("inner_radius"), + ["fill_angle"] = hash("fill_angle"), + ["text_tracking"] = hash("tracking"), + ["text_leading"] = hash("leading"), } local PROPERTY_TO_DEFOLD_TRIGGER_PROPERTY = { @@ -273,7 +280,7 @@ local function create_get_node_function(template, nodes) if nodes then return nodes[node_id] else - return gui.get_node(node_id) + return gui_get_node(node_id) end end end @@ -320,7 +327,7 @@ local function set_node_property(node, property_id, value) if IS_DEFOLD_180 then -- Handle Defold 1.2.180+ properties local defold_number_property_id = PROPERTY_TO_DEFOLD_TWEEN_PROPERTY[property_id] - gui.set(node, defold_number_property_id, value) + gui_set(node, defold_number_property_id, value) else -- Handle Defold 1.2.179- properties local tween_info = TWEEN_DEFOLD_SET_GET[property_id] @@ -359,7 +366,7 @@ local function get_node_property(node, property_id) if IS_DEFOLD_180 then -- Handle Defold 1.2.180+ properties local defold_number_property_id = PROPERTY_TO_DEFOLD_TWEEN_PROPERTY[property_id] - return gui.get(node, defold_number_property_id) + return gui_get(node, defold_number_property_id) else -- Handle Defold 1.2.179- properties local tween_info = TWEEN_DEFOLD_SET_GET[property_id] @@ -399,7 +406,7 @@ local function tween_animation_key(node, property_id, easing, duration, end_valu set_node_property(node, property_id, end_value) else property_id = PROPERTY_TO_DEFOLD_TWEEN_PROPERTY[property_id] - gui.animate(node, property_id, end_value, easing, duration) + gui_animate(node, property_id, end_value, easing, duration) end end diff --git a/panthera/editor_scripts/panthera.editor_script b/panthera/editor_scripts/panthera.editor_script new file mode 100644 index 0000000..0bc2303 --- /dev/null +++ b/panthera/editor_scripts/panthera.editor_script @@ -0,0 +1,113 @@ +local M = {} + + +---@param str string +---@param ending string +---@return boolean +local function ends_with(str, ending) + return ending == "" or str:sub(-#ending) == ending +end + + +---@param path string +---@param file_data string +---@return boolean +local function is_panthera_animation_file(path, file_data) + local is_json = ends_with(path, ".json") + local is_lua = ends_with(path, ".lua") + + if is_json then + -- Check contains "type":"animation_editor" + return (not not file_data:find("\"type\"%s*:%s*\"animation_editor\"")) + end + + if is_lua then + -- Check contains type = "animation_editor" + return (not not file_data:find("type%s*=%s*\"animation_editor\"")) + end + + return false +end + + +local function save_file_from_dependency(dependency_file_path, output_file_path) + local content = editor.get(dependency_file_path, "text") + local file, err = io.open(output_file_path, "w") + if not file then + print("Error:", err) + return false + end + file:write(content) + file:close() + return true +end + + +function M.get_commands() + return { + { + label = "Edit Panthera Animation", + locations = { "Assets" }, + query = { selection = { type = "resource", cardinality = "one" } }, + active = function(opts) + if not editor.can_get(opts.selection, "text") then + return false + end + + local path = editor.get(opts.selection, "path") + local text = editor.get(opts.selection, "text") + local is_extension_ok = ends_with(path, ".lua") or ends_with(path, ".json") + if not is_extension_ok then + return false + end + + return is_panthera_animation_file(path, text) + end, + run = function(opts) + local path = editor.get(opts.selection, "path") + print("Edit Panthera Animation", path) + save_file_from_dependency("/panthera/editor_scripts/panthera_command.sh", "./build/panthera_command.sh") + return { + { + action = "shell", + command = { + "bash", + "./build/panthera_command.sh", + "open", + path + } + } + } + end + }, + + { + label = "Create Panthera Animation", + locations = { "Assets" }, + query = { selection = { type = "resource", cardinality = "one" } }, + active = function(opts) + local path = editor.get(opts.selection, "path") + return ends_with(path, ".collection") or ends_with(path, ".gui") or ends_with(path, ".go") + end, + run = function(opts) + local path = editor.get(opts.selection, "path") + print("Create Panthera Animation", path) + save_file_from_dependency("/panthera/editor_scripts/panthera_command.sh", "./build/panthera_command.sh") + return { + { + action = "shell", + command = { + "bash", + "./build/panthera_command.sh", + "create", + path + } + } + } + end + }, + } +end + + +return M \ No newline at end of file diff --git a/panthera/editor_scripts/panthera_command.sh b/panthera/editor_scripts/panthera_command.sh new file mode 100755 index 0000000..86ee302 --- /dev/null +++ b/panthera/editor_scripts/panthera_command.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Panthera Editor Port +PORT=16114 + +CURRENT_DIR=$(pwd) + +# Check if the port is open +if nc -z localhost "$PORT" &>/dev/null; then + # The port is open and we ready to send commands + echo "Panthera port $PORT is open, sending command..." + echo "Current dir: $CURRENT_DIR" + echo "Args: $@" + + # Send command to Panthera + command_type=$1 + # The $2 is a relative path to the file, concat it with current dir + path="$CURRENT_DIR$2" + + if [ "$command_type" == "open" ]; then + # Open animation file + JSON_PAYLOAD="{\"command\": \"open\", \"path\": \"$path\"}" + echo "Send command: $JSON_PAYLOAD" + echo "$JSON_PAYLOAD" | nc -w 5 localhost "$PORT" & + elif [ "$command_type" == "create" ]; then + # Create gui file + JSON_PAYLOAD="{\"command\": \"create\", \"path\": \"$path\"}" + echo "Send command: $JSON_PAYLOAD" + echo "$JSON_PAYLOAD" | nc -w 5 localhost "$PORT" & + fi +else + echo "Port $PORT is closed." + # Panthera is not found + echo "Panthera should be started first." + echo "If you don't have Panthera, download it at:" + echo "Download Panthera at: https://github.com/Insality/panthera" +fi \ No newline at end of file diff --git a/panthera/panthera.lua b/panthera/panthera.lua index 11694dd..3a8526f 100644 --- a/panthera/panthera.lua +++ b/panthera/panthera.lua @@ -5,9 +5,11 @@ local panthera_internal = require("panthera.panthera_internal") ---@class panthera local M = {} local TIMER_DELAY = 1/60 +local EMPTY_OPTIONS = {} ----@param logger_instance panthera.logger|nil +---Customize the logging mechanism used by **Panthera Runtime**. You can use **Defold Log** library or provide a custom logger. +---@param logger_instance panthera.logger|table|nil function M.set_logger(logger_instance) panthera_internal.logger = logger_instance or panthera_internal.empty_logger end @@ -109,7 +111,7 @@ function M.play(animation_state, animation_id, options) M.stop(animation_state) end - options = options or {} + options = options or EMPTY_OPTIONS animation_state.animation_id = animation.animation_id animation_state.animation_keys_index = 1 @@ -135,7 +137,7 @@ function M.play(animation_state, animation_id, options) local last_time = socket.gettime() animation_state.timer_id = timer.delay(TIMER_DELAY, true, function() local current_time = socket.gettime() - local dt = (current_time - last_time) + local dt = current_time - last_time last_time = current_time local speed = (options.speed or 1) * animation_state.speed @@ -166,22 +168,24 @@ function M._update_animation(animation, animation_state, options) if key.key_type ~= "animation" then panthera_internal.run_timeline_key(animation_state, key, options) else - -- Create a new animation child track - local animation_path = animation_state.animation_path - local adapter = animation_state.adapter - local get_node = animation_state.get_node - - local child_state = M.create(animation_path, adapter, get_node) + local child_state = M.clone_state(animation_state) if child_state then + -- Time Overflow + local time_overflow = math.max(0, animation_state.current_time - key.start_time) + child_state.current_time = time_overflow + animation_state.childs = animation_state.childs or {} table.insert(animation_state.childs, child_state) local animation_duration = M.get_duration(child_state, key.property_id) if animation_duration > 0 and key.duration > 0 then local speed = (options.speed or 1) * animation_state.speed + local key_duration = (key.duration - time_overflow) + local play_speed = (animation_duration / key_duration) * speed + M.play(child_state, key.property_id, { is_skip_init = true, - speed = (animation_duration / key.duration) * speed, + speed = play_speed, callback = function() panthera_internal.remove_child_animation(animation_state, child_state) end @@ -196,6 +200,7 @@ function M._update_animation(animation, animation_state, options) -- If current time >= animation duration - stop animation if animation_state.current_time >= animation.duration then + local time_overflow = animation_state.current_time - animation.duration M.stop(animation_state) if options.callback then @@ -203,6 +208,8 @@ function M._update_animation(animation, animation_state, options) end if options.is_loop then + -- Compensate the time overflow + animation_state.current_time = time_overflow M.play(animation_state, animation.animation_id, options) end end @@ -237,7 +244,7 @@ function M.set_time(animation_state, animation_id, time) end ----Get current animation time in seconds +---Retrieve the current playback time in seconds of an animation. If the animation is not playing, the function returns 0. ---@param animation_state panthera.animation.state ---@return number @Current animation time in seconds function M.get_time(animation_state) @@ -303,7 +310,7 @@ function M.stop(animation_state) end ----Get animation duration +---Retrieve the total duration of a specific animation. ---@param animation_state panthera.animation.state ---@param animation_id string ---@return number @@ -318,7 +325,7 @@ function M.get_duration(animation_state, animation_id) end ----Check if animation is playing +---Check if an animation is currently playing. ---@param animation_state panthera.animation.state ---@return boolean function M.is_playing(animation_state) @@ -326,7 +333,7 @@ function M.is_playing(animation_state) end ----Get current animation +---Get the ID of the last animation that was started. ---@param animation_state panthera.animation.state @Animation state ---@return string|nil @Animation id or nil if animation is not playing function M.get_latest_animation_id(animation_state) diff --git a/panthera/panthera_internal.lua b/panthera/panthera_internal.lua index 1cd45fd..61b2238 100644 --- a/panthera/panthera_internal.lua +++ b/panthera/panthera_internal.lua @@ -2,6 +2,8 @@ local tweener = require("tweener.tweener") local M = {} +local TYPE_TABLE = "table" + --- Use empty function to save a bit of memory local EMPTY_FUNCTION = function(_, message, context) end @@ -42,13 +44,13 @@ local IS_DEBUG = sys.get_engine_info().is_debug ---@return panthera.animation.data|nil, string|nil, string|nil @animation_data, animation_path, error_reason. function M.load(animation_path_or_data, is_cache_reset) -- If we have already loaded animation table - local is_table = type(animation_path_or_data) == "table" + local is_table = type(animation_path_or_data) == TYPE_TABLE if is_table then - local animation_path = M._get_fake_animation_path() + local animation_path = M.get_fake_animation_path() local project_data = animation_path_or_data --[[@as panthera.animation.project_file]] local data = project_data.data - M._preprocess_animation_keys(data) + M.preprocess_animation_keys(data) M.LOADED_ANIMATIONS[animation_path] = data M.INLINE_ANIMATIONS[animation_path] = true @@ -64,12 +66,12 @@ function M.load(animation_path_or_data, is_cache_reset) end if not M.LOADED_ANIMATIONS[animation_path] then - local animation, error_reason = M._get_animation_by_path(animation_path) + local animation, error_reason = M.get_animation_by_path(animation_path) if not animation then return nil, nil, error_reason end - M._preprocess_animation_keys(animation) + M.preprocess_animation_keys(animation) M.LOADED_ANIMATIONS[animation_path] = animation end @@ -92,35 +94,6 @@ function M.get_animation_by_animation_id(animation_data, animation_id) end ----@param animation_data panthera.animation.data ----@param node_id string ----@param animation_id string ----@return string[] -function M.get_animated_node_properties(animation_data, node_id, animation_id) - local node_properties = {} - for index = 1, #animation_data.animations do - local animation = animation_data.animations[index] - if animation.animation_id == animation_id then - local animation_keys = animation.animation_keys - for key_index = 1, #animation_keys do - local key = animation_keys[key_index] - if key.node_id == node_id then - node_properties[key.property_id] = true - end - end - end - end - - -- Return list of properties - local result = {} - for property_id in pairs(node_properties) do - table.insert(result, property_id) - end - - return result -end - - ---@param animation_state panthera.animation.state ---@param animation_id string ---@param time number @@ -181,12 +154,7 @@ function M.get_node_value_at_time(animation_state, animation_id, node_id, proper local animation_data = M.get_animation_data(animation_state) --[[@as panthera.animation.data]] local group_keys = animation_data.group_animation_keys[animation_id] - local node_keys = group_keys[node_id] - if not node_keys then - return nil - end - - local keys = node_keys[property_id] + local keys = group_keys[node_id] and group_keys[node_id][property_id] if not keys then return nil end @@ -205,7 +173,7 @@ function M.get_node_value_at_time(animation_state, animation_id, node_id, proper local key = keys[index] if key.start_time <= time then if key.key_type == "tween" then - set_value = M._get_key_value_at_time(key, time) + set_value = M.get_key_value_at_time(key, time) end if key.key_type == "trigger" then @@ -234,12 +202,14 @@ function M.set_node_value_at_time(animation_state, animation_id, node_id, proper end local node = M.get_node(animation_state, node_id) - if node then - local set_value = M.get_node_value_at_time(animation_state, animation_id, node_id, property_id, time) - if set_value ~= nil then - adapter.set_node_property(node, property_id, set_value) - return set_value - end + if not node then + return nil + end + + local set_value = M.get_node_value_at_time(animation_state, animation_id, node_id, property_id, time) + if set_value ~= nil then + adapter.set_node_property(node, property_id, set_value) + return set_value end return nil @@ -288,6 +258,7 @@ function M.get_node(animation_state, node_id) return end node = result + animation_state.nodes[node_id] = node end if not node then @@ -314,32 +285,34 @@ function M.run_timeline_key(animation_state, key, options) local adapter = animation_state.adapter local node = M.get_node(animation_state, key.node_id) - - if node and key.key_type == "tween" then - local easing = key.easing_custom or adapter.get_easing(key.easing) - local delta = key.end_value - key.start_value - local start_value = key.start_value - - if options.is_relative then - local current_value = adapter.get_node_property(node, key.property_id) --[[@as number]] - if current_value then - start_value = current_value + local time_overflow = animation_state.current_time - key.start_time + local key_duration = math.max(key.duration - time_overflow, 0) / speed + + if key.key_type == "tween" then + if node then + local easing = key.easing_custom or adapter.get_easing(key.easing) + local delta = key.end_value - key.start_value + local start_value = key.start_value + + if options.is_relative then + local current_value = adapter.get_node_property(node, key.property_id) --[[@as number]] + if current_value then + start_value = current_value + end end - end - - local target_value = start_value + delta - adapter.tween_animation_key(node, key.property_id, easing, key.duration / speed, target_value) - return true - end - - if node and key.key_type == "trigger" then - adapter.trigger_animation_key(node, key.property_id, key.data) - return true - end - - if key.key_type == "event" then - M.event_animation_key(node, key, options.callback_event) + adapter.tween_animation_key(node, key.property_id, easing, key_duration, start_value + delta) + return true + end + return false + elseif key.key_type == "trigger" then + if node then + adapter.trigger_animation_key(node, key.property_id, key.data) + return true + end + return false + elseif key.key_type == "event" then + M.event_animation_key(node, key, key_duration, options.callback_event) return true end @@ -349,17 +322,18 @@ end ---@param node node|nil ---@param key panthera.animation.data.animation_key +---@param duration number @Duration of the key, calculated with animation speed and time overflow ---@param callback_event fun(event_id: string, node: node|nil, data: any, end_value: number): nil -function M.event_animation_key(node, key, callback_event) +function M.event_animation_key(node, key, duration, callback_event) if not callback_event then return end - if key.duration == 0 then + if duration == 0 then callback_event(key.event_id, node, key.data, key.end_value) else local easing = key.easing_custom or tweener[key.easing] or tweener.linear - tweener.tween(easing, key.start_value, key.end_value, key.duration, function(value) + tweener.tween(easing, key.start_value, key.end_value, duration, function(value) callback_event(key.event_id, node, key.data, value) end) end @@ -390,7 +364,7 @@ end ---@private ---@param path string The save path ---@return table|nil, string|nil -function M._load_by_path(path) +function M.load_by_path(path) local file = io.open(path) if file then local file_data = file:read("*all") @@ -401,7 +375,7 @@ function M._load_by_path(path) return nil, "Failed to parse json: " .. path end local parsed_data = result - if parsed_data and type(parsed_data) == "table" then + if parsed_data and type(parsed_data) == TYPE_TABLE then return parsed_data, nil end end @@ -413,7 +387,7 @@ end ---@private ---@param path string The resource path ---@return table|nil, string|nil -function M._load_by_resource_path(path) +function M.load_by_resource_path(path) local data, error = sys.load_resource(path) if error then return nil, error @@ -424,7 +398,7 @@ function M._load_by_resource_path(path) return nil, "Failed to parse json: " .. path end local parsed_data = result - if parsed_data and type(parsed_data) == "table" then + if parsed_data and type(parsed_data) == TYPE_TABLE then return parsed_data end @@ -432,47 +406,20 @@ function M._load_by_resource_path(path) end -local current_dir = nil ----Get current directory ----@private ----@return string|nil -function M._get_current_dir() - if current_dir then - return current_dir - end - - local tmp_path = os.tmpname() - os.execute("pwd > " .. tmp_path) - local file = io.open(tmp_path, "r") - if not file then - return nil - end - - local pwd_result = file:read("*l") - file:close() - os.remove(tmp_path) - current_dir = pwd_result - return current_dir -end - - ---Load animation from JSON file and return it ---@private ---@param path string ---@return panthera.animation.data|nil, string|nil -function M._get_animation_by_path(path) +function M.get_animation_by_path(path) local resource, error if M.IS_HOTRELOAD_ANIMATIONS then - local project_path = M.PROJECT_FOLDER .. path - if not project_path then - return nil, "Can't get current game project folder" - end - resource, error = M._load_by_path(project_path) + local relative_path = M.PROJECT_FOLDER .. path + resource, error = M.load_by_path(relative_path) M.logger:debug("Panthera animation reloaded", path) else - resource, error = M._load_by_resource_path(path) + resource, error = M.load_by_resource_path(path) end if not resource then @@ -492,7 +439,7 @@ end ---@private ---@param a panthera.animation.data.animation_key ---@param b panthera.animation.data.animation_key -function M._sort_keys_function(a, b) +function M.sort_keys_function(a, b) if a.start_time ~= b.start_time then return a.start_time < b.start_time end @@ -512,7 +459,7 @@ end ---@private ---@param data panthera.animation.data -function M._preprocess_animation_keys(data) +function M.preprocess_animation_keys(data) for index = 1, #data.animations do local animation = data.animations[index] @@ -536,7 +483,7 @@ function M._preprocess_animation_keys(data) end end - table.sort(animation.animation_keys, M._sort_keys_function) + table.sort(animation.animation_keys, M.sort_keys_function) end -- For fast search @@ -546,7 +493,7 @@ function M._preprocess_animation_keys(data) data.animations_dict[animation.animation_id] = animation end - data.group_animation_keys = M._get_group_animation_keys(data) + data.group_animation_keys = M.get_group_animation_keys(data) end @@ -554,7 +501,7 @@ end ---@private ---@param animation_data panthera.animation.data ---@return table> -function M._get_group_animation_keys(animation_data) +function M.get_group_animation_keys(animation_data) local group_animations = {} for index = 1, #animation_data.animations do local animation = animation_data.animations[index] @@ -580,7 +527,7 @@ end ---@param key panthera.animation.data.animation_key ---@param time number ---@return number -function M._get_key_value_at_time(key, time) +function M.get_key_value_at_time(key, time) if time < key.start_time then return key.start_value end @@ -597,8 +544,13 @@ end ---Get current application folder (only desktop) +---@private ---@return string|nil @Current application folder, nil if failed -function M._get_current_game_project_folder() +function M.get_current_game_project_folder() + if not io.popen or html5 then + return nil + end + local file = io.popen("pwd") if not file then return nil @@ -624,7 +576,8 @@ end local path_counter = 0 -function M._get_fake_animation_path() +---@private +function M.get_fake_animation_path() path_counter = path_counter + 1 return "panthera_animation_table_" .. path_counter end @@ -634,7 +587,7 @@ end if IS_DEBUG then M.IS_HOTRELOAD_ANIMATIONS = sys.get_config_int("panthera.hotreload_animations", 0) == 1 if M.IS_HOTRELOAD_ANIMATIONS then - M.PROJECT_FOLDER = M._get_current_game_project_folder() + M.PROJECT_FOLDER = M.get_current_game_project_folder() if not M.PROJECT_FOLDER then M.logger:error("Can't get current game project folder") M.IS_HOTRELOAD_ANIMATIONS = false