Quellcode durchsuchen

chore: cleanup and optimization of scripting engine + anonymous namespace methods (#599)

* chore: cleanup rotateVec2 and UserSetting creation

* chore: cleanup and simplification of DynamicValue in scripts

* hotfix: vao errors

* chore: run format

* chore: reduce the amount of builds we're doing

* chore: more cleanup

* chore: formatting

* chore: include clang build steps too

* chore: add missed includes and build files

* fix: build

* fix: build

* chore: be verbose with build commands in pipelines

* chore: dump ubuntu 22 build

* fix: build parameters

* chore: change how properties are bound to the script so no manual sync needs to happen

* chore: more improvements to how properties are bound and what things we provide
chore: all scripts are "ticked" on each frame all at once

* fix: build in clangd

* chore: remove unused variable

* chore: remove unused variable

* fix: parse colors when needed

* fix: javascript leaks

* chore: formatting

* chore: bring back script properties support

* fix: memory leak on property object creation

* chore: ensure the right script properties are created

* chore: script properties do allow user settings as origin, so respect that

* chore: ensure text, image and particles create a debug group

* feat: implement setInterval and setTimeout into the engine

* chore: stop messages about color parsing as they're not really relevant errors

* chore: formatting
Alexis Maiquez vor 3 Monaten
Ursprung
Commit
100b664482
84 geänderte Dateien mit 4279 neuen und 2331 gelöschten Zeilen
  1. 47 95
      .github/workflows/cmake.yml
  2. 34 2
      CMakeLists.txt
  3. 3 2
      src/WallpaperEngine/Application/ApplicationContext.cpp
  4. 99 97
      src/WallpaperEngine/Application/WallpaperApplication.cpp
  5. 3 3
      src/WallpaperEngine/Application/WallpaperApplication.h
  6. 69 0
      src/WallpaperEngine/Data/Builders/ColorBuilder.cpp
  7. 22 0
      src/WallpaperEngine/Data/Builders/ColorBuilder.h
  8. 1 1
      src/WallpaperEngine/Data/Builders/VectorBuilder.h
  9. 7 1
      src/WallpaperEngine/Data/JSON.cpp
  10. 17 0
      src/WallpaperEngine/Data/JSON.h
  11. 21 0
      src/WallpaperEngine/Data/Model/Color.h
  12. 76 131
      src/WallpaperEngine/Data/Model/DynamicValue.cpp
  13. 61 29
      src/WallpaperEngine/Data/Model/DynamicValue.h
  14. 3 5
      src/WallpaperEngine/Data/Model/Object.h
  15. 24 58
      src/WallpaperEngine/Data/Model/Property.h
  16. 0 74
      src/WallpaperEngine/Data/Model/ScriptedDynamicValue.cpp
  17. 0 52
      src/WallpaperEngine/Data/Model/ScriptedDynamicValue.h
  18. 6 6
      src/WallpaperEngine/Data/Model/Wallpaper.h
  19. 80 0
      src/WallpaperEngine/Data/Parsers/DynamicValueParser.cpp
  20. 14 0
      src/WallpaperEngine/Data/Parsers/DynamicValueParser.h
  21. 1 0
      src/WallpaperEngine/Data/Parsers/EffectParser.cpp
  22. 1 0
      src/WallpaperEngine/Data/Parsers/ModelParser.cpp
  23. 37 246
      src/WallpaperEngine/Data/Parsers/ObjectParser.cpp
  24. 4 79
      src/WallpaperEngine/Data/Parsers/UserSettingParser.cpp
  25. 2 1
      src/WallpaperEngine/Data/Parsers/UserSettingParser.h
  26. 6 6
      src/WallpaperEngine/Data/Parsers/WallpaperParser.cpp
  27. 35 0
      src/WallpaperEngine/Data/Utils/ScopeGuard.h
  28. 5 7
      src/WallpaperEngine/Data/Utils/TypeCaster.h
  29. 23 26
      src/WallpaperEngine/Input/Drivers/WaylandMouseInput.cpp
  30. 5 1
      src/WallpaperEngine/Input/Drivers/WaylandMouseInput.h
  31. 1 1
      src/WallpaperEngine/Logging/Log.cpp
  32. 2 2
      src/WallpaperEngine/Logging/Log.h
  33. 32 0
      src/WallpaperEngine/Maths.cpp
  34. 11 0
      src/WallpaperEngine/Maths.h
  35. 1 0
      src/WallpaperEngine/Render/CObject.cpp
  36. 2 19
      src/WallpaperEngine/Render/CObject.h
  37. 10 8
      src/WallpaperEngine/Render/CWallpaper.cpp
  38. 4 2
      src/WallpaperEngine/Render/CWallpaper.h
  39. 5 5
      src/WallpaperEngine/Render/Camera.cpp
  40. 2 2
      src/WallpaperEngine/Render/Drivers/Output/OutputViewport.h
  41. 1 2
      src/WallpaperEngine/Render/Drivers/Output/WaylandOutputViewport.cpp
  42. 77 83
      src/WallpaperEngine/Render/Objects/CImage.cpp
  43. 8 14
      src/WallpaperEngine/Render/Objects/CImage.h
  44. 164 175
      src/WallpaperEngine/Render/Objects/CParticle.cpp
  45. 2 1
      src/WallpaperEngine/Render/Objects/CParticle.h
  46. 2 0
      src/WallpaperEngine/Render/Objects/CRenderable.cpp
  47. 2 2
      src/WallpaperEngine/Render/Objects/CRenderable.h
  48. 95 52
      src/WallpaperEngine/Render/Objects/CText.cpp
  49. 5 4
      src/WallpaperEngine/Render/Objects/CText.h
  50. 45 37
      src/WallpaperEngine/Render/Objects/Effects/CPass.cpp
  51. 9 8
      src/WallpaperEngine/Render/Objects/Effects/CPass.h
  52. 3 2
      src/WallpaperEngine/Render/RenderContext.cpp
  53. 7 7
      src/WallpaperEngine/Render/Shaders/ShaderUnit.cpp
  54. 11 11
      src/WallpaperEngine/Render/TextureCache.cpp
  55. 2 2
      src/WallpaperEngine/Render/TextureProvider.h
  56. 24 111
      src/WallpaperEngine/Render/Wallpapers/CScene.cpp
  57. 4 5
      src/WallpaperEngine/Render/Wallpapers/CScene.h
  58. 30 0
      src/WallpaperEngine/Scripting/Adapters/ObjectAdapter.cpp
  59. 32 0
      src/WallpaperEngine/Scripting/Adapters/ObjectAdapter.h
  60. 90 0
      src/WallpaperEngine/Scripting/Adapters/ScriptableObjectAdapter.cpp
  61. 17 0
      src/WallpaperEngine/Scripting/Adapters/ScriptableObjectAdapter.h
  62. 1005 0
      src/WallpaperEngine/Scripting/Adapters/VectorAdapter.cpp
  63. 34 0
      src/WallpaperEngine/Scripting/Adapters/VectorAdapter.h
  64. 69 0
      src/WallpaperEngine/Scripting/ConsoleObject.cpp
  65. 25 0
      src/WallpaperEngine/Scripting/ConsoleObject.h
  66. 291 0
      src/WallpaperEngine/Scripting/EngineObject.cpp
  67. 47 0
      src/WallpaperEngine/Scripting/EngineObject.h
  68. 66 0
      src/WallpaperEngine/Scripting/InputObject.cpp
  69. 25 0
      src/WallpaperEngine/Scripting/InputObject.h
  70. 266 0
      src/WallpaperEngine/Scripting/Modules/ColorModule.cpp
  71. 16 0
      src/WallpaperEngine/Scripting/Modules/ColorModule.h
  72. 94 0
      src/WallpaperEngine/Scripting/Modules/MathModule.cpp
  73. 16 0
      src/WallpaperEngine/Scripting/Modules/MathModule.h
  74. 12 0
      src/WallpaperEngine/Scripting/Modules/ScriptModule.cpp
  75. 26 0
      src/WallpaperEngine/Scripting/Modules/ScriptModule.h
  76. 310 0
      src/WallpaperEngine/Scripting/SceneObject.cpp
  77. 26 0
      src/WallpaperEngine/Scripting/SceneObject.h
  78. 194 813
      src/WallpaperEngine/Scripting/ScriptEngine.cpp
  79. 56 40
      src/WallpaperEngine/Scripting/ScriptEngine.h
  80. 185 0
      src/WallpaperEngine/Scripting/ScriptPropertiesObject.cpp
  81. 36 0
      src/WallpaperEngine/Scripting/ScriptPropertiesObject.h
  82. 43 0
      src/WallpaperEngine/Scripting/ScriptableObject.cpp
  83. 30 0
      src/WallpaperEngine/Scripting/ScriptableObject.h
  84. 1 1
      src/WallpaperEngine/Testing/Cases/PropertyParser.cpp

+ 47 - 95
.github/workflows/cmake.yml

@@ -20,17 +20,17 @@ on:
   workflow_dispatch:
 
 jobs:
-#  formatting:
-#    name: Formatting check
-#    runs-on: ubuntu-latest
-#    steps:
-#      - uses: actions/checkout@v4
-#      - name: Run clang-format style check for C/C++ programs.
-#        uses: jidicula/clang-format-action@v4.16.0
-#        with:
-#          check-path: "src"
-#          exclude-regex: "(src/External)"
-#      - run: echo "Formatted with clang-format version ${{ steps.clang-format.outputs.clang-format-version }}"
+  formatting:
+    name: Formatting check
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Run clang-format style check for C/C++ programs.
+        uses: jidicula/clang-format-action@v4.18.0
+        with:
+          check-path: "src"
+          exclude-regex: "(src/External)"
+      - run: echo "Formatted with clang-format version ${{ steps.clang-format.outputs.clang-format-version }}"
 
   tests:
     #needs: formatting
@@ -54,10 +54,9 @@ jobs:
       - name: ccache
         uses: hendrikmuhs/ccache-action@v1.2
         with:
-          key: ${{ github.job }}-${{ matrix.os }}  # Eg. "some_build-ubuntu-latest"
+          key: ${{ github.job }}-${{ matrix.os }}
 
       - name: Install dependencies
-        if: matrix.os == 'ubuntu-24.04'
         run: sudo apt-get update && sudo apt-get -y install libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv2 libfftw3-dev libfreetype-dev
 
       - name: Configure CMake
@@ -67,53 +66,13 @@ jobs:
 
       - name: Build
         # Build your program with the given configuration
-        run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --target tests
+        run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --target tests --verbose
 
       - name: Run tests
         # Run the unit tests available
         run: ${{github.workspace}}/build/output/tests
 
-
-  build-x11:
-    needs: tests
-    strategy:
-      matrix:
-        os: [ubuntu-22.04, ubuntu-24.04]
-        build_type: [Release, Debug]
-
-    # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac.
-    # You can convert this to a matrix build if you need cross-platform coverage.
-    # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix
-    runs-on: ${{ matrix.os }}
-
-    steps:
-    - uses: actions/checkout@v3
-      with:
-        submodules: true
-
-    # cache some build information
-    - name: ccache
-      uses: hendrikmuhs/ccache-action@v1.2
-      with:
-        key: ${{ github.job }}-${{ matrix.os }}-${{ matrix.build_type }}  # Eg. "some_build-ubuntu-latest"
-
-    - name: Install dependencies
-      if: matrix.os == 'ubuntu-22.04'
-      run: sudo apt-get update && sudo apt-get -y install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libxxf86vm-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv1 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev
-    - name: Install dependencies
-      if: matrix.os == 'ubuntu-24.04'
-      run: sudo apt-get update && sudo apt-get -y install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libxxf86vm-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv2 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev
-
-    - name: Configure CMake
-      # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
-      # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
-      run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache
-
-    - name: Build
-      # Build your program with the given configuration
-      run: cmake --build ${{github.workspace}}/build --config ${{matrix.build_type}}
-
-  build-x11-wayland:
+  build:
     needs: tests
     permissions:
       actions: read
@@ -122,12 +81,11 @@ jobs:
 
     strategy:
       matrix:
-        os: [ubuntu-22.04, ubuntu-24.04]
         build_type: [Release, Debug]
     # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac.
     # You can convert this to a matrix build if you need cross-platform coverage.
     # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix
-    runs-on: ${{ matrix.os }}
+    runs-on: ubuntu-24.04
 
     steps:
       - uses: actions/checkout@v3
@@ -137,21 +95,16 @@ jobs:
       - name: ccache
         uses: hendrikmuhs/ccache-action@v1.2
         with:
-          key: ${{ github.job }}-${{ matrix.os }}-${{ matrix.build_type }}  # Eg. "some_build-ubuntu-latest"
+          key: ${{ github.job }}-ubuntu-24.04-${{ matrix.build_type }}
 
       - name: Initialize CodeQL
         # Initializes the CodeQL tools for scanning.
         uses: github/codeql-action/init@v3
-        if: ${{ matrix.os == 'ubuntu-24.04' && matrix.build_type == 'Debug' }}
+        if: ${{ matrix.build_type == 'Debug' }}
         with:
           languages: 'cpp'
 
       - name: Install dependencies
-        if: matrix.os == 'ubuntu-22.04'
-        run: sudo apt-get update && sudo apt-get -y install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libxxf86vm-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv1 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev wayland-scanner++ wayland-protocols libwayland-dev
-
-      - name: Install dependencies
-        if: matrix.os == 'ubuntu-24.04'
         run: sudo apt-get update && sudo apt-get -y install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libxxf86vm-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv2 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev wayland-scanner++ wayland-protocols libwayland-dev
 
       - name: Configure CMake
@@ -161,46 +114,45 @@ jobs:
 
       - name: Build
         # Build your program with the given configuration
-        run: cmake --build ${{github.workspace}}/build --config ${{matrix.build_type}}
+        run: cmake --build ${{github.workspace}}/build --config ${{matrix.build_type}} --verbose
 
       - name: Perform CodeQL Analysis
-        if: ${{ matrix.os == 'ubuntu-24.04' && matrix.build_type == 'Debug' }}
+        if: ${{ matrix.build_type == 'Debug' }}
         uses: github/codeql-action/analyze@v3
 
-  build-wayland:
+  clang-build:
     needs: tests
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
     strategy:
       matrix:
-        os: [ubuntu-22.04, ubuntu-24.04]
-        build_type: [Release, Debug]
+        build_type: [Debug, Release]
     # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac.
     # You can convert this to a matrix build if you need cross-platform coverage.
     # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix
-    runs-on: ${{ matrix.os }}
+    runs-on: ubuntu-24.04
 
     steps:
-    - uses: actions/checkout@v3
-      with:
-        submodules: true
-
-    - name: ccache
-      uses: hendrikmuhs/ccache-action@v1.2
-      with:
-        key: ${{ github.job }}-${{ matrix.os }}-${{ matrix.build_type }}  # Eg. "some_build-ubuntu-latest"
-
-    - name: Install dependencies
-      if: matrix.os == 'ubuntu-22.04'
-      run: sudo apt-get update && sudo apt-get -y install wayland-scanner++ wayland-protocols libwayland-dev libwayland-egl-backend-dev libglfw3-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libglm-dev libmpv-dev mpv libmpv1 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev
-
-    - name: Install dependencies
-      if: matrix.os == 'ubuntu-24.04'
-      run: sudo apt-get update && sudo apt-get -y install wayland-scanner++ wayland-protocols libwayland-dev libwayland-egl-backend-dev libglfw3-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libglm-dev libmpv-dev mpv libmpv2 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev
-
-    - name: Configure CMake
-      # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
-      # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
-      run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache
-
-    - name: Build
-      # Build your program with the given configuration
-      run: cmake --build ${{github.workspace}}/build --config ${{matrix.build_type}}
+      - uses: actions/checkout@v3
+        with:
+          submodules: true
+
+      - name: ccache
+        uses: hendrikmuhs/ccache-action@v1.2
+        with:
+          key: ${{ github.job }}-ubuntu-24.04-${{ matrix.build_type }}
+
+      - name: Install dependencies
+        run: sudo apt-get update && sudo apt-get -y install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libxxf86vm-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv2 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev wayland-scanner++ wayland-protocols libwayland-dev
+
+      - name: Configure CMake
+        # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
+        # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
+        run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache -D CMAKE_C_COMPILER=clang -D CMAKE_CXX_COMPILER=clang++
+
+      - name: Build
+        # Build your program with the given configuration
+        run: cmake --build ${{github.workspace}}/build --config ${{matrix.build_type}} --verbose

+ 34 - 2
CMakeLists.txt

@@ -454,6 +454,8 @@ set(COMMON_SOURCES
     src/WallpaperEngine/Data/Utils/BinaryReader.h
     src/WallpaperEngine/Data/Utils/MemoryStream.h
     src/WallpaperEngine/Data/Utils/SFINAE.h
+    src/WallpaperEngine/Data/Parsers/DynamicValueParser.h
+    src/WallpaperEngine/Data/Parsers/DynamicValueParser.cpp
     src/WallpaperEngine/Data/Parsers/EffectParser.cpp
     src/WallpaperEngine/Data/Parsers/EffectParser.h
     src/WallpaperEngine/Data/Parsers/ProjectParser.cpp
@@ -476,6 +478,8 @@ set(COMMON_SOURCES
     src/WallpaperEngine/Data/Parsers/PropertyParser.h
     src/WallpaperEngine/Data/Parsers/TextureParser.cpp
     src/WallpaperEngine/Data/Parsers/TextureParser.h
+    src/WallpaperEngine/Data/Builders/ColorBuilder.h
+    src/WallpaperEngine/Data/Builders/ColorBuilder.cpp
     src/WallpaperEngine/Data/Builders/UserSettingBuilder.h
     src/WallpaperEngine/Data/Builders/VectorBuilder.cpp
     src/WallpaperEngine/Data/Builders/VectorBuilder.h
@@ -487,10 +491,38 @@ set(COMMON_SOURCES
     src/WallpaperEngine/VideoPlayback/MPV/GLPlayer.h
     src/WallpaperEngine/VideoPlayback/MPV/MemoryStreamProtocol.cpp
     src/WallpaperEngine/VideoPlayback/MPV/MemoryStreamProtocol.h
+
+    src/WallpaperEngine/Scripting/Adapters/ObjectAdapter.h
+    src/WallpaperEngine/Scripting/Adapters/ObjectAdapter.cpp
+    src/WallpaperEngine/Scripting/Adapters/VectorAdapter.h
+    src/WallpaperEngine/Scripting/Adapters/VectorAdapter.cpp
+    src/WallpaperEngine/Scripting/Adapters/ScriptableObjectAdapter.h
+    src/WallpaperEngine/Scripting/Adapters/ScriptableObjectAdapter.cpp
+
+    src/WallpaperEngine/Scripting/Modules/ColorModule.h
+    src/WallpaperEngine/Scripting/Modules/ColorModule.cpp
+    src/WallpaperEngine/Scripting/Modules/MathModule.h
+    src/WallpaperEngine/Scripting/Modules/MathModule.cpp
+    src/WallpaperEngine/Scripting/Modules/ScriptModule.h
+    src/WallpaperEngine/Scripting/Modules/ScriptModule.cpp
+
+    src/WallpaperEngine/Scripting/ConsoleObject.h
+    src/WallpaperEngine/Scripting/ConsoleObject.cpp
+    src/WallpaperEngine/Scripting/EngineObject.h
+    src/WallpaperEngine/Scripting/EngineObject.cpp
+    src/WallpaperEngine/Scripting/InputObject.h
+    src/WallpaperEngine/Scripting/InputObject.cpp
+    src/WallpaperEngine/Scripting/SceneObject.h
+    src/WallpaperEngine/Scripting/SceneObject.cpp
     src/WallpaperEngine/Scripting/ScriptEngine.h
+    src/WallpaperEngine/Scripting/ScriptPropertiesObject.h
+    src/WallpaperEngine/Scripting/ScriptPropertiesObject.cpp
     src/WallpaperEngine/Scripting/ScriptEngine.cpp
-    src/WallpaperEngine/Data/Model/ScriptedDynamicValue.h
-    src/WallpaperEngine/Data/Model/ScriptedDynamicValue.cpp)
+    src/WallpaperEngine/Scripting/ScriptableObject.h
+    src/WallpaperEngine/Scripting/ScriptableObject.cpp
+
+    src/WallpaperEngine/Maths.h
+    src/WallpaperEngine/Maths.cpp)
 
 if(BUILD_TESTING)
     add_executable(

+ 3 - 2
src/WallpaperEngine/Application/ApplicationContext.cpp

@@ -9,8 +9,8 @@
 #include <cstring>
 #include <fstream>
 #include <iostream>
-#include <sstream>
 #include <optional>
+#include <sstream>
 #include <stdexcept>
 #include <string_view>
 
@@ -603,7 +603,8 @@ void ApplicationContext::loadSettingsFromArgv () {
 
     debuggingGroup.add_argument ("--render-debug")
 	.help (
-		    "Scene render debug mode: base-only, no-solid-final, pass-log, object=<id>, skip-object=<id>, or skip-effect=<id>. Can be repeated."
+	    "Scene render debug mode: base-only, no-solid-final, pass-log, object=<id>, skip-object=<id>, or "
+	    "skip-effect=<id>. Can be repeated."
 	)
 	.action ([this] (const std::string& value) -> void {
 	    const auto parseDebugId = [&value] (const std::string& prefix) -> std::optional<int> {

+ 99 - 97
src/WallpaperEngine/Application/WallpaperApplication.cpp

@@ -497,7 +497,7 @@ void WallpaperApplication::setupPropertiesForProject (const Project& project) {
 	if (override != this->m_context.settings.general.properties.end ()) {
 	    sLog.out ("Applying override value for ", key);
 
-	    cur->update (override->second);
+	    cur->update (override->second, DynamicValue::UpdateSource::User);
 	}
 
 	if (this->m_context.settings.general.onlyListProperties) {
@@ -764,8 +764,10 @@ void WallpaperApplication::prepareOutputs () {
 	    const int y = vp->globalPosition.y;
 	    const int w = vp->logicalSize.x;
 	    const int h = vp->logicalSize.y;
-	    sLog.debug ("SPAN DEBUG prepareOutputs: screen '", screenName,
-		"' globalPos=(", x, ",", y, ") logicalSize=", w, "x", h);
+	    sLog.debug (
+		"SPAN DEBUG prepareOutputs: screen '", screenName, "' globalPos=(", x, ",", y, ") logicalSize=", w, "x",
+		h
+	    );
 	    minX = std::min (minX, x);
 	    minY = std::min (minY, y);
 	    maxX = std::max (maxX, x + w);
@@ -777,7 +779,9 @@ void WallpaperApplication::prepareOutputs () {
 	    continue;
 	}
 
-	sLog.debug ("SPAN DEBUG prepareOutputs: bounding box=(", minX, ",", minY, ",", maxX - minX, ",", maxY - minY, ")");
+	sLog.debug (
+	    "SPAN DEBUG prepareOutputs: bounding box=(", minX, ",", minY, ",", maxX - minX, ",", maxY - minY, ")"
+	);
 
 	WallpaperEngine::Render::CWallpaper::SpanInfo spanInfo;
 	spanInfo.totalBounds = { minX, minY, maxX - minX, maxY - minY };
@@ -840,115 +844,115 @@ void WallpaperApplication::render () {
     static time_t seconds;
     static struct tm* timeinfo;
 
-	if (this->m_isPaused) {
-		usleep (FULLSCREEN_CHECK_WAIT_TIME);
-		if (this->m_fullScreenDetector->anythingFullscreen () && this->m_context.state.general.keepRunning) {
-			return;
-		}
-	    m_renderContext->setPause (false);
+    if (this->m_isPaused) {
+	usleep (FULLSCREEN_CHECK_WAIT_TIME);
+	if (this->m_fullScreenDetector->anythingFullscreen () && this->m_context.state.general.keepRunning) {
+	    return;
+	}
+	m_renderContext->setPause (false);
 
-	    // account for paused duration in playlist timers
-	    const auto pausedNow = std::chrono::steady_clock::now ();
-	    const auto pausedDuration = pausedNow - this->m_pauseStart;
+	// account for paused duration in playlist timers
+	const auto pausedNow = std::chrono::steady_clock::now ();
+	const auto pausedDuration = pausedNow - this->m_pauseStart;
 
-	    for (auto& [_, playlist] : this->m_activePlaylists) {
-		if (!playlist.definition.settings.updateOnPause) {
-		    playlist.nextSwitch += pausedDuration;
-		    playlist.lastUpdate += pausedDuration;
-		}
+	for (auto& [_, playlist] : this->m_activePlaylists) {
+	    if (!playlist.definition.settings.updateOnPause) {
+		playlist.nextSwitch += pausedDuration;
+		playlist.lastUpdate += pausedDuration;
 	    }
+	}
 
-	    this->m_isPaused = false;
-	} else {
-		// update g_Daytime
-		time (&seconds);
-		timeinfo = localtime (&seconds);
-		g_Daytime = static_cast<float>((timeinfo->tm_hour * 60) + timeinfo->tm_min) / (24.0f * 60.0f);
-
-		// keep track of the previous frame's time
-		g_TimeLast = g_Time;
-		// calculate the current time value
-		g_Time = m_videoDriver->getRenderTime ();
-		// update audio recorder
-		m_audioDriver->update ();
-		// update input information
-		m_videoDriver->getInputContext ().update ();
-		// process driver events
-		m_videoDriver->dispatchEventQueue ();
-
-		if (m_videoDriver->closeRequested ()) {
-			sLog.out ("Stop requested by driver");
-			this->m_context.state.general.keepRunning = false;
-		}
+	this->m_isPaused = false;
+    } else {
+	// update g_Daytime
+	time (&seconds);
+	timeinfo = localtime (&seconds);
+	g_Daytime = static_cast<float> ((timeinfo->tm_hour * 60) + timeinfo->tm_min) / (24.0f * 60.0f);
+
+	// keep track of the previous frame's time
+	g_TimeLast = g_Time;
+	// calculate the current time value
+	g_Time = m_videoDriver->getRenderTime ();
+	// update audio recorder
+	m_audioDriver->update ();
+	// update input information
+	m_videoDriver->getInputContext ().update ();
+	// process driver events
+	m_videoDriver->dispatchEventQueue ();
+
+	if (m_videoDriver->closeRequested ()) {
+	    sLog.out ("Stop requested by driver");
+	    this->m_context.state.general.keepRunning = false;
+	}
 
 #if DEMOMODE
-		// wait for a full render cycle before actually starting
-		// this gives some extra time for video and web decoders to set themselves up
-		// because of size changes
-		if (m_videoDriver->getFrameCounter () > (uint32_t)this->m_context.settings.render.maximumFPS) {
-			if (!initialized) {
-			width = this->m_renderContext->getWallpapers ().begin ()->second->getWidth ();
-			height = this->m_renderContext->getWallpapers ().begin ()->second->getHeight ();
-			pixels.reserve (width * height * 3);
-			init_encoder ("output.webm", width, height);
-			initialized = true;
-			}
-
-			glBindFramebuffer (
-			GL_FRAMEBUFFER, this->m_renderContext->getWallpapers ().begin ()->second->getWallpaperFramebuffer ()
-			);
-
-			glPixelStorei (GL_PACK_ALIGNMENT, 1);
-			glReadPixels (0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels.data ());
-			write_video_frame (pixels.data ());
-			frame++;
-
-			// stop after the given framecount
-			if (frame >= FRAME_COUNT) {
-			this->m_context.state.general.keepRunning = false;
-			}
-		}
-#endif /* DEMOMODE */
-		// check for fullscreen windows and wait until there's none fullscreen
-		if (this->m_fullScreenDetector->anythingFullscreen () && this->m_context.state.general.keepRunning) {
-			this->m_isPaused = true;
-			this->m_pauseStart = std::chrono::steady_clock::now ();
+	// wait for a full render cycle before actually starting
+	// this gives some extra time for video and web decoders to set themselves up
+	// because of size changes
+	if (m_videoDriver->getFrameCounter () > (uint32_t)this->m_context.settings.render.maximumFPS) {
+	    if (!initialized) {
+		width = this->m_renderContext->getWallpapers ().begin ()->second->getWidth ();
+		height = this->m_renderContext->getWallpapers ().begin ()->second->getHeight ();
+		pixels.reserve (width * height * 3);
+		init_encoder ("output.webm", width, height);
+		initialized = true;
+	    }
 
-			m_renderContext->setPause (true);
-			return;
-		}
-	}
+	    glBindFramebuffer (
+		GL_FRAMEBUFFER, this->m_renderContext->getWallpapers ().begin ()->second->getWallpaperFramebuffer ()
+	    );
 
-	this->updatePlaylists ();
+	    glPixelStorei (GL_PACK_ALIGNMENT, 1);
+	    glReadPixels (0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels.data ());
+	    write_video_frame (pixels.data ());
+	    frame++;
 
-	if (!this->m_context.settings.screenshot.take || this->m_screenShotTaken == true) {
-	    return;
+	    // stop after the given framecount
+	    if (frame >= FRAME_COUNT) {
+		this->m_context.state.general.keepRunning = false;
+	    }
 	}
+#endif /* DEMOMODE */
+	// check for fullscreen windows and wait until there's none fullscreen
+	if (this->m_fullScreenDetector->anythingFullscreen () && this->m_context.state.general.keepRunning) {
+	    this->m_isPaused = true;
+	    this->m_pauseStart = std::chrono::steady_clock::now ();
 
-	if (this->m_videoDriver->getFrameCounter () < this->m_nextFrameScreenshot) {
+	    m_renderContext->setPause (true);
 	    return;
 	}
+    }
+
+    this->updatePlaylists ();
 
-	this->takeScreenshot (this->m_context.settings.screenshot.path);
-	this->m_screenShotTaken = true;
+    if (!this->m_context.settings.screenshot.take || this->m_screenShotTaken == true) {
+	return;
+    }
+
+    if (this->m_videoDriver->getFrameCounter () < this->m_nextFrameScreenshot) {
+	return;
+    }
+
+    this->takeScreenshot (this->m_context.settings.screenshot.path);
+    this->m_screenShotTaken = true;
 }
 
 void WallpaperApplication::cleanup () {
-	sLog.out ("Stopping");
+    sLog.out ("Stopping");
 
-	#if DEMOMODE
-		close_encoder ();
-	#endif /* DEMOMODE */
+#if DEMOMODE
+    close_encoder ();
+#endif /* DEMOMODE */
 
-		SDL_Quit ();
+    SDL_Quit ();
 }
 
 void WallpaperApplication::show () {
-	setup();
+    setup ();
     while (this->m_context.state.general.keepRunning) {
-		render();
+	render ();
     }
-    cleanup();
+    cleanup ();
 }
 
 void WallpaperApplication::update (Render::Drivers::Output::OutputViewport* viewport) {
@@ -972,13 +976,11 @@ const WallpaperEngine::Render::Drivers::Output::Output& WallpaperApplication::ge
 }
 
 void WallpaperApplication::setDestinationFramebuffer (GLuint framebuffer) {
-	this->m_destinationFramebuffer = framebuffer;
-	// Update all wallpapers with the new destination framebuffer
-	for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
-		wallpaper->setDestinationFramebuffer (framebuffer);
-	};
+    this->m_destinationFramebuffer = framebuffer;
+    // Update all wallpapers with the new destination framebuffer
+    for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
+	wallpaper->setDestinationFramebuffer (framebuffer);
+    };
 }
 
-GLuint WallpaperApplication::getDestinationFramebuffer () const { 
-	return this->m_destinationFramebuffer;
-}
+GLuint WallpaperApplication::getDestinationFramebuffer () const { return this->m_destinationFramebuffer; }

+ 3 - 3
src/WallpaperEngine/Application/WallpaperApplication.h

@@ -35,15 +35,15 @@ public:
     /**
      * Prepares the application for rendering.
      */
-    void setup();
+    void setup ();
     /**
      * Renders a frame of the application.
      */
-    void render();
+    void render ();
     /**
      * Cleans up all the resources used by the application.
      */
-    static void cleanup();
+    static void cleanup ();
     /**
      * Shows the application until it's closed
      */

+ 69 - 0
src/WallpaperEngine/Data/Builders/ColorBuilder.cpp

@@ -0,0 +1,69 @@
+#include "ColorBuilder.h"
+#include "VectorBuilder.h"
+
+#include <algorithm>
+#include <format>
+#include <glm/vec3.hpp>
+
+const WallpaperEngine::Data::Model::Color WallpaperEngine::Data::Builders::ColorBuilder::White
+    = WallpaperEngine::Data::Model::Color (1.0f, 1.0f, 1.0f, 1.0f);
+const WallpaperEngine::Data::Model::Color WallpaperEngine::Data::Builders::ColorBuilder::Black
+    = WallpaperEngine::Data::Model::Color (0.0f, 0.0f, 0.0f, 1.0f);
+
+WallpaperEngine::Data::Model::Color
+WallpaperEngine::Data::Builders::ColorBuilder::parse (const std::string& value, float alpha) {
+    auto copy = value;
+
+    // replace the actual separators with spaces to normalize them
+    if (copy.find (',') != std::string::npos) {
+	// replace comma separator with spaces so it's
+	std::ranges::replace (copy, ',', ' ');
+    }
+
+    // hex colors should be converted to int colors
+    if (copy.find ('#') == 0) {
+	auto number = copy.substr (1);
+
+	// expand short css notation into the right one
+	// support for css notation
+	if (number.size () == 3) {
+	    number = std::format (
+		"{}{}{}{}{}{}{:02x}", number.at (0), number.at (0), number.at (1), number.at (1), number.at (2),
+		number.at (2), static_cast<int> (alpha * 255)
+	    );
+	} else if (number.size () == 4) {
+	    number = std::format (
+		"{}{}{}{}{}{}{}{}", number.at (0), number.at (0), number.at (1), number.at (1), number.at (2),
+		number.at (2), number.at (3), number.at (3)
+	    );
+	} else if (number.size () != 6 && number.size () != 8) {
+	    sLog.exception ("Invalid CSS color notation for ", value);
+	}
+
+	// parse hex color
+	const auto color = std::stoi (number, nullptr, 16);
+
+	return WallpaperEngine::Data::Model::Color (
+	    (color >> 24 & 0xFF) / 255.0f, (color >> 16 & 0xFF) / 255.0f, (color >> 8 & 0xFF) / 255.0f,
+	    (color & 0xFF) / 255.0f
+	);
+    }
+
+    int vectorSize = VectorBuilder::preparseSize (copy);
+
+    if (vectorSize != 3 && vectorSize != 4) {
+	throw std::invalid_argument ("Invalid color value");
+    }
+
+    if (copy.find ('.') == std::string::npos) {
+	const auto final = vectorSize == 3 ? glm::ivec4 (VectorBuilder::parse<glm::ivec3> (copy), alpha * 255)
+					   : VectorBuilder::parse<glm::ivec4> (copy);
+
+	return { final.r / 255.0f, final.g / 255.0f, final.b / 255.0f, final.a / 255.0f };
+    }
+
+    return Model::Color (
+	vectorSize == 3 ? glm::vec4 (VectorBuilder::parse<glm::vec3> (copy), alpha)
+			: VectorBuilder::parse<glm::vec4> (copy)
+    );
+}

+ 22 - 0
src/WallpaperEngine/Data/Builders/ColorBuilder.h

@@ -0,0 +1,22 @@
+#pragma once
+
+#include "WallpaperEngine/Data/Model/Color.h"
+
+#include <glm/vec4.hpp>
+#include <string>
+
+namespace WallpaperEngine::Data::Builders {
+class ColorBuilder {
+public:
+    /**
+     * White color constant
+     */
+    static const Model::Color White;
+    /**
+     * Black color constant
+     */
+    static const Model::Color Black;
+
+    static WallpaperEngine::Data::Model::Color parse (const std::string& value, float alpha = 1.0f);
+};
+}

+ 1 - 1
src/WallpaperEngine/Data/Builders/VectorBuilder.h

@@ -38,7 +38,7 @@ public:
 	const char* third = second ? strchr (second + 1, ' ') : nullptr;
 
 	if (first == nullptr) {
-	    return 0;
+	    return 1;
 	}
 
 	if (second == nullptr) {

+ 7 - 1
src/WallpaperEngine/Data/JSON.cpp

@@ -10,4 +10,10 @@ UserSettingUniquePtr JsonExtensions::user (const std::string& key, const Propert
     const auto value = this->require (key, "User setting without default value must be present");
 
     return UserSettingParser::parse (value, properties);
-}
+}
+
+UserSettingUniquePtr JsonExtensions::color (const std::string& key, const Properties& properties) const {
+    const auto value = this->require (key, "User setting without default value must be present");
+
+    return UserSettingParser::parse (value, properties, true);
+}

+ 17 - 0
src/WallpaperEngine/Data/JSON.h

@@ -1,5 +1,7 @@
 #pragma once
 
+#include "Builders/ColorBuilder.h"
+
 #include <glm/detail/qualifier.hpp>
 #include <glm/detail/type_vec1.hpp>
 #include <nlohmann/json.hpp>
@@ -45,6 +47,7 @@ public:
     [[nodiscard]] glm::vec<length, type, qualifier> get () const {
 	return VectorBuilder::parse<length, type, qualifier> (this->base ().get<std::string> ());
     }
+    [[nodiscard]] Model::Color get () const { return ColorBuilder::parse (this->base ().get<std::string> ()); }
     [[nodiscard]] base_type require (const std::string& key, const std::string& message) const {
 	auto base = this->base ();
 	const auto it = base.find (key);
@@ -100,6 +103,7 @@ public:
     template <typename T>
     [[nodiscard]] UserSettingUniquePtr
     user (const std::string& key, const Properties& properties, T defaultValue) const {
+	static_assert (std::is_same_v<T, Color> == false, "Use color() for color properties");
 	const auto value = this->optional (key);
 
 	if (!value.has_value ()) {
@@ -110,6 +114,19 @@ public:
 	// this resolving the include loop
 	return this->user (key, properties);
     }
+    [[nodiscard]] UserSettingUniquePtr color (const std::string& key, const Properties& properties) const;
+    [[nodiscard]] UserSettingUniquePtr
+    color (const std::string& key, const Properties& properties, Color defaultValue) const {
+	const auto value = this->optional (key);
+
+	if (!value.has_value ()) {
+	    return UserSettingBuilder::fromValue<Color> (defaultValue);
+	}
+
+	// performs a second lookup, but handles the actual call to UserSettingParser outside of this header
+	// this resolving the include loop
+	return this->color (key, properties);
+    }
 
     template <int length, typename type, glm::qualifier qualifier> operator glm::vec<length, type, qualifier> () const {
 	return get<length, type, qualifier> ();

+ 21 - 0
src/WallpaperEngine/Data/Model/Color.h

@@ -0,0 +1,21 @@
+#pragma once
+#include <glm/vec4.hpp>
+
+namespace WallpaperEngine::Data::Model {
+/**
+ * Representation of a color using RGBA components in the 0.0f - 1.0f range
+ */
+struct Color : private glm::vec4 {
+    using glm::vec4::vec4;
+    using glm::vec4::operator=;
+    using glm::vec4::operator+=;
+    using glm::vec4::operator-=;
+    using glm::vec4::operator*=;
+    using glm::vec4::operator/=;
+    explicit Color (const glm::vec4& v) : glm::vec4 (v) { }
+    using glm::vec4::a;
+    using glm::vec4::b;
+    using glm::vec4::g;
+    using glm::vec4::r;
+};
+}

+ 76 - 131
src/WallpaperEngine/Data/Model/DynamicValue.cpp

@@ -1,29 +1,44 @@
 #include "DynamicValue.h"
+#include "UserSetting.h"
+
+#include "WallpaperEngine/Data/Utils/ScopeGuard.h"
 #include "WallpaperEngine/Logging/Log.h"
+#include "WallpaperEngine/Scripting/ScriptEngine.h"
 
 using namespace WallpaperEngine::Data::Model;
 
-DynamicValue::DynamicValue (const glm::ivec4& value) { this->DynamicValue::update (value); }
-
-DynamicValue::DynamicValue (const glm::ivec3& value) { this->DynamicValue::update (value); }
+DynamicValue::DynamicValue (const DynamicValue& other) {
+    this->DynamicValue::update (other, UpdateSource::Initialization);
+}
 
-DynamicValue::DynamicValue (const glm::ivec2& value) { this->DynamicValue::update (value); }
+DynamicValue::DynamicValue (const glm::vec4& value) {
+    this->DynamicValue::update (value, UpdateSource::Initialization);
+}
 
-DynamicValue::DynamicValue (const glm::vec4& value) { this->DynamicValue::update (value); }
+DynamicValue::DynamicValue (const glm::vec3& value) {
+    this->DynamicValue::update (value, UpdateSource::Initialization);
+}
 
-DynamicValue::DynamicValue (const glm::vec3& value) { this->DynamicValue::update (value); }
+DynamicValue::DynamicValue (const glm::vec2& value) {
+    this->DynamicValue::update (value, UpdateSource::Initialization);
+}
 
-DynamicValue::DynamicValue (const glm::vec2& value) { this->DynamicValue::update (value); }
+DynamicValue::DynamicValue (float value) { this->DynamicValue::update (value, UpdateSource::Initialization); }
 
-DynamicValue::DynamicValue (float value) { this->DynamicValue::update (value); }
+DynamicValue::DynamicValue (int value) { this->DynamicValue::update (value, UpdateSource::Initialization); }
 
-DynamicValue::DynamicValue (int value) { this->DynamicValue::update (value); }
+DynamicValue::DynamicValue (bool value) { this->DynamicValue::update (value, UpdateSource::Initialization); }
 
-DynamicValue::DynamicValue (bool value) { this->DynamicValue::update (value); }
+DynamicValue::DynamicValue (const std::string& value) {
+    this->DynamicValue::update (value, UpdateSource::Initialization);
+}
 
-DynamicValue::DynamicValue (const std::string& value) { this->DynamicValue::update (value); }
+DynamicValue::DynamicValue (const Model::Color& value) {
+    this->DynamicValue::update (value, UpdateSource::Initialization);
+}
 
 DynamicValue::~DynamicValue () {
+    // TODO: PROPERLY FIX THESE
     if (this->m_aliveFlag) {
 	*this->m_aliveFlag = false;
     }
@@ -31,12 +46,6 @@ DynamicValue::~DynamicValue () {
     this->m_listeners.clear ();
 }
 
-const glm::ivec4& DynamicValue::getIVec4 () const { return this->m_ivec4; }
-
-const glm::ivec3& DynamicValue::getIVec3 () const { return this->m_ivec3; }
-
-const glm::ivec2& DynamicValue::getIVec2 () const { return this->m_ivec2; }
-
 const glm::vec4& DynamicValue::getVec4 () const { return this->m_vec4; }
 
 const glm::vec3& DynamicValue::getVec3 () const { return this->m_vec3; }
@@ -69,14 +78,6 @@ std::string DynamicValue::toString () const {
 	case UnderlyingType::Vec4:
 	    return std::to_string (this->m_vec4.x) + ", " + std::to_string (this->m_vec4.y) + ", "
 		+ std::to_string (this->m_vec4.z) + ", " + std::to_string (this->m_vec4.w);
-	case UnderlyingType::IVec2:
-	    return std::to_string (this->m_ivec2.x) + ", " + std::to_string (this->m_ivec2.y);
-	case UnderlyingType::IVec3:
-	    return std::to_string (this->m_ivec3.x) + ", " + std::to_string (this->m_ivec3.y) + ", "
-		+ std::to_string (this->m_ivec3.z);
-	case UnderlyingType::IVec4:
-	    return std::to_string (this->m_ivec4.x) + ", " + std::to_string (this->m_ivec4.y) + ", "
-		+ std::to_string (this->m_ivec4.z) + ", " + std::to_string (this->m_ivec4.w);
 	case UnderlyingType::String:
 	    return this->m_string;
 	default:
@@ -84,10 +85,7 @@ std::string DynamicValue::toString () const {
     }
 }
 
-void DynamicValue::update (const float newValue) {
-    this->m_ivec4 = glm::ivec4 (static_cast<int> (newValue));
-    this->m_ivec3 = glm::ivec3 (static_cast<int> (newValue));
-    this->m_ivec2 = glm::ivec2 (static_cast<int> (newValue));
+void DynamicValue::update (const float newValue, UpdateSource source) {
     this->m_vec4 = glm::vec4 (newValue);
     this->m_vec3 = glm::vec3 (newValue);
     this->m_vec2 = glm::vec2 (newValue);
@@ -97,13 +95,10 @@ void DynamicValue::update (const float newValue) {
     this->m_string = "";
     this->m_type = UnderlyingType::Float;
 
-    this->propagate ();
+    this->propagate (source);
 }
 
-void DynamicValue::update (const int newValue) {
-    this->m_ivec4 = glm::ivec4 (newValue);
-    this->m_ivec3 = glm::ivec3 (newValue);
-    this->m_ivec2 = glm::ivec2 (newValue);
+void DynamicValue::update (const int newValue, UpdateSource source) {
     this->m_vec4 = glm::vec4 (static_cast<float> (newValue));
     this->m_vec3 = glm::vec3 (static_cast<float> (newValue));
     this->m_vec2 = glm::vec2 (static_cast<float> (newValue));
@@ -113,13 +108,10 @@ void DynamicValue::update (const int newValue) {
     this->m_string = "";
     this->m_type = UnderlyingType::Int;
 
-    this->propagate ();
+    this->propagate (source);
 }
 
-void DynamicValue::update (const bool newValue) {
-    this->m_ivec4 = glm::ivec4 (newValue);
-    this->m_ivec3 = glm::ivec3 (newValue);
-    this->m_ivec2 = glm::ivec2 (newValue);
+void DynamicValue::update (const bool newValue, UpdateSource source) {
     this->m_vec4 = glm::vec4 (newValue);
     this->m_vec3 = glm::vec3 (newValue);
     this->m_vec2 = glm::vec2 (newValue);
@@ -129,13 +121,10 @@ void DynamicValue::update (const bool newValue) {
     this->m_string = "";
     this->m_type = UnderlyingType::Boolean;
 
-    this->propagate ();
+    this->propagate (source);
 }
 
-void DynamicValue::update (const glm::vec2& newValue) {
-    this->m_ivec4 = glm::ivec4 (newValue, 0, 0);
-    this->m_ivec3 = glm::ivec3 (newValue, 0);
-    this->m_ivec2 = glm::ivec2 (newValue);
+void DynamicValue::update (const glm::vec2& newValue, UpdateSource source) {
     this->m_vec2 = newValue;
     this->m_vec3 = glm::vec3 (newValue, 0.0f);
     this->m_vec4 = glm::vec4 (newValue, 0.0f, 0.0f);
@@ -145,13 +134,10 @@ void DynamicValue::update (const glm::vec2& newValue) {
     this->m_string = "";
     this->m_type = UnderlyingType::Vec2;
 
-    this->propagate ();
+    this->propagate (source);
 }
 
-void DynamicValue::update (const glm::vec3& newValue) {
-    this->m_ivec4 = glm::ivec4 (newValue, 0);
-    this->m_ivec3 = glm::ivec3 (newValue);
-    this->m_ivec2 = glm::ivec2 (newValue);
+void DynamicValue::update (const glm::vec3& newValue, UpdateSource source) {
     this->m_vec2 = glm::vec2 (newValue);
     this->m_vec3 = newValue;
     this->m_vec4 = glm::vec4 (newValue, 0.0f);
@@ -161,13 +147,10 @@ void DynamicValue::update (const glm::vec3& newValue) {
     this->m_string = "";
     this->m_type = UnderlyingType::Vec3;
 
-    this->propagate ();
+    this->propagate (source);
 }
 
-void DynamicValue::update (const glm::vec4& newValue) {
-    this->m_ivec4 = glm::ivec4 (newValue);
-    this->m_ivec3 = glm::ivec3 (newValue);
-    this->m_ivec2 = glm::ivec2 (newValue);
+void DynamicValue::update (const glm::vec4& newValue, UpdateSource source) {
     this->m_vec2 = glm::vec2 (newValue);
     this->m_vec3 = glm::vec3 (newValue);
     this->m_vec4 = newValue;
@@ -177,61 +160,10 @@ void DynamicValue::update (const glm::vec4& newValue) {
     this->m_string = "";
     this->m_type = UnderlyingType::Vec4;
 
-    this->propagate ();
-}
-
-void DynamicValue::update (const glm::ivec2& newValue) {
-    this->m_ivec4 = glm::ivec4 (newValue, 0, 0);
-    this->m_ivec3 = glm::ivec3 (newValue, 0);
-    this->m_ivec2 = glm::ivec2 (newValue);
-    this->m_vec2 = newValue;
-    this->m_vec3 = glm::vec3 (newValue, 0.0f);
-    this->m_vec4 = glm::vec4 (newValue, 0.0f, 0.0f);
-    this->m_float = static_cast<float> (newValue.x);
-    this->m_int = static_cast<int> (newValue.x);
-    this->m_bool = newValue.x != 0;
-    this->m_string = "";
-    this->m_type = UnderlyingType::IVec2;
-
-    this->propagate ();
-}
-
-void DynamicValue::update (const glm::ivec3& newValue) {
-    this->m_ivec4 = glm::ivec4 (newValue, 0);
-    this->m_ivec3 = glm::ivec3 (newValue);
-    this->m_ivec2 = glm::ivec2 (newValue);
-    this->m_vec2 = glm::vec2 (newValue);
-    this->m_vec3 = newValue;
-    this->m_vec4 = glm::vec4 (newValue, 0.0f);
-    this->m_float = static_cast<float> (newValue.x);
-    this->m_int = static_cast<int> (newValue.x);
-    this->m_bool = newValue.x != 0;
-    this->m_string = "";
-    this->m_type = UnderlyingType::IVec3;
-
-    this->propagate ();
-}
-
-void DynamicValue::update (const glm::ivec4& newValue) {
-    this->m_ivec4 = glm::ivec4 (newValue);
-    this->m_ivec3 = glm::ivec3 (newValue);
-    this->m_ivec2 = glm::ivec2 (newValue);
-    this->m_vec2 = glm::vec2 (newValue);
-    this->m_vec3 = glm::vec3 (newValue);
-    this->m_vec4 = newValue;
-    this->m_float = static_cast<float> (newValue.x);
-    this->m_int = static_cast<int> (newValue.x);
-    this->m_bool = newValue.x != 0;
-    this->m_string = "";
-    this->m_type = UnderlyingType::IVec4;
-
-    this->propagate ();
+    this->propagate (source);
 }
 
-void DynamicValue::update (const std::string& newValue) {
-    this->m_ivec4 = glm::ivec4 (0);
-    this->m_ivec3 = glm::ivec3 (0);
-    this->m_ivec2 = glm::ivec2 (0);
+void DynamicValue::update (const std::string& newValue, UpdateSource source) {
     this->m_vec2 = glm::vec2 (0.0f);
     this->m_vec3 = glm::vec3 (0.0f);
     this->m_vec4 = glm::vec4 (0.0f);
@@ -243,9 +175,6 @@ void DynamicValue::update (const std::string& newValue) {
 
     if (this->m_condition.has_value ()) {
 	const bool boolValue = this->m_condition.value ().condition == newValue;
-	this->m_ivec4 = glm::ivec4 (boolValue ? 1 : 0);
-	this->m_ivec3 = glm::ivec3 (boolValue ? 1 : 0);
-	this->m_ivec2 = glm::ivec2 (boolValue ? 1 : 0);
 	this->m_vec2 = glm::vec2 (boolValue ? 1.0f : 0.0f);
 	this->m_vec3 = glm::vec3 (boolValue ? 1.0f : 0.0f);
 	this->m_vec4 = glm::vec4 (boolValue ? 1.0f : 0.0f);
@@ -255,13 +184,23 @@ void DynamicValue::update (const std::string& newValue) {
 	this->m_type = UnderlyingType::Boolean;
     }
 
-    this->propagate ();
+    this->propagate (source);
 }
 
-void DynamicValue::update (const DynamicValue& other) {
-    this->m_ivec4 = other.getIVec4 ();
-    this->m_ivec3 = other.getIVec3 ();
-    this->m_ivec2 = other.getIVec2 ();
+void DynamicValue::update (const Model::Color& newValue, UpdateSource source) {
+    this->m_vec2 = glm::vec2 (newValue.r, newValue.g);
+    this->m_vec3 = glm::vec3 (newValue.r, newValue.g, newValue.b);
+    this->m_vec4 = glm::vec4 (newValue.r, newValue.g, newValue.b, newValue.a);
+    this->m_float = newValue.r;
+    this->m_int = 255 * newValue.r;
+    this->m_bool = newValue.a != 0.0f;
+    this->m_string = "";
+    this->m_type = UnderlyingType::Vec4;
+
+    this->propagate (source);
+}
+
+void DynamicValue::update (const DynamicValue& other, UpdateSource source) {
     this->m_vec2 = other.getVec2 ();
     this->m_vec3 = other.getVec3 ();
     this->m_vec4 = other.getVec4 ();
@@ -273,9 +212,6 @@ void DynamicValue::update (const DynamicValue& other) {
 
     if (this->m_condition.has_value () && other.getType () == UnderlyingType::String) {
 	const bool boolValue = this->m_condition.value ().condition == other.getString ();
-	this->m_ivec4 = glm::ivec4 (boolValue ? 1 : 0);
-	this->m_ivec3 = glm::ivec3 (boolValue ? 1 : 0);
-	this->m_ivec2 = glm::ivec2 (boolValue ? 1 : 0);
 	this->m_vec2 = glm::vec2 (boolValue ? 1.0f : 0.0f);
 	this->m_vec3 = glm::vec3 (boolValue ? 1.0f : 0.0f);
 	this->m_vec4 = glm::vec4 (boolValue ? 1.0f : 0.0f);
@@ -285,13 +221,10 @@ void DynamicValue::update (const DynamicValue& other) {
 	this->m_type = UnderlyingType::Boolean;
     }
 
-    this->propagate ();
+    this->propagate (source);
 }
 
-void DynamicValue::update () {
-    this->m_ivec4 = glm::ivec4 (0);
-    this->m_ivec3 = glm::ivec3 (0);
-    this->m_ivec2 = glm::ivec2 (0);
+void DynamicValue::update (UpdateSource source) {
     this->m_vec2 = glm::vec2 (0.0f);
     this->m_vec3 = glm::vec3 (0.0f);
     this->m_vec4 = glm::vec4 (0.0f);
@@ -300,10 +233,10 @@ void DynamicValue::update () {
     this->m_bool = false;
     this->m_type = UnderlyingType::Null;
 
-    this->propagate ();
+    this->propagate (source);
 }
 
-std::function<void ()> DynamicValue::listen (const std::function<void (const DynamicValue&)>& callback) {
+std::function<void ()> DynamicValue::listen (const std::function<void (const DynamicValue&, UpdateSource)>& callback) {
     const auto it = this->m_listeners.insert (this->m_listeners.end (), callback);
     auto alive = this->m_aliveFlag;
 
@@ -316,20 +249,20 @@ std::function<void ()> DynamicValue::listen (const std::function<void (const Dyn
 }
 
 void DynamicValue::connect (DynamicValue* other) {
-    const auto lambda = [this] (const DynamicValue& other) {
+    const auto lambda = [this] (const DynamicValue& other, UpdateSource source) {
 	// null is a special case, copying everything to 0 is different,
 	// so calling the update without parameters is required
 	if (other.getType () == UnderlyingType::Null) {
-	    this->update ();
+	    this->update (source);
 	} else {
-	    this->update (other);
+	    this->update (other, source);
 	}
     };
 
     const auto deregisterFunction = other->listen (lambda);
 
     // same update cycle has to happen as in the lambda, so trigger it
-    lambda (*other);
+    lambda (*other, UpdateSource::Initialization);
 
     this->m_connections.push_back (deregisterFunction);
 }
@@ -351,8 +284,20 @@ void DynamicValue::disconnect () {
 
 void DynamicValue::attachCondition (const ConditionInfo& condition) { this->m_condition = condition; }
 
-void DynamicValue::propagate () const {
+void DynamicValue::setScriptSource (const std::string& source) { this->m_scriptSource = source; }
+
+void DynamicValue::clearScriptSource () { this->m_scriptSource = std::nullopt; }
+
+const std::optional<std::string>& DynamicValue::getScriptSource () const { return this->m_scriptSource; }
+
+std::map<std::string, UserSettingUniquePtr>& DynamicValue::getProperties () { return this->m_properties; }
+
+void DynamicValue::setProperties (std::map<std::string, UserSettingUniquePtr> properties) {
+    this->m_properties = std::move (properties);
+}
+
+void DynamicValue::propagate (UpdateSource source) const {
     for (const auto& callback : this->m_listeners) {
-	callback (*this);
+	callback (*this, source);
     }
 }

+ 61 - 29
src/WallpaperEngine/Data/Model/DynamicValue.h

@@ -1,18 +1,31 @@
 #pragma once
 
+#include "Color.h"
+#include "Types.h"
+
 #include <functional>
 #include <glm/glm.hpp>
 #include <list>
+#include <map>
 #include <memory>
 #include <optional>
 #include <string>
 
 namespace WallpaperEngine::Data::Model {
+class DynamicValue;
+
 struct ConditionInfo {
     std::string name;
     std::string condition;
 };
 
+struct ScriptContext {
+    struct Object {
+	int id;
+	std::string name;
+    } object;
+};
+
 /**
  * Class that represents different types of dynamic values
  */
@@ -20,22 +33,23 @@ class DynamicValue {
 public:
     enum UnderlyingType {
 	Null = 0,
-	IVec4 = 1,
-	IVec3 = 2,
-	IVec2 = 3,
 	Vec4 = 4,
 	Vec3 = 5,
 	Vec2 = 6,
 	Float = 7,
 	Int = 8,
 	Boolean = 9,
-	String = 10
+	String = 10,
+    };
+
+    enum UpdateSource {
+	Initialization = 0,
+	Script = 1,
+	User = 2,
     };
 
     DynamicValue () = default;
-    explicit DynamicValue (const glm::ivec4& value);
-    explicit DynamicValue (const glm::ivec3& value);
-    explicit DynamicValue (const glm::ivec2& value);
+    DynamicValue (const DynamicValue& other);
     explicit DynamicValue (const glm::vec4& value);
     explicit DynamicValue (const glm::vec3& value);
     explicit DynamicValue (const glm::vec2& value);
@@ -43,11 +57,9 @@ public:
     explicit DynamicValue (int value);
     explicit DynamicValue (bool value);
     explicit DynamicValue (const std::string& value);
+    explicit DynamicValue (const Model::Color& value);
     virtual ~DynamicValue ();
 
-    [[nodiscard]] const glm::ivec4& getIVec4 () const;
-    [[nodiscard]] const glm::ivec3& getIVec3 () const;
-    [[nodiscard]] const glm::ivec2& getIVec2 () const;
     [[nodiscard]] const glm::vec4& getVec4 () const;
     [[nodiscard]] const glm::vec3& getVec3 () const;
     [[nodiscard]] const glm::vec2& getVec2 () const;
@@ -58,21 +70,19 @@ public:
     [[nodiscard]] UnderlyingType getType () const;
     [[nodiscard]] virtual std::string toString () const;
 
-    virtual void update (float newValue);
-    virtual void update (int newValue);
-    virtual void update (bool newValue);
-    virtual void update (const glm::vec2& newValue);
-    virtual void update (const glm::vec3& newValue);
-    virtual void update (const glm::vec4& newValue);
-    virtual void update (const glm::ivec2& newValue);
-    virtual void update (const glm::ivec3& newValue);
-    virtual void update (const glm::ivec4& newValue);
-    virtual void update (const std::string& newValue);
-    virtual void update (const DynamicValue& other);
+    virtual void update (float newValue, UpdateSource source);
+    virtual void update (int newValue, UpdateSource source);
+    virtual void update (bool newValue, UpdateSource source);
+    virtual void update (const glm::vec2& newValue, UpdateSource source);
+    virtual void update (const glm::vec3& newValue, UpdateSource source);
+    virtual void update (const glm::vec4& newValue, UpdateSource source);
+    virtual void update (const std::string& newValue, UpdateSource source);
+    virtual void update (const Model::Color& newValue, UpdateSource source);
+    virtual void update (const DynamicValue& other, UpdateSource source);
     /**
      * Sets the current value to null
      */
-    virtual void update ();
+    virtual void update (UpdateSource source);
 
     /**
      * Registers the given callback to be called when the value changes
@@ -81,7 +91,7 @@ public:
      *
      * @return The de-register function to call when the listener is no longer needed
      */
-    std::function<void ()> listen (const std::function<void (const DynamicValue&)>& callback);
+    std::function<void ()> listen (const std::function<void (const DynamicValue&, UpdateSource)>& callback);
     /**
      * Connects the current instance to the given instance, updating this instance's value
      * based on the given instance's value
@@ -104,28 +114,50 @@ public:
      * @param condition
      */
     void attachCondition (const ConditionInfo& condition);
+    /**
+     * Associates a script source with the dynamic value to allow for dynamic updates
+     *
+     * @param source The script code to associate to this value
+     */
+    void setScriptSource (const std::string& source);
+    /**
+     * Clears the associated script to this dynamic value
+     */
+    void clearScriptSource ();
+    /**
+     * @return The current script source associated with this dynamic value
+     */
+    [[nodiscard]] const std::optional<std::string>& getScriptSource () const;
+    /**
+     * @return The script properties associated with this dynamic value (if any)
+     */
+    [[nodiscard]] std::map<std::string, UserSettingUniquePtr>& getProperties ();
+    /**
+     * Updates the script properties associated with this dynamic value
+     */
+    void setProperties (std::map<std::string, UserSettingUniquePtr> properties);
 
 private:
     /**
      * Notifies any listeners that the value has changed
      */
-    void propagate () const;
+    void propagate (UpdateSource source) const;
 
     std::shared_ptr<bool> m_aliveFlag = std::make_shared<bool> (true);
-    std::list<std::function<void (const DynamicValue&)>> m_listeners = {};
+    std::list<std::function<void (const DynamicValue&, UpdateSource)>> m_listeners = {};
     std::vector<std::function<void ()>> m_connections = {};
+    std::optional<std::string> m_scriptSource = std::nullopt;
 
-    glm::ivec4 m_ivec4 = {};
-    glm::ivec3 m_ivec3 = {};
-    glm::ivec2 m_ivec2 = {};
     glm::vec4 m_vec4 = {};
     glm::vec3 m_vec3 = {};
     glm::vec2 m_vec2 = {};
     float m_float = 0.0f;
     int m_int = 0;
     bool m_bool = false;
-    std::string m_string = "";
+    std::string m_string;
     UnderlyingType m_type = Null;
     std::optional<ConditionInfo> m_condition = std::nullopt;
+    /** All the properties this script takes in */
+    std::map<std::string, UserSettingUniquePtr> m_properties;
 };
 }

+ 3 - 5
src/WallpaperEngine/Data/Model/Object.h

@@ -8,6 +8,7 @@
 
 #include <glm/glm.hpp>
 
+#include "DynamicValue.h"
 #include "Effect.h"
 #include "Material.h"
 #include "Model.h"
@@ -589,11 +590,7 @@ public:
  */
 struct TextData {
     /** Initial text content to render (for scripted text, this is the `value` placeholder) */
-    std::string text;
-    /** Scripted text source — full JS, resolved inline or loaded from a .js asset. Empty for static text. */
-    std::string script;
-    /** Typed initial values for the script's scriptProperties, keyed by property name */
-    std::map<std::string, UserSettingUniquePtr> scriptProperties;
+    UserSettingUniquePtr text;
     /** Font reference from scene (e.g. "fonts/VCR_OSD_MONO.ttf" or "systemfont_arial") */
     std::string font;
     /** Font size in points, optionally bound to a user setting or script */
@@ -614,6 +611,7 @@ struct TextData {
     std::string verticalalign;
     /** Padding inside the bounding box */
     int padding;
+    // TODO: PARSE LIMITS TOO!
 };
 
 class Text : public Object, public TextData {

+ 24 - 58
src/WallpaperEngine/Data/Model/Property.h

@@ -1,7 +1,10 @@
 #pragma once
 
+#include "../Builders/ColorBuilder.h"
 #include "../Utils/TypeCaster.h"
 #include "DynamicValue.h"
+#include "WallpaperEngine/Logging/Log.h"
+
 #include <map>
 #include <string>
 #include <utility>
@@ -30,19 +33,19 @@ public:
     explicit Property (PropertyData data) : DynamicValue (), TypeCaster (), PropertyData (std::move (data)) { }
 
     using DynamicValue::update;
-    virtual void update (const std::string& value) = 0;
+    virtual void update (const std::string& value, UpdateSource source) = 0;
     [[nodiscard]] virtual std::string dump () const = 0;
 };
 
 class PropertySlider final : public Property, SliderData {
 public:
     PropertySlider (PropertyData data, SliderData sliderData, const float value) :
-	Property (std::move (data)), SliderData (std::move (sliderData)) {
-	this->Property::update (value);
+	Property (std::move (data)), SliderData (sliderData) {
+	this->Property::update (value, UpdateSource::Initialization);
     }
 
     using Property::update;
-    void update (const std::string& value) override { this->update (std::stof (value)); }
+    void update (const std::string& value, UpdateSource source) override { this->update (std::stof (value), source); }
 
     [[nodiscard]] std::string dump () const override {
 	std::stringstream ss;
@@ -61,11 +64,13 @@ public:
 class PropertyBoolean final : public Property {
 public:
     explicit PropertyBoolean (PropertyData data, const bool value) : Property (std::move (data)) {
-	this->Property::update (value);
+	this->Property::update (value, UpdateSource::Initialization);
     }
 
     using Property::update;
-    void update (const std::string& value) override { this->update (value == "true" || value == "1"); }
+    void update (const std::string& value, UpdateSource source) override {
+	this->update (value == "true" || value == "1", source);
+    }
 
     [[nodiscard]] std::string dump () const override {
 	std::stringstream ss;
@@ -81,49 +86,12 @@ public:
 class PropertyColor final : public Property {
 public:
     explicit PropertyColor (PropertyData data, const std::string& value) : Property (std::move (data)) {
-	this->PropertyColor::update (value);
+	this->PropertyColor::update (value, UpdateSource::Initialization);
     }
 
     using Property::update;
-    void update (const std::string& value) override {
-	auto copy = value;
-
-	// TODO: ENSURE ALL THIS PARSING IS CORRECT
-	if (copy.find (',') != std::string::npos) {
-	    // replace comma separator with spaces so it's
-	    std::ranges::replace (copy, ',', ' ');
-	}
-
-	// hex colors should be converted to int colors
-	if (copy.find ('#') == 0) {
-	    auto number = copy.substr (1);
-
-	    // support for css notation
-	    if (number.size () == 3) {
-		number = number[0] + number[0] + number[1] + number[1] + number[2] + number[2];
-	    }
-
-	    // remove alpha if it's present, should look into it more closely
-	    if (number.size () > 6) {
-		sLog.error ("Color value has alpha channel, which is not supported");
-		number = number.substr (0, 6);
-	    }
-
-	    const auto color = std::stoi (number, nullptr, 16);
-
-	    // format the number as float vector
-	    copy = std::to_string (((color >> 16) & 0xFF) / 255.0) + " "
-		+ std::to_string (((color >> 8) & 0xFF) / 255.0) + " " + std::to_string ((color & 0xFF) / 255.0);
-	} else if (copy.find ('.') == std::string::npos) {
-	    // integer vector, convert it to float vector
-	    const auto intcolor = VectorBuilder::parse<glm::ivec3> (copy);
-
-	    copy = std::to_string (intcolor.r / 255.0) + " " + std::to_string (intcolor.g / 255.0) + " "
-		+ std::to_string (intcolor.b / 255.0);
-	}
-
-	// finally parse the string as a float vector
-	this->update (VectorBuilder::parse<glm::vec3> (copy));
+    void update (const std::string& value, UpdateSource source) override {
+	this->update (ColorBuilder::parse (value), source);
     }
 
     [[nodiscard]] std::string dump () const override {
@@ -141,18 +109,18 @@ class PropertyCombo final : public Property, ComboData {
 public:
     PropertyCombo (PropertyData data, ComboData comboData, const std::string& value) :
 	Property (std::move (data)), ComboData (std::move (comboData)) {
-	this->PropertyCombo::update (value);
+	this->PropertyCombo::update (value, UpdateSource::Initialization);
     }
 
     using Property::update;
-    void update (const std::string& value) override {
+    void update (const std::string& value, UpdateSource source) override {
 	if (this->values.contains (value) == false) {
 	    sLog.error ("Combo value not found in combo options: ", value);
 	    return;
 	}
 
 	// search for the value in the combo options or default to the textual value
-	this->DynamicValue::update (value);
+	this->DynamicValue::update (value, source);
     }
 
     [[nodiscard]] std::string dump () const override {
@@ -176,9 +144,7 @@ public:
     explicit PropertyText (PropertyData data) : Property (std::move (data)) { }
 
     using Property::update;
-    void update (const std::string& value) override {
-	throw std::runtime_error ("PropertyText::update() is not implemented");
-    }
+    void update (const std::string& value, UpdateSource source) override { this->DynamicValue::update (value, source); }
 
     [[nodiscard]] std::string toString () const override { return this->text; }
 
@@ -196,10 +162,10 @@ public:
 class PropertySceneTexture final : public Property {
 public:
     explicit PropertySceneTexture (PropertyData data, const std::string& value) : Property (std::move (data)) {
-	this->PropertySceneTexture::update (value);
+	this->PropertySceneTexture::update (value, UpdateSource::Initialization);
     }
 
-    void update (const std::string& value) override { this->m_value = value; }
+    void update (const std::string& value, UpdateSource source) override { this->DynamicValue::update (value, source); }
 
     [[nodiscard]] std::string dump () const override {
 	std::stringstream ss;
@@ -218,10 +184,10 @@ private:
 class PropertyFile final : public Property {
 public:
     explicit PropertyFile (PropertyData data, const std::string& value) : Property (std::move (data)) {
-	this->PropertyFile::update (value);
+	this->PropertyFile::update (value, UpdateSource::Initialization);
     }
 
-    void update (const std::string& value) override { this->m_value = value; }
+    void update (const std::string& value, UpdateSource source) override { this->DynamicValue::update (value, source); }
 
     [[nodiscard]] std::string dump () const override {
 	std::stringstream ss;
@@ -240,10 +206,10 @@ private:
 class PropertyTextInput final : public Property {
 public:
     explicit PropertyTextInput (PropertyData data, const std::string& value) : Property (std::move (data)) {
-	this->PropertyTextInput::update (value);
+	this->PropertyTextInput::update (value, UpdateSource::Initialization);
     }
 
-    void update (const std::string& value) override { this->m_value = value; }
+    void update (const std::string& value, UpdateSource source) override { this->DynamicValue::update (value, source); }
 
     [[nodiscard]] std::string dump () const override {
 	std::stringstream ss;

+ 0 - 74
src/WallpaperEngine/Data/Model/ScriptedDynamicValue.cpp

@@ -1,74 +0,0 @@
-#include "ScriptedDynamicValue.h"
-#include "WallpaperEngine/Logging/Log.h"
-#include "WallpaperEngine/Scripting/ScriptEngine.h"
-
-using namespace WallpaperEngine::Data::Model;
-
-ScriptedDynamicValue::ScriptedDynamicValue (
-    std::string scriptSource,
-    std::map<std::string, DynamicValueUniquePtr> scriptProps,
-    DynamicValue baseValue
-) :
-    DynamicValue (),
-    m_scriptSource (std::move (scriptSource)),
-    m_scriptProps (std::move (scriptProps)),
-    m_baseValue (std::move (baseValue)) {
-    // Listen for changes on each script property
-    for (auto& [name, prop] : this->m_scriptProps) {
-	if (prop) {
-	    prop->listen ([this] (const DynamicValue&) {
-		this->reevaluate ();
-	    });
-	}
-    }
-
-    // Do an initial evaluation
-    this->reevaluate ();
-}
-
-ScriptedDynamicValue::~ScriptedDynamicValue () {
-    WallpaperEngine::Scripting::ScriptEngine::instance ().releaseBinding (this);
-}
-
-void ScriptedDynamicValue::setBindingContext (ScriptBindingContext context) {
-    this->m_bindingContext = std::move (context);
-}
-
-const std::optional<ScriptBindingContext>& ScriptedDynamicValue::getBindingContext () const {
-    return this->m_bindingContext;
-}
-
-void ScriptedDynamicValue::reevaluate () { this->reevaluate (nullptr); }
-
-void ScriptedDynamicValue::reevaluate (WallpaperEngine::Render::Wallpapers::CScene* scene) {
-    if (this->m_evaluating) {
-	return;
-    }
-
-    this->m_evaluating = true;
-    struct EvaluationGuard {
-	bool& evaluating;
-	~EvaluationGuard () { evaluating = false; }
-    } guard { this->m_evaluating };
-
-    // Build raw pointer map for the engine
-    std::map<std::string, DynamicValue*> propsMap;
-    for (const auto& [name, prop] : this->m_scriptProps) {
-	propsMap[name] = prop.get ();
-    }
-
-    const DynamicValue& currentValue = this->getType () == DynamicValue::Null ? this->m_baseValue : *this;
-
-    auto result = WallpaperEngine::Scripting::ScriptEngine::instance ().evaluate (
-	this,
-	this->m_scriptSource,
-	propsMap,
-	currentValue,
-	scene,
-	this->m_bindingContext.has_value () ? &this->m_bindingContext.value () : nullptr
-    );
-
-    if (result) {
-	this->update (*result);
-    }
-}

+ 0 - 52
src/WallpaperEngine/Data/Model/ScriptedDynamicValue.h

@@ -1,52 +0,0 @@
-#pragma once
-
-#include <map>
-#include <optional>
-#include <string>
-
-#include "DynamicValue.h"
-#include "Types.h"
-
-namespace WallpaperEngine::Render::Wallpapers {
-class CScene;
-}
-
-namespace WallpaperEngine::Data::Model {
-
-struct ScriptBindingContext {
-    int objectId = -1;
-    std::string objectName;
-    std::string propertyName;
-};
-
-/**
- * A DynamicValue whose value is computed by evaluating a WallpaperEngine script.
- *
- * Holds the script source, a map of script property names to their DynamicValue
- * pointers (connected to user Properties), and the base value from the JSON.
- * When any connected scriptProperty changes, re-evaluates the script.
- */
-class ScriptedDynamicValue : public DynamicValue {
-public:
-    ScriptedDynamicValue (
-	std::string scriptSource,
-	std::map<std::string, DynamicValueUniquePtr> scriptProps,
-	DynamicValue baseValue
-    );
-
-    ~ScriptedDynamicValue () override;
-
-    void setBindingContext (ScriptBindingContext context);
-    [[nodiscard]] const std::optional<ScriptBindingContext>& getBindingContext () const;
-    void reevaluate (WallpaperEngine::Render::Wallpapers::CScene* scene);
-
-private:
-    void reevaluate ();
-
-    std::string m_scriptSource;
-    std::map<std::string, DynamicValueUniquePtr> m_scriptProps;
-    DynamicValue m_baseValue;
-    std::optional<ScriptBindingContext> m_bindingContext = std::nullopt;
-    bool m_evaluating = false;
-};
-} // namespace WallpaperEngine::Data::Model

+ 6 - 6
src/WallpaperEngine/Data/Model/Wallpaper.h

@@ -39,8 +39,8 @@ public:
 
 struct SceneData {
     struct {
-	glm::vec3 ambient;
-	glm::vec3 skylight;
+	UserSettingUniquePtr ambient;
+	UserSettingUniquePtr skylight;
 	UserSettingUniquePtr clear;
     } colors;
     /**
@@ -48,7 +48,7 @@ struct SceneData {
      */
     struct Camera {
 	/** Enable fade effect */
-	bool fade;
+	UserSettingUniquePtr fade;
 	/** Used by the software to allow the users to preview the background or not? */
 	bool preview;
 
@@ -99,9 +99,9 @@ struct SceneData {
 	    int width;
 	    int height;
 	    bool isAuto;
-	    float nearz;
-	    float farz;
-	    float fov;
+	    UserSettingUniquePtr nearz;
+	    UserSettingUniquePtr farz;
+	    UserSettingUniquePtr fov;
 	} projection;
     } camera;
 

+ 80 - 0
src/WallpaperEngine/Data/Parsers/DynamicValueParser.cpp

@@ -0,0 +1,80 @@
+#include "DynamicValueParser.h"
+
+#include "UserSettingParser.h"
+#include "WallpaperEngine/Data/Model/DynamicValue.h"
+
+using namespace WallpaperEngine::Data::Parsers;
+
+DynamicValueUniquePtr DynamicValueParser::parse (const json& data, const Properties& properties, bool expectColor) {
+    auto value = std::make_unique<DynamicValue> ();
+    auto valueIt = data;
+    std::optional<std::string> scriptSource = std::nullopt;
+    std::optional<json> scriptPropsJson = std::nullopt;
+
+    if (data.is_object ()) {
+	const auto user = data.optional ("user");
+	const auto script = data.optional ("script");
+	valueIt = data.require ("value", "User setting must have a value");
+
+	if (script.has_value () && !script->is_null ()) {
+	    scriptSource = script->get<std::string> ();
+	    scriptPropsJson = data.optional ("scriptproperties");
+	}
+    }
+
+    // actual value parsing
+    if (valueIt.is_string ()) {
+	if (expectColor) {
+	    value->update (Builders::ColorBuilder::parse (valueIt), DynamicValue::UpdateSource::Initialization);
+	} else {
+	    std::string str = valueIt;
+	    int size = Builders::VectorBuilder::preparseSize (str);
+
+	    if (size == 1) {
+		// scalar? text value?
+		std::size_t parsed = 0;
+		try {
+		    float f = std::stof (str, &parsed);
+
+		    if (parsed == str.size ()) {
+			value->update (f, DynamicValue::UpdateSource::Initialization);
+		    } else {
+			value->update (str, DynamicValue::UpdateSource::Initialization);
+		    }
+		} catch (const std::exception&) {
+		    value->update (str, DynamicValue::UpdateSource::Initialization);
+		}
+	    } else if (size == 2) {
+		value->update (static_cast<glm::vec2> (valueIt), DynamicValue::UpdateSource::Initialization);
+	    } else if (size == 3) {
+		value->update (static_cast<glm::vec3> (valueIt), DynamicValue::UpdateSource::Initialization);
+	    } else {
+		value->update (static_cast<glm::vec4> (valueIt), DynamicValue::UpdateSource::Initialization);
+	    }
+	}
+    } else if (valueIt.is_number_integer ()) {
+	value->update (valueIt.get<int> (), DynamicValue::UpdateSource::Initialization);
+    } else if (valueIt.is_number_float ()) {
+	value->update (valueIt.get<float> (), DynamicValue::UpdateSource::Initialization);
+    } else if (valueIt.is_boolean ()) {
+	value->update (valueIt.get<bool> (), DynamicValue::UpdateSource::Initialization);
+    } else if (valueIt.is_null ()) {
+	// null value with no connection to property
+	value->update (DynamicValue::UpdateSource::Initialization);
+    }
+
+    if (scriptSource.has_value ()) {
+	std::map<std::string, UserSettingUniquePtr> scriptProps;
+
+	if (scriptPropsJson.has_value () && scriptPropsJson->is_object ()) {
+	    for (const auto& [key, propData] : scriptPropsJson->items ()) {
+		scriptProps[key] = UserSettingParser::parse (propData, properties);
+	    }
+	}
+
+	value->setProperties (std::move (scriptProps));
+	value->setScriptSource (scriptSource.value ());
+    }
+
+    return value;
+}

+ 14 - 0
src/WallpaperEngine/Data/Parsers/DynamicValueParser.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include "WallpaperEngine/Data/JSON.h"
+#include "WallpaperEngine/Data/Model/Types.h"
+
+namespace WallpaperEngine::Data::Parsers {
+using json = WallpaperEngine::Data::JSON::JSON;
+using namespace WallpaperEngine::Data::Model;
+
+class DynamicValueParser {
+public:
+    static Model::DynamicValueUniquePtr parse (const json& data, const Properties& properties, bool expectColor);
+};
+}

+ 1 - 0
src/WallpaperEngine/Data/Parsers/EffectParser.cpp

@@ -1,6 +1,7 @@
 #include "EffectParser.h"
 #include "MaterialParser.h"
 
+#include "WallpaperEngine/Data/Model/DynamicValue.h"
 #include "WallpaperEngine/Data/Model/Effect.h"
 #include "WallpaperEngine/Data/Model/Material.h"
 #include "WallpaperEngine/Data/Model/Project.h"

+ 1 - 0
src/WallpaperEngine/Data/Parsers/ModelParser.cpp

@@ -1,6 +1,7 @@
 #include "ModelParser.h"
 #include "MaterialParser.h"
 
+#include "WallpaperEngine/Data/Model/DynamicValue.h"
 #include "WallpaperEngine/Data/Model/Material.h"
 #include "WallpaperEngine/Data/Model/Model.h"
 #include "WallpaperEngine/Data/Model/Project.h"

+ 37 - 246
src/WallpaperEngine/Data/Parsers/ObjectParser.cpp

@@ -5,9 +5,9 @@
 
 #include "ShaderConstantParser.h"
 #include "UserSettingParser.h"
+#include "WallpaperEngine/Data/Builders/ColorBuilder.h"
 #include "WallpaperEngine/Data/Model/Object.h"
 #include "WallpaperEngine/Data/Model/Project.h"
-#include "WallpaperEngine/Data/Model/ScriptedDynamicValue.h"
 #include "WallpaperEngine/Logging/Log.h"
 
 #include <glm/gtc/constants.hpp>
@@ -16,100 +16,6 @@
 using namespace WallpaperEngine::Data::Parsers;
 using namespace WallpaperEngine::Data::Model;
 
-namespace {
-// Wraps a string value in a UserSetting with a String-typed DynamicValue.
-// UserSettingParser would try to numeric-parse single-char strings like "-"/":",
-// so we bypass it for raw string script-property values.
-UserSettingUniquePtr makeStringSetting (const std::string& s) {
-    auto dv = std::make_unique<DynamicValue> ();
-    dv->update (s);
-    return std::make_unique<UserSetting> (UserSetting {
-	.value = std::move (dv),
-	.property = nullptr,
-	.condition = std::nullopt,
-    });
-}
-
-// Resolves the `script` field of a scripted text node: if it looks like a
-// single-line path ending in .js, read it through the asset locator.
-// Returns empty string on failure (caller treats empty script as static).
-std::string resolveScriptSource (std::string raw, const Project& project) {
-    const bool looksLikePath =
-	!raw.empty () && raw.find ('\n') == std::string::npos && raw.size () >= 3
-	&& raw.compare (raw.size () - 3, 3, ".js") == 0;
-
-    if (!looksLikePath || project.assetLocator == nullptr)
-	return raw;
-
-    try {
-	return project.assetLocator->readString (raw);
-    } catch (const std::exception& e) {
-	sLog.error ("CText: cannot load script asset '", raw, "': ", e.what ());
-	return {};
-    }
-}
-
-// Parses a text object's `scriptproperties` subtree into a map of UserSettings.
-// Plain string values bypass UserSettingParser (which stof-throws on "-"/":"),
-// object values with a string `value` field also go through the string path.
-std::map<std::string, UserSettingUniquePtr>
-parseScriptProperties (const JSON& propsObj, const Properties& properties) {
-    std::map<std::string, UserSettingUniquePtr> out;
-    for (const auto& [key, propData] : propsObj.items ()) {
-	try {
-	    if (propData.is_string ()) {
-		out.emplace (key, makeStringSetting (propData.template get<std::string> ()));
-		continue;
-	    }
-	    if (propData.is_object ()) {
-		if (const auto valueField = propData.find ("value");
-		    valueField != propData.end () && valueField->is_string ()) {
-		    out.emplace (key, makeStringSetting (valueField->template get<std::string> ()));
-		    continue;
-		}
-	    }
-	    out.emplace (key, UserSettingParser::parse (propData, properties));
-	} catch (const std::exception& e) {
-	    sLog.error ("CText: failed to parse scriptProperty '", key, "': ", e.what ());
-	}
-    }
-    return out;
-}
-
-// The `color` user setting may arrive as vec3 or ivec3; normalize to vec4 with
-// full alpha so downstream renderers can assume a uniform type.
-void widenColorToVec4 (DynamicValue& v) {
-    if (v.getType () == DynamicValue::UnderlyingType::Vec3) {
-	v.update (glm::vec4 (v.getVec3 (), 1.0f));
-    } else if (v.getType () == DynamicValue::UnderlyingType::IVec3) {
-	const glm::ivec3 icolor = v.getIVec3 ();
-	v.update (glm::vec4 (glm::vec3 (icolor), 255.0f) / 255.0f);
-    }
-}
-
-static void bindScriptContext (
-    const UserSettingUniquePtr& setting,
-    int objectId,
-    const std::string& objectName,
-    const std::string& propertyName
-) {
-    if (!setting || !setting->value) {
-	return;
-    }
-
-    auto* scripted = dynamic_cast<ScriptedDynamicValue*> (setting->value.get ());
-    if (!scripted) {
-	return;
-    }
-
-    scripted->setBindingContext (ScriptBindingContext {
-	.objectId = objectId,
-	.objectName = objectName,
-	.propertyName = propertyName,
-    });
-}
-} // namespace
-
 ObjectUniquePtr ObjectParser::parse (const JSON& it, const Project& project) {
     const auto imageIt = it.find ("image");
     const auto soundIt = it.find ("sound");
@@ -158,11 +64,6 @@ ObjectUniquePtr ObjectParser::parse (const JSON& it, const Project& project) {
 	};
     }
 
-    bindScriptContext (basedata.origin, basedata.id, basedata.name, "origin");
-    bindScriptContext (basedata.groupScale, basedata.id, basedata.name, "scale");
-    bindScriptContext (basedata.groupAngles, basedata.id, basedata.name, "angles");
-    bindScriptContext (basedata.groupVisible, basedata.id, basedata.name, "visible");
-
     if (imageIt != it.end () && imageIt->is_string ()) {
 	return parseImage (it, project, std::move (basedata), *imageIt);
     } else if (soundIt != it.end () && soundIt->is_array ()) {
@@ -221,55 +122,22 @@ SoundUniquePtr ObjectParser::parseSound (const JSON& it, ObjectData base) {
 }
 
 TextUniquePtr ObjectParser::parseText (const JSON& it, const Project& project, ObjectData base) {
-    const auto& properties = project.properties;
-    const auto textIt = it.require ("text", "Text object must have a text field");
-
-    std::string text;
-    std::string script;
-    std::map<std::string, UserSettingUniquePtr> scriptProps;
-
-    if (textIt.is_string ()) {
-	text = textIt.get<std::string> ();
-    } else if (textIt.is_object ()) {
-	// Scripted text: carries the JS source (either inline or as a .js asset path),
-	// an initial placeholder `value`, and typed initial values for scriptProperties.
-	if (const auto scriptIt = textIt.optional<std::string> ("script"); scriptIt.has_value ())
-	    script = resolveScriptSource (*scriptIt, project);
-
-	if (const auto valueIt = textIt.optional<std::string> ("value"); valueIt.has_value ())
-	    text = *valueIt;
-
-	if (const auto propsIt = textIt.find ("scriptproperties"); propsIt != textIt.end () && propsIt->is_object ())
-	    scriptProps = parseScriptProperties (*propsIt, properties);
-    }
-
-    auto result = std::make_unique<Text> (
+    return std::make_unique<Text> (
 	std::move (base),
 	TextData {
-	    .text = std::move (text),
-	    .script = std::move (script),
-	    .scriptProperties = std::move (scriptProps),
+	    .text = it.user ("text", project.properties),
 	    .font = it.optional ("font", std::string ()),
-	    .pointSize = it.user ("pointsize", properties, 32.0f),
+	    .pointSize = it.user ("pointsize", project.properties, 32.0f),
 	    .size = it.optional ("size", glm::vec2 (0.0f)),
-	    .scale = it.user ("scale", properties, glm::vec3 (1.0f)),
-	    .color = it.user ("color", properties, glm::vec4 (1.0f)),
-	    .alpha = it.user ("alpha", properties, 1.0f),
-	    .visible = it.user ("visible", properties, true),
+	    .scale = it.user ("scale", project.properties, glm::vec3 (1.0f)),
+	    .color = it.color ("color", project.properties, Builders::ColorBuilder::White),
+	    .alpha = it.user ("alpha", project.properties, 1.0f),
+	    .visible = it.user ("visible", project.properties, true),
 	    .alignment = it.optional ("horizontalalign", it.optional ("alignment", std::string ("center"))),
 	    .verticalalign = it.optional ("verticalalign", std::string ("center")),
 	    .padding = it.optional ("padding", 0),
 	}
     );
-
-    widenColorToVec4 (*result->color->value);
-    bindScriptContext (result->visible, result->id, result->name, "visible");
-    bindScriptContext (result->color, result->id, result->name, "color");
-    bindScriptContext (result->alpha, result->id, result->name, "alpha");
-    bindScriptContext (result->scale, result->id, result->name, "scale");
-    bindScriptContext (result->pointSize, result->id, result->name, "pointSize");
-
-    return result;
 }
 
 ImageUniquePtr
@@ -282,12 +150,12 @@ ObjectParser::parseImage (const JSON& it, const Project& project, ObjectData bas
 	std::move (base),
 	ImageData {
 	    .scale = it.user ("scale", properties, glm::vec3 (1.0f)),
-	    .angles = it.user ("angles", properties, glm::vec3 (0.0)),
+	    .angles = it.user ("angles", properties, glm::vec3 (0.0f)),
 	    .visible = it.user ("visible", properties, true),
 	    .alpha = it.user ("alpha", properties, 1.0f),
-	    .color = it.user ("color", properties, glm::vec4 (1.0f)),
+	    .color = it.color ("color", properties, Builders::ColorBuilder::White),
 	    .alignment = it.optional ("horizontalalign", it.optional ("alignment", std::string ("center"))),
-	    .size = it.user ("size", project.properties, glm::vec2 (0.0f))->value->getVec2 (),
+	    .size = it.user ("size", properties, glm::vec2 (0.0f))->value->getVec2 (),
 	    .parallaxDepth = it.user ("parallaxDepth", properties, glm::vec2 (0.0f)),
 	    .colorBlendMode = it.user ("colorBlendMode", properties, 0),
 	    .brightness = it.user ("brightness", properties, 1.0f),
@@ -298,20 +166,6 @@ ObjectParser::parseImage (const JSON& it, const Project& project, ObjectData bas
 	}
     );
 
-    // color should be a vec4 for alpha, but it's read as vec3
-    if (result->color->value->getType () == DynamicValue::UnderlyingType::Vec3) {
-	result->color->value->update (glm::vec4 (result->color->value->getVec3 (), 1.0f));
-    } else if (result->color->value->getType () == DynamicValue::UnderlyingType::IVec3) {
-	result->color->value->update (glm::vec4 (result->color->value->getIVec3 (), 255));
-    }
-
-    bindScriptContext (result->scale, result->id, result->name, "scale");
-    bindScriptContext (result->angles, result->id, result->name, "angles");
-    bindScriptContext (result->visible, result->id, result->name, "visible");
-    bindScriptContext (result->alpha, result->id, result->name, "alpha");
-    bindScriptContext (result->color, result->id, result->name, "color");
-    bindScriptContext (result->parallaxDepth, result->id, result->name, "parallaxDepth");
-
     const auto instance = it.optional ("instance");
     if (instance.has_value () && instance->is_object () && !result->model->material->passes.empty ()) {
 	auto& firstPass = **result->model->material->passes.begin ();
@@ -327,14 +181,6 @@ ObjectParser::parseImage (const JSON& it, const Project& project, ObjectData bas
 	}
     }
 
-    for (const auto& effect : result->effects) {
-	for (const auto& pass : effect->passOverrides) {
-	    for (const auto& [name, constant] : pass->constants) {
-		bindScriptContext (constant, result->id, result->name, name);
-	    }
-	}
-    }
-
     return result;
 }
 
@@ -489,43 +335,17 @@ ParticleUniquePtr ObjectParser::parseParticle (const JSON& it, const Project& pr
 		    .renderers = {},
 		    .controlPoints = {},
 		    .children = {},
-		    .instanceOverride
-		    = { .enabled
-			= std::make_unique<UserSetting> (UserSetting { .value = std::make_unique<DynamicValue> (false),
-								       .property = nullptr,
-								       .condition = std::nullopt }),
-			.alpha
-			= std::make_unique<UserSetting> (UserSetting { .value = std::make_unique<DynamicValue> (1.0f),
-								       .property = nullptr,
-								       .condition = std::nullopt }),
-			.size
-			= std::make_unique<UserSetting> (UserSetting { .value = std::make_unique<DynamicValue> (1.0f),
-								       .property = nullptr,
-								       .condition = std::nullopt }),
-			.lifetime
-			= std::make_unique<UserSetting> (UserSetting { .value = std::make_unique<DynamicValue> (1.0f),
-								       .property = nullptr,
-								       .condition = std::nullopt }),
-			.rate
-			= std::make_unique<UserSetting> (UserSetting { .value = std::make_unique<DynamicValue> (1.0f),
-								       .property = nullptr,
-								       .condition = std::nullopt }),
-			.speed
-			= std::make_unique<UserSetting> (UserSetting { .value = std::make_unique<DynamicValue> (1.0f),
-								       .property = nullptr,
-								       .condition = std::nullopt }),
-			.count
-			= std::make_unique<UserSetting> (UserSetting { .value = std::make_unique<DynamicValue> (1.0f),
-								       .property = nullptr,
-								       .condition = std::nullopt }),
-			.color = std::make_unique<UserSetting> (UserSetting {
-			    .value = std::make_unique<DynamicValue> (glm::vec3 (1.0f)),
-			    .property = nullptr,
-			    .condition = std::nullopt }),
-			.colorn = std::make_unique<UserSetting> (UserSetting {
-			    .value = std::make_unique<DynamicValue> (glm::vec3 (1.0f)),
-			    .property = nullptr,
-			    .condition = std::nullopt }) },
+		    .instanceOverride = {
+		        .enabled = Builders::UserSettingBuilder::fromValue(false),
+			.alpha = Builders::UserSettingBuilder::fromValue(1.0f),
+			.size = Builders::UserSettingBuilder::fromValue(1.0f),
+			.lifetime = Builders::UserSettingBuilder::fromValue(1.0f),
+			.rate = Builders::UserSettingBuilder::fromValue(1.0f),
+			.speed = Builders::UserSettingBuilder::fromValue(1.0f),
+			.count = Builders::UserSettingBuilder::fromValue(1.0f),
+			.color = Builders::UserSettingBuilder::fromValue(1.0f),
+			.colorn = Builders::UserSettingBuilder::fromValue(1.0f),
+		    },
 		}
 	    );
 	}
@@ -629,28 +449,15 @@ ParticleUniquePtr ObjectParser::parseParticle (const JSON& it, const Project& pr
 
 	// Parse instance override
 	ParticleInstanceOverride instanceOverride = {
-	    .enabled = std::make_unique<UserSetting> (UserSetting {
-		.value = std::make_unique<DynamicValue> (false), .property = nullptr, .condition = std::nullopt }),
-	    .alpha = std::make_unique<UserSetting> (UserSetting {
-		.value = std::make_unique<DynamicValue> (1.0f), .property = nullptr, .condition = std::nullopt }),
-	    .size = std::make_unique<UserSetting> (UserSetting {
-		.value = std::make_unique<DynamicValue> (1.0f), .property = nullptr, .condition = std::nullopt }),
-	    .lifetime = std::make_unique<UserSetting> (UserSetting {
-		.value = std::make_unique<DynamicValue> (1.0f), .property = nullptr, .condition = std::nullopt }),
-	    .rate = std::make_unique<UserSetting> (UserSetting {
-		.value = std::make_unique<DynamicValue> (1.0f), .property = nullptr, .condition = std::nullopt }),
-	    .speed = std::make_unique<UserSetting> (UserSetting {
-		.value = std::make_unique<DynamicValue> (1.0f), .property = nullptr, .condition = std::nullopt }),
-	    .count = std::make_unique<UserSetting> (UserSetting {
-		.value = std::make_unique<DynamicValue> (1.0f), .property = nullptr, .condition = std::nullopt }),
-	    .color
-	    = std::make_unique<UserSetting> (UserSetting { .value = std::make_unique<DynamicValue> (glm::vec3 (1.0f)),
-							   .property = nullptr,
-							   .condition = std::nullopt }),
-	    .colorn
-	    = std::make_unique<UserSetting> (UserSetting { .value = std::make_unique<DynamicValue> (glm::vec3 (1.0f)),
-							   .property = nullptr,
-							   .condition = std::nullopt })
+	    .enabled = Builders::UserSettingBuilder::fromValue (false),
+	    .alpha = Builders::UserSettingBuilder::fromValue (1.0f),
+	    .size = Builders::UserSettingBuilder::fromValue (1.0f),
+	    .lifetime = Builders::UserSettingBuilder::fromValue (1.0f),
+	    .rate = Builders::UserSettingBuilder::fromValue (1.0f),
+	    .speed = Builders::UserSettingBuilder::fromValue (1.0f),
+	    .count = Builders::UserSettingBuilder::fromValue (1.0f),
+	    .color = Builders::UserSettingBuilder::fromValue (1.0f),
+	    .colorn = Builders::UserSettingBuilder::fromValue (1.0f),
 	};
 	const auto instanceOverrideIt = it.optional ("instanceoverride");
 	if (instanceOverrideIt.has_value ()) {
@@ -718,7 +525,7 @@ ParticleUniquePtr ObjectParser::parseParticle (const JSON& it, const Project& pr
 	    flags = flagsIt->get<uint32_t> ();
 	}
 
-	auto result = std::make_unique<Particle> (
+	return std::make_unique<Particle> (
 	    std::move (base),
 	    ParticleData {
 		.scale = it.user ("scale", properties, glm::vec3 (1.0f)),
@@ -741,11 +548,6 @@ ParticleUniquePtr ObjectParser::parseParticle (const JSON& it, const Project& pr
 		.instanceOverride = std::move (instanceOverride),
 	    }
 	);
-	bindScriptContext (result->scale, result->id, result->name, "scale");
-	bindScriptContext (result->angles, result->id, result->name, "angles");
-	bindScriptContext (result->visible, result->id, result->name, "visible");
-	bindScriptContext (result->parallaxDepth, result->id, result->name, "parallaxDepth");
-	return result;
     } catch (nlohmann::json::exception& e) {
 	sLog.error ("Error parsing particle '", base.name, "': ", e.what ());
 	sLog.error ("Particle JSON: ", it.dump ());
@@ -755,7 +557,7 @@ ParticleUniquePtr ObjectParser::parseParticle (const JSON& it, const Project& pr
 
 ParticleEmitter ObjectParser::parseParticleEmitter (const JSON& it) {
     // Parse string name safely
-    std::string name = "";
+    std::string name;
     const auto nameIt = it.find ("name");
     if (nameIt != it.end () && nameIt->is_string ()) {
 	name = nameIt->get<std::string> ();
@@ -845,21 +647,10 @@ ParticleInitializerUniquePtr ObjectParser::parseParticleInitializer (const JSON&
     std::string name = it.optional<std::string> ("name", "");
 
     if (name == "colorrandom") {
-	// Only normalize if there's no property connection or values are > 1.0
-	auto minSetting = it.user ("min", properties, glm::vec3 (0.0f));
-	auto maxSetting = it.user ("max", properties, glm::vec3 (255.0f));
-
-	auto minVec = minSetting->value->getVec3 ();
-	if (minSetting->property == nullptr && (minVec.x > 1.0f || minVec.y > 1.0f || minVec.z > 1.0f)) {
-	    minSetting->value->update (minVec / 255.0f);
-	}
-
-	auto maxVec = maxSetting->value->getVec3 ();
-	if (maxSetting->property == nullptr && (maxVec.x > 1.0f || maxVec.y > 1.0f || maxVec.z > 1.0f)) {
-	    maxSetting->value->update (maxVec / 255.0f);
-	}
-
-	return std::make_unique<ColorRandomInitializer> (std::move (minSetting), std::move (maxSetting));
+	return std::make_unique<ColorRandomInitializer> (
+	    it.color ("min", properties, Builders::ColorBuilder::Black),
+	    it.color ("max", properties, Builders::ColorBuilder::White)
+	);
     } else if (name == "sizerandom") {
 	return std::make_unique<SizeRandomInitializer> (
 	    it.user ("min", properties, 0.0f), it.user ("max", properties, 20.0f),

+ 4 - 79
src/WallpaperEngine/Data/Parsers/UserSettingParser.cpp

@@ -1,32 +1,19 @@
 #include "UserSettingParser.h"
+#include "DynamicValueParser.h"
 
 #include "WallpaperEngine/Data/Model/Property.h"
-#include "WallpaperEngine/Data/Model/ScriptedDynamicValue.h"
 #include "WallpaperEngine/Data/Model/UserSetting.h"
-#include "WallpaperEngine/Logging/Log.h"
 
 using namespace WallpaperEngine::Data::Parsers;
 using namespace WallpaperEngine::Data::Builders;
 
-UserSettingUniquePtr UserSettingParser::parse (const json& data, const Properties& properties) {
-    auto value = std::make_unique<DynamicValue> ();
+UserSettingUniquePtr UserSettingParser::parse (const json& data, const Properties& properties, bool expectColor) {
+    auto value = DynamicValueParser::parse (data, properties, expectColor);
     PropertySharedPtr property;
-    std::optional<ConditionInfo> condition;
-    auto valueIt = data;
-    std::string content = data.dump ();
-
-    std::optional<std::string> scriptSource;
-    std::optional<json> scriptPropsJson;
+    std::optional<ConditionInfo> condition = std::nullopt;
 
     if (data.is_object ()) {
 	const auto user = data.optional ("user");
-	const auto script = data.optional ("script");
-	valueIt = data.require ("value", "User setting must have a value");
-
-	if (script.has_value () && !script->is_null ()) {
-	    scriptSource = script->get<std::string> ();
-	    scriptPropsJson = data.optional ("scriptproperties");
-	}
 
 	if (user.has_value () && !user->is_null ()) {
 	    std::string source;
@@ -49,68 +36,6 @@ UserSettingUniquePtr UserSettingParser::parse (const json& data, const Propertie
 	}
     }
 
-    // actual value parsing
-    if (valueIt.is_string ()) {
-	std::string str = valueIt;
-
-	// TODO: VALIDATE THIS IS RIGHT?
-	if (int size = VectorBuilder::preparseSize (str); size == 1) {
-	    size_t parsed = 0;
-	    const float scalar = std::stof (str, &parsed);
-	    if (parsed != str.size ()) {
-		sLog.exception ("Invalid scalar format: ", str);
-	    }
-	    value->update (scalar);
-	} else if (size == 2) {
-	    value->update (static_cast<glm::vec2> (valueIt));
-	} else if (size == 3) {
-	    value->update (static_cast<glm::vec3> (valueIt));
-	} else if (size == 4) {
-	    value->update (static_cast<glm::vec4> (valueIt));
-	} else {
-	    // preparseSize returned 0: no spaces found — try parsing as a scalar float,
-	    // fall back to a plain string for non-numeric values (e.g. "bottom", "center").
-	    std::size_t parsed = 0;
-	    try {
-		float f = std::stof (str, &parsed);
-		if (parsed == str.size ()) {
-		    value->update (f);
-		} else {
-		    value->update (str);
-		}
-	    } catch (const std::exception&) {
-		value->update (str);
-	    }
-	}
-    } else if (valueIt.is_number_integer ()) {
-	value->update (valueIt.get<int> ());
-    } else if (valueIt.is_number_float ()) {
-	value->update (valueIt.get<float> ());
-    } else if (valueIt.is_boolean ()) {
-	value->update (valueIt.get<bool> ());
-    } else if (valueIt.is_null ()) {
-	// null value with no connection to property
-	value->update ();
-    }
-
-    // If the setting has a script, wrap the base value in a ScriptedDynamicValue
-    if (scriptSource.has_value ()) {
-	std::map<std::string, DynamicValueUniquePtr> scriptProps;
-
-	if (scriptPropsJson.has_value () && scriptPropsJson->is_object ()) {
-	    for (const auto& [key, propData] : scriptPropsJson->items ()) {
-		auto propSetting = UserSettingParser::parse (propData, properties);
-		scriptProps[key] = std::move (propSetting->value);
-	    }
-	}
-
-	value = std::make_unique<ScriptedDynamicValue> (
-	    std::move (scriptSource.value ()),
-	    std::move (scriptProps),
-	    std::move (*value)
-	);
-    }
-
     // TODO: This might need to be removed if it causes issues with default values
     // Connect to property if one is specified (this allows property overrides to propagate)
     if (property != nullptr) {

+ 2 - 1
src/WallpaperEngine/Data/Parsers/UserSettingParser.h

@@ -1,6 +1,7 @@
 #pragma once
 
 #include "WallpaperEngine/Data/JSON.h"
+#include "WallpaperEngine/Data/Model/DynamicValue.h"
 #include "WallpaperEngine/Data/Model/Types.h"
 #include "WallpaperEngine/Data/Model/UserSetting.h"
 #include "WallpaperEngine/Logging/Log.h"
@@ -11,6 +12,6 @@ using namespace WallpaperEngine::Data::Model;
 
 class UserSettingParser {
 public:
-    static UserSettingUniquePtr parse (const json& data, const Properties& properties);
+    static UserSettingUniquePtr parse (const json& data, const Properties& properties, bool expectColor = false);
 };
 } // namespace WallpaperEngine::Data::Parsers

+ 6 - 6
src/WallpaperEngine/Data/Parsers/WallpaperParser.cpp

@@ -39,12 +39,12 @@ SceneUniquePtr WallpaperParser::parseScene (const JSON& file, Project& project)
             .project = project
         }, SceneData {
             .colors = {
-                .ambient  = general.optional ("ambientcolor", glm::vec3 (0.0f)),
-                .skylight = general.optional ("skylightcolor", glm::vec3 (0.0f)),
+                .ambient  = general.user ("ambientcolor", properties, glm::vec3 (0.0f)),
+                .skylight = general.user ("skylightcolor", properties, glm::vec3 (0.0f)),
                 .clear = general.user ("clearcolor", properties, glm::vec3 (1.0f)),
             },
             .camera = {
-                .fade = general.optional ("camerafade", false),
+                .fade = general.user ("camerafade", properties, false),
                 .preview = general.optional ("camerapreview", false),
                 .bloom = {
                     .enabled = general.user ("bloom", properties, false),
@@ -72,9 +72,9 @@ SceneUniquePtr WallpaperParser::parseScene (const JSON& file, Project& project)
                     .width  = projection.optional ("auto", false) ? 0 : projection.require <int> ("width",  "Projection must have a width"),
                     .height = projection.optional ("auto", false) ? 0 : projection.require <int> ("height", "Projection must have a height"),
                     .isAuto = projection.optional ("auto", false),
-                    .nearz = camera.optional <float> ("nearz", 0.0f),
-                    .farz = camera.optional <float> ("farz", 1000.0f),
-                    .fov = camera.optional <float> ("fov", 50.0f)
+                    .nearz = camera.user ("nearz", properties, 0.0f),
+                    .farz = camera.user ("farz", properties, 1000.0f),
+                    .fov = camera.user ("fov", properties, 50.0f)
                 }
             },
             .objects = parseObjects (objects, project),

+ 35 - 0
src/WallpaperEngine/Data/Utils/ScopeGuard.h

@@ -0,0 +1,35 @@
+#pragma once
+#include <utility>
+
+namespace WallpaperEngine::Data::Utils {
+/**
+ * Simple basic class that runs code on scope exit
+ */
+template <typename F> struct ScopeGuard {
+    ScopeGuard (const ScopeGuard& other) = delete;
+    ScopeGuard& operator= (const ScopeGuard& other) = delete;
+
+    explicit ScopeGuard (F&& f) : func (std::forward<F> (f)), owner (true) { }
+    explicit ScopeGuard (ScopeGuard&& other) noexcept : func (std::move (other.func)), owner (other.owner) {
+	other.owner = false;
+    }
+    ~ScopeGuard () { this->execute (); }
+
+    /**
+     * Prevents the guard from being run on scope exit
+     */
+    void cancel () { this->owner = false; }
+
+protected:
+    void execute () {
+	if (this->owner) {
+	    this->func ();
+	}
+
+	this->owner = false;
+    }
+
+    F func;
+    bool owner;
+};
+};

+ 5 - 7
src/WallpaperEngine/Data/Utils/TypeCaster.h

@@ -6,29 +6,27 @@ namespace WallpaperEngine::Data::Utils {
 /**
  * A simple implementation of type and sub-types casting to be as explicit
  * as possible while being performant
- *
- * Uses typeid from C++17, so base types must have a virtual, default destructor
  */
 class TypeCaster {
 public:
     virtual ~TypeCaster () = default;
 
     template <class T> [[nodiscard]] const T* as () const {
-	if (is<T> ()) {
-	    return static_cast<const T*> (this);
+	if (auto* ptr = dynamic_cast<const T*> (this)) {
+	    return ptr;
 	}
 
 	throw std::bad_cast ();
     }
 
     template <class T> [[nodiscard]] T* as () {
-	if (is<T> ()) {
-	    return static_cast<T*> (this);
+	if (auto* ptr = dynamic_cast<T*> (this)) {
+	    return ptr;
 	}
 
 	throw std::bad_cast ();
     }
 
-    template <class T> [[nodiscard]] bool is () const { return typeid (*this) == typeid (T); }
+    template <class T> [[nodiscard]] bool is () const { return dynamic_cast<const T*> (this) != nullptr; }
 };
 } // namespace WallpaperEngine::Data::Utils

+ 23 - 26
src/WallpaperEngine/Input/Drivers/WaylandMouseInput.cpp

@@ -1,9 +1,9 @@
 #include "WaylandMouseInput.h"
 #include "WallpaperEngine/Render/Drivers/WaylandOpenGLDriver.h"
-#include <glm/common.hpp>
 #include <chrono>
 #include <cstdlib>
 #include <cstring>
+#include <glm/common.hpp>
 #include <regex>
 #include <string>
 #include <sys/socket.h>
@@ -13,24 +13,6 @@
 
 using namespace WallpaperEngine::Input::Drivers;
 
-namespace {
-const WallpaperEngine::Render::Drivers::Output::WaylandOutputViewport* getActiveViewport (
-    const WallpaperEngine::Render::Drivers::WaylandOpenGLDriver& driver
-) {
-    if (driver.viewportInFocus && driver.viewportInFocus->rendering) {
-	return driver.viewportInFocus;
-    }
-
-    for (const auto* viewport : driver.m_screens) {
-	if (viewport && viewport->rendering) {
-	    return viewport;
-	}
-    }
-
-    return nullptr;
-}
-}
-
 WaylandMouseInput::WaylandMouseInput (const WallpaperEngine::Render::Drivers::WaylandOpenGLDriver& driver) :
     m_waylandDriver (driver) { }
 
@@ -80,7 +62,8 @@ glm::dvec2 WaylandMouseInput::position () const {
 	return { 0, 0 };
     }
 
-    const auto* viewport = getActiveViewport (m_waylandDriver);
+    const auto* viewport = this->getActiveOutputViewport ();
+
     if (!viewport) {
 	return { 0, 0 };
     }
@@ -100,7 +83,7 @@ glm::dvec2 WaylandMouseInput::position () const {
 }
 
 WallpaperEngine::Input::MouseClickStatus WaylandMouseInput::leftClick () const {
-    const auto* viewport = getActiveViewport (m_waylandDriver);
+    const auto* viewport = this->getActiveOutputViewport ();
     if (viewport) {
 	return viewport->leftClick;
     }
@@ -108,6 +91,21 @@ WallpaperEngine::Input::MouseClickStatus WaylandMouseInput::leftClick () const {
     return MouseClickStatus::Released;
 }
 
+const WallpaperEngine::Render::Drivers::Output::WaylandOutputViewport*
+WaylandMouseInput::getActiveOutputViewport () const {
+    if (this->m_waylandDriver.viewportInFocus && this->m_waylandDriver.viewportInFocus->rendering) {
+	return this->m_waylandDriver.viewportInFocus;
+    }
+
+    for (const auto* viewport : this->m_waylandDriver.m_screens) {
+	if (viewport && viewport->rendering) {
+	    return viewport;
+	}
+    }
+
+    return nullptr;
+}
+
 std::optional<glm::dvec2> WaylandMouseInput::queryHyprlandCursorPosition () const {
     const char* signature = std::getenv ("HYPRLAND_INSTANCE_SIGNATURE");
     const char* runtime = std::getenv ("XDG_RUNTIME_DIR");
@@ -124,10 +122,8 @@ std::optional<glm::dvec2> WaylandMouseInput::queryHyprlandCursorPosition () cons
 
     timeval timeout {};
     timeout.tv_usec = 50000;
-    if (
-	setsockopt (fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof (timeout)) != 0
-	|| setsockopt (fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof (timeout)) != 0
-    ) {
+    if (setsockopt (fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof (timeout)) != 0
+	|| setsockopt (fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof (timeout)) != 0) {
 	close (fd);
 	return std::nullopt;
     }
@@ -176,7 +172,8 @@ std::optional<glm::dvec2> WaylandMouseInput::queryHyprlandCursorPosition () cons
 }
 
 WallpaperEngine::Input::MouseClickStatus WaylandMouseInput::rightClick () const {
-    const auto* viewport = getActiveViewport (m_waylandDriver);
+    const auto* viewport = this->getActiveOutputViewport ();
+
     if (viewport) {
 	return viewport->rightClick;
     }

+ 5 - 1
src/WallpaperEngine/Input/Drivers/WaylandMouseInput.h

@@ -4,11 +4,14 @@
 
 #include "WallpaperEngine/Input/MouseInput.h"
 
-#include <glm/vec2.hpp>
 #include <chrono>
+#include <glm/vec2.hpp>
 #include <optional>
 
 namespace WallpaperEngine::Render::Drivers {
+namespace Output {
+    class WaylandOutputViewport;
+}
 class WaylandOpenGLDriver;
 };
 
@@ -41,6 +44,7 @@ public:
     [[nodiscard]] MouseClickStatus rightClick () const override;
 
 private:
+    [[nodiscard]] const Render::Drivers::Output::WaylandOutputViewport* getActiveOutputViewport () const;
     [[nodiscard]] std::optional<glm::dvec2> queryHyprlandCursorPosition () const;
 
     /**

+ 1 - 1
src/WallpaperEngine/Logging/Log.cpp

@@ -9,7 +9,7 @@ Log::Log () { assert (this->sInstance == nullptr); }
 
 Log& Log::get () {
     if (sInstance == nullptr) {
-	sInstance = std::make_unique<Log> ();
+	sInstance = std::unique_ptr<Log> (new Log ());
     }
 
     return *sInstance;

+ 2 - 2
src/WallpaperEngine/Logging/Log.h

@@ -12,8 +12,6 @@ namespace WallpaperEngine::Logging {
  */
 class Log {
 public:
-    Log ();
-
     void addOutput (std::ostream* stream);
     void addError (std::ostream* stream);
 
@@ -75,6 +73,8 @@ public:
     static Log& get ();
 
 private:
+    Log ();
+
     template <typename... Data> std::string buildBuffer (Data... data) {
 	// buffer the string first
 	std::stringbuf buffer;

+ 32 - 0
src/WallpaperEngine/Maths.cpp

@@ -0,0 +1,32 @@
+#include "Maths.h"
+
+using namespace WallpaperEngine::Maths;
+
+float WallpaperEngine::Maths::randomFloat (std::mt19937& rng, float min, float max) {
+    if (max < min) {
+	std::swap (min, max);
+    }
+    std::uniform_real_distribution<float> dist (min, max);
+    return dist (rng);
+}
+
+glm::vec3 WallpaperEngine::Maths::randomVec3 (std::mt19937& rng, const glm::vec3& min, const glm::vec3& max) {
+    return glm::vec3 (
+	randomFloat (rng, min.x, max.x), randomFloat (rng, min.y, max.y), randomFloat (rng, min.z, max.z)
+    );
+}
+
+// Helper: Linear interpolation
+float WallpaperEngine::Maths::lerp (float t, float a, float b) { return a + t * (b - a); }
+
+// Helper: Fade value change over lifetime
+float WallpaperEngine::Maths::fadeValue (float life, float startTime, float endTime, float startValue, float endValue) {
+    if (life <= startTime) {
+	return startValue;
+    } else if (life >= endTime) {
+	return endValue;
+    } else {
+	float t = (life - startTime) / (endTime - startTime);
+	return lerp (t, startValue, endValue);
+    }
+}

+ 11 - 0
src/WallpaperEngine/Maths.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#include <glm/vec3.hpp>
+#include <random>
+
+namespace WallpaperEngine::Maths {
+float randomFloat (std::mt19937& rng, float min, float max);
+glm::vec3 randomVec3 (std::mt19937& rng, const glm::vec3& min, const glm::vec3& max);
+float lerp (float t, float a, float b);
+float fadeValue (float life, float startTime, float endTime, float startValue, float endValue);
+}

+ 1 - 0
src/WallpaperEngine/Render/CObject.cpp

@@ -9,6 +9,7 @@ using namespace WallpaperEngine::Render::Wallpapers;
 CObject::CObject (Wallpapers::CScene& scene, const Object& object) :
     Helpers::ContextAware (scene), m_scene (scene), m_object (object) { }
 
+void CObject::setup () { }
 void CObject::render () { }
 
 Wallpapers::CScene& CObject::getScene () const { return this->m_scene; }

+ 2 - 19
src/WallpaperEngine/Render/CObject.h

@@ -11,29 +11,12 @@ class CScene;
 }
 
 namespace WallpaperEngine::Render {
-class CObject : public Helpers::ContextAware {
+class CObject : public Helpers::ContextAware, public TypeCaster {
 public:
-    template <class T> [[nodiscard]] const T* as () const {
-	if (is<T> ()) {
-	    return static_cast<const T*> (this);
-	}
-
-	throw std::bad_cast ();
-    }
-
-    template <class T> [[nodiscard]] T* as () {
-	if (is<T> ()) {
-	    return static_cast<T*> (this);
-	}
-
-	throw std::bad_cast ();
-    }
-
-    template <class T> [[nodiscard]] bool is () const { return typeid (*this) == typeid (T); }
-
     CObject (Wallpapers::CScene& scene, const Object& object);
     virtual ~CObject () override = default;
 
+    virtual void setup ();
     virtual void render ();
 
     [[nodiscard]] Wallpapers::CScene& getScene () const;

+ 10 - 8
src/WallpaperEngine/Render/CWallpaper.cpp

@@ -196,8 +196,9 @@ void CWallpaper::updateUVs (const glm::ivec4& viewport, const bool vflip) {
     }
 }
 
-void CWallpaper::render (const glm::ivec4& viewport, const bool vflip, const glm::ivec2& globalPosition,
-    const glm::ivec2& logicalSize) {
+void CWallpaper::render (
+    const glm::ivec4& viewport, const bool vflip, const glm::ivec2& globalPosition, const glm::ivec2& logicalSize
+) {
     // Get current frame counter from the driver to avoid redundant scene renders
     const uint32_t currentFrame = this->getContext ().getDriver ().getFrameCounter ();
     const bool needsSceneRender = (currentFrame != this->m_lastRenderedFrame);
@@ -250,12 +251,13 @@ void CWallpaper::render (const glm::ivec4& viewport, const bool vflip, const glm
 
 	// Log span debug info only on first few frames
 	if (this->m_lastRenderedFrame < 5) {
-	    sLog.debug ("SPAN DEBUG: viewport=", viewport.z, "x", viewport.w,
-		" globalPos=(", globalPosition.x, ",", globalPosition.y, ")",
-		" span=(", span.totalBounds.x, ",", span.totalBounds.y, ",", span.totalBounds.z, ",", span.totalBounds.w, ")",
-		" rel=[", relLeft, ",", relRight, "]x[", relTop, ",", relBottom, "]",
-		" baseUV=[", baseUstart, ",", baseUend, "]x[", baseVstart, ",", baseVend, "]",
-		" finalUV=[", ustart, ",", uend, "]x[", vstart, ",", vend, "]");
+	    sLog.debug (
+		"SPAN DEBUG: viewport=", viewport.z, "x", viewport.w, " globalPos=(", globalPosition.x, ",",
+		globalPosition.y, ")", " span=(", span.totalBounds.x, ",", span.totalBounds.y, ",", span.totalBounds.z,
+		",", span.totalBounds.w, ")", " rel=[", relLeft, ",", relRight, "]x[", relTop, ",", relBottom, "]",
+		" baseUV=[", baseUstart, ",", baseUend, "]x[", baseVstart, ",", baseVend, "]", " finalUV=[", ustart,
+		",", uend, "]x[", vstart, ",", vend, "]"
+	    );
 	}
     } else {
 	// Normal mode: compute UVs based on viewport dimensions and wallpaper resolution

+ 4 - 2
src/WallpaperEngine/Render/CWallpaper.h

@@ -66,8 +66,10 @@ public:
     /**
      * Performs a render pass of the wallpaper
      */
-    void render (const glm::ivec4& viewport, const bool vflip, const glm::ivec2& globalPosition = {0, 0},
-	const glm::ivec2& logicalSize = {0, 0});
+    void render (
+	const glm::ivec4& viewport, const bool vflip, const glm::ivec2& globalPosition = { 0, 0 },
+	const glm::ivec2& logicalSize = { 0, 0 }
+    );
 
     /**
      * Pause the renderer

+ 5 - 5
src/WallpaperEngine/Render/Camera.cpp

@@ -33,18 +33,18 @@ float Camera::getWidth () const { return this->m_width; }
 
 float Camera::getHeight () const { return this->m_height; }
 
-float Camera::getFov () const { return this->m_camera.projection.fov; }
+float Camera::getFov () const { return this->m_camera.projection.fov->value->getFloat (); }
 
-float Camera::getNearZ () const { return this->m_camera.projection.nearz; }
+float Camera::getNearZ () const { return this->m_camera.projection.nearz->value->getFloat (); }
 
-float Camera::getFarZ () const { return this->m_camera.projection.farz; }
+float Camera::getFarZ () const { return this->m_camera.projection.farz->value->getFloat (); }
 
 void Camera::setOrthogonalProjection (const float width, const float height) {
     this->m_width = width;
     this->m_height = height;
 
-    float nearz = this->m_camera.projection.nearz;
-    float farz = this->m_camera.projection.farz;
+    float nearz = this->m_camera.projection.nearz->value->getFloat ();
+    float farz = this->m_camera.projection.farz->value->getFloat ();
 
     this->m_projection = glm::ortho<float> (-width / 2.0, width / 2.0, -height / 2.0, height / 2.0, nearz, farz);
     this->m_projection = glm::translate (this->m_projection, this->getEye ());

+ 2 - 2
src/WallpaperEngine/Render/Drivers/Output/OutputViewport.h

@@ -13,9 +13,9 @@ public:
     glm::ivec4 viewport;
     std::string name;
     /** Global position of this viewport in the combined desktop coordinate space */
-    glm::ivec2 globalPosition = {0, 0};
+    glm::ivec2 globalPosition = { 0, 0 };
     /** Logical (unscaled) size in the same coordinate space as globalPosition */
-    glm::ivec2 logicalSize = {0, 0};
+    glm::ivec2 logicalSize = { 0, 0 };
 
     /** Whether this viewport is single in the framebuffer or shares space with more viewports */
     bool single;

+ 1 - 2
src/WallpaperEngine/Render/Drivers/Output/WaylandOutputViewport.cpp

@@ -181,8 +181,7 @@ void WaylandOutputViewport::setupLS () {
     }
 
     layerSurface = zwlr_layer_shell_v1_get_layer_surface (
-	m_driver->getWaylandContext ()->layerShell, surface, output, wlrLayer,
-	"linux-wallpaperengine"
+	m_driver->getWaylandContext ()->layerShell, surface, output, wlrLayer, "linux-wallpaperengine"
     );
 
     if (!layerSurface) {

+ 77 - 83
src/WallpaperEngine/Render/Objects/CImage.cpp

@@ -3,13 +3,16 @@
 #include "CRenderable.h"
 
 #include <algorithm>
-#include <cmath>
 #include <cstring>
 #include <iterator>
 #include <optional>
 #include <sstream>
 
+#include <glm/glm.hpp>
 #include <glm/gtc/matrix_transform.hpp>
+#define GLM_ENABLE_EXPERIMENTAL
+#include <glm/gtx/rotate_vector.hpp>
+#undef GLM_ENABLE_EXPERIMENTAL
 
 #include "WallpaperEngine/Data/Model/DynamicValue.h"
 #include "WallpaperEngine/Data/Model/Material.h"
@@ -31,23 +34,7 @@ glm::vec2 rotateVec2 (const glm::vec2& value, float angle) {
     return { value.x * cosAngle - value.y * sinAngle, value.x * sinAngle + value.y * cosAngle };
 }
 
-UserSettingUniquePtr makeStaticSetting (const glm::vec3& value) {
-    auto dynamicValue = std::make_unique<DynamicValue> (value);
-    return std::make_unique<UserSetting> (
-	UserSetting { .value = std::move (dynamicValue), .property = nullptr, .condition = std::nullopt }
-    );
-}
-
-UserSettingUniquePtr makeStaticSetting (float value) {
-    auto dynamicValue = std::make_unique<DynamicValue> (value);
-    return std::make_unique<UserSetting> (
-	UserSetting { .value = std::move (dynamicValue), .property = nullptr, .condition = std::nullopt }
-    );
-}
-
-bool isMagentaNeonTint (const glm::vec3& color) {
-    return color.r > 0.55f && color.g < 0.25f && color.b > 0.45f;
-}
+bool isMagentaNeonTint (const glm::vec3& color) { return color.r > 0.55f && color.g < 0.25f && color.b > 0.45f; }
 
 std::optional<glm::vec3> findMagentaCompositeTint (const Image& image, const std::vector<int>& skippedEffectIds) {
     for (const auto& effect : image.effects) {
@@ -119,7 +106,9 @@ std::optional<PuppetMeshBlock> findPuppetMeshBlock (
 	    continue;
 	}
 
-	return PuppetMeshBlock { .headerOffset = offset, .vertexBytes = candidateVertexBytes, .indexBytes = candidateIndexBytes };
+	return PuppetMeshBlock { .headerOffset = offset,
+				 .vertexBytes = candidateVertexBytes,
+				 .indexBytes = candidateIndexBytes };
     }
 
     return std::nullopt;
@@ -174,8 +163,8 @@ CImage::ResolvedTransform CImage::resolveTransform (const Object& object) const
     ResolvedTransform resolved = localTransform (*chain[count - 1]);
     for (int i = count - 2; i >= 0; --i) {
 	ResolvedTransform local = localTransform (*chain[i]);
-	const glm::vec2 offset =
-	    rotateVec2 ({ local.origin.x * resolved.scale.x, local.origin.y * resolved.scale.y }, resolved.angle);
+	const glm::vec2 offset
+	    = rotateVec2 ({ local.origin.x * resolved.scale.x, local.origin.y * resolved.scale.y }, resolved.angle);
 	local.origin.x = resolved.origin.x + offset.x;
 	local.origin.y = resolved.origin.y + offset.y;
 	local.origin.z = resolved.origin.z + local.origin.z * resolved.scale.z;
@@ -186,12 +175,21 @@ CImage::ResolvedTransform CImage::resolveTransform (const Object& object) const
 }
 
 CImage::CImage (Wallpapers::CScene& scene, const Image& image) :
-    CRenderable (scene, image, *image.model->material), m_sceneSpacePosition (GL_NONE), m_copySpacePosition (GL_NONE),
-    m_passSpacePosition (GL_NONE), m_texcoordCopy (GL_NONE), m_texcoordPass (GL_NONE), m_modelViewProjectionScreen (),
+    CObject (scene, image), CRenderable (scene, image, *image.model->material), ScriptableObject (scene, image),
+    m_sceneSpacePosition (GL_NONE), m_copySpacePosition (GL_NONE), m_passSpacePosition (GL_NONE),
+    m_texcoordCopy (GL_NONE), m_texcoordPass (GL_NONE), m_modelViewProjectionScreen (),
     m_modelViewProjectionPass (glm::mat4 (1.0)), m_modelViewProjectionCopy (), m_modelViewProjectionScreenInverse (),
     m_modelViewProjectionPassInverse (glm::inverse (m_modelViewProjectionPass)), m_modelViewProjectionCopyInverse (),
-    m_modelMatrix (), m_viewProjectionMatrix (), m_image (image), m_material (nullptr), m_colorBlendMaterial (nullptr),
-    m_pos (), m_initialized (false) {
+    m_modelMatrix (), m_viewProjectionMatrix (), m_image (image), m_pos (), m_initialized (false) {
+    // register any properties in use on this object
+    this->registerProperty ("origin", *image.origin->value);
+    this->registerProperty ("scale", *image.scale->value);
+    this->registerProperty ("angles", *image.angles->value);
+    this->registerProperty ("visible", *image.visible->value);
+    this->registerProperty ("alpha", *image.alpha->value);
+    this->registerProperty ("color", *image.color->value);
+    this->registerProperty ("parallaxDepth", *image.parallaxDepth->value);
+
     // get scene width and height to calculate positions
     auto scene_width = static_cast<float> (scene.getWidth ());
     auto scene_height = static_cast<float> (scene.getHeight ());
@@ -222,8 +220,10 @@ CImage::CImage (Wallpapers::CScene& scene, const Image& image) :
     if ((size.x == 0.0f || size.y == 0.0f) && this->m_texture != nullptr) {
 	size.x = static_cast<float> (this->m_texture->getRealWidth ());
 	size.y = static_cast<float> (this->m_texture->getRealHeight ());
-    } else if ((size.x == 0.0f || size.y == 0.0f) && this->getImage ().model->width.has_value ()
-	       && this->getImage ().model->height.has_value ()) {
+    } else if (
+	(size.x == 0.0f || size.y == 0.0f) && this->getImage ().model->width.has_value ()
+	&& this->getImage ().model->height.has_value ()
+    ) {
 	size.x = static_cast<float> (this->getImage ().model->width.value ());
 	size.y = static_cast<float> (this->getImage ().model->height.value ());
     }
@@ -298,9 +298,11 @@ CImage::CImage (Wallpapers::CScene& scene, const Image& image) :
 	    / static_cast<float> (this->getTexture ()->getTextureHeight (0));
     }
     // calculate the correct texCoord limits for the texture based on the texture screen size and real size
-    else if (this->getTexture () != nullptr
-	     && (this->getTexture ()->getTextureWidth (0) != this->getTexture ()->getRealWidth ()
-		 || this->getTexture ()->getTextureHeight (0) != this->getTexture ()->getRealHeight ())) {
+    else if (
+	this->getTexture () != nullptr
+	&& (this->getTexture ()->getTextureWidth (0) != this->getTexture ()->getRealWidth ()
+	    || this->getTexture ()->getTextureHeight (0) != this->getTexture ()->getRealHeight ())
+    ) {
 	// Account for padding in non-power-of-two textures: clamp UVs to the real content
 	width = static_cast<float> (this->getTexture ()->getRealWidth ())
 	    / static_cast<float> (this->getTexture ()->getTextureWidth (0));
@@ -380,11 +382,8 @@ CImage::CImage (Wallpapers::CScene& scene, const Image& image) :
     this->m_hasPuppetMesh = this->loadPuppetMesh (size);
 
     // compute the center of the image in scene space for rotation
-    this->m_sceneCenter = glm::vec3 (
-	(this->m_pos.x + this->m_pos.z) / 2.0f,
-	(this->m_pos.y + this->m_pos.w) / 2.0f,
-	0.0f
-    );
+    this->m_sceneCenter
+	= glm::vec3 ((this->m_pos.x + this->m_pos.z) / 2.0f, (this->m_pos.y + this->m_pos.w) / 2.0f, 0.0f);
 
     this->m_modelViewProjectionScreen
 	= this->getScene ().getCamera ().getProjection () * this->getScene ().getCamera ().getLookAt ();
@@ -445,7 +444,8 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 	constexpr size_t positionOffset = 0;
 	constexpr size_t uvOffset = 72;
 
-	const std::string puppetVersion = data.size () >= markerSize ? std::string (data.data (), strlen ("MDLV0021")) : "";
+	const std::string puppetVersion
+	    = data.size () >= markerSize ? std::string (data.data (), strlen ("MDLV0021")) : "";
 	if (puppetVersion != "MDLV0021" && puppetVersion != "MDLV0023") {
 	    sLog.error ("Unsupported puppet model header ", puppetVersion, " in ", *this->getImage ().model->puppet);
 	    return false;
@@ -458,7 +458,7 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 		}
 	    }
 	    return data.size ();
-	} ();
+	}();
 
 	const auto meshBlock = findPuppetMeshBlock (data, markerSize, mdlsOffset, meshHeaderSize, vertexStride);
 	if (!meshBlock.has_value ()) {
@@ -515,8 +515,8 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 
 	this->m_puppetIndexCount = static_cast<GLsizei> (indices.size ());
 	sLog.out (
-	    "Loaded puppet mesh ", *this->getImage ().model->puppet, " version=", puppetVersion, " vertices=", vertexCount,
-	    " indices=", this->m_puppetIndexCount
+	    "Loaded puppet mesh ", *this->getImage ().model->puppet, " version=", puppetVersion,
+	    " vertices=", vertexCount, " indices=", this->m_puppetIndexCount
 	);
 
 	return true;
@@ -572,7 +572,9 @@ void CImage::setupPuppetGeometryCallback (Effects::CPass* pass) const {
 		glGetFloatv (GL_COLOR_CLEAR_VALUE, previousClearColor);
 		glClearColor (0.0f, 0.0f, 0.0f, 0.0f);
 		glClear (GL_COLOR_BUFFER_BIT);
-		glClearColor (previousClearColor[0], previousClearColor[1], previousClearColor[2], previousClearColor[3]);
+		glClearColor (
+		    previousClearColor[0], previousClearColor[1], previousClearColor[2], previousClearColor[3]
+		);
 	    }
 	    glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, this->m_puppetIndices);
 	    glDrawElements (GL_TRIANGLES, this->m_puppetIndexCount, GL_UNSIGNED_SHORT, nullptr);
@@ -602,23 +604,23 @@ void CImage::setup () {
     // COMPOSITE_B
     // TODO: SUPPORT PASSTHROUGH (IT'S A SHADER)
     if (this->m_image.model->passthrough) {
-		// passthrough images without effects are bad, do not draw them
-		if(this->m_image.effects.empty ()) {
-			return;
-		}
+	// passthrough images without effects are bad, do not draw them
+	if (this->m_image.effects.empty ()) {
+	    return;
+	}
 
-		// Some have attempted to declare effects with visible set to false.
-		bool allEffectsInvisible = true;
-		for (const auto& cur : this->m_image.effects) {
-			if (cur->visible->value->getBool()) {
-				allEffectsInvisible = false;
-				break;
-			}
-		}
+	// Some have attempted to declare effects with visible set to false.
+	bool allEffectsInvisible = true;
+	for (const auto& cur : this->m_image.effects) {
+	    if (cur->visible->value->getBool ()) {
+		allEffectsInvisible = false;
+		break;
+	    }
+	}
 
-		if (allEffectsInvisible) {
-			return;
-		}
+	if (allEffectsInvisible) {
+	    return;
+	}
     }
 
     const auto& debug = this->getScene ().getContext ().getApp ().getContext ().settings.render.debug;
@@ -730,8 +732,8 @@ void CImage::setup () {
 		.constants = {},
 		.textures = {},
 	    });
-	    tintOverride->constants.emplace ("color", makeStaticSetting (magentaCompositeTint.value ()));
-	    tintOverride->constants.emplace ("alpha", makeStaticSetting (1.0f));
+	    tintOverride->constants.emplace ("color", UserSettingBuilder::fromValue (magentaCompositeTint.value ()));
+	    tintOverride->constants.emplace ("alpha", UserSettingBuilder::fromValue (1.0f));
 
 	    this->m_materials.compatibilityMaterials.emplace_back (
 		MaterialParser::load (this->getScene ().getScene ().project, "materials/effects/tint.json")
@@ -747,7 +749,7 @@ void CImage::setup () {
     }
 
     // extra render pass if there's any blending to be done
-    if (!debug.baseOnly && this->m_image.colorBlendMode->value->getInt() > 0) {
+    if (!debug.baseOnly && this->m_image.colorBlendMode->value->getInt () > 0) {
 	this->m_materials.colorBlending.material
 	    = MaterialParser::load (this->getScene ().getScene ().project, "materials/util/effectpassthrough.json");
 	this->m_materials.colorBlending.override = std::make_unique<ImageEffectPassOverride> (ImageEffectPassOverride {
@@ -803,7 +805,8 @@ void CImage::setupPasses () {
 	GLuint spacePosition = (isFirstPass)
 	    ? (this->m_hasPuppetMesh ? this->m_puppetSpacePosition : this->getCopySpacePosition ())
 	    : this->getPassSpacePosition ();
-	const glm::mat4* projection = (isFirstPass) ? &this->m_modelViewProjectionCopy : &this->m_modelViewProjectionPass;
+	const glm::mat4* projection
+	    = (isFirstPass) ? &this->m_modelViewProjectionCopy : &this->m_modelViewProjectionPass;
 	const glm::mat4* inverseProjection
 	    = (isFirstPass) ? &this->m_modelViewProjectionCopyInverse : &this->m_modelViewProjectionPassInverse;
 	first = false;
@@ -859,11 +862,8 @@ bool CImage::shouldRenderFinalPass (bool isLastPass) const {
 }
 
 bool CImage::configurePassTarget (
-    Effects::CPass* pass,
-    std::shared_ptr<const CFBO>& drawTo,
-    const std::shared_ptr<const TextureProvider>& asInput,
-    std::shared_ptr<const TextureProvider>& effectInput,
-    bool& inTargetEffectSequence
+    Effects::CPass* pass, std::shared_ptr<const CFBO>& drawTo, const std::shared_ptr<const TextureProvider>& asInput,
+    std::shared_ptr<const TextureProvider>& effectInput, bool& inTargetEffectSequence
 ) {
     if (!pass->getTarget ().has_value ()) {
 	return false;
@@ -923,7 +923,7 @@ void CImage::render () {
     this->updateScreenSpacePosition ();
 
 #if !NDEBUG
-    std::string str = "Rendering ";
+    std::string str = "Image ";
 
     if (this->getScene ().getScene ().camera.bloom.enabled->value->getBool () && this->getId () == -1) {
 	str += "bloom";
@@ -950,7 +950,7 @@ void CImage::render () {
 #endif /* DEBUG */
 }
 
-const float& CImage::getBrightness () const { return this->m_image.brightness->value->getFloat(); }
+const float& CImage::getBrightness () const { return this->m_image.brightness->value->getFloat (); }
 
 const float& CImage::getUserAlpha () const { return this->m_image.alpha->value->getFloat (); }
 
@@ -968,8 +968,10 @@ glm::vec2 CImage::resolveGeometrySize (float sceneWidth, float sceneHeight, glm:
     if ((size.x == 0.0f || size.y == 0.0f) && this->m_texture != nullptr) {
 	size.x = static_cast<float> (this->m_texture->getRealWidth ());
 	size.y = static_cast<float> (this->m_texture->getRealHeight ());
-    } else if ((size.x == 0.0f || size.y == 0.0f) && this->getImage ().model->width.has_value ()
-	       && this->getImage ().model->height.has_value ()) {
+    } else if (
+	(size.x == 0.0f || size.y == 0.0f) && this->getImage ().model->width.has_value ()
+	&& this->getImage ().model->height.has_value ()
+    ) {
 	size.x = static_cast<float> (this->getImage ().model->width.value ());
 	size.y = static_cast<float> (this->getImage ().model->height.value ());
     }
@@ -1020,13 +1022,9 @@ void CImage::uploadGeometryBuffers (const glm::vec2& size) {
 
     float width = 1.0f;
     float height = 1.0f;
-    if (
-	this->getTexture () != nullptr && !this->getTexture ()->isAnimated ()
-	&& (
-	    this->getTexture ()->getTextureWidth (0) != this->getTexture ()->getRealWidth ()
-	    || this->getTexture ()->getTextureHeight (0) != this->getTexture ()->getRealHeight ()
-	)
-    ) {
+    if (this->getTexture () != nullptr && !this->getTexture ()->isAnimated ()
+	&& (this->getTexture ()->getTextureWidth (0) != this->getTexture ()->getRealWidth ()
+	    || this->getTexture ()->getTextureHeight (0) != this->getTexture ()->getRealHeight ())) {
 	width = static_cast<float> (this->getTexture ()->getRealWidth ())
 	    / static_cast<float> (this->getTexture ()->getTextureWidth (0));
 	height = static_cast<float> (this->getTexture ()->getRealHeight ())
@@ -1067,7 +1065,8 @@ void CImage::uploadGeometryBuffers (const glm::vec2& size) {
     glBindBuffer (GL_ARRAY_BUFFER, this->m_texcoordCopy);
     glBufferData (GL_ARRAY_BUFFER, sizeof (texcoordCopy), texcoordCopy, GL_DYNAMIC_DRAW);
 
-    this->m_sceneCenter = glm::vec3 ((this->m_pos.x + this->m_pos.z) / 2.0f, (this->m_pos.y + this->m_pos.w) / 2.0f, 0.0f);
+    this->m_sceneCenter
+	= glm::vec3 ((this->m_pos.x + this->m_pos.z) / 2.0f, (this->m_pos.y + this->m_pos.w) / 2.0f, 0.0f);
     this->m_modelViewProjectionCopy = this->getImage ().model->passthrough
 	? this->m_modelViewProjectionScreen
 	: glm::ortho<float> (0.0, size.x, 0.0, size.y);
@@ -1106,9 +1105,8 @@ void CImage::updateScreenSpacePosition () {
 	rotModel = glm::translate (rotModel, -this->m_sceneCenter);
     }
 
-    glm::mat4 mvp = this->getScene ().getCamera ().getProjection ()
-		   * this->getScene ().getCamera ().getLookAt ()
-		   * rotModel;
+    glm::mat4 mvp
+	= this->getScene ().getCamera ().getProjection () * this->getScene ().getCamera ().getLookAt () * rotModel;
 
     // Apply parallax displacement if enabled
     if (this->getScene ().getScene ().camera.parallax.enabled
@@ -1132,10 +1130,6 @@ void CImage::updateScreenSpacePosition () {
 
 const Image& CImage::getImage () const { return this->m_image; }
 
-const std::vector<CEffect*>& CImage::getEffects () const { return this->m_effects; }
-
-const Effects::CMaterial* CImage::getMaterial () const { return this->m_material; }
-
 glm::vec2 CImage::getSize () const {
     if (this->m_texture == nullptr) {
 	return this->getImage ().size;

+ 8 - 14
src/WallpaperEngine/Render/Objects/CImage.h

@@ -8,22 +8,21 @@
 #include "WallpaperEngine/Render/Shaders/Shader.h"
 
 #include "../TextureProvider.h"
+#include "WallpaperEngine/Scripting/ScriptableObject.h"
 
 #include <glm/vec3.hpp>
 #include <vector>
 
 using namespace WallpaperEngine;
 using namespace WallpaperEngine::Render;
-
+using namespace WallpaperEngine::Scripting;
 namespace WallpaperEngine::Render::Objects::Effects {
 class CMaterial;
 class CPass;
 } // namespace WallpaperEngine::Render::Objects::Effects
 
 namespace WallpaperEngine::Render::Objects {
-class CEffect;
-
-class CImage final : public CRenderable {
+class CImage final : public CRenderable, public ScriptableObject {
     friend CObject;
 
 public:
@@ -34,8 +33,6 @@ public:
     void render () override;
 
     [[nodiscard]] const Image& getImage () const;
-    [[nodiscard]] const std::vector<CEffect*>& getEffects () const;
-    [[nodiscard]] const Effects::CMaterial* getMaterial () const;
     [[nodiscard]] glm::vec2 getSize () const;
 
     [[nodiscard]] GLuint getSceneSpacePosition () const;
@@ -84,14 +81,14 @@ private:
     void setupPuppetGeometryCallback (Effects::CPass* pass) const;
     ResolvedTransform updateGeometryBuffers ();
     [[nodiscard]] glm::vec2 resolveGeometrySize (float sceneWidth, float sceneHeight, glm::vec3& origin) const;
-    void updateScenePosition (const glm::vec3& origin, const glm::vec2& size, const glm::vec3& scale, float sceneWidth, float sceneHeight);
+    void updateScenePosition (
+	const glm::vec3& origin, const glm::vec2& size, const glm::vec3& scale, float sceneWidth, float sceneHeight
+    );
     void uploadGeometryBuffers (const glm::vec2& size);
     [[nodiscard]] bool shouldRenderFinalPass (bool isLastPass) const;
     bool configurePassTarget (
-	Effects::CPass* pass,
-	std::shared_ptr<const CFBO>& drawTo,
-	const std::shared_ptr<const TextureProvider>& asInput,
-	std::shared_ptr<const TextureProvider>& effectInput,
+	Effects::CPass* pass, std::shared_ptr<const CFBO>& drawTo,
+	const std::shared_ptr<const TextureProvider>& asInput, std::shared_ptr<const TextureProvider>& effectInput,
 	bool& inTargetEffectSequence
     );
 
@@ -124,9 +121,6 @@ private:
 
     const Image& m_image;
 
-    std::vector<CEffect*> m_effects = {};
-    Effects::CMaterial* m_material = nullptr;
-    Effects::CMaterial* m_colorBlendMaterial = nullptr;
     std::vector<Effects::CPass*> m_passes = {};
     std::vector<MaterialPassUniquePtr> m_virtualPassess = {};
 

+ 164 - 175
src/WallpaperEngine/Render/Objects/CParticle.cpp

@@ -1,6 +1,8 @@
 #include "CParticle.h"
+
 #include "WallpaperEngine/Data/Model/Property.h"
 #include "WallpaperEngine/Logging/Log.h"
+#include "WallpaperEngine/Maths.h"
 #include "WallpaperEngine/Render/Utils/NoiseUtils.h"
 
 #include <GL/glew.h>
@@ -15,41 +17,14 @@ using namespace WallpaperEngine::Render::Objects;
 using namespace WallpaperEngine::Render::Utils;
 using namespace WallpaperEngine::Data::Model;
 
-namespace {
-// Helper: Random float in range
-inline float randomFloat (std::mt19937& rng, float min, float max) {
-    if (max < min) {
-	std::swap (min, max);
-    }
-    std::uniform_real_distribution<float> dist (min, max);
-    return dist (rng);
-}
-
-// Helper: Random vec3 in range
-inline glm::vec3 randomVec3 (std::mt19937& rng, const glm::vec3& min, const glm::vec3& max) {
-    return glm::vec3 (
-	randomFloat (rng, min.x, max.x), randomFloat (rng, min.y, max.y), randomFloat (rng, min.z, max.z)
-    );
-}
-
-// Helper: Linear interpolation
-inline float lerp (float t, float a, float b) { return a + t * (b - a); }
-
-// Helper: Fade value change over lifetime
-inline float fadeValue (float life, float startTime, float endTime, float startValue, float endValue) {
-    if (life <= startTime) {
-	return startValue;
-    } else if (life >= endTime) {
-	return endValue;
-    } else {
-	float t = (life - startTime) / (endTime - startTime);
-	return lerp (t, startValue, endValue);
-    }
-}
-}
-
 CParticle::CParticle (Wallpapers::CScene& scene, const Particle& particle) :
-    CRenderable (scene, particle, *particle.material->material), m_particle (particle) {
+    CObject (scene, particle), CRenderable (scene, particle, *particle.material->material),
+    ScriptableObject (scene, particle), m_particle (particle) {
+    this->registerProperty ("scale", *particle.scale->value);
+    this->registerProperty ("angles", *particle.angles->value);
+    this->registerProperty ("visible", *particle.visible->value);
+    this->registerProperty ("parallaxDepth", *particle.parallaxDepth->value);
+
     this->detectTexture ();
     // Initialize random number generator with time-based seed
     std::random_device rd;
@@ -412,132 +387,136 @@ EmitterFunc CParticle::createBoxEmitter (const ParticleEmitter& emitter) {
     bool limitOnePerFrame = (emitter.flags & 2) != 0;
     bool randomPeriodicEmission = (emitter.flags & 4) != 0;
 
-    return [this, emitter, transformedEmitterOrigin, controlPointIndex, rate, flippedDirections, limitOnePerFrame,
-	    randomPeriodicEmission, emissionTimer = 0.0f, delayTimer = emitter.delay, durationTimer = 0.0f,
-	    periodicTimer = 0.0f, periodicDuration = 0.0f, periodicDelay = 0.0f, emitting = false,
-	    instantaneousEmitted
-	    = false] (std::vector<ParticleInstance>& particles, uint32_t& count, float dt) mutable {
-	if (count >= particles.size ()) {
-	    return;
-	}
-
-	// Handle delay
-	if (delayTimer > 0.0f) {
-	    delayTimer -= dt;
-	    return;
-	}
-
-	// Handle duration
-	if (emitter.duration > 0.0f) {
-	    durationTimer += dt;
-	    if (durationTimer >= emitter.duration) {
+    return
+	[this, emitter, transformedEmitterOrigin, controlPointIndex, rate, flippedDirections, limitOnePerFrame,
+	 randomPeriodicEmission, emissionTimer = 0.0f, delayTimer = emitter.delay, durationTimer = 0.0f,
+	 periodicTimer = 0.0f, periodicDuration = 0.0f, periodicDelay = 0.0f, emitting = false,
+	 instantaneousEmitted = false] (std::vector<ParticleInstance>& particles, uint32_t& count, float dt) mutable {
+	    if (count >= particles.size ()) {
 		return;
 	    }
-	}
 
-	// Handle random periodic emission
-	if (randomPeriodicEmission) {
-	    periodicTimer += dt;
+	    // Handle delay
+	    if (delayTimer > 0.0f) {
+		delayTimer -= dt;
+		return;
+	    }
 
-	    if (!emitting) {
-		if (periodicTimer >= periodicDelay) {
-		    emitting = true;
-		    periodicTimer = 0.0f;
-		    periodicDuration = randomFloat (m_rng, emitter.minPeriodicDuration, emitter.maxPeriodicDuration);
-		} else {
-		    return;
-		}
-	    } else {
-		if (periodicTimer >= periodicDuration) {
-		    emitting = false;
-		    periodicTimer = 0.0f;
-		    periodicDelay = randomFloat (m_rng, emitter.minPeriodicDelay, emitter.maxPeriodicDelay);
+	    // Handle duration
+	    if (emitter.duration > 0.0f) {
+		durationTimer += dt;
+		if (durationTimer >= emitter.duration) {
 		    return;
 		}
 	    }
-	}
 
-	// TODO: Audio processing (audioProcessingMode, audioProcessingBounds, etc.)
-
-	// Handle instantaneous emission
-	uint32_t toEmit = 0;
-	if (emitter.instantaneous > 0 && !instantaneousEmitted) {
-	    toEmit = emitter.instantaneous;
-	    instantaneousEmitted = true;
-	}
-
-	// Rate-based emission with optional cap at 1 per frame
-	if (emitter.rate > 0.0f) {
-	    emissionTimer += dt * rate;
-	    uint32_t rateEmit = static_cast<uint32_t> (emissionTimer);
-	    emissionTimer -= static_cast<float> (rateEmit);
-	    // limitOnePerFrame (flags bit 1): cap at 1 to prevent rope artifacts
-	    if (limitOnePerFrame && rateEmit > 1) {
-		rateEmit = 1;
+	    // Handle random periodic emission
+	    if (randomPeriodicEmission) {
+		periodicTimer += dt;
+
+		if (!emitting) {
+		    if (periodicTimer >= periodicDelay) {
+			emitting = true;
+			periodicTimer = 0.0f;
+			periodicDuration = WallpaperEngine::Maths::randomFloat (
+			    m_rng, emitter.minPeriodicDuration, emitter.maxPeriodicDuration
+			);
+		    } else {
+			return;
+		    }
+		} else {
+		    if (periodicTimer >= periodicDuration) {
+			emitting = false;
+			periodicTimer = 0.0f;
+			periodicDelay = WallpaperEngine::Maths::randomFloat (
+			    m_rng, emitter.minPeriodicDelay, emitter.maxPeriodicDelay
+			);
+			return;
+		    }
+		}
 	    }
-	    toEmit += rateEmit;
-	}
 
-	// Emit particles
-	for (uint32_t i = 0; i < toEmit && count < particles.size (); i++) {
-	    auto& p = particles[count];
+	    // TODO: Audio processing (audioProcessingMode, audioProcessingBounds, etc.)
 
-	    glm::vec3 spawnOrigin = transformedEmitterOrigin;
-	    if (controlPointIndex >= 0 && controlPointIndex < static_cast<int> (m_controlPoints.size ())) {
-		spawnOrigin += m_controlPoints[controlPointIndex].position;
+	    // Handle instantaneous emission
+	    uint32_t toEmit = 0;
+	    if (emitter.instantaneous > 0 && !instantaneousEmitted) {
+		toEmit = emitter.instantaneous;
+		instantaneousEmitted = true;
 	    }
 
-	    // Generate random position within box volume centered on origin
-	    // This creates a centered box (or hollow box if distanceMin > 0)
-	    glm::vec3 randomPos;
-	    for (int axis = 0; axis < 3; axis++) {
-		float minDist = emitter.distanceMin[axis];
-		float maxDist = emitter.distanceMax[axis];
-		// Generate value in [minDist, maxDist]
-		float dist = randomFloat (m_rng, minDist, maxDist);
-		// Randomly flip sign to center the distribution
-		if (randomFloat (m_rng, 0.0f, 1.0f) < 0.5f) {
-		    dist = -dist;
+	    // Rate-based emission with optional cap at 1 per frame
+	    if (emitter.rate > 0.0f) {
+		emissionTimer += dt * rate;
+		uint32_t rateEmit = static_cast<uint32_t> (emissionTimer);
+		emissionTimer -= static_cast<float> (rateEmit);
+		// limitOnePerFrame (flags bit 1): cap at 1 to prevent rope artifacts
+		if (limitOnePerFrame && rateEmit > 1) {
+		    rateEmit = 1;
 		}
-		randomPos[axis] = dist;
+		toEmit += rateEmit;
 	    }
-	    randomPos *= flippedDirections;
 
-	    p.position = spawnOrigin + randomPos;
+	    // Emit particles
+	    for (uint32_t i = 0; i < toEmit && count < particles.size (); i++) {
+		auto& p = particles[count];
 
-	    // Emitter does not set velocity - initializers handle that
-	    p.velocity = glm::vec3 (0.0f);
-	    p.acceleration = glm::vec3 (0.0f);
-	    p.rotation = glm::vec3 (0.0f);
-	    p.angularVelocity = glm::vec3 (0.0f);
-	    p.angularAcceleration = glm::vec3 (0.0f);
+		glm::vec3 spawnOrigin = transformedEmitterOrigin;
+		if (controlPointIndex >= 0 && controlPointIndex < static_cast<int> (m_controlPoints.size ())) {
+		    spawnOrigin += m_controlPoints[controlPointIndex].position;
+		}
 
-	    p.color = glm::vec3 (1.0f) * m_particle.instanceOverride.colorn->value->getVec3 ();
-	    p.alpha = 1.0f * m_particle.instanceOverride.alpha->value->getFloat ();
-	    p.size = 20.0f * m_particle.instanceOverride.size->value->getFloat ();
-	    p.lifetime = 1.0f * m_particle.instanceOverride.lifetime->value->getFloat ();
-	    p.age = 0.0f;
-	    p.alive = true;
-	    p.frame = -1.0f;
+		// Generate random position within box volume centered on origin
+		// This creates a centered box (or hollow box if distanceMin > 0)
+		glm::vec3 randomPos;
+		for (int axis = 0; axis < 3; axis++) {
+		    float minDist = emitter.distanceMin[axis];
+		    float maxDist = emitter.distanceMax[axis];
+		    // Generate value in [minDist, maxDist]
+		    float dist = WallpaperEngine::Maths::randomFloat (m_rng, minDist, maxDist);
+		    // Randomly flip sign to center the distribution
+		    if (WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f) < 0.5f) {
+			dist = -dist;
+		    }
+		    randomPos[axis] = dist;
+		}
+		randomPos *= flippedDirections;
 
-	    p.initial.color = p.color;
-	    p.initial.alpha = p.alpha;
-	    p.initial.size = p.size;
-	    p.initial.lifetime = p.lifetime;
+		p.position = spawnOrigin + randomPos;
 
-	    // Reset oscillator state for reused particles
-	    p.oscillateAlpha = {};
-	    p.oscillateSize = {};
-	    p.oscillatePosition = {};
+		// Emitter does not set velocity - initializers handle that
+		p.velocity = glm::vec3 (0.0f);
+		p.acceleration = glm::vec3 (0.0f);
+		p.rotation = glm::vec3 (0.0f);
+		p.angularVelocity = glm::vec3 (0.0f);
+		p.angularAcceleration = glm::vec3 (0.0f);
+
+		p.color = glm::vec3 (1.0f) * m_particle.instanceOverride.colorn->value->getVec3 ();
+		p.alpha = 1.0f * m_particle.instanceOverride.alpha->value->getFloat ();
+		p.size = 20.0f * m_particle.instanceOverride.size->value->getFloat ();
+		p.lifetime = 1.0f * m_particle.instanceOverride.lifetime->value->getFloat ();
+		p.age = 0.0f;
+		p.alive = true;
+		p.frame = -1.0f;
+
+		p.initial.color = p.color;
+		p.initial.alpha = p.alpha;
+		p.initial.size = p.size;
+		p.initial.lifetime = p.lifetime;
+
+		// Reset oscillator state for reused particles
+		p.oscillateAlpha = {};
+		p.oscillateSize = {};
+		p.oscillatePosition = {};
+
+		// Apply initializers
+		for (auto& init : m_initializers) {
+		    init (p);
+		}
 
-	    // Apply initializers
-	    for (auto& init : m_initializers) {
-		init (p);
+		count++;
 	    }
-
-	    count++;
-	}
-    };
+	};
 }
 
 EmitterFunc CParticle::createSphereEmitter (const ParticleEmitter& emitter) {
@@ -598,24 +577,25 @@ EmitterFunc CParticle::createSphereEmitter (const ParticleEmitter& emitter) {
 	    // Perspective particles (flags & 4 != 0): use 3D spherical shell distribution
 	    if ((m_particle.flags & 4) == 0) {
 		// 2D disk distribution with random Z offset
-		float angle = randomFloat (m_rng, 0.0f, glm::two_pi<float> ());
+		float angle = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, glm::two_pi<float> ());
 		float minRadius = emitter.distanceMin.x;
 		float maxRadius = emitter.distanceMax.x;
 
 		// Use sqrt for uniform area distribution in annulus
 		float minRadiusSq = minRadius * minRadius;
 		float maxRadiusSq = maxRadius * maxRadius;
-		float radiusXY = std::sqrt (randomFloat (m_rng, minRadiusSq, maxRadiusSq));
+		float radiusXY = std::sqrt (WallpaperEngine::Maths::randomFloat (m_rng, minRadiusSq, maxRadiusSq));
 
 		randomPos = glm::vec3 (
-		    radiusXY * std::cos (angle), radiusXY * std::sin (angle), randomFloat (m_rng, -maxRadius, maxRadius)
+		    radiusXY * std::cos (angle), radiusXY * std::sin (angle),
+		    WallpaperEngine::Maths::randomFloat (m_rng, -maxRadius, maxRadius)
 		);
 
 		randomPos *= emitter.directions;
 	    } else {
 		// 3D spherical shell distribution
-		float theta = randomFloat (m_rng, 0.0f, glm::two_pi<float> ());
-		float cosTheta = randomFloat (m_rng, -1.0f, 1.0f);
+		float theta = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, glm::two_pi<float> ());
+		float cosTheta = WallpaperEngine::Maths::randomFloat (m_rng, -1.0f, 1.0f);
 		float sinTheta = std::sqrt (1.0f - cosTheta * cosTheta);
 
 		randomPos = glm::vec3 (sinTheta * std::cos (theta), sinTheta * std::sin (theta), cosTheta);
@@ -625,7 +605,7 @@ EmitterFunc CParticle::createSphereEmitter (const ParticleEmitter& emitter) {
 		float maxRadius = emitter.distanceMax.x;
 		float minRadiusCubed = minRadius * minRadius * minRadius;
 		float maxRadiusCubed = maxRadius * maxRadius * maxRadius;
-		float radius = std::cbrt (randomFloat (m_rng, minRadiusCubed, maxRadiusCubed));
+		float radius = std::cbrt (WallpaperEngine::Maths::randomFloat (m_rng, minRadiusCubed, maxRadiusCubed));
 
 		randomPos *= radius;
 		randomPos *= emitter.directions;
@@ -648,7 +628,7 @@ EmitterFunc CParticle::createSphereEmitter (const ParticleEmitter& emitter) {
 		// Velocity pointing outward from ellipsoid (randomPos already includes directions scaling)
 		glm::vec3 direction
 		    = glm::length (randomPos) > 0.0f ? glm::normalize (randomPos) : glm::vec3 (0.0f, 1.0f, 0.0f);
-		float speed = randomFloat (m_rng, emitter.speedMin, emitter.speedMax);
+		float speed = WallpaperEngine::Maths::randomFloat (m_rng, emitter.speedMin, emitter.speedMax);
 		p.velocity = direction * speed;
 	    } else {
 		// No emitter speed specified, velocity will be set by initializers
@@ -735,7 +715,8 @@ InitializerFunc CParticle::createColorRandomInitializer (const ColorRandomInitia
     DynamicValue* colorOverride = m_particle.instanceOverride.colorn->value.get ();
 
     return [this, minValue, maxValue, colorOverride] (ParticleInstance& p) {
-	p.color = randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ()) * colorOverride->getVec3 ();
+	p.color = WallpaperEngine::Maths::randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ())
+	    * colorOverride->getVec3 ();
 	p.initial.color = p.color;
     };
 }
@@ -747,7 +728,7 @@ InitializerFunc CParticle::createSizeRandomInitializer (const SizeRandomInitiali
     DynamicValue* sizeOverride = m_particle.instanceOverride.size->value.get ();
 
     return [this, minValue, maxValue, exponentValue, sizeOverride] (ParticleInstance& p) {
-	float t = randomFloat (m_rng, 0.0f, 1.0f);
+	float t = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f);
 	float exponent = exponentValue->getFloat ();
 	float min = minValue->getFloat ();
 	float max = maxValue->getFloat ();
@@ -765,7 +746,8 @@ InitializerFunc CParticle::createAlphaRandomInitializer (const AlphaRandomInitia
     DynamicValue* alphaOverride = m_particle.instanceOverride.alpha->value.get ();
 
     return [this, minValue, maxValue, alphaOverride] (ParticleInstance& p) {
-	p.alpha = randomFloat (m_rng, minValue->getFloat (), maxValue->getFloat ()) * alphaOverride->getFloat ();
+	p.alpha = WallpaperEngine::Maths::randomFloat (m_rng, minValue->getFloat (), maxValue->getFloat ())
+	    * alphaOverride->getFloat ();
 	p.initial.alpha = p.alpha;
     };
 }
@@ -776,7 +758,8 @@ InitializerFunc CParticle::createLifetimeRandomInitializer (const LifetimeRandom
     DynamicValue* lifetimeOverride = m_particle.instanceOverride.lifetime->value.get ();
 
     return [this, minValue, maxValue, lifetimeOverride] (ParticleInstance& p) {
-	p.lifetime = randomFloat (m_rng, minValue->getFloat (), maxValue->getFloat ()) * lifetimeOverride->getFloat ();
+	p.lifetime = WallpaperEngine::Maths::randomFloat (m_rng, minValue->getFloat (), maxValue->getFloat ())
+	    * lifetimeOverride->getFloat ();
 	p.initial.lifetime = p.lifetime;
     };
 }
@@ -787,7 +770,8 @@ InitializerFunc CParticle::createVelocityRandomInitializer (const VelocityRandom
     DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
 
     return [this, minValue, maxValue, speedOverride] (ParticleInstance& p) {
-	glm::vec3 vel = randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ()) * speedOverride->getFloat ();
+	glm::vec3 vel = WallpaperEngine::Maths::randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ())
+	    * speedOverride->getFloat ();
 	vel.y = -vel.y;
 	p.velocity += vel;
     };
@@ -799,7 +783,8 @@ InitializerFunc CParticle::createRotationRandomInitializer (const RotationRandom
     DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
 
     return [this, minValue, maxValue, speedOverride] (ParticleInstance& p) {
-	p.rotation = randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ()) * speedOverride->getFloat ();
+	p.rotation = WallpaperEngine::Maths::randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ())
+	    * speedOverride->getFloat ();
     };
 }
 
@@ -820,7 +805,7 @@ InitializerFunc CParticle::createAngularVelocityRandomInitializer (const Angular
 	// exponent >= 2: bias towards min
 	glm::vec3 result;
 	for (int i = 0; i < 3; i++) {
-	    float t = randomFloat (m_rng, 0.0f, 1.0f);
+	    float t = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f);
 	    t = std::pow (t, exponent);
 	    result[i] = minVec[i] + t * (maxVec[i] - minVec[i]);
 	}
@@ -862,7 +847,7 @@ InitializerFunc CParticle::createTurbulentVelocityRandomInitializer (const Turbu
 	    right = glm::vec3 (1.0f, 0.0f, 0.0f);
 	}
 
-	float speed = randomFloat (m_rng, speedMin->getFloat (), speedMax->getFloat ());
+	float speed = WallpaperEngine::Maths::randomFloat (m_rng, speedMin->getFloat (), speedMax->getFloat ());
 	float scale = scaleVal->getFloat ();
 	float offset = offsetVal->getFloat ();
 	float timeScale = timeScaleVal->getFloat ();
@@ -877,7 +862,7 @@ InitializerFunc CParticle::createTurbulentVelocityRandomInitializer (const Turbu
 	noisePos += glm::vec3 (static_cast<float> (m_time) * timeScale);
 
 	// Phase adds per-particle randomization to noise position
-	float phase = randomFloat (m_rng, phaseMin, phaseMax);
+	float phase = WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax);
 	glm::vec3 samplePos = noisePos + glm::vec3 (phase, phase * 0.7f, phase * 1.3f);
 
 	// Sample curl noise for direction and normalize
@@ -965,7 +950,7 @@ CParticle::createMapSequenceAroundControlPointInitializer (const MapSequenceArou
 	// Set velocity based on angle and speed range
 	glm::vec3 speedMin = speedMinValue->getVec3 ();
 	glm::vec3 speedMax = speedMaxValue->getVec3 ();
-	glm::vec3 speed = randomVec3 (m_rng, speedMin, speedMax);
+	glm::vec3 speed = WallpaperEngine::Maths::randomVec3 (m_rng, speedMin, speedMax);
 
 	// Flip Y before rotation to convert to centered space
 	speed.y = -speed.y;
@@ -1134,10 +1119,10 @@ OperatorFunc CParticle::createAlphaFadeOperator (const AlphaFadeOperator& op) {
 		float life = p.getLifetimePos ();
 
 		if (life <= fadeInTime) {
-		    float fade = fadeValue (life, 0.0f, fadeInTime, 0.0f, 1.0f);
+		    float fade = WallpaperEngine::Maths::fadeValue (life, 0.0f, fadeInTime, 0.0f, 1.0f);
 		    p.alpha = p.initial.alpha * fade;
 		} else if (life > fadeOutTime) {
-		    float fade = 1.0f - fadeValue (life, fadeOutTime, 1.0f, 0.0f, 1.0f);
+		    float fade = 1.0f - WallpaperEngine::Maths::fadeValue (life, fadeOutTime, 1.0f, 0.0f, 1.0f);
 		    p.alpha = p.initial.alpha * fade;
 		} else {
 		    p.alpha = p.initial.alpha;
@@ -1171,7 +1156,7 @@ OperatorFunc CParticle::createSizeChangeOperator (const SizeChangeOperator& op)
 		}
 
 		float life = p.getLifetimePos ();
-		float multiplier = fadeValue (life, startTime, endTime, startValue, endValue);
+		float multiplier = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue, endValue);
 		p.size = p.initial.size * multiplier;
 
 		// Update oscillator base so oscillateSize combines properly
@@ -1202,7 +1187,7 @@ OperatorFunc CParticle::createAlphaChangeOperator (const AlphaChangeOperator& op
 		}
 
 		float life = p.getLifetimePos ();
-		float multiplier = fadeValue (life, startTime, endTime, startValue, endValue);
+		float multiplier = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue, endValue);
 		p.alpha = p.initial.alpha * multiplier;
 
 		// Update oscillator base so oscillateAlpha combines properly
@@ -1235,9 +1220,9 @@ OperatorFunc CParticle::createColorChangeOperator (const ColorChangeOperator& op
 		float life = p.getLifetimePos ();
 
 		glm::vec3 color;
-		color.r = fadeValue (life, startTime, endTime, startValue.r, endValue.r);
-		color.g = fadeValue (life, startTime, endTime, startValue.g, endValue.g);
-		color.b = fadeValue (life, startTime, endTime, startValue.b, endValue.b);
+		color.r = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue.r, endValue.r);
+		color.g = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue.g, endValue.g);
+		color.b = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue.b, endValue.b);
 
 		p.color = p.initial.color * color;
 	    }
@@ -1262,8 +1247,10 @@ OperatorFunc CParticle::createTurbulenceOperator (const TurbulenceOperator& op)
     // DynamicValue* audioFreqEndValue = op.audioProcessingFrequencyEnd->value.get ();
 
     // Phase and speed are randomized once per operator instance, not per particle
-    const float phase = randomFloat (m_rng, phaseMinValue->getFloat (), phaseMaxValue->getFloat ());
-    const float turbSpeed = randomFloat (m_rng, speedMinValue->getFloat (), speedMaxValue->getFloat ());
+    const float phase
+	= WallpaperEngine::Maths::randomFloat (m_rng, phaseMinValue->getFloat (), phaseMaxValue->getFloat ());
+    const float turbSpeed
+	= WallpaperEngine::Maths::randomFloat (m_rng, speedMinValue->getFloat (), speedMaxValue->getFloat ());
 
     return [scaleValue, timeScaleValue, maskValue, speedOverride, phase, turbSpeed] (
 	       std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&,
@@ -1530,9 +1517,10 @@ OperatorFunc CParticle::createOscillateAlphaOperator (const OscillateAlphaOperat
 
 		// Initialize per-particle oscillator values on first use
 		if (!p.oscillateAlpha.initialized) {
-		    p.oscillateAlpha.frequency = randomFloat (m_rng, freqMin, freqMax);
-		    p.oscillateAlpha.scale = randomFloat (m_rng, scaleMin, scaleMax);
-		    p.oscillateAlpha.phase = randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
+		    p.oscillateAlpha.frequency = WallpaperEngine::Maths::randomFloat (m_rng, freqMin, freqMax);
+		    p.oscillateAlpha.scale = WallpaperEngine::Maths::randomFloat (m_rng, scaleMin, scaleMax);
+		    p.oscillateAlpha.phase
+			= WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
 		    p.oscillateAlpha.base = p.alpha; // Capture initial base
 		    p.oscillateAlpha.initialized = true;
 		}
@@ -1573,9 +1561,10 @@ OperatorFunc CParticle::createOscillateSizeOperator (const OscillateSizeOperator
 
 		// Initialize per-particle oscillator values on first use
 		if (!p.oscillateSize.initialized) {
-		    p.oscillateSize.frequency = randomFloat (m_rng, freqMin, freqMax);
-		    p.oscillateSize.scale = randomFloat (m_rng, scaleMin, scaleMax);
-		    p.oscillateSize.phase = randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
+		    p.oscillateSize.frequency = WallpaperEngine::Maths::randomFloat (m_rng, freqMin, freqMax);
+		    p.oscillateSize.scale = WallpaperEngine::Maths::randomFloat (m_rng, scaleMin, scaleMax);
+		    p.oscillateSize.phase
+			= WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
 		    p.oscillateSize.base = p.size; // Capture initial base
 		    p.oscillateSize.initialized = true;
 		}
@@ -1621,10 +1610,10 @@ OperatorFunc CParticle::createOscillatePositionOperator (const OscillatePosition
 	    // Initialize per-particle oscillator values on first use (per axis)
 	    if (!p.oscillatePosition.initialized) {
 		for (int axis = 0; axis < 3; axis++) {
-		    p.oscillatePosition.frequency[axis] = randomFloat (m_rng, freqMin, freqMax);
-		    p.oscillatePosition.scale[axis] = randomFloat (m_rng, scaleMin, scaleMax);
+		    p.oscillatePosition.frequency[axis] = WallpaperEngine::Maths::randomFloat (m_rng, freqMin, freqMax);
+		    p.oscillatePosition.scale[axis] = WallpaperEngine::Maths::randomFloat (m_rng, scaleMin, scaleMax);
 		    p.oscillatePosition.phase[axis]
-			= randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
+			= WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
 		}
 		p.oscillatePosition.initialized = true;
 	    }
@@ -2055,7 +2044,7 @@ void CParticle::renderSprites () {
     }
 
 #if !NDEBUG
-    std::string str = "Rendering particles ";
+    std::string str = "Particles ";
     str += this->getParticle ().name + " (" + std::to_string (this->getId ()) + ", " + this->getParticle ().particleFile
 	+ ")";
     glPushDebugGroup (GL_DEBUG_SOURCE_APPLICATION, 0, -1, str.c_str ());
@@ -2292,7 +2281,7 @@ void CParticle::renderRope () {
     }
 
 #if !NDEBUG
-    std::string str = "Rendering rope particles ";
+    std::string str = "Rope particles ";
     str += this->getParticle ().name + " (" + std::to_string (this->getId ()) + ", " + this->getParticle ().particleFile
 	+ ")";
     glPushDebugGroup (GL_DEBUG_SOURCE_APPLICATION, 0, -1, str.c_str ());

+ 2 - 1
src/WallpaperEngine/Render/Objects/CParticle.h

@@ -4,6 +4,7 @@
 #include "WallpaperEngine/Data/Model/Object.h"
 #include "WallpaperEngine/Render/Objects/Effects/CPass.h"
 #include "WallpaperEngine/Render/Wallpapers/CScene.h"
+#include "WallpaperEngine/Scripting/ScriptableObject.h"
 
 #include <functional>
 #include <glm/mat4x4.hpp>
@@ -104,7 +105,7 @@ using InitializerFunc = std::function<void (ParticleInstance&)>;
 using OperatorFunc = std::function<
     void (std::vector<ParticleInstance>&, uint32_t, const std::vector<ControlPointData>&, float, float)>;
 
-class CParticle final : public CRenderable {
+class CParticle final : public CRenderable, public Scripting::ScriptableObject {
     friend CObject;
 
 public:

+ 2 - 0
src/WallpaperEngine/Render/Objects/CRenderable.cpp

@@ -26,6 +26,8 @@ void CRenderable::detectTexture () {
 }
 
 void CRenderable::setup () {
+    CObject::setup ();
+
     // calculate full animation time (if any)
     this->m_animationTime = 0.0f;
 

+ 2 - 2
src/WallpaperEngine/Render/Objects/CRenderable.h

@@ -10,7 +10,7 @@ using namespace WallpaperEngine;
 using namespace WallpaperEngine::Render;
 
 namespace WallpaperEngine::Render::Objects {
-class CRenderable : public CObject, public FBOProvider {
+class CRenderable : virtual public CObject, public FBOProvider {
     friend CObject;
 
 public:
@@ -20,7 +20,7 @@ public:
 
     [[nodiscard]] double getAnimationTime () const;
 
-    virtual void setup ();
+    void setup () override;
 
     [[nodiscard]] virtual const float& getBrightness () const = 0;
     [[nodiscard]] virtual const float& getUserAlpha () const = 0;

+ 95 - 52
src/WallpaperEngine/Render/Objects/CText.cpp

@@ -73,33 +73,59 @@ GLuint compileShader (GLenum type, const char* source) {
 }
 } // namespace
 
-CText::CText (Wallpapers::CScene& scene, const Text& text) : CObject (scene, text), m_text (text) {}
+CText::CText (Wallpapers::CScene& scene, const Text& text) :
+    CObject (scene, text), ScriptableObject (scene, text), m_text (text) {
+    this->registerProperty ("color", *text.color->value);
+    this->registerProperty ("alpha", *text.alpha->value);
+    this->registerProperty ("origin", *text.origin->value);
+    this->registerProperty ("scale", *text.scale->value);
+    this->registerProperty ("visible", *text.visible->value);
+    this->registerProperty ("pointSize", *text.pointSize->value);
+    this->registerProperty ("text", *text.text->value);
+    this->registerProperty ("pointSize", *text.pointSize->value);
+}
 
 CText::~CText () {
     if (m_layerHandle != Scripting::kInvalidLayerHandle) {
-	Scripting::ScriptEngine::instance ().destroyLayer (m_layerHandle);
+	this->getScene ().getScriptEngine ().destroyLayer (m_layerHandle);
 	m_layerHandle = Scripting::kInvalidLayerHandle;
     }
-    if (m_vbo != 0) glDeleteBuffers (1, &m_vbo);
-    if (m_vao != 0) glDeleteVertexArrays (1, &m_vao);
-    if (m_program != 0) glDeleteProgram (m_program);
-    if (m_texture != 0) glDeleteTextures (1, &m_texture);
-    if (m_ftFace != nullptr) FT_Done_Face (m_ftFace);
-    if (m_ftLibrary != nullptr) FT_Done_FreeType (m_ftLibrary);
+    if (m_vbo != 0) {
+	glDeleteBuffers (1, &m_vbo);
+    }
+    if (m_vao != 0) {
+	glDeleteVertexArrays (1, &m_vao);
+    }
+    if (m_program != 0) {
+	glDeleteProgram (m_program);
+    }
+    if (m_texture != 0) {
+	glDeleteTextures (1, &m_texture);
+    }
+    if (m_ftFace != nullptr) {
+	FT_Done_Face (m_ftFace);
+    }
+    if (m_ftLibrary != nullptr) {
+	FT_Done_FreeType (m_ftLibrary);
+    }
 }
 
 void CText::setup () {
-    const bool scripted = !m_text.script.empty ();
+    const bool scripted = m_text.text->value->getScriptSource ().has_value ();
+    const auto& text = m_text.text->value->getString ();
 
     // Nothing to render and no script to produce text later → bail.
-    if (m_text.text.empty () && !scripted)
+    if (text.empty () && !scripted) {
 	return;
+    }
 
-    if (!initFreeType ())
+    if (!initFreeType ()) {
 	return;
+    }
 
-    if (!loadEmbeddedFont () && !loadSystemFont ())
+    if (!loadEmbeddedFont () && !loadSystemFont ()) {
 	return;
+    }
 
     m_lastPixelSize = computeEffectivePixelSize ();
     FT_Set_Pixel_Sizes (m_ftFace, 0, static_cast<FT_UInt> (m_lastPixelSize));
@@ -107,17 +133,19 @@ void CText::setup () {
     buildShader ();
     // Scripted text may have an empty placeholder; use a single space so the
     // glyph texture has non-zero dimensions until the script produces a value.
-    rebuildTextureFrom (m_text.text.empty () ? std::string (" ") : m_text.text);
+    rebuildTextureFrom (text.empty () ? std::string (" ") : text);
 
-    if (scripted)
+    if (scripted) {
 	initScriptLayer ();
+    }
 
     m_valid = m_texture != 0 && m_program != 0 && m_vao != 0;
 }
 
 bool CText::initFreeType () {
-    if (FT_Init_FreeType (&m_ftLibrary) == 0)
+    if (FT_Init_FreeType (&m_ftLibrary) == 0) {
 	return true;
+    }
     sLog.error ("CText: FT_Init_FreeType failed for object ", m_text.name);
     return false;
 }
@@ -126,8 +154,9 @@ bool CText::loadEmbeddedFont () {
     // Wallpapers packed in .pkg don't expose physical paths, so we read the font
     // into memory and use FT_New_Memory_Face. m_fontData must outlive the face.
     // `systemfont_*` references signal "use a system font"; let the fallback handle them.
-    if (m_text.font.empty () || m_text.font.rfind ("systemfont_", 0) == 0)
+    if (m_text.font.empty () || m_text.font.rfind ("systemfont_", 0) == 0) {
 	return false;
+    }
 
     try {
 	auto stream = getAssetLocator ().read (m_text.font);
@@ -137,9 +166,12 @@ bool CText::loadEmbeddedFont () {
 	m_fontData.resize (static_cast<size_t> (size));
 	stream->read (reinterpret_cast<char*> (m_fontData.data ()), size);
 
-	if (FT_New_Memory_Face (m_ftLibrary, m_fontData.data (),
-		static_cast<FT_Long> (m_fontData.size ()), 0, &m_ftFace) == 0)
+	if (FT_New_Memory_Face (
+		m_ftLibrary, m_fontData.data (), static_cast<FT_Long> (m_fontData.size ()), 0, &m_ftFace
+	    )
+	    == 0) {
 	    return true;
+	}
 
 	sLog.error ("CText: FT_New_Memory_Face failed for '", m_text.font, "', falling back to system font");
     } catch (const std::exception& e) {
@@ -176,24 +208,24 @@ unsigned int CText::computeEffectivePixelSize () const {
     // the on-screen size matches the intended pointsize.
     const glm::vec3 initialScale = m_text.scale->value->getVec3 ();
     const float avgScale = (initialScale.x + initialScale.y) * 0.5f;
-    const float compensate = (avgScale > 0.0f && avgScale < 1.0f)
-	? std::min (1.0f / avgScale, 32.0f)
-	: 1.0f;
-    return std::max<unsigned int> (
-	1u, static_cast<unsigned int> (m_text.pointSize->value->getFloat () * compensate));
+    const float compensate = (avgScale > 0.0f && avgScale < 1.0f) ? std::min (1.0f / avgScale, 32.0f) : 1.0f;
+    return std::max<unsigned int> (1u, static_cast<unsigned int> (m_text.pointSize->value->getFloat () * compensate));
 }
 
 void CText::initScriptLayer () {
-    std::map<std::string, DynamicValue*> initialProps;
-    for (const auto& [k, setting] : m_text.scriptProperties) {
-	if (setting && setting->value)
-	    initialProps.emplace (k, setting->value.get ());
+    const auto& script = m_text.text->value->getScriptSource ();
+
+    if (!script.has_value ()) {
+	return;
     }
-    m_layerHandle = Scripting::ScriptEngine::instance ().createLayerScript (
-	m_text.script, initialProps, m_text.text
+
+    m_layerHandle = this->getScene ().getScriptEngine ().createLayerScript (
+	*script, m_text.text->value->getProperties (), m_text.text->value->getString ()
     );
-    if (m_layerHandle == Scripting::kInvalidLayerHandle)
+
+    if (m_layerHandle == Scripting::kInvalidLayerHandle) {
 	sLog.error ("CText: createLayerScript failed for '", m_text.name, "'");
+    }
 }
 
 void CText::rebuildTextureFrom (const std::string& text) {
@@ -237,7 +269,9 @@ void CText::rebuildTextureFrom (const std::string& text) {
 	    for (unsigned int col = 0; col < bmp.width; ++col) {
 		const int dstX = originX + static_cast<int> (col);
 		const int dstY = originY + static_cast<int> (row);
-		if (dstX < 0 || dstX >= width || dstY < 0 || dstY >= height) continue;
+		if (dstX < 0 || dstX >= width || dstY < 0 || dstY >= height) {
+		    continue;
+		}
 		pixels[static_cast<size_t> (dstY) * width + dstX] = bmp.buffer[row * bmp.pitch + col];
 	    }
 	}
@@ -259,8 +293,8 @@ void CText::rebuildTextureFrom (const std::string& text) {
 	glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
     }
 
-    m_textureSize = {width, height};
-    m_quadSize = {static_cast<float> (width), static_cast<float> (height)};
+    m_textureSize = { width, height };
+    m_quadSize = { static_cast<float> (width), static_cast<float> (height) };
     m_lastRenderedText = text;
 
     uploadQuadVertices ();
@@ -270,8 +304,12 @@ void CText::buildShader () {
     GLuint vs = compileShader (GL_VERTEX_SHADER, kVertexShader);
     GLuint fs = compileShader (GL_FRAGMENT_SHADER, kFragmentShader);
     if (vs == 0 || fs == 0) {
-	if (vs) glDeleteShader (vs);
-	if (fs) glDeleteShader (fs);
+	if (vs) {
+	    glDeleteShader (vs);
+	}
+	if (fs) {
+	    glDeleteShader (fs);
+	}
 	return;
     }
 
@@ -309,12 +347,8 @@ void CText::uploadQuadVertices () {
     // lower GL y) appears at screen top. UV.v=0 here = FT glyph top → shows at screen top ✓
     const float verts[] = {
 	// pos        // uv
-	-hx, -hy, 0.0f, 0.0f,
-	 hx, -hy, 1.0f, 0.0f,
-	 hx,  hy, 1.0f, 1.0f,
-	-hx, -hy, 0.0f, 0.0f,
-	 hx,  hy, 1.0f, 1.0f,
-	-hx,  hy, 0.0f, 1.0f,
+	-hx, -hy, 0.0f, 0.0f, hx, -hy, 1.0f, 0.0f, hx,  hy, 1.0f, 1.0f,
+	-hx, -hy, 0.0f, 0.0f, hx, hy,  1.0f, 1.0f, -hx, hy, 0.0f, 1.0f,
     };
 
     const bool firstUpload = (m_vao == 0);
@@ -329,23 +363,31 @@ void CText::uploadQuadVertices () {
 	glEnableVertexAttribArray (0);
 	glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof (float), reinterpret_cast<void*> (0));
 	glEnableVertexAttribArray (1);
-	glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof (float), reinterpret_cast<void*> (2 * sizeof (float)));
+	glVertexAttribPointer (
+	    1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof (float), reinterpret_cast<void*> (2 * sizeof (float))
+	);
     }
     glBindVertexArray (0);
 }
 
 void CText::render () {
-    if (!m_valid) return;
-    if (!m_text.visible->value->getBool ()) return;
+    if (!m_valid) {
+	return;
+    }
+    if (!m_text.visible->value->getBool ()) {
+	return;
+    }
 
+#if !NDEBUG
+    std::string str = "Text " + this->getObject ().name + " (" + std::to_string (this->getObject ().id) + ")";
+    glPushDebugGroup (GL_DEBUG_SOURCE_APPLICATION, 0, -1, str.c_str ());
+#endif /* DEBUG */
     std::string renderedText = m_lastRenderedText;
     if (m_layerHandle != Scripting::kInvalidLayerHandle) {
-	auto& se = Scripting::ScriptEngine::instance ();
+	auto& se = this->getScene ().getScriptEngine ();
 	se.tickLayer (
-	    m_layerHandle,
-	    static_cast<double> (getScene ().getTime ()),
-	    static_cast<double> (getScene ().getDeltaTime ()),
-	    static_cast<double> (getScene ().getFps ())
+	    m_layerHandle, static_cast<double> (getScene ().getTime ()),
+	    static_cast<double> (getScene ().getDeltaTime ()), static_cast<double> (getScene ().getFps ())
 	);
 	const std::string current = se.layerText (m_layerHandle);
 	renderedText = current.empty () ? std::string (" ") : current;
@@ -382,9 +424,7 @@ void CText::render () {
     glm::mat4 model = glm::translate (glm::mat4 (1.0f), gl_origin);
     model = glm::scale (model, scale);
 
-    const glm::mat4 mvp = getScene ().getCamera ().getProjection ()
-			  * getScene ().getCamera ().getLookAt ()
-			  * model;
+    const glm::mat4 mvp = getScene ().getCamera ().getProjection () * getScene ().getCamera ().getLookAt () * model;
 
     glEnable (GL_BLEND);
     glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
@@ -400,4 +440,7 @@ void CText::render () {
     glBindVertexArray (m_vao);
     glDrawArrays (GL_TRIANGLES, 0, 6);
     glBindVertexArray (0);
+#if !NDEBUG
+    glPopDebugGroup ();
+#endif /* DEBUG */
 }

+ 5 - 4
src/WallpaperEngine/Render/Objects/CText.h

@@ -9,6 +9,7 @@
 
 #include "WallpaperEngine/Render/CObject.h"
 #include "WallpaperEngine/Scripting/ScriptEngine.h"
+#include "WallpaperEngine/Scripting/ScriptableObject.h"
 
 // Forward-declare FreeType types to avoid leaking the header into users.
 struct FT_LibraryRec_;
@@ -36,12 +37,12 @@ using namespace WallpaperEngine::Data::Model;
  * the data model but renders an empty string — the Wallpaper Engine JS
  * runtime required to evaluate it is out of scope for Phase 1.
  */
-class CText final : public CObject {
+class CText final : virtual public CObject, public Scripting::ScriptableObject {
 public:
     CText (Wallpapers::CScene& scene, const Text& text);
     ~CText () override;
 
-    void setup ();
+    void setup () override;
     void render () override;
 
 private:
@@ -77,8 +78,8 @@ private:
     GLint m_uColor = -1;
     GLint m_uTexture = -1;
 
-    glm::ivec2 m_textureSize = {0, 0};
-    glm::vec2 m_quadSize = {0.0f, 0.0f};
+    glm::ivec2 m_textureSize = { 0, 0 };
+    glm::vec2 m_quadSize = { 0.0f, 0.0f };
 
     bool m_valid = false;
 };

+ 45 - 37
src/WallpaperEngine/Render/Objects/Effects/CPass.cpp

@@ -51,27 +51,35 @@ CPass::CPass (
     Helpers::ContextAware (renderable), m_renderable (renderable), m_fboProvider (std::move (fboProvider)),
     m_pass (pass), m_binds (binds.has_value () ? binds.value ().get () : DEFAULT_BINDS),
     m_override (override.has_value () ? override.value ().get () : DEFAULT_OVERRIDE), m_target (target),
-    m_blendingmode (pass.blending) {
+    m_blendingmode (pass.blending), m_vao (GL_NONE) {
     this->setupShaders ();
+    glGenVertexArrays (1, &m_vao);
 }
 
 CPass::~CPass () {
+    glDeleteVertexArrays (1, &m_vao);
+    this->m_vao = GL_NONE;
+
     // destroy shader programs
-    if (!glIsProgram(this->m_programID)) return; // program already invalid or deleted
+    if (!glIsProgram (this->m_programID)) {
+	return; // program already invalid or deleted
+    }
 
     GLint shaderCount = 0;
-    glGetProgramiv(this->m_programID, GL_ATTACHED_SHADERS, &shaderCount);
+    glGetProgramiv (this->m_programID, GL_ATTACHED_SHADERS, &shaderCount);
 
     if (shaderCount > 0) {
-        std::vector<GLuint> attachedShaders(shaderCount);
-        glGetAttachedShaders(this->m_programID, shaderCount, nullptr, attachedShaders.data());
+	std::vector<GLuint> attachedShaders (shaderCount);
+	glGetAttachedShaders (this->m_programID, shaderCount, nullptr, attachedShaders.data ());
 
-        for (GLuint s : attachedShaders) {
-            if (glIsShader(s)) glDeleteShader(s);
-        }
+	for (GLuint s : attachedShaders) {
+	    if (glIsShader (s)) {
+		glDeleteShader (s);
+	    }
+	}
     }
 
-    glDeleteProgram(this->m_programID);
+    glDeleteProgram (this->m_programID);
     this->m_programID = 0;
 }
 
@@ -201,15 +209,15 @@ void CPass::setupRenderTexture () {
 std::shared_ptr<const TextureProvider> CPass::resolveTexture0 () {
     auto texture0 = this->resolveTexture (this->m_input, 0, this->m_input);
     if (const auto texture0Override = this->m_textures.find (0); texture0Override != this->m_textures.end ()) {
-	texture0 = texture0Override->second == nullptr ? (this->m_previousInput ?: this->m_input) : texture0Override->second;
+	texture0
+	    = texture0Override->second == nullptr ? (this->m_previousInput ?: this->m_input) : texture0Override->second;
     }
 
     return texture0;
 }
 
-CPass::TextureAnimationState CPass::resolveTextureAnimationState (
-    const std::shared_ptr<const TextureProvider>& texture
-) const {
+CPass::TextureAnimationState
+CPass::resolveTextureAnimationState (const std::shared_ptr<const TextureProvider>& texture) const {
     TextureAnimationState state;
 
     if (texture == nullptr || !texture->isAnimated ()) {
@@ -217,8 +225,7 @@ CPass::TextureAnimationState CPass::resolveTextureAnimationState (
     }
 
     double currentRenderTime = fmod (
-	static_cast<double> (this->getContext ().getDriver ().getRenderTime ()),
-	this->m_renderable.getAnimationTime ()
+	static_cast<double> (this->getContext ().getDriver ().getRenderTime ()), this->m_renderable.getAnimationTime ()
     );
 
     for (const auto& frameCur : texture->getFrames ()) {
@@ -242,9 +249,7 @@ CPass::TextureAnimationState CPass::resolveTextureAnimationState (
     return state;
 }
 
-void CPass::bindTextureUnit (
-    int index, const std::shared_ptr<const TextureProvider>& texture, uint32_t frame
-) const {
+void CPass::bindTextureUnit (int index, const std::shared_ptr<const TextureProvider>& texture, uint32_t frame) const {
     if (texture == nullptr) {
 	return;
     }
@@ -253,9 +258,7 @@ void CPass::bindTextureUnit (
     glBindTexture (GL_TEXTURE_2D, texture->getTextureID (frame));
 }
 
-void CPass::bindTextureOverrides (
-    uint32_t currentTexture, std::shared_ptr<const TextureProvider>& texture0
-) const {
+void CPass::bindTextureOverrides (uint32_t currentTexture, std::shared_ptr<const TextureProvider>& texture0) const {
     for (const auto& [index, expectedTexture] : this->m_textures) {
 	auto texture = expectedTexture == nullptr ? (this->m_previousInput ?: this->m_input) : expectedTexture;
 	if (texture == nullptr) {
@@ -396,11 +399,14 @@ void CPass::cleanupRenderSetup () {
 }
 
 void CPass::render () {
+    // set the VAO for now
+    glBindVertexArray (this->m_vao);
+
     const auto& debug = this->getContext ().getApp ().getContext ().settings.render.debug;
     if (debug.passLog) {
 	sLog.out (
-	    "Render pass object=", this->m_renderable.getId (), " shader=", this->m_pass.shader, " target=",
-	    this->m_target.has_value () ? this->m_target.value ().get () : std::string ("<screen/local>"),
+	    "Render pass object=", this->m_renderable.getId (), " shader=", this->m_pass.shader,
+	    " target=", this->m_target.has_value () ? this->m_target.value ().get () : std::string ("<screen/local>"),
 	    " drawTo=", this->m_drawTo ? this->m_drawTo->getName () : std::string ("<null>"),
 	    " drawSize=", textureSizeLabel (this->m_drawTo), " inputSize=", textureSizeLabel (this->m_input)
 	);
@@ -411,16 +417,18 @@ void CPass::render () {
 	    }
 
 	    switch (uniform->second->type) {
-		case Vector3: {
-		    const auto* v = static_cast<const glm::vec3*> (uniform->second->value);
-		    sLog.out ("  uniform ", uniformName, "=", v->x, " ", v->y, " ", v->z);
-		    break;
-		}
-		case Float: {
-		    const auto* v = static_cast<const float*> (uniform->second->value);
-		    sLog.out ("  uniform ", uniformName, "=", *v);
-		    break;
-		}
+		case Vector3:
+		    {
+			const auto* v = static_cast<const glm::vec3*> (uniform->second->value);
+			sLog.out ("  uniform ", uniformName, "=", v->x, " ", v->y, " ", v->z);
+			break;
+		    }
+		case Float:
+		    {
+			const auto* v = static_cast<const float*> (uniform->second->value);
+			sLog.out ("  uniform ", uniformName, "=", *v);
+			break;
+		    }
 		default:
 		    break;
 	    }
@@ -561,8 +569,8 @@ void CPass::setupShaders () {
     }
 
     this->m_shader = new Render::Shaders::Shader (
-	this->m_renderable.getAssetLocator (), shaderName, this->m_combos, this->m_override.combos,
-	passTextures, this->m_override.textures, this->m_override.constants
+	this->m_renderable.getAssetLocator (), shaderName, this->m_combos, this->m_override.combos, passTextures,
+	this->m_override.textures, this->m_override.constants
     );
 
     const auto [vertex, fragment]
@@ -746,8 +754,8 @@ void CPass::setupUniforms () {
     const auto& recorder = this->m_renderable.getScene ().getAudioContext ().getRecorder ();
 
     // lighting variables
-    this->addUniform ("g_LightAmbientColor", sceneData.colors.ambient);
-    this->addUniform ("g_LightSkylightColor", sceneData.colors.skylight);
+    this->addUniform ("g_LightAmbientColor", sceneData.colors.ambient->value->getVec3 ());
+    this->addUniform ("g_LightSkylightColor", sceneData.colors.skylight->value->getVec3 ());
     // register variables like brightness and alpha with some default value
     this->addUniform ("g_Brightness", renderable.getBrightness ());
     this->addUniform ("g_UserAlpha", renderable.getUserAlpha ());

+ 9 - 8
src/WallpaperEngine/Render/Objects/Effects/CPass.h

@@ -31,11 +31,11 @@ public:
     );
     ~CPass ();
 
-	    void render ();
+    void render ();
 
-	    void setDestination (std::shared_ptr<const CFBO> drawTo);
-	    void setInput (std::shared_ptr<const TextureProvider> input);
-	    void setPreviousInput (std::shared_ptr<const TextureProvider> input);
+    void setDestination (std::shared_ptr<const CFBO> drawTo);
+    void setInput (std::shared_ptr<const TextureProvider> input);
+    void setPreviousInput (std::shared_ptr<const TextureProvider> input);
     void setTexCoord (GLuint texcoord);
     void setPosition (GLuint position);
     void setModelViewProjectionMatrix (const glm::mat4* projection);
@@ -192,10 +192,10 @@ private:
 
     Render::Shaders::Shader* m_shader = nullptr;
 
-	    std::shared_ptr<const CFBO> m_drawTo = nullptr;
-	    std::shared_ptr<const TextureProvider> m_input = nullptr;
-	    std::shared_ptr<const TextureProvider> m_previousInput = nullptr;
-	    glm::vec4 m_texture0Resolution = {};
+    std::shared_ptr<const CFBO> m_drawTo = nullptr;
+    std::shared_ptr<const TextureProvider> m_input = nullptr;
+    std::shared_ptr<const TextureProvider> m_previousInput = nullptr;
+    glm::vec4 m_texture0Resolution = {};
 
     GLuint m_programID;
 
@@ -204,6 +204,7 @@ private:
     GLint g_Texture0Translation;
     GLuint a_TexCoord;
     GLuint a_Position;
+    GLuint m_vao;
 
     // Custom geometry callbacks (for particles, etc.)
     GeometryCallback m_setupAttribsCallback;

+ 3 - 2
src/WallpaperEngine/Render/RenderContext.cpp

@@ -24,8 +24,9 @@ void RenderContext::render (Drivers::Output::OutputViewport* viewport) {
 
     // render the background
     if (const auto ref = this->m_wallpapers.find (viewport->name); ref != this->m_wallpapers.end ()) {
-	ref->second->render (viewport->viewport, this->getOutput ().renderVFlip (), viewport->globalPosition,
-	    viewport->logicalSize);
+	ref->second->render (
+	    viewport->viewport, this->getOutput ().renderVFlip (), viewport->globalPosition, viewport->logicalSize
+	);
     }
 
 #if !NDEBUG

+ 7 - 7
src/WallpaperEngine/Render/Shaders/ShaderUnit.cpp

@@ -35,16 +35,16 @@
 	  "#define CAST3(x) (vec3(x))\n"                                                                               \
 	  "#define CAST4(x) (vec4(x))\n"                                                                               \
 	  "#define CAST3X3(x) (mat3(x))\n"                                                                             \
-	  "#define float2 vec2\n"                                                                                     \
-	  "#define float3 vec3\n"                                                                                     \
-	  "#define float4 vec4\n"                                                                                     \
-	  "#define int2 ivec2\n"                                                                                      \
-	  "#define int3 ivec3\n"                                                                                      \
-	  "#define int4 ivec4\n"                                                                                      \
+	  "#define float2 vec2\n"                                                                                      \
+	  "#define float3 vec3\n"                                                                                      \
+	  "#define float4 vec4\n"                                                                                      \
+	  "#define int2 ivec2\n"                                                                                       \
+	  "#define int3 ivec3\n"                                                                                       \
+	  "#define int4 ivec4\n"                                                                                       \
 	  "#define saturate(x) (clamp(x, 0.0, 1.0))\n"                                                                 \
 	  "#define texSample2D texture\n"                                                                              \
 	  "#define texSample2DLod textureLod\n"                                                                        \
-	  "#define log10(x) (log2(x) * 0.301029995663981)\n"                                                             \
+	  "#define log10(x) (log2(x) * 0.301029995663981)\n"                                                           \
 	  "#define atan2 atan\n"                                                                                       \
 	  "#define fmod(x, y) ((x)-(y)*trunc((x)/(y)))\n"                                                              \
 	  "#define ddx dFdx\n"                                                                                         \

+ 11 - 11
src/WallpaperEngine/Render/TextureCache.cpp

@@ -52,9 +52,8 @@ std::vector<char*> makeArgv (const std::string& program, const std::vector<std::
     return argv;
 }
 
-std::optional<pid_t> spawnProcessWithStdout (
-    const std::string& program, const std::vector<std::string>& args, int& readFd
-) {
+std::optional<pid_t>
+spawnProcessWithStdout (const std::string& program, const std::vector<std::string>& args, int& readFd) {
     int outputPipe[2] {};
     if (pipe (outputPipe) != 0) {
 	return std::nullopt;
@@ -99,9 +98,8 @@ bool waitForProcessUntil (pid_t pid, const std::chrono::steady_clock::time_point
 }
 
 bool pollReadable (int readFd, const std::chrono::steady_clock::time_point& deadline) {
-    const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds> (
-	deadline - std::chrono::steady_clock::now ()
-    );
+    const auto remaining
+	= std::chrono::duration_cast<std::chrono::milliseconds> (deadline - std::chrono::steady_clock::now ());
     if (remaining.count () <= 0) {
 	return false;
     }
@@ -152,9 +150,8 @@ std::string processReadFirstLine (const std::string& program, const std::vector<
     return trimTrailingNewlines (output);
 }
 
-std::vector<char> processReadBytes (
-    const std::string& program, const std::vector<std::string>& args, std::chrono::milliseconds timeout
-) {
+std::vector<char>
+processReadBytes (const std::string& program, const std::vector<std::string>& args, std::chrono::milliseconds timeout) {
     const auto deadline = std::chrono::steady_clock::now () + timeout;
 
     int outputFd = -1;
@@ -310,7 +307,8 @@ std::shared_ptr<const TextureProvider> tryCreateImageTexture (RenderContext& con
     int height = 0;
     int channels = 0;
     if (!stbi_info_from_memory (
-	    reinterpret_cast<const stbi_uc*> (bytes.data ()), static_cast<int> (bytes.size ()), &width, &height, &channels
+	    reinterpret_cast<const stbi_uc*> (bytes.data ()), static_cast<int> (bytes.size ()), &width, &height,
+	    &channels
 	)) {
 	return nullptr;
     }
@@ -344,7 +342,9 @@ std::shared_ptr<const TextureProvider> tryLoadMediaThumbnailUrl (RenderContext&
 	    bytes = readFileBytes (*path);
 	}
     } else if (artUrl.rfind ("http://", 0) == 0 || artUrl.rfind ("https://", 0) == 0) {
-	bytes = processReadBytes ("curl", { "-L", "-s", "--max-time", "3", "--output", "-", artUrl }, std::chrono::milliseconds (3500));
+	bytes = processReadBytes (
+	    "curl", { "-L", "-s", "--max-time", "3", "--output", "-", artUrl }, std::chrono::milliseconds (3500)
+	);
     }
 
     return tryCreateImageTexture (context, bytes);

+ 2 - 2
src/WallpaperEngine/Render/TextureProvider.h

@@ -12,8 +12,8 @@
 namespace WallpaperEngine::Render {
 using namespace WallpaperEngine::Data::Assets;
 /**
- * Base interface that describes the minimum information required for a texture
- * to be displayed by the engine
+ * Base interface that describes the minimum information required for the engine
+ * to display a texture
  */
 class TextureProvider {
 public:

+ 24 - 111
src/WallpaperEngine/Render/Wallpapers/CScene.cpp

@@ -21,38 +21,6 @@ using namespace WallpaperEngine::Render;
 using namespace WallpaperEngine::Data::Model;
 using namespace WallpaperEngine::Data::Parsers;
 using namespace WallpaperEngine::Render::Wallpapers;
-using JSON = WallpaperEngine::Data::JSON::JSON;
-
-namespace {
-Render::CObject* createImageObject (CScene& scene, const Image& imageData) {
-    auto* image = new Objects::CImage (scene, imageData);
-    try {
-	image->setup ();
-    } catch (std::runtime_error&) {
-	sLog.error ("Cannot setup image ", image->getImage ().name);
-	delete image;
-	return nullptr;
-    }
-    return image;
-}
-
-Render::CObject* createParticleObject (CScene& scene, const Particle& particleData) {
-    if (scene.getContext ().getApp ().getContext ().settings.general.disableParticles == true) {
-	sLog.debug ("Ignoring particle system (disabled in settings): ", particleData.name);
-	return nullptr;
-    }
-
-    auto* particle = new Objects::CParticle (scene, particleData);
-    try {
-	particle->setup ();
-    } catch (std::runtime_error&) {
-	sLog.error ("Cannot setup particle ", particle->getParticle ().name);
-	delete particle;
-	return nullptr;
-    }
-    return particle;
-}
-}
 
 CScene::CScene (
     const Wallpaper& wallpaper, RenderContext& context, AudioContext& audioContext,
@@ -61,6 +29,8 @@ CScene::CScene (
     // caller should check this, if not a std::bad_cast is good to throw
     auto scene = wallpaper.as<Scene> ();
 
+    // setup scripting engine
+    this->m_scriptEngine = std::make_unique<Scripting::ScriptEngine> (*this);
     // setup the scene camera
     this->m_camera = std::make_unique<Camera> (*this, scene->camera);
 
@@ -130,8 +100,6 @@ CScene::CScene (
 	this->addObjectToRenderOrder (*object);
     }
 
-    this->collectScriptedValues ();
-
     // create extra framebuffers for the bloom effect
     this->_rt_4FrameBuffer = this->create (
 	"_rt_4FrameBuffer", TextureFormat_ARGB8888, TextureFlags_ClampUVs, 1.0, { sceneWidth / 4, sceneHeight / 4 },
@@ -261,26 +229,33 @@ Render::CObject* CScene::dispatchObjectType (const Object& object) {
     Render::CObject* renderObject = nullptr;
 
     if (object.is<Image> ()) {
-	renderObject = createImageObject (*this, *object.as<Image> ());
+	renderObject = new Objects::CImage (*this, *object.as<Image> ());
     } else if (object.is<Sound> ()) {
 	renderObject = new Objects::CSound (*this, *object.as<Sound> ());
     } else if (object.is<Text> ()) {
-	auto* text = new Objects::CText (*this, *object.as<Text> ());
-	try {
-	    text->setup ();
-	} catch (std::runtime_error&) {
-	    sLog.error ("Cannot setup text ", text->getObject ().name);
-	    delete text;
+	renderObject = new Objects::CText (*this, *object.as<Text> ());
+    } else if (object.is<Particle> ()) {
+	const auto& particleData = *object.as<Particle> ();
+
+	if (this->getContext ().getApp ().getContext ().settings.general.disableParticles == true) {
+	    sLog.debug ("Ignoring particle system (disabled in settings): ", particleData.name);
 	    return nullptr;
 	}
-	renderObject = text;
-    } else if (object.is<Particle> ()) {
-	renderObject = createParticleObject (*this, *object.as<Particle> ());
+
+	renderObject = new Objects::CParticle (*this, particleData);
     } else {
-	sLog.debug ("Unknown object type, creating placeholder, empty object: ", object.id);
+	sLog.error ("Unknown object type, creating placeholder, empty object: ", object.id);
 	renderObject = new CObject (*this, object);
     }
 
+    try {
+	renderObject->setup ();
+    } catch (const std::exception& e) {
+	sLog.error ("Failed to setup object ", object.id, ": ", e.what ());
+	delete renderObject;
+	renderObject = nullptr;
+    }
+
     return renderObject;
 }
 
@@ -319,78 +294,13 @@ void CScene::addObjectToRenderOrder (const Object& object) {
     }
 }
 
-void CScene::registerScriptedValue (const UserSettingUniquePtr& setting) {
-    if (!setting || !setting->value) {
-	return;
-    }
-
-    auto* scripted = dynamic_cast<ScriptedDynamicValue*> (setting->value.get ());
-    if (!scripted) {
-	return;
-    }
-
-    if (std::ranges::find (this->m_scriptedValues, scripted) == this->m_scriptedValues.end ()) {
-	this->m_scriptedValues.emplace_back (scripted);
-    }
-}
-
-void CScene::collectScriptedValues () {
-    this->m_scriptedValues.clear ();
-
-    for (const auto& object : this->getScene ().objects) {
-	this->registerScriptedValue (object->origin);
-	this->registerScriptedValue (object->groupScale);
-	this->registerScriptedValue (object->groupAngles);
-	this->registerScriptedValue (object->groupVisible);
-
-	if (object->is<Image> ()) {
-	    const auto* image = object->as<Image> ();
-	    this->registerScriptedValue (image->scale);
-	    this->registerScriptedValue (image->angles);
-	    this->registerScriptedValue (image->visible);
-	    this->registerScriptedValue (image->alpha);
-	    this->registerScriptedValue (image->color);
-	    this->registerScriptedValue (image->parallaxDepth);
-
-	    for (const auto& effect : image->effects) {
-		this->registerScriptedValue (effect->visible);
-		for (const auto& pass : effect->passOverrides) {
-		    for (const auto& constant : pass->constants | std::views::values) {
-			this->registerScriptedValue (constant);
-		    }
-		}
-	    }
-	} else if (object->is<Particle> ()) {
-	    const auto* particle = object->as<Particle> ();
-	    this->registerScriptedValue (particle->scale);
-	    this->registerScriptedValue (particle->angles);
-	    this->registerScriptedValue (particle->visible);
-	    this->registerScriptedValue (particle->parallaxDepth);
-	} else if (object->is<Text> ()) {
-	    const auto* text = object->as<Text> ();
-	    this->registerScriptedValue (text->visible);
-	    this->registerScriptedValue (text->color);
-	    this->registerScriptedValue (text->alpha);
-	    this->registerScriptedValue (text->scale);
-	    this->registerScriptedValue (text->pointSize);
-	}
-    }
-}
-
-void CScene::updateScriptedValues () {
-    for (const auto& scripted : this->m_scriptedValues) {
-	scripted->reevaluate (this);
-    }
-}
-
+ScriptEngine& CScene::getScriptEngine () const { return *this->m_scriptEngine; }
 Camera& CScene::getCamera () const { return *this->m_camera; }
 
 void CScene::renderFrame (const glm::ivec4& viewport) {
     // ensure the virtual mouse position is up to date
     this->updateMouse (viewport);
 
-    this->updateScriptedValues ();
-
     // update the parallax position if required
     if (this->getScene ().camera.parallax.enabled->value->getBool ()
 	&& !this->getContext ().getApp ().getContext ().settings.mouse.disableparallax) {
@@ -405,6 +315,9 @@ void CScene::renderFrame (const glm::ivec4& viewport) {
 	    = glm::mix (this->m_parallaxDisplacement, (centeredMouse * amount) * influence, delay);
     }
 
+    // run a tick in the javascript logic
+    this->getScriptEngine ().tick ();
+
     // update main textures for images
     for (const auto& cur : this->m_objectsByRenderOrder) {
 	if (!cur->is<Objects::CImage> ()) {

+ 4 - 5
src/WallpaperEngine/Render/Wallpapers/CScene.h

@@ -2,8 +2,8 @@
 
 #include "WallpaperEngine/Render/Camera.h"
 
-#include "WallpaperEngine/Data/Model/ScriptedDynamicValue.h"
 #include "WallpaperEngine/Render/CWallpaper.h"
+#include "WallpaperEngine/Scripting/ScriptEngine.h"
 
 namespace WallpaperEngine::Render {
 class Camera;
@@ -22,6 +22,7 @@ public:
 
     ~CScene () override;
 
+    [[nodiscard]] Scripting::ScriptEngine& getScriptEngine () const;
     [[nodiscard]] Camera& getCamera () const;
 
     [[nodiscard]] const Scene& getScene () const;
@@ -54,16 +55,14 @@ private:
     Render::CObject* createObject (const Object& object);
     Render::CObject* dispatchObjectType (const Object& object);
     void addObjectToRenderOrder (const Object& object);
-    void collectScriptedValues ();
-    void registerScriptedValue (const UserSettingUniquePtr& setting);
-    void updateScriptedValues ();
 
+    std::unique_ptr<Scripting::ScriptEngine> m_scriptEngine;
     std::unique_ptr<Camera> m_camera;
     ObjectUniquePtr m_bloomObjectData;
     CObject* m_bloomObject = nullptr;
     std::map<int, CObject*> m_objects = {};
     std::vector<CObject*> m_objectsByRenderOrder = {};
-    std::vector<ScriptedDynamicValue*> m_scriptedValues = {};
+    std::vector<DynamicValue*> m_scriptedValues = {};
     glm::vec2 m_mousePosition = {};
     glm::vec2 m_mousePositionLast = {};
     glm::vec2 m_mousePositionNormalized = {};

+ 30 - 0
src/WallpaperEngine/Scripting/Adapters/ObjectAdapter.cpp

@@ -0,0 +1,30 @@
+#include "ObjectAdapter.h"
+
+#include "WallpaperEngine/Scripting/ScriptEngine.h"
+
+using namespace WallpaperEngine::Data::Model;
+using namespace WallpaperEngine::Scripting::Adapters;
+
+ObjectAdapter::ObjectAdapter (ScriptEngine& engine) :
+    m_engine (engine), m_classId (JS_INVALID_CLASS_ID), m_definition () { }
+
+JSValue ObjectAdapter::instantiate (ScriptableObject& object) {
+    JSValue result = JS_NewObjectClass (this->m_engine.getContext (), this->m_classId);
+    JS_SetOpaque (result, &object);
+
+    return result;
+}
+
+JSValue ObjectAdapter::instantiate (DynamicValue& value) {
+    JSValue result = JS_NewObjectClass (this->m_engine.getContext (), this->m_classId);
+    JS_SetOpaque (result, &value);
+
+    return result;
+}
+
+void ObjectAdapter::registerType (const JSClassDef& definition) {
+    this->m_definition = definition;
+
+    JS_NewClassID (this->m_engine.getRuntime (), &this->m_classId);
+    JS_NewClass (this->m_engine.getRuntime (), this->m_classId, &definition);
+}

+ 32 - 0
src/WallpaperEngine/Scripting/Adapters/ObjectAdapter.h

@@ -0,0 +1,32 @@
+#pragma once
+
+#include "WallpaperEngine/Data/Utils/TypeCaster.h"
+#include "quickjs.h"
+
+namespace WallpaperEngine::Data::Model {
+class DynamicValue;
+}
+namespace WallpaperEngine::Scripting {
+class ScriptEngine;
+class ScriptableObject;
+}
+
+namespace WallpaperEngine::Scripting::Adapters {
+class ObjectAdapter : public Data::Utils::TypeCaster {
+public:
+    explicit ObjectAdapter (ScriptEngine& engine);
+    ~ObjectAdapter () override = default;
+
+    virtual JSValue instantiate (ScriptableObject& object);
+    virtual JSValue instantiate (Data::Model::DynamicValue& value);
+
+    ScriptEngine& getEngine () const { return m_engine; }
+
+protected:
+    void registerType (const JSClassDef& definition);
+
+    ScriptEngine& m_engine;
+    JSClassID m_classId;
+    JSClassDef m_definition;
+};
+}

+ 90 - 0
src/WallpaperEngine/Scripting/Adapters/ScriptableObjectAdapter.cpp

@@ -0,0 +1,90 @@
+#include "ScriptableObjectAdapter.h"
+
+#include <utility>
+
+#include "WallpaperEngine/Data/Utils/ScopeGuard.h"
+#include "WallpaperEngine/Scripting/ScriptEngine.h"
+#include "WallpaperEngine/Scripting/ScriptableObject.h"
+
+using namespace WallpaperEngine::Data::Model;
+using namespace WallpaperEngine::Data::Utils;
+using namespace WallpaperEngine::Scripting::Adapters;
+
+#define SCRIPTABLE_OPAQUE_MAGIC 0xdeadbeef
+
+struct OpaqueScriptableObjectAdapter {
+    unsigned int magic;
+    ScriptableObjectAdapter& adapter;
+    WallpaperEngine::Scripting::ScriptableObject& object;
+};
+
+JSValue scriptableobject_property_get (JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst receiver) {
+    JSClassID classId = 0;
+
+    auto* container = static_cast<OpaqueScriptableObjectAdapter*> (JS_GetAnyOpaque (obj_val, &classId));
+
+    if (!container || container->magic != SCRIPTABLE_OPAQUE_MAGIC) {
+	return JS_EXCEPTION;
+    }
+
+    const char* name = JS_AtomToCString (ctx, atom);
+
+    if (name == nullptr) {
+	return JS_EXCEPTION;
+    }
+
+    ScopeGuard guard ([=] { JS_FreeCString (ctx, name); });
+
+    try {
+	// find the property inside, otherwise return undefined
+	auto& property = container->object.getProperty (name);
+
+	return container->adapter.getEngine ().dynamicToJs (property);
+    } catch (const std::exception& e) {
+	return JS_UNDEFINED;
+    }
+}
+
+int scriptableobject_property_set (
+    JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst val, JSValueConst receiver, int flags
+) {
+    JSClassID classId = 0;
+
+    auto* container = static_cast<OpaqueScriptableObjectAdapter*> (JS_GetAnyOpaque (obj_val, &classId));
+
+    if (!container || container->magic != SCRIPTABLE_OPAQUE_MAGIC) {
+	return -1;
+    }
+
+    const char* name = JS_AtomToCString (ctx, atom);
+
+    if (name == nullptr) {
+	return -1;
+    }
+
+    return 0;
+}
+
+ScriptableObjectAdapter::ScriptableObjectAdapter (ScriptEngine& engine, std::string name) :
+    ObjectAdapter (engine), m_exoticMethods (), m_name (std::move (name)) {
+    this->registerType (
+	{
+	    .class_name = m_name.c_str (),
+	    .exotic = &m_exoticMethods,
+	}
+    );
+}
+
+JSValue ScriptableObjectAdapter::instantiate (ScriptableObject& object) {
+    JSValue result = this->ObjectAdapter::instantiate (object);
+    JS_SetOpaque (
+	result,
+	new OpaqueScriptableObjectAdapter { .magic = SCRIPTABLE_OPAQUE_MAGIC, .adapter = *this, .object = object }
+    );
+
+    return result;
+}
+
+JSValue ScriptableObjectAdapter::instantiate (DynamicValue& value) {
+    throw std::runtime_error ("Cannot create a ScriptableObject instance from a DynamicValue");
+}

+ 17 - 0
src/WallpaperEngine/Scripting/Adapters/ScriptableObjectAdapter.h

@@ -0,0 +1,17 @@
+#pragma once
+
+#include "ObjectAdapter.h"
+
+namespace WallpaperEngine::Scripting::Adapters {
+class ScriptableObjectAdapter : public ObjectAdapter {
+public:
+    explicit ScriptableObjectAdapter (ScriptEngine& engine, std::string name);
+
+    JSValue instantiate (ScriptableObject& object) override;
+    JSValue instantiate (Data::Model::DynamicValue& value) override;
+
+private:
+    JSClassExoticMethods m_exoticMethods;
+    std::string m_name;
+};
+}

+ 1005 - 0
src/WallpaperEngine/Scripting/Adapters/VectorAdapter.cpp

@@ -0,0 +1,1005 @@
+#include "VectorAdapter.h"
+
+#include "../ScriptEngine.h"
+#include "WallpaperEngine/Data/Utils/SFINAE.h"
+#include "WallpaperEngine/Data/Utils/ScopeGuard.h"
+
+#include <variant>
+
+using namespace WallpaperEngine::Data::Utils;
+using namespace WallpaperEngine::Data::Model;
+using namespace WallpaperEngine::Scripting::Adapters;
+
+static uint32_t VectorInstanceId = 0;
+static uint32_t VectorAdapterInstanceId = 0;
+static constexpr int InvalidVectorInstanceId = 0;
+
+// magic value used to ensure the assigned opaque value we got back is valid
+#define VEC_OPAQUE_MAGIC 0xdeadbee0
+#define VEC_MAGIC_CHECK_EXCEPTION(container, components)                                                               \
+    do {                                                                                                               \
+	if (!container || container->magic != (int)(VEC_OPAQUE_MAGIC + components)) {                                  \
+	    return JS_EXCEPTION;                                                                                       \
+	}                                                                                                              \
+    } while (0)
+#define VEC_MAGIC_CHECK_ERROR(container, components)                                                                   \
+    do {                                                                                                               \
+	if (!container || container->magic != (int)(VEC_OPAQUE_MAGIC + components)) {                                  \
+	    return -1;                                                                                                 \
+	}                                                                                                              \
+    } while (0)
+
+template <int components> std::map<uint32_t, VectorAdapter<components>&> vectorAdapterInstances;
+
+template <int components> struct VectorOpaqueContainer {
+    int magic;
+    VectorAdapter<components>& adapter;
+    DynamicValue& value;
+    uint32_t id;
+};
+
+template <int components> auto vector_new () -> decltype (auto) {
+    static_assert (components >= 2 && components <= 4, "Unsupported vector type");
+
+    if constexpr (components == 2) {
+	return glm::vec2 {};
+    } else if constexpr (components == 3) {
+	return glm::vec3 {};
+    } else if constexpr (components == 4) {
+	return glm::vec4 {};
+    }
+}
+
+template auto vector_new<2> () -> decltype (auto);
+template auto vector_new<3> () -> decltype (auto);
+template auto vector_new<4> () -> decltype (auto);
+
+template <int components> auto vector_new (float value) -> decltype (auto) {
+    static_assert (components >= 2 && components <= 4, "Unsupported vector type");
+
+    if constexpr (components == 2) {
+	return glm::vec2 (value);
+    } else if constexpr (components == 3) {
+	return glm::vec3 (value);
+    } else if constexpr (components == 4) {
+	return glm::vec4 (value);
+    }
+}
+
+template auto vector_new<2> (float value) -> decltype (auto);
+template auto vector_new<3> (float value) -> decltype (auto);
+template auto vector_new<4> (float value) -> decltype (auto);
+
+template <int components> auto vector_get (DynamicValue& value) -> decltype (auto) {
+    static_assert (components >= 2 && components <= 4, "Unsupported vector type");
+
+    if constexpr (components == 2) {
+	return value.getVec2 ();
+    } else if constexpr (components == 3) {
+	return value.getVec3 ();
+    } else if constexpr (components == 4) {
+	return value.getVec4 ();
+    }
+}
+
+template <int components> auto vector_get (JSContext* ctx, JSValue source) -> decltype (auto) {
+    static_assert (components >= 2 && components <= 4, "Unsupported vector type");
+
+    int tag = JS_VALUE_GET_TAG (source);
+
+    if (tag == JS_TAG_INT) {
+	int32_t value = 0;
+
+	JS_ToInt32 (ctx, &value, source);
+
+	return vector_new<components> (value);
+    }
+
+    if (JS_TAG_IS_FLOAT64 (tag)) {
+	double value = 0.0f;
+
+	JS_ToFloat64 (ctx, &value, source);
+
+	return vector_new<components> (static_cast<float> (value));
+    }
+
+    if (tag == JS_TAG_OBJECT) {
+	// check components, extract x, y, z and w and create the appropriate vector
+	JSValue x = JS_GetPropertyStr (ctx, source, "x");
+	JSValue y = JS_GetPropertyStr (ctx, source, "y");
+	JSValue z = JS_GetPropertyStr (ctx, source, "z");
+	JSValue w = JS_GetPropertyStr (ctx, source, "w");
+
+	if (!JS_IsNumber (x) || !JS_IsNumber (y)) {
+	    throw std::runtime_error ("Unsupported type conversion for VectorAdapter");
+	}
+
+	// do not accept bigger vectors
+	if (components <= 2 && JS_IsNumber (z)) {
+	    throw std::runtime_error ("Unsupported type conversion for VectorAdapter");
+	}
+
+	if (components <= 3 && JS_IsNumber (w)) {
+	    throw std::runtime_error ("Unsupported type conversion for VectorAdapter");
+	}
+
+	double xVal = 0.0f, yVal = 0.0f, zVal = 0.0f, wVal = 0.0f;
+
+	JS_ToFloat64 (ctx, &xVal, x);
+	JS_ToFloat64 (ctx, &yVal, y);
+
+	if (JS_IsNumber (z)) {
+	    JS_ToFloat64 (ctx, &zVal, z);
+	}
+
+	if (JS_IsNumber (w)) {
+	    JS_ToFloat64 (ctx, &wVal, w);
+	}
+
+	if constexpr (components == 2) {
+	    return glm::vec2 (xVal, yVal);
+	} else if constexpr (components == 3) {
+	    return glm::vec3 (xVal, yVal, zVal);
+	} else if constexpr (components == 4) {
+	    return glm::vec4 (xVal, yVal, zVal, wVal);
+	}
+    }
+
+    throw std::runtime_error ("Unsupported type conversion for VectorAdapter");
+}
+
+template auto vector_get<2> (JSContext* ctx, JSValue source) -> decltype (auto);
+template auto vector_get<3> (JSContext* ctx, JSValue source) -> decltype (auto);
+template auto vector_get<4> (JSContext* ctx, JSValue source) -> decltype (auto);
+template auto vector_get<2> (DynamicValue& value) -> decltype (auto);
+template auto vector_get<3> (DynamicValue& value) -> decltype (auto);
+template auto vector_get<4> (DynamicValue& value) -> decltype (auto);
+
+template <int components>
+JSValue vector_property_get (JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst receiver) {
+    JSClassID classId = 0;
+
+    auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (obj_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    const char* name = JS_AtomToCString (ctx, atom);
+
+    if (name == nullptr) {
+	return JS_EXCEPTION;
+    }
+
+    ScopeGuard guard ([=] { JS_FreeCString (ctx, name); });
+    const auto value = vector_get<components> (container->value);
+
+    if (strcmp (name, "x") == 0) {
+	return JS_NewFloat64 (ctx, value.x);
+    }
+    if (strcmp (name, "y") == 0) {
+	return JS_NewFloat64 (ctx, value.y);
+    }
+    if constexpr (components >= 3) {
+	if (strcmp (name, "z") == 0) {
+	    return JS_NewFloat64 (ctx, value.z);
+	}
+
+	if constexpr (components >= 4) {
+	    if (strcmp (name, "w") == 0) {
+		return JS_NewFloat64 (ctx, value.w);
+	    }
+	}
+    }
+
+    return JS_EXCEPTION;
+}
+
+template JSValue vector_property_get<2> (JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst receiver);
+template JSValue vector_property_get<3> (JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst receiver);
+template JSValue vector_property_get<4> (JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst receiver);
+
+template <int components>
+int vector_property_set (
+    JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst val, JSValueConst receiver, int flags
+) {
+    JSClassID classId = 0;
+    auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (obj_val, &classId));
+
+    VEC_MAGIC_CHECK_ERROR (container, components);
+
+    int tag = JS_VALUE_GET_TAG (val);
+
+    if (tag != JS_TAG_INT && !JS_TAG_IS_FLOAT64 (tag)) {
+	return -1;
+    }
+
+    const char* name = JS_AtomToCString (ctx, atom);
+
+    if (name == nullptr) {
+	return -1;
+    }
+
+    ScopeGuard guard ([=] { JS_FreeCString (ctx, name); });
+    auto vec = vector_get<components> (container->value);
+    void* into = nullptr;
+
+    if (strcmp (name, "x") == 0) {
+	if constexpr (
+	    std::is_same_v<decltype (vec), glm::vec2> || std::is_same_v<decltype (vec), glm::vec3>
+	    || std::is_same_v<decltype (vec), glm::vec4>
+	) {
+	    into = &vec.x;
+	} else if constexpr (std::is_same_v<decltype (vec), Color>) {
+	    into = &vec.r;
+	}
+    } else if (strcmp (name, "y") == 0) {
+	if constexpr (
+	    std::is_same_v<decltype (vec), glm::vec2> || std::is_same_v<decltype (vec), glm::vec3>
+	    || std::is_same_v<decltype (vec), glm::vec4>
+	) {
+	    into = &vec.y;
+	} else if constexpr (std::is_same_v<decltype (vec), Color>) {
+	    into = &vec.g;
+	}
+    } else if constexpr (components >= 3) {
+	if (strcmp (name, "z") == 0) {
+	    if constexpr (std::is_same_v<decltype (vec), glm::vec3> || std::is_same_v<decltype (vec), glm::vec4>) {
+		into = &vec.z;
+	    } else if constexpr (std::is_same_v<decltype (vec), Color>) {
+		into = &vec.b;
+	    }
+	} else if constexpr (components >= 4) {
+	    if (strcmp (name, "w") == 0) {
+		if constexpr (std::is_same_v<decltype (vec), glm::vec4>) {
+		    into = &vec.w;
+		} else if constexpr (std::is_same_v<decltype (vec), Color>) {
+		    into = &vec.a;
+		}
+	    }
+	}
+    }
+
+    if (into == nullptr) {
+	return -1;
+    }
+
+    double value = 0;
+
+    JS_ToFloat64 (ctx, &value, val);
+
+    *static_cast<float*> (into) = static_cast<float> (value);
+
+    container->value.update (vec, DynamicValue::UpdateSource::Script);
+
+    return 0;
+}
+
+template int vector_property_set<2> (
+    JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst val, JSValueConst receiver, int flags
+);
+template int vector_property_set<3> (
+    JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst val, JSValueConst receiver, int flags
+);
+template int vector_property_set<4> (
+    JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst val, JSValueConst receiver, int flags
+);
+
+template <int components> JSValue vector_copy (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    JSClassID classId = 0;
+
+    auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    // create a new DynamicValue
+    return container->adapter.instantiate (container->value, true);
+}
+
+template JSValue vector_copy<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_copy<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_copy<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+bool vector_value_equals (JSContext* ctx, JSValue left, JSValue right) {
+    if (JS_TAG_IS_FLOAT64 (JS_VALUE_GET_TAG (left)) || JS_TAG_IS_FLOAT64 (JS_VALUE_GET_TAG (right))) {
+	double x1Val = 0.0f, x2Val = 0.0f;
+
+	JS_ToFloat64 (ctx, &x1Val, left);
+	JS_ToFloat64 (ctx, &x2Val, right);
+
+	if (std::abs (x1Val - x2Val) > 0.00001) {
+	    return false;
+	}
+    } else {
+	if (!JS_IsEqual (ctx, left, right)) {
+	    return false;
+	}
+    }
+
+    return true;
+}
+
+template <int components> JSValue vector_equals (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    if (argc == 0) {
+	return JS_FALSE;
+    }
+
+    JSClassID classId = 0;
+
+    auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue other = argv[0];
+    auto* otherContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (other, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (otherContainer, components);
+
+    const auto vector = vector_get<components> (container->value);
+    const auto otherVector = vector_get<components> (otherContainer->value);
+
+    if constexpr (components == 2) {
+	return vector.x == otherVector.x && vector.y == otherVector.y ? JS_TRUE : JS_FALSE;
+    } else if constexpr (components == 3) {
+	return vector.x == otherVector.x && vector.y == otherVector.y && vector.z == otherVector.z ? JS_TRUE : JS_FALSE;
+    } else if constexpr (components == 4) {
+	return vector.x == otherVector.x && vector.y == otherVector.y && vector.z == otherVector.z
+		&& vector.w == otherVector.w
+	    ? JS_TRUE
+	    : JS_FALSE;
+    }
+
+    return JS_FALSE;
+}
+
+template JSValue vector_equals<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_equals<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_equals<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components> JSValue vector_length (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    JSClassID classId = 0;
+
+    auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    return JS_NewFloat64 (ctx, glm::length (vector_get<components> (container->value)));
+}
+
+template JSValue vector_length<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_length<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_length<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components>
+JSValue vector_constructor (JSContext* ctx, JSValueConst new_target, int argc, JSValueConst* argv, int magic) {
+    if (argc == 0) {
+	return JS_EXCEPTION;
+    }
+
+    auto it = vectorAdapterInstances<components>.find (magic);
+
+    if (it == vectorAdapterInstances<components>.end ()) {
+	return JS_EXCEPTION;
+    }
+
+    JSValue result = it->second.instantiate ();
+    JSClassID classId = 0;
+    auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (result, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    container->value.update (vector_get<components> (ctx, argv[0]), DynamicValue::UpdateSource::Initialization);
+
+    return result;
+}
+
+template JSValue vector_constructor<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic);
+template JSValue vector_constructor<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic);
+template JSValue vector_constructor<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic);
+
+template <int components> void vector_finalizer (JSRuntime* rt, JSValueConst val) {
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (val, &classId));
+
+    if (!container || container->magic != (int)(VEC_OPAQUE_MAGIC + components)) {
+	return;
+    }
+
+    // free container and the associated DynamicValue if temporal
+    if (container->id != InvalidVectorInstanceId) {
+	container->adapter.free (container->id);
+    }
+
+    delete container;
+}
+
+template void vector_finalizer<2> (JSRuntime* rt, JSValueConst val);
+template void vector_finalizer<3> (JSRuntime* rt, JSValueConst val);
+template void vector_finalizer<4> (JSRuntime* rt, JSValueConst val);
+
+template <int components>
+JSValue vector_normalize (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate (container->value, true);
+
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    newContainer->value.update (
+	glm::normalize (vector_get<components> (container->value)), DynamicValue::UpdateSource::Script
+    );
+
+    return newVector;
+}
+
+template JSValue vector_normalize<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_normalize<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_normalize<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components> JSValue vector_add (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    if (argc != 1) {
+	return JS_UNDEFINED;
+    }
+
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (
+	vector_get<components> (ctx, argv[0]) + vector_get<components> (container->value),
+	DynamicValue::UpdateSource::Initialization
+    );
+
+    return newVector;
+}
+
+template JSValue vector_add<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_add<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_add<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components>
+JSValue vector_subtract (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    if (argc != 1) {
+	return JS_UNDEFINED;
+    }
+
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (
+	vector_get<components> (ctx, argv[0]) - vector_get<components> (container->value),
+	DynamicValue::UpdateSource::Initialization
+    );
+
+    return newVector;
+}
+
+template JSValue vector_subtract<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_subtract<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_subtract<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components>
+JSValue vector_multiply (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    if (argc != 1) {
+	return JS_UNDEFINED;
+    }
+
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (
+	vector_get<components> (ctx, argv[0]) * vector_get<components> (container->value),
+	DynamicValue::UpdateSource::Initialization
+    );
+
+    return newVector;
+}
+
+template JSValue vector_multiply<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_multiply<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_multiply<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components> JSValue vector_divide (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    if (argc != 1) {
+	return JS_UNDEFINED;
+    }
+
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (
+	vector_get<components> (ctx, argv[0]) / vector_get<components> (container->value),
+	DynamicValue::UpdateSource::Initialization
+    );
+
+    return newVector;
+}
+
+template JSValue vector_divide<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_divide<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_divide<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components> JSValue vector_dot (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    if (argc != 1) {
+	return JS_UNDEFINED;
+    }
+
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (
+	glm::dot (vector_get<components> (ctx, argv[0]), vector_get<components> (container->value)),
+	DynamicValue::UpdateSource::Initialization
+    );
+
+    return newVector;
+}
+
+template JSValue vector_dot<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_dot<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_dot<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components> JSValue vector_cross (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    if (argc != 1) {
+	return JS_UNDEFINED;
+    }
+
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (
+	glm::cross (vector_get<components> (ctx, argv[0]), vector_get<components> (container->value)),
+	DynamicValue::UpdateSource::Initialization
+    );
+
+    return newVector;
+}
+
+template JSValue vector_cross<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components> JSValue vector_mix (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    if (argc != 2) {
+	return JS_EXCEPTION;
+    }
+
+    if (!JS_IsNumber (argv[1])) {
+	return JS_EXCEPTION;
+    }
+
+    double amount = 0.0f;
+
+    JS_ToFloat64 (ctx, &amount, argv[1]);
+
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (
+	glm::mix (vector_get<components> (ctx, argv[0]), vector_get<components> (container->value), amount),
+	DynamicValue::UpdateSource::Initialization
+    );
+
+    return newVector;
+}
+
+template JSValue vector_mix<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_mix<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_mix<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components> JSValue vector_min (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    if (argc != 1) {
+	return JS_UNDEFINED;
+    }
+
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (
+	glm::min (vector_get<components> (ctx, argv[0]), vector_get<components> (container->value)),
+	DynamicValue::UpdateSource::Initialization
+    );
+
+    return newVector;
+}
+
+template JSValue vector_min<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_min<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_min<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components> JSValue vector_max (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    if (argc != 1) {
+	return JS_UNDEFINED;
+    }
+
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (
+	glm::max (vector_get<components> (ctx, argv[0]), vector_get<components> (container->value)),
+	DynamicValue::UpdateSource::Initialization
+    );
+
+    return newVector;
+}
+
+template JSValue vector_max<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_max<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_max<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components> JSValue vector_abs (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (
+	glm::abs (vector_get<components> (container->value)), DynamicValue::UpdateSource::Initialization
+    );
+
+    return newVector;
+}
+
+template JSValue vector_abs<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_abs<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_abs<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components> JSValue vector_sign (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (
+	glm::sign (vector_get<components> (container->value)), DynamicValue::UpdateSource::Initialization
+    );
+
+    return newVector;
+}
+
+template JSValue vector_sign<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_sign<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_sign<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components> JSValue vector_round (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (
+	glm::round (vector_get<components> (container->value)), DynamicValue::UpdateSource::Initialization
+    );
+
+    return newVector;
+}
+
+template JSValue vector_round<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_round<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_round<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components> JSValue vector_floor (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (
+	glm::floor (vector_get<components> (container->value)), DynamicValue::UpdateSource::Initialization
+    );
+
+    return newVector;
+}
+
+template JSValue vector_floor<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_floor<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_floor<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components> JSValue vector_ceil (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    JSValue newVector = container->adapter.instantiate ();
+    const auto* newContainer = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (newVector, &classId));
+    const auto vector = vector_get<components> (container->value);
+
+    VEC_MAGIC_CHECK_EXCEPTION (newContainer, components);
+
+    newContainer->value.update (glm::ceil (vector), DynamicValue::UpdateSource::Initialization);
+
+    return newVector;
+}
+
+template JSValue vector_ceil<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_ceil<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_ceil<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components>
+JSValue vector_toString (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    JSClassID classId = 0;
+    const auto* container = static_cast<VectorOpaqueContainer<components>*> (JS_GetAnyOpaque (this_val, &classId));
+
+    VEC_MAGIC_CHECK_EXCEPTION (container, components);
+
+    return JS_NewString (ctx, container->value.toString ().c_str ());
+}
+
+template JSValue vector_toString<2> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_toString<3> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+template JSValue vector_toString<4> (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv);
+
+template <int components>
+VectorAdapter<components>::VectorAdapter (ScriptEngine& engine) :
+    ObjectAdapter (engine), m_instanceId (++VectorAdapterInstanceId), m_name ("Vec" + std::to_string (components)),
+    m_exoticMethods (
+	{
+	    .get_property = vector_property_get<components>,
+	    .set_property = vector_property_set<components>,
+	}
+    ) {
+    vectorAdapterInstances<components>.emplace (this->m_instanceId, *this);
+    this->registerType (
+	{
+	    .class_name = this->m_name.c_str (),
+	    .finalizer = vector_finalizer<components>,
+	    .exotic = &this->m_exoticMethods,
+	}
+    );
+
+    // build the prototype for the Vector and assign the required methods
+    m_prototype = JS_NewObject (this->m_engine.getContext ());
+
+    JS_DupValue (this->m_engine.getContext (), m_prototype);
+
+    JSValue ctor = JS_NewCFunctionMagic (
+	this->m_engine.getContext (), vector_constructor<components>, this->m_name.c_str (), 1,
+	JS_CFUNC_constructor_magic, this->m_instanceId
+    );
+
+    JS_SetConstructor (this->m_engine.getContext (), ctor, m_prototype);
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "copy",
+	JS_NewCFunction (this->m_engine.getContext (), vector_copy<components>, "copy", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "equals",
+	JS_NewCFunction (this->m_engine.getContext (), vector_equals<components>, "equals", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "length",
+	JS_NewCFunction (this->m_engine.getContext (), vector_length<components>, "length", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "lengthSqr",
+	JS_NewCFunction (this->m_engine.getContext (), vector_length<components>, "lengthSqr", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "normalize",
+	JS_NewCFunction (this->m_engine.getContext (), vector_normalize<components>, "normalize", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "add",
+	JS_NewCFunction (this->m_engine.getContext (), vector_add<components>, "add", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "subtract",
+	JS_NewCFunction (this->m_engine.getContext (), vector_subtract<components>, "subtract", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "multiply",
+	JS_NewCFunction (this->m_engine.getContext (), vector_multiply<components>, "multiply", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "divide",
+	JS_NewCFunction (this->m_engine.getContext (), vector_divide<components>, "divide", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "dot",
+	JS_NewCFunction (this->m_engine.getContext (), vector_dot<components>, "dot", 1), JS_PROP_ENUMERABLE
+    );
+    if constexpr (components == 3) {
+	JS_DefinePropertyValueStr (
+	    this->m_engine.getContext (), m_prototype, "cross",
+	    JS_NewCFunction (this->m_engine.getContext (), vector_cross<components>, "cross", 1), JS_PROP_ENUMERABLE
+	);
+    }
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "mix",
+	JS_NewCFunction (this->m_engine.getContext (), vector_mix<components>, "mix", 2), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "min",
+	JS_NewCFunction (this->m_engine.getContext (), vector_min<components>, "min", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "max",
+	JS_NewCFunction (this->m_engine.getContext (), vector_max<components>, "max", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "abs",
+	JS_NewCFunction (this->m_engine.getContext (), vector_abs<components>, "abs", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "sign",
+	JS_NewCFunction (this->m_engine.getContext (), vector_sign<components>, "sign", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "round",
+	JS_NewCFunction (this->m_engine.getContext (), vector_round<components>, "round", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "floor",
+	JS_NewCFunction (this->m_engine.getContext (), vector_floor<components>, "floor", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "ceil",
+	JS_NewCFunction (this->m_engine.getContext (), vector_ceil<components>, "ceil", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), m_prototype, "toString",
+	JS_NewCFunction (this->m_engine.getContext (), vector_toString<components>, "toString", 0), JS_PROP_ENUMERABLE
+    );
+
+    JS_SetClassProto (this->m_engine.getContext (), this->m_classId, m_prototype);
+    JS_FreeValue (this->m_engine.getContext (), ctor);
+}
+
+template <int components> VectorAdapter<components>::~VectorAdapter () {
+    vectorAdapterInstances<components>.erase (this->m_instanceId);
+
+    JS_FreeValue (this->m_engine.getContext (), m_prototype);
+}
+
+template <int components> JSValue VectorAdapter<components>::instantiate (ScriptableObject& object) {
+    throw new std::runtime_error ("Cannot create a Vector4 instance from a ScriptableObject");
+}
+
+template <int components> JSValue VectorAdapter<components>::instantiate (DynamicValue& value) {
+    JSValue result = this->ObjectAdapter::instantiate (value);
+    JS_SetOpaque (
+	result,
+	new VectorOpaqueContainer<components> {
+	    .magic = VEC_OPAQUE_MAGIC + components,
+	    .adapter = *this,
+	    .value = value,
+	    .id = InvalidVectorInstanceId,
+	}
+    );
+
+    return result;
+}
+
+template <int components> JSValue VectorAdapter<components>::instantiate (DynamicValue& source, bool temporal) {
+    auto value = std::make_unique<DynamicValue> (source);
+    uint32_t id = ++VectorInstanceId;
+    JSValue result = this->ObjectAdapter::instantiate (*value);
+    JS_SetOpaque (
+	result,
+	new VectorOpaqueContainer<components> {
+	    .magic = VEC_OPAQUE_MAGIC + components,
+	    .adapter = *this,
+	    .value = *value,
+	    .id = id,
+	}
+    );
+
+    this->m_values.emplace (id, std::move (value));
+
+    return result;
+}
+
+template <int components> JSValue VectorAdapter<components>::instantiate () {
+    auto value = std::make_unique<DynamicValue> (vector_new<components> ());
+    uint32_t id = ++VectorInstanceId;
+    JSValue result = this->ObjectAdapter::instantiate (*value);
+    JS_SetOpaque (
+	result,
+	new VectorOpaqueContainer<components> {
+	    .magic = VEC_OPAQUE_MAGIC + components,
+	    .adapter = *this,
+	    .value = *value,
+	    .id = id,
+	}
+    );
+
+    this->m_values.emplace (id, std::move (value));
+
+    return result;
+}
+
+template <int components> void VectorAdapter<components>::free (uint32_t vectorId) {
+    auto it = this->m_values.find (vectorId);
+
+    if (it != this->m_values.end ()) {
+	this->m_values.erase (it);
+    }
+}
+
+namespace WallpaperEngine::Scripting::Adapters {
+template class VectorAdapter<2>;
+template class VectorAdapter<3>;
+template class VectorAdapter<4>;
+} // namespace WallpaperEngine::Scripting::Adapters

+ 34 - 0
src/WallpaperEngine/Scripting/Adapters/VectorAdapter.h

@@ -0,0 +1,34 @@
+#pragma once
+
+#include "ObjectAdapter.h"
+#include "WallpaperEngine/Data/Model/Types.h"
+
+namespace WallpaperEngine::Scripting::Adapters {
+template <int components> class VectorAdapter : public ObjectAdapter {
+public:
+    explicit VectorAdapter (ScriptEngine& engine);
+    ~VectorAdapter () override;
+
+    int length () { return components; }
+    JSValue instantiate (Data::Model::DynamicValue& value) override;
+    JSValue instantiate (ScriptableObject& object) override;
+    /**
+     * @return A new, anonymous JSValue representing the vector
+     */
+    JSValue instantiate (Data::Model::DynamicValue& source, bool temporal);
+    JSValue instantiate ();
+
+    void free (uint32_t vectorId);
+
+private:
+    JSValue m_prototype;
+    uint32_t m_instanceId;
+    std::string m_name;
+    JSClassExoticMethods m_exoticMethods;
+    std::map<uint32_t, Data::Model::DynamicValueUniquePtr> m_values;
+};
+
+extern template class VectorAdapter<2>;
+extern template class VectorAdapter<3>;
+extern template class VectorAdapter<4>;
+}

+ 69 - 0
src/WallpaperEngine/Scripting/ConsoleObject.cpp

@@ -0,0 +1,69 @@
+#include "ConsoleObject.h"
+
+#include "EngineObject.h"
+#include "ScriptEngine.h"
+#include "WallpaperEngine/Data/Utils/ScopeGuard.h"
+#include "WallpaperEngine/Render/Wallpapers/CScene.h"
+
+using namespace WallpaperEngine::Scripting;
+
+JSValue console_log (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    if (argc < 1) {
+	return JS_UNDEFINED;
+    }
+
+    std::stringstream stream;
+
+    for (int i = 0; i < argc; i++) {
+	const char* str = JS_ToCString (ctx, argv[i]);
+	ScopeGuard guard ([ctx, str] { JS_FreeCString (ctx, str); });
+
+	stream << str;
+    }
+
+    sLog.out (stream.str ());
+
+    return JS_UNDEFINED;
+}
+
+JSValue console_error (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    if (argc < 1) {
+	return JS_UNDEFINED;
+    }
+
+    std::stringstream stream;
+
+    for (int i = 0; i < argc; i++) {
+	const char* str = JS_ToCString (ctx, argv[i]);
+	ScopeGuard guard ([ctx, str] { JS_FreeCString (ctx, str); });
+
+	stream << str;
+    }
+
+    sLog.error (stream.str ());
+
+    return JS_UNDEFINED;
+}
+
+ConsoleObject::ConsoleObject (ScriptEngine& engine, Render::Wallpapers::CScene& scene) :
+    m_scene (scene), m_engine (engine), m_classId (0) {
+    this->m_definition = { .class_name = "IConsole" };
+    JS_NewClassID (this->m_engine.getRuntime (), &this->m_classId);
+    JS_NewClass (this->m_engine.getRuntime (), this->m_classId, &this->m_definition);
+    this->m_instance = JS_NewObjectClass (this->m_engine.getContext (), this->m_classId);
+
+    JS_DupValue (this->m_engine.getContext (), this->m_instance);
+
+    // set properties
+    JS_SetOpaque (this->m_instance, this);
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_instance, "log",
+	JS_NewCFunction (this->m_engine.getContext (), console_log, "log", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_instance, "error",
+	JS_NewCFunction (this->m_engine.getContext (), console_error, "error", 1), JS_PROP_ENUMERABLE
+    );
+}
+
+ConsoleObject::~ConsoleObject () { JS_FreeValue (this->m_engine.getContext (), this->m_instance); }

+ 25 - 0
src/WallpaperEngine/Scripting/ConsoleObject.h

@@ -0,0 +1,25 @@
+#pragma once
+#include "quickjs.h"
+
+namespace WallpaperEngine::Render::Wallpapers {
+class CScene;
+}
+namespace WallpaperEngine::Scripting {
+class ScriptEngine;
+class ConsoleObject {
+public:
+    ConsoleObject (ScriptEngine& engine, Render::Wallpapers::CScene& scene);
+    ~ConsoleObject ();
+
+    const Render::Wallpapers::CScene& getScene () const { return m_scene; }
+    JSValue getInstance () const { return m_instance; }
+
+protected:
+    Render::Wallpapers::CScene& m_scene;
+    ScriptEngine& m_engine;
+
+    JSClassID m_classId;
+    JSClassDef m_definition;
+    JSValue m_instance;
+};
+}

+ 291 - 0
src/WallpaperEngine/Scripting/EngineObject.cpp

@@ -0,0 +1,291 @@
+#include "EngineObject.h"
+#include "ScriptEngine.h"
+#include "WallpaperEngine/Logging/Log.h"
+
+#include <ranges>
+
+using namespace WallpaperEngine::Scripting;
+
+extern float g_Time;
+extern float g_TimeLast;
+extern float g_Daytime;
+
+static uint32_t EngineInstanceId = 0;
+std::map<uint32_t, EngineObject&> engineInstances;
+
+JSValue engine_set_value (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) { return JS_EXCEPTION; }
+
+JSValue engine_open_user_shortcut (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    return JS_UNDEFINED;
+}
+
+JSValue engine_get_frametime (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    return JS_NewFloat64 (ctx, g_Time - g_TimeLast);
+}
+
+JSValue engine_get_runtime (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    return JS_NewFloat64 (ctx, g_Time);
+}
+
+JSValue engine_get_daytime (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    return JS_NewFloat64 (ctx, g_Daytime);
+}
+
+JSValue engine_stop_interval (
+    JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
+) {
+    if (argc != 1) {
+	return JS_EXCEPTION;
+    }
+
+    const auto it = engineInstances.find (magic);
+
+    if (it == engineInstances.end ()) {
+	return JS_EXCEPTION;
+    }
+
+    int id = 0;
+
+    JS_ToInt32 (ctx, &id, argv[0]);
+
+    it->second.clearInterval (id);
+
+    return JS_UNDEFINED;
+}
+
+JSValue engine_stop_timeout (
+    JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
+) {
+    if (argc != 1) {
+	return JS_EXCEPTION;
+    }
+
+    const auto it = engineInstances.find (magic);
+
+    if (it == engineInstances.end ()) {
+	return JS_EXCEPTION;
+    }
+
+    int id = 0;
+
+    JS_ToInt32 (ctx, &id, argv[0]);
+
+    it->second.clearTimeout (id);
+
+    return JS_UNDEFINED;
+}
+
+JSValue engine_set_interval (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
+    if (argc < 1) {
+	return JS_EXCEPTION;
+    }
+
+    int delay = 0;
+
+    if (argc > 1) {
+	JS_ToInt32 (ctx, &delay, argv[1]);
+    }
+
+    JSValue function = argv[0];
+
+    if (!JS_IsFunction (ctx, function)) {
+	return JS_EXCEPTION;
+    }
+
+    const auto it = engineInstances.find (magic);
+
+    if (it == engineInstances.end ()) {
+	return JS_EXCEPTION;
+    }
+
+    int id = it->second.reserveNextIntervalId (function, delay);
+
+    JSValue args[] = { JS_NewInt32 (ctx, id) };
+
+    return JS_NewCFunctionData (ctx, engine_stop_interval, 2, magic, 1, args);
+}
+
+JSValue engine_set_timeout (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
+    if (argc < 1) {
+	return JS_EXCEPTION;
+    }
+
+    int delay = 0;
+
+    if (argc > 1) {
+	JS_ToInt32 (ctx, &delay, argv[1]);
+    }
+
+    JSValue function = argv[0];
+
+    if (!JS_IsFunction (ctx, function)) {
+	return JS_EXCEPTION;
+    }
+
+    const auto it = engineInstances.find (magic);
+
+    if (it == engineInstances.end ()) {
+	return JS_EXCEPTION;
+    }
+
+    int id = it->second.reserveNextTimeoutId (function, delay);
+
+    JSValue args[] = { JS_NewInt32 (ctx, id) };
+
+    return JS_NewCFunctionData (ctx, engine_stop_timeout, 2, magic, 1, args);
+}
+
+EngineObject::EngineObject (ScriptEngine& engine, Render::Wallpapers::CScene& scene) :
+    m_scene (scene), m_engine (engine), m_instanceId (++EngineInstanceId), m_classId (0) {
+    this->m_definition = { .class_name = "IEngine" };
+    JS_NewClassID (this->m_engine.getRuntime (), &this->m_classId);
+    JS_NewClass (this->m_engine.getRuntime (), this->m_classId, &this->m_definition);
+    this->m_instance = JS_NewObjectClass (this->m_engine.getContext (), this->m_classId);
+
+    JS_DupValue (this->m_engine.getContext (), this->m_instance);
+
+    // set properties
+    JS_SetOpaque (this->m_instance, this);
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "frametime"),
+	JS_NewCFunction (this->m_engine.getContext (), engine_get_frametime, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), engine_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "runtime"),
+	JS_NewCFunction (this->m_engine.getContext (), engine_get_runtime, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), engine_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "timeOfDay"),
+	JS_NewCFunction (this->m_engine.getContext (), engine_get_daytime, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), engine_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_instance, "AUDIO_RESOLUTION_16",
+	JS_NewInt32 (this->m_engine.getContext (), 16), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_instance, "AUDIO_RESOLUTION_32",
+	JS_NewInt32 (this->m_engine.getContext (), 32), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_instance, "AUDIO_RESOLUTION_64",
+	JS_NewInt32 (this->m_engine.getContext (), 64), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_instance, "setInterval",
+	JS_NewCFunctionMagic (
+	    this->m_engine.getContext (), engine_set_interval, "setInterval", 2, JS_CFUNC_generic, this->m_instanceId
+	),
+	JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_instance, "setTimeout",
+	JS_NewCFunctionMagic (
+	    this->m_engine.getContext (), engine_set_timeout, "setTimeout", 2, JS_CFUNC_generic, this->m_instanceId
+	),
+	JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_instance, "openUserShortcut",
+	JS_NewCFunction (this->m_engine.getContext (), engine_open_user_shortcut, "openUserShortcut", 0),
+	JS_PROP_ENUMERABLE
+    );
+    // TODO: ADD THE REST OF THE DEFINITION!
+}
+
+EngineObject::~EngineObject () {
+    // clear all the timeouts and intervals
+    for (const auto& [id, timeout] : this->m_timeouts) {
+	JS_FreeValue (this->m_engine.getContext (), timeout.callback);
+    }
+    for (const auto& [id, interval] : this->m_intervals) {
+	JS_FreeValue (this->m_engine.getContext (), interval.callback);
+    }
+
+    engineInstances.erase (this->m_instanceId);
+    this->m_intervals.clear ();
+    this->m_timeouts.clear ();
+
+    JS_FreeValue (this->m_engine.getContext (), this->m_instance);
+}
+
+uint32_t EngineObject::reserveNextTimeoutId (JSValue function, uint64_t duration) {
+    const auto id = ++this->m_nextTimeoutId;
+
+    this->m_timeouts[id] = Timeout { .callback = function,
+				     .duration = std::chrono::milliseconds (duration),
+				     .next = std::chrono::steady_clock::now () + std::chrono::milliseconds (duration) };
+
+    return id;
+}
+
+uint32_t EngineObject::reserveNextIntervalId (JSValue function, uint64_t duration) {
+    const auto id = ++this->m_nextIntervalId;
+
+    this->m_intervals[id]
+	= Timeout { .callback = function,
+		    .duration = std::chrono::milliseconds (duration),
+		    .next = std::chrono::steady_clock::now () + std::chrono::milliseconds (duration) };
+
+    return id;
+}
+
+void EngineObject::clearInterval (uint32_t id) {
+    const auto it = this->m_intervals.find (id);
+
+    if (it == this->m_intervals.end ()) {
+	return;
+    }
+
+    JS_FreeValue (this->getEngine ().getContext (), it->second.callback);
+
+    this->m_intervals.erase (id);
+}
+
+void EngineObject::clearTimeout (uint32_t id) {
+    const auto it = this->m_timeouts.find (id);
+
+    if (it == this->m_timeouts.end ()) {
+	return;
+    }
+
+    JS_FreeValue (this->getEngine ().getContext (), it->second.callback);
+
+    this->m_timeouts.erase (id);
+}
+
+void EngineObject::tick () {
+    const auto now = std::chrono::steady_clock::now ();
+
+    // check any interval and run them if needed
+    for (auto& timeout : this->m_intervals | std::views::values) {
+	if (timeout.next > now) {
+	    continue;
+	}
+
+	timeout.next = now + timeout.duration;
+
+	JS_Call (this->m_engine.getContext (), timeout.callback, JS_NULL, 0, nullptr);
+    }
+
+    std::vector<uint32_t> removeTimeouts;
+
+    // check any timeout and run them if needed
+    for (auto& [id, timeout] : this->m_timeouts) {
+	if (timeout.next > now) {
+	    continue;
+	}
+
+	JS_Call (this->m_engine.getContext (), timeout.callback, JS_NULL, 0, nullptr);
+
+	JS_FreeValue (this->m_engine.getContext (), timeout.callback);
+
+	removeTimeouts.push_back (id);
+    }
+
+    for (auto id : removeTimeouts) {
+	this->m_timeouts.erase (id);
+    }
+}

+ 47 - 0
src/WallpaperEngine/Scripting/EngineObject.h

@@ -0,0 +1,47 @@
+#pragma once
+#include "quickjs.h"
+
+#include <chrono>
+#include <map>
+
+namespace WallpaperEngine::Render::Wallpapers {
+class CScene;
+}
+namespace WallpaperEngine::Scripting {
+class ScriptEngine;
+class EngineObject {
+public:
+    EngineObject (ScriptEngine& engine, Render::Wallpapers::CScene& scene);
+    ~EngineObject ();
+
+    const Render::Wallpapers::CScene& getScene () const { return m_scene; }
+    JSValue getInstance () const { return m_instance; }
+    ScriptEngine& getEngine () const { return m_engine; }
+    uint32_t getInstanceId () const { return m_instanceId; }
+    uint32_t reserveNextTimeoutId (JSValue function, uint64_t duration);
+    uint32_t reserveNextIntervalId (JSValue function, uint64_t duration);
+    void clearTimeout (uint32_t id);
+    void clearInterval (uint32_t id);
+
+    void tick ();
+
+protected:
+    struct Timeout {
+	JSValue callback;
+	std::chrono::milliseconds duration;
+	std::chrono::steady_clock::time_point next;
+    };
+
+    uint32_t m_nextTimeoutId = 0;
+    uint32_t m_nextIntervalId = 0;
+    std::map<uint32_t, Timeout> m_intervals;
+    std::map<uint32_t, Timeout> m_timeouts;
+    Render::Wallpapers::CScene& m_scene;
+    ScriptEngine& m_engine;
+
+    uint32_t m_instanceId;
+    JSClassID m_classId;
+    JSClassDef m_definition;
+    JSValue m_instance;
+};
+}

+ 66 - 0
src/WallpaperEngine/Scripting/InputObject.cpp

@@ -0,0 +1,66 @@
+#include "InputObject.h"
+
+#include "EngineObject.h"
+#include "ScriptEngine.h"
+#include "WallpaperEngine/Render/Wallpapers/CScene.h"
+
+using namespace WallpaperEngine::Scripting;
+
+JSValue get_cursor_world_position (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    JSClassID classId;
+    auto* input = static_cast<InputObject*> (JS_GetAnyOpaque (this_val, &classId));
+
+    // TODO: PROPERLY IMPLEMENT THIS
+    return input->getScene ().getScriptEngine ().getAdapters ().vec3->instantiate ();
+}
+
+JSValue get_cursor_screen_position (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    JSClassID classId;
+    auto* input = static_cast<InputObject*> (JS_GetAnyOpaque (this_val, &classId));
+    auto position = input->getScene ().getMousePositionNormalized ();
+
+    JSValue result = input->getScene ().getScriptEngine ().getAdapters ().vec2->instantiate ();
+
+    JS_SetPropertyStr (ctx, result, "x", JS_NewFloat64 (ctx, position->x));
+    JS_SetPropertyStr (ctx, result, "y", JS_NewFloat64 (ctx, position->y));
+
+    return result;
+}
+
+JSValue get_cursor_left_down (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    // TODO: IMPLEMENT THIS
+    return JS_NewBool (ctx, false);
+}
+
+JSValue input_set_value (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) { return JS_EXCEPTION; }
+
+InputObject::InputObject (ScriptEngine& engine, Render::Wallpapers::CScene& scene) :
+    m_scene (scene), m_engine (engine), m_classId (0) {
+    this->m_definition = { .class_name = "IInput" };
+    JS_NewClassID (this->m_engine.getRuntime (), &this->m_classId);
+    JS_NewClass (this->m_engine.getRuntime (), this->m_classId, &this->m_definition);
+    this->m_instance = JS_NewObjectClass (this->m_engine.getContext (), this->m_classId);
+
+    JS_DupValue (this->m_engine.getContext (), this->m_instance);
+
+    // set properties
+    JS_SetOpaque (this->m_instance, this);
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance,
+	JS_NewAtom (this->m_engine.getContext (), "cursorWorldPosition"),
+	JS_NewCFunction (this->m_engine.getContext (), get_cursor_world_position, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), input_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance,
+	JS_NewAtom (this->m_engine.getContext (), "cursorScreenPosition"),
+	JS_NewCFunction (this->m_engine.getContext (), get_cursor_screen_position, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), input_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "cursorLeftDown"),
+	JS_NewCFunction (this->m_engine.getContext (), get_cursor_left_down, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), input_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+}
+InputObject::~InputObject () { JS_FreeValue (this->m_engine.getContext (), this->m_instance); }

+ 25 - 0
src/WallpaperEngine/Scripting/InputObject.h

@@ -0,0 +1,25 @@
+#pragma once
+#include "quickjs.h"
+
+namespace WallpaperEngine::Render::Wallpapers {
+class CScene;
+}
+namespace WallpaperEngine::Scripting {
+class ScriptEngine;
+class InputObject {
+public:
+    InputObject (ScriptEngine& engine, Render::Wallpapers::CScene& scene);
+    ~InputObject ();
+
+    const Render::Wallpapers::CScene& getScene () const { return m_scene; }
+    JSValue getInstance () const { return m_instance; }
+
+protected:
+    Render::Wallpapers::CScene& m_scene;
+    ScriptEngine& m_engine;
+
+    JSClassID m_classId;
+    JSClassDef m_definition;
+    JSValue m_instance;
+};
+}

+ 266 - 0
src/WallpaperEngine/Scripting/Modules/ColorModule.cpp

@@ -0,0 +1,266 @@
+#include "ColorModule.h"
+
+#include "WallpaperEngine/Scripting/ScriptEngine.h"
+
+using namespace WallpaperEngine::Scripting::Modules;
+
+#define min_f(a, b, c) (fminf (a, fminf (b, c)))
+#define max_f(a, b, c) (fmaxf (a, fmaxf (b, c)))
+
+static uint32_t ColorModuleInstanceId = 0;
+std::map<uint32_t, ColorModule&> colorModules;
+
+int wecolor_init (JSContext* ctx, JSModuleDef* m) {
+
+    JS_AddModuleExport (ctx, m, "rgb2hsv");
+    JS_AddModuleExport (ctx, m, "hsv2rgb");
+    JS_AddModuleExport (ctx, m, "normalizeColor");
+    JS_AddModuleExport (ctx, m, "expandColor");
+
+    return 0;
+}
+
+JSValue wecolor_rgb2hsv (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
+    if (argc != 1) {
+	return JS_EXCEPTION;
+    }
+
+    if (JS_VALUE_GET_TAG (argv[0]) != JS_TAG_OBJECT) {
+	return JS_EXCEPTION;
+    }
+
+    JSValue x = JS_GetPropertyStr (ctx, argv[0], "x");
+    JSValue y = JS_GetPropertyStr (ctx, argv[0], "y");
+    JSValue z = JS_GetPropertyStr (ctx, argv[0], "z");
+
+    double xVal = 0.0f, yVal = 0.0f, zVal = 0.0f;
+
+    JS_ToFloat64 (ctx, &xVal, x);
+    JS_ToFloat64 (ctx, &yVal, y);
+    JS_ToFloat64 (ctx, &zVal, z);
+
+    // conversion code from https://gist.github.com/yoggy/8999625
+    float h, s, v; // h:0-360.0, s:0.0-1.0, v:0.0-1.0
+
+    float max = max_f (xVal, yVal, zVal);
+    float min = min_f (xVal, yVal, zVal);
+
+    v = max;
+
+    if (max == 0.0f) {
+	s = 0;
+	h = 0;
+    } else if (max - min == 0.0f) {
+	s = 0;
+	h = 0;
+    } else {
+	s = (max - min) / max;
+
+	if (max == xVal) {
+	    h = 60 * ((yVal - zVal) / (max - min)) + 0;
+	} else if (max == yVal) {
+	    h = 60 * ((zVal - xVal) / (max - min)) + 120;
+	} else {
+	    h = 60 * ((xVal - yVal) / (max - min)) + 240;
+	}
+    }
+
+    if (h < 0) {
+	h += 360.0f;
+    }
+
+    const auto it = colorModules.find (magic);
+
+    if (it == colorModules.end ()) {
+	return JS_UNDEFINED;
+    }
+
+    JSValue value = it->second.getEngine ().getAdapters ().vec3->instantiate ();
+
+    JS_SetPropertyStr (ctx, value, "x", JS_NewFloat64 (ctx, h));
+    JS_SetPropertyStr (ctx, value, "y", JS_NewFloat64 (ctx, s));
+    JS_SetPropertyStr (ctx, value, "z", JS_NewFloat64 (ctx, v));
+
+    return value;
+}
+
+JSValue wecolor_hsv2rgb (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
+    if (argc != 1) {
+	return JS_EXCEPTION;
+    }
+
+    if (JS_VALUE_GET_TAG (argv[0]) != JS_TAG_OBJECT) {
+	return JS_EXCEPTION;
+    }
+
+    JSValue x = JS_GetPropertyStr (ctx, argv[0], "x");
+    JSValue y = JS_GetPropertyStr (ctx, argv[0], "y");
+    JSValue z = JS_GetPropertyStr (ctx, argv[0], "z");
+
+    double xVal = 0.0f, yVal = 0.0f, zVal = 0.0f;
+
+    JS_ToFloat64 (ctx, &xVal, x);
+    JS_ToFloat64 (ctx, &yVal, y);
+    JS_ToFloat64 (ctx, &zVal, z);
+
+    // conversion code from https://gist.github.com/yoggy/8999625
+    float r, g, b; // 0.0-1.0
+
+    int hi = (int)(xVal / 60.0f) % 6;
+    float f = (xVal / 60.0f) - hi;
+    float p = zVal * (1.0f - yVal);
+    float q = zVal * (1.0f - yVal * f);
+    float t = zVal * (1.0f - yVal * (1.0f - f));
+
+    switch (hi) {
+	case 0:
+	    r = zVal, g = t, b = p;
+	    break;
+	case 1:
+	    r = q, g = zVal, b = p;
+	    break;
+	case 2:
+	    r = p, g = zVal, b = t;
+	    break;
+	case 3:
+	    r = p, g = q, b = zVal;
+	    break;
+	case 4:
+	    r = t, g = p, b = zVal;
+	    break;
+	case 5:
+	    r = zVal, g = p, b = q;
+	    break;
+	default:
+	    r = 0, g = 0, b = 0;
+	    break;
+    }
+
+    const auto it = colorModules.find (magic);
+
+    if (it == colorModules.end ()) {
+	return JS_UNDEFINED;
+    }
+
+    JSValue value = it->second.getEngine ().getAdapters ().vec3->instantiate ();
+
+    JS_SetPropertyStr (ctx, value, "x", JS_NewFloat64 (ctx, r));
+    JS_SetPropertyStr (ctx, value, "y", JS_NewFloat64 (ctx, g));
+    JS_SetPropertyStr (ctx, value, "z", JS_NewFloat64 (ctx, b));
+
+    return value;
+}
+
+JSValue wecolor_normalizecolor (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
+    if (argc != 1) {
+	return JS_EXCEPTION;
+    }
+
+    if (JS_VALUE_GET_TAG (argv[0]) != JS_TAG_OBJECT) {
+	return JS_EXCEPTION;
+    }
+
+    const auto it = colorModules.find (magic);
+
+    if (it == colorModules.end ()) {
+	return JS_UNDEFINED;
+    }
+
+    JSValue x = JS_GetPropertyStr (ctx, argv[0], "x");
+    JSValue y = JS_GetPropertyStr (ctx, argv[0], "y");
+    JSValue z = JS_GetPropertyStr (ctx, argv[0], "z");
+
+    if (!JS_IsNumber (x) || !JS_IsNumber (y) || !JS_IsNumber (z)) {
+	return JS_EXCEPTION;
+    }
+
+    double xVal = 0.0f, yVal = 0.0f, zVal = 0.0f;
+
+    JS_ToFloat64 (ctx, &xVal, x);
+    JS_ToFloat64 (ctx, &yVal, y);
+    JS_ToFloat64 (ctx, &zVal, z);
+
+    JSValue value = it->second.getEngine ().getAdapters ().vec3->instantiate ();
+
+    JS_SetPropertyStr (ctx, value, "x", JS_NewFloat64 (ctx, xVal / 255.0f));
+    JS_SetPropertyStr (ctx, value, "y", JS_NewFloat64 (ctx, yVal / 255.0f));
+    JS_SetPropertyStr (ctx, value, "z", JS_NewFloat64 (ctx, zVal / 255.0f));
+
+    return value;
+}
+
+JSValue wecolor_expandcolor (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
+    if (argc != 1) {
+	return JS_EXCEPTION;
+    }
+
+    if (JS_VALUE_GET_TAG (argv[0]) != JS_TAG_OBJECT) {
+	return JS_EXCEPTION;
+    }
+
+    const auto it = colorModules.find (magic);
+
+    if (it == colorModules.end ()) {
+	return JS_UNDEFINED;
+    }
+
+    JSValue x = JS_GetPropertyStr (ctx, argv[0], "x");
+    JSValue y = JS_GetPropertyStr (ctx, argv[0], "y");
+    JSValue z = JS_GetPropertyStr (ctx, argv[0], "z");
+
+    if (!JS_IsNumber (x) || !JS_IsNumber (y) || !JS_IsNumber (z)) {
+	return JS_EXCEPTION;
+    }
+
+    double xVal = 0.0f, yVal = 0.0f, zVal = 0.0f;
+
+    JS_ToFloat64 (ctx, &xVal, x);
+    JS_ToFloat64 (ctx, &yVal, y);
+    JS_ToFloat64 (ctx, &zVal, z);
+
+    JSValue value = it->second.getEngine ().getAdapters ().vec3->instantiate ();
+
+    JS_SetPropertyStr (ctx, value, "x", JS_NewFloat64 (ctx, xVal * 255.0f));
+    JS_SetPropertyStr (ctx, value, "y", JS_NewFloat64 (ctx, yVal * 255.0f));
+    JS_SetPropertyStr (ctx, value, "z", JS_NewFloat64 (ctx, zVal * 255.0f));
+
+    return value;
+}
+
+ColorModule::ColorModule (ScriptEngine& engine) : ScriptModule (engine, "WEColor", wecolor_init) {
+    this->m_instanceId = ++ColorModuleInstanceId;
+
+    JS_SetModuleExport (
+	this->getEngine ().getContext (), this->getDefinition (), "rgb2hsv",
+	JS_NewCFunctionMagic (
+	    this->getEngine ().getContext (), wecolor_rgb2hsv, "rgb2hsv", 1, JS_CFUNC_generic_magic, this->m_instanceId
+	)
+    );
+
+    JS_SetModuleExport (
+	this->getEngine ().getContext (), this->getDefinition (), "hsv2rgb",
+	JS_NewCFunctionMagic (
+	    this->getEngine ().getContext (), wecolor_hsv2rgb, "hsv2rgb", 1, JS_CFUNC_generic_magic, this->m_instanceId
+	)
+    );
+
+    JS_SetModuleExport (
+	this->getEngine ().getContext (), this->getDefinition (), "normalizeColor",
+	JS_NewCFunctionMagic (
+	    this->getEngine ().getContext (), wecolor_normalizecolor, "normalizeColor", 1, JS_CFUNC_generic_magic,
+	    this->m_instanceId
+	)
+    );
+
+    JS_SetModuleExport (
+	this->getEngine ().getContext (), this->getDefinition (), "expandColor",
+	JS_NewCFunctionMagic (
+	    this->getEngine ().getContext (), wecolor_expandcolor, "expandColor", 1, JS_CFUNC_generic_magic,
+	    this->m_instanceId
+	)
+    );
+
+    colorModules.emplace (this->m_instanceId, *this);
+}
+
+ColorModule::~ColorModule () { colorModules.erase (this->m_instanceId); }

+ 16 - 0
src/WallpaperEngine/Scripting/Modules/ColorModule.h

@@ -0,0 +1,16 @@
+#pragma once
+#include "ScriptModule.h"
+
+namespace WallpaperEngine::Scripting {
+class ScriptEngine;
+}
+namespace WallpaperEngine::Scripting::Modules {
+class ColorModule : public ScriptModule {
+public:
+    ColorModule (ScriptEngine& engine);
+    ~ColorModule () override;
+
+protected:
+    uint32_t m_instanceId;
+};
+}

+ 94 - 0
src/WallpaperEngine/Scripting/Modules/MathModule.cpp

@@ -0,0 +1,94 @@
+#include "MathModule.h"
+
+#include "WallpaperEngine/Scripting/ScriptEngine.h"
+
+using namespace WallpaperEngine::Scripting::Modules;
+
+#define min_f(a, b, c) (fminf (a, fminf (b, c)))
+#define max_f(a, b, c) (fmaxf (a, fmaxf (b, c)))
+
+static uint32_t MathModuleInstanceId = 0;
+std::map<uint32_t, MathModule&> mathModules;
+
+int wemath_init (JSContext* ctx, JSModuleDef* m) {
+
+    JS_AddModuleExport (ctx, m, "smoothStep");
+    JS_AddModuleExport (ctx, m, "mix");
+    JS_AddModuleExport (ctx, m, "deg2rad");
+    JS_AddModuleExport (ctx, m, "rad2deg");
+
+    return 0;
+}
+
+JSValue wemath_smoothstep (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
+    if (argc != 3) {
+	return JS_EXCEPTION;
+    }
+
+    if (!JS_IsNumber (argv[0]) || !JS_IsNumber (argv[1]) || !JS_IsNumber (argv[2])) {
+	return JS_EXCEPTION;
+    }
+
+    double edge0 = 0.0f;
+    double edge1 = 1.0f;
+    double x = 0.0f;
+
+    JS_ToFloat64 (ctx, &edge0, argv[0]);
+    JS_ToFloat64 (ctx, &edge1, argv[1]);
+    JS_ToFloat64 (ctx, &x, argv[2]);
+
+    return JS_NewFloat64 (ctx, glm::smoothstep (edge0, edge1, x));
+}
+
+JSValue wemath_mix (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
+    if (argc != 3) {
+	return JS_EXCEPTION;
+    }
+
+    if (!JS_IsNumber (argv[0]) || !JS_IsNumber (argv[1]) || !JS_IsNumber (argv[2])) {
+	return JS_EXCEPTION;
+    }
+
+    double a = 0.0f;
+    double b = 1.0f;
+    double value = 0.0f;
+
+    JS_ToFloat64 (ctx, &a, argv[0]);
+    JS_ToFloat64 (ctx, &b, argv[1]);
+    JS_ToFloat64 (ctx, &value, argv[2]);
+
+    return JS_NewFloat64 (ctx, glm::mix (a, b, value));
+}
+
+MathModule::MathModule (ScriptEngine& engine) : ScriptModule (engine, "WEMath", wemath_init) {
+    this->m_instanceId = ++MathModuleInstanceId;
+
+    JS_SetModuleExport (
+	this->getEngine ().getContext (), this->getDefinition (), "smoothStep",
+	JS_NewCFunctionMagic (
+	    this->getEngine ().getContext (), wemath_smoothstep, "smoothStep", 3, JS_CFUNC_generic_magic,
+	    this->m_instanceId
+	)
+    );
+
+    JS_SetModuleExport (
+	this->getEngine ().getContext (), this->getDefinition (), "mix",
+	JS_NewCFunctionMagic (
+	    this->getEngine ().getContext (), wemath_mix, "mix", 1, JS_CFUNC_generic_magic, this->m_instanceId
+	)
+    );
+
+    JS_SetModuleExport (
+	this->getEngine ().getContext (), this->getDefinition (), "deg2rad",
+	JS_NewFloat64 (this->getEngine ().getContext (), 0.01745329251994329576923690768489)
+    );
+
+    JS_SetModuleExport (
+	this->getEngine ().getContext (), this->getDefinition (), "rad2deg",
+	JS_NewFloat64 (this->getEngine ().getContext (), 57.295779513082320876798154814105)
+    );
+
+    mathModules.emplace (this->m_instanceId, *this);
+}
+
+MathModule::~MathModule () { mathModules.erase (this->m_instanceId); }

+ 16 - 0
src/WallpaperEngine/Scripting/Modules/MathModule.h

@@ -0,0 +1,16 @@
+#pragma once
+#include "ScriptModule.h"
+
+namespace WallpaperEngine::Scripting {
+class ScriptEngine;
+}
+namespace WallpaperEngine::Scripting::Modules {
+class MathModule : public ScriptModule {
+public:
+    MathModule (ScriptEngine& engine);
+    ~MathModule () override;
+
+protected:
+    uint32_t m_instanceId;
+};
+}

+ 12 - 0
src/WallpaperEngine/Scripting/Modules/ScriptModule.cpp

@@ -0,0 +1,12 @@
+#include "ScriptModule.h"
+
+#include "WallpaperEngine/Scripting/ScriptEngine.h"
+
+using namespace WallpaperEngine::Scripting::Modules;
+
+ScriptModule::ScriptModule (ScriptEngine& engine, const std::string& name, JSModuleInitFunc* init) :
+    m_name (name), m_engine (engine) {
+    this->m_definition = JS_NewCModule (this->m_engine.getContext (), this->m_name.c_str (), init);
+}
+
+ScriptModule::~ScriptModule () { }

+ 26 - 0
src/WallpaperEngine/Scripting/Modules/ScriptModule.h

@@ -0,0 +1,26 @@
+#pragma once
+
+#include "quickjs.h"
+
+#include <string>
+
+namespace WallpaperEngine::Scripting {
+class ScriptEngine;
+}
+namespace WallpaperEngine::Scripting::Modules {
+class ScriptModule {
+public:
+    virtual ~ScriptModule ();
+
+    ScriptEngine& getEngine () const { return m_engine; }
+    const std::string& getName () const { return m_name; }
+    JSModuleDef* getDefinition () const { return m_definition; }
+
+protected:
+    ScriptModule (ScriptEngine& engine, const std::string& name, JSModuleInitFunc* init);
+
+    const std::string m_name;
+    ScriptEngine& m_engine;
+    JSModuleDef* m_definition;
+};
+}

+ 310 - 0
src/WallpaperEngine/Scripting/SceneObject.cpp

@@ -0,0 +1,310 @@
+#include "SceneObject.h"
+
+#include "Adapters/ScriptableObjectAdapter.h"
+#include "ScriptEngine.h"
+#include "ScriptableObject.h"
+#include "WallpaperEngine/Data/Utils/ScopeGuard.h"
+#include "WallpaperEngine/Render/Wallpapers/CScene.h"
+
+using namespace WallpaperEngine::Scripting;
+
+SceneObject* get_opaque (JSValueConst this_val) {
+    JSClassID classId;
+    return static_cast<SceneObject*> (JS_GetAnyOpaque (this_val, &classId));
+}
+
+JSValue get_bloom (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewBool (ctx, container->getScene ().getScene ().camera.bloom.enabled->value->getBool ());
+}
+
+JSValue get_bloomstrength (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewInt32 (ctx, container->getScene ().getScene ().camera.bloom.strength->value->getInt ());
+}
+
+JSValue get_bloomthreshold (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewInt32 (ctx, container->getScene ().getScene ().camera.bloom.threshold->value->getInt ());
+}
+
+JSValue get_clearenabled (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewBool (ctx, container->getScene ().getScene ().camera.bloom.enabled->value->getBool ());
+}
+
+JSValue get_clearcolor (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return container->getEngine ().getAdapters ().vec3->instantiate (
+	*container->getScene ().getScene ().colors.clear->value
+    );
+}
+
+JSValue get_ambientcolor (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return container->getEngine ().getAdapters ().vec3->instantiate (
+	*container->getScene ().getScene ().colors.ambient->value
+    );
+}
+
+JSValue get_skylightcolor (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return container->getEngine ().getAdapters ().vec3->instantiate (
+	*container->getScene ().getScene ().colors.ambient->value
+    );
+}
+
+JSValue get_fov (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewFloat64 (ctx, container->getScene ().getScene ().camera.projection.fov->value->getFloat ());
+}
+
+JSValue get_nearz (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewFloat64 (ctx, container->getScene ().getScene ().camera.projection.nearz->value->getFloat ());
+}
+
+JSValue get_farz (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewFloat64 (ctx, container->getScene ().getScene ().camera.projection.farz->value->getFloat ());
+}
+
+JSValue get_camerafade (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewBool (ctx, container->getScene ().getScene ().camera.fade->value->getBool ());
+}
+
+JSValue get_camerashake (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewBool (ctx, container->getScene ().getScene ().camera.shake.enabled->value->getBool ());
+}
+
+JSValue get_camerashakespeed (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewFloat64 (ctx, container->getScene ().getScene ().camera.shake.speed->value->getFloat ());
+}
+
+JSValue get_camerashakeamplitude (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewFloat64 (ctx, container->getScene ().getScene ().camera.shake.amplitude->value->getFloat ());
+}
+
+JSValue get_camerashakeroughness (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewFloat64 (ctx, container->getScene ().getScene ().camera.shake.roughness->value->getFloat ());
+}
+
+JSValue get_cameraparallax (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewBool (ctx, container->getScene ().getScene ().camera.parallax.enabled->value->getBool ());
+}
+
+JSValue get_cameraparallaxamount (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewFloat64 (ctx, container->getScene ().getScene ().camera.parallax.amount->value->getFloat ());
+}
+
+JSValue get_cameraparallaxdelay (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewFloat64 (ctx, container->getScene ().getScene ().camera.parallax.delay->value->getFloat ());
+}
+
+JSValue get_cameraparallaxmouseinfluence (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    auto* container = get_opaque (this_val);
+
+    return JS_NewFloat64 (ctx, container->getScene ().getScene ().camera.parallax.mouseInfluence->value->getFloat ());
+}
+
+JSValue get_layer (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    if (argc != 1) {
+	return JS_EXCEPTION;
+    }
+
+    auto* container = get_opaque (this_val);
+
+    JSValue layer = argv[0];
+
+    if (JS_IsNumber (layer)) {
+	int id = 0;
+
+	JS_ToInt32 (ctx, &id, layer);
+
+	auto* object = container->getScene ().getObject (id);
+
+	if (object == nullptr) {
+	    return JS_UNDEFINED;
+	}
+
+	if (!object->is<ScriptableObject> ()) {
+	    return JS_UNDEFINED;
+	}
+
+	// TODO: REMOVE THIS CONST_CAST?
+	return container->getEngine ().getAdapters ().object->instantiate (
+	    const_cast<ScriptableObject&> (*object->as<ScriptableObject> ())
+	);
+    } else if (JS_IsString (layer)) {
+	// find by name, this is harder
+	const char* result = JS_ToCString (ctx, layer);
+
+	if (result == nullptr) {
+	    return JS_UNDEFINED;
+	}
+
+	ScopeGuard guard ([=] { JS_FreeCString (ctx, result); });
+
+	for (auto object : container->getScene ().getObjectsByRenderOrder ()) {
+	    if (object->getObject ().name != result) {
+		continue;
+	    }
+
+	    if (!object->is<ScriptableObject> ()) {
+		continue;
+	    }
+
+	    return container->getEngine ().getAdapters ().object->instantiate (*object->as<ScriptableObject> ());
+	}
+    }
+
+    return JS_EXCEPTION;
+}
+
+JSValue scene_set_value (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) { return JS_EXCEPTION; }
+
+SceneObject::SceneObject (ScriptEngine& engine, Render::Wallpapers::CScene& scene) :
+    m_scene (scene), m_engine (engine), m_classId (0) {
+    this->m_definition = { .class_name = "IScene" };
+    JS_NewClassID (this->m_engine.getRuntime (), &this->m_classId);
+    JS_NewClass (this->m_engine.getRuntime (), this->m_classId, &this->m_definition);
+    this->m_instance = JS_NewObjectClass (this->m_engine.getContext (), this->m_classId);
+
+    JS_DupValue (this->m_engine.getContext (), this->m_instance);
+
+    // set properties
+    JS_SetOpaque (this->m_instance, this);
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "bloom"),
+	JS_NewCFunction (this->m_engine.getContext (), get_bloom, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "bloomstrength"),
+	JS_NewCFunction (this->m_engine.getContext (), get_bloomstrength, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "bloomthreshold"),
+	JS_NewCFunction (this->m_engine.getContext (), get_bloomthreshold, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "clearenabled"),
+	JS_NewCFunction (this->m_engine.getContext (), get_clearenabled, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "clearcolor"),
+	JS_NewCFunction (this->m_engine.getContext (), get_clearcolor, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "ambientcolor"),
+	JS_NewCFunction (this->m_engine.getContext (), get_ambientcolor, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "skylightcolor"),
+	JS_NewCFunction (this->m_engine.getContext (), get_skylightcolor, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "fov"),
+	JS_NewCFunction (this->m_engine.getContext (), get_fov, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "nearz"),
+	JS_NewCFunction (this->m_engine.getContext (), get_nearz, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "farz"),
+	JS_NewCFunction (this->m_engine.getContext (), get_farz, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "camerafade"),
+	JS_NewCFunction (this->m_engine.getContext (), get_camerafade, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "camerashake"),
+	JS_NewCFunction (this->m_engine.getContext (), get_camerashake, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "camerashakespeed"),
+	JS_NewCFunction (this->m_engine.getContext (), get_camerashakespeed, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance,
+	JS_NewAtom (this->m_engine.getContext (), "camerashakeamplitude"),
+	JS_NewCFunction (this->m_engine.getContext (), get_camerashakeamplitude, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance,
+	JS_NewAtom (this->m_engine.getContext (), "camerashakeroughness"),
+	JS_NewCFunction (this->m_engine.getContext (), get_camerashakeroughness, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "cameraparallax"),
+	JS_NewCFunction (this->m_engine.getContext (), get_cameraparallax, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance,
+	JS_NewAtom (this->m_engine.getContext (), "cameraparallaxamount"),
+	JS_NewCFunction (this->m_engine.getContext (), get_cameraparallaxamount, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance,
+	JS_NewAtom (this->m_engine.getContext (), "cameraparallaxdelay"),
+	JS_NewCFunction (this->m_engine.getContext (), get_cameraparallaxdelay, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyGetSet (
+	this->m_engine.getContext (), this->m_instance,
+	JS_NewAtom (this->m_engine.getContext (), "cameraparallaxmouseinfluence"),
+	JS_NewCFunction (this->m_engine.getContext (), get_cameraparallaxmouseinfluence, "get", 0),
+	JS_NewCFunction (this->m_engine.getContext (), scene_set_value, "set", 1), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_instance, "getLayer",
+	JS_NewCFunction (this->m_engine.getContext (), get_layer, "getLayer", 1), JS_PROP_ENUMERABLE
+    );
+    // TODO: ADD REST OF THE METHODS
+}
+
+SceneObject::~SceneObject () { JS_FreeValue (this->m_engine.getContext (), this->m_instance); }

+ 26 - 0
src/WallpaperEngine/Scripting/SceneObject.h

@@ -0,0 +1,26 @@
+#pragma once
+#include "quickjs.h"
+
+namespace WallpaperEngine::Render::Wallpapers {
+class CScene;
+}
+namespace WallpaperEngine::Scripting {
+class ScriptEngine;
+class SceneObject {
+public:
+    SceneObject (ScriptEngine& engine, Render::Wallpapers::CScene& scene);
+    ~SceneObject ();
+
+    const Render::Wallpapers::CScene& getScene () const { return m_scene; }
+    JSValue getInstance () const { return m_instance; }
+    ScriptEngine& getEngine () const { return m_engine; }
+
+private:
+    Render::Wallpapers::CScene& m_scene;
+    ScriptEngine& m_engine;
+
+    JSClassID m_classId;
+    JSClassDef m_definition;
+    JSValue m_instance;
+};
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 194 - 813
src/WallpaperEngine/Scripting/ScriptEngine.cpp


+ 56 - 40
src/WallpaperEngine/Scripting/ScriptEngine.h

@@ -1,5 +1,12 @@
 #pragma once
 
+#include "Adapters/VectorAdapter.h"
+#include "ConsoleObject.h"
+#include "EngineObject.h"
+#include "InputObject.h"
+#include "Modules/ScriptModule.h"
+#include "SceneObject.h"
+
 #include <chrono>
 #include <future>
 #include <map>
@@ -10,7 +17,6 @@
 #include <unordered_set>
 
 #include "WallpaperEngine/Data/Model/DynamicValue.h"
-#include "WallpaperEngine/Data/Model/ScriptedDynamicValue.h"
 #include "WallpaperEngine/Data/Model/Types.h"
 
 extern "C" {
@@ -22,6 +28,10 @@ class CScene;
 }
 
 namespace WallpaperEngine::Scripting {
+class ScriptPropertiesObject;
+namespace Adapters {
+    class ScriptableObjectAdapter;
+}
 using namespace WallpaperEngine::Data::Model;
 
 // Opaque handle returned by createLayerScript. 0 means invalid / not created.
@@ -30,28 +40,42 @@ static constexpr ScriptLayerHandle kInvalidLayerHandle = 0;
 
 class ScriptEngine {
 public:
-    static ScriptEngine& instance ();
+    struct LoadedModule {
+	DynamicValue& value;
+	JSValue module;
+    };
+    struct JSObjectAdapters {
+	std::unique_ptr<Adapters::VectorAdapter<4>> vec4;
+	std::unique_ptr<Adapters::VectorAdapter<3>> vec3;
+	std::unique_ptr<Adapters::VectorAdapter<2>> vec2;
+	std::unique_ptr<Adapters::ScriptableObjectAdapter> object;
+    };
 
     ~ScriptEngine ();
+    ScriptEngine (Render::Wallpapers::CScene& scene);
     ScriptEngine (const ScriptEngine&) = delete;
     ScriptEngine& operator= (const ScriptEngine&) = delete;
 
+    JSRuntime* getRuntime () const { return m_runtime; }
+    JSContext* getContext () const { return m_context; }
+    JSValue getGlobalThis () const { return m_globalThis; }
+    LoadedModule* getRunningModule () const { return m_runningModule; }
+    JSValue dynamicToJs (DynamicValue& value) const;
+
     /**
      * Evaluate a WallpaperEngine script's update() function.
      *
-     * @param scriptSource The full JS script text (ES6 module with export function update(value))
-     * @param scriptProperties Map of property name to current DynamicValue*
+     * @param key The full JS script text (ES6 module with export function update(value))
      * @param currentValue The current value to pass to update()
      * @return The modified value from update(), or a copy of currentValue on error
      */
-    DynamicValueUniquePtr evaluate (
-	const void* bindingKey,
-	const std::string& scriptSource,
-	const std::map<std::string, DynamicValue*>& scriptProperties,
-	const DynamicValue& currentValue,
-	WallpaperEngine::Render::Wallpapers::CScene* scene = nullptr,
-	const ScriptBindingContext* bindingContext = nullptr
-    );
+    void queueScript (const std::string& key, DynamicValue& currentValue, ScriptableObject& object);
+
+    /**
+     * Runs a frame tick in the javascript engine. Dispatches any pending events,
+     * timeouts, intervals AND calls any update() functions.
+     */
+    void tick ();
 
     // -------------------------------------------------------------------
     // Layer-script API (Phase 2 — dynamic text)
@@ -82,8 +106,7 @@ public:
      * @return A positive handle, or kInvalidLayerHandle if evaluation failed.
      */
     ScriptLayerHandle createLayerScript (
-	const std::string& scriptSource,
-	const std::map<std::string, DynamicValue*>& initialScriptProps,
+	const std::string& scriptSource, std::map<std::string, UserSettingUniquePtr>& initialScriptProps,
 	const std::string& initialText
     );
 
@@ -107,41 +130,20 @@ public:
      */
     void destroyLayer (ScriptLayerHandle handle);
 
-    JSValue dynamicValueToJS (const DynamicValue& value) const;
-    DynamicValueUniquePtr jsToDynamicValue (JSValue val, DynamicValue::UnderlyingType hint) const;
-    void releaseBinding (const void* bindingKey);
+    const JSObjectAdapters& getAdapters () const { return m_adapters; }
+    const Render::Wallpapers::CScene& getScene () const { return m_scene; }
+    const std::map<std::string, std::unique_ptr<Modules::ScriptModule>>& getModules () const { return m_modules; }
 
 private:
-    ScriptEngine ();
+    JSValue call (JSValue module, int argc, JSValueConst argv[], const char* name);
 
-    JSValue ensureModule (const void* bindingKey, const std::string& scriptSource);
     void installBuiltins ();
     void refreshMediaState ();
     void dispatchMediaEvents (JSValue module, const void* bindingKey);
-    void updateRuntimeGlobals (JSContext* ctx, JSValue globalObj) const;
-    void updateSceneInputGlobals (JSContext* ctx, JSValue globalObj, WallpaperEngine::Render::Wallpapers::CScene* scene);
     void callModuleWithProps (JSContext* ctx, JSValue module, const char* name, JSValue propsObj) const;
-    void initializeModuleIfNeeded (JSContext* ctx, JSValue module, const void* bindingKey, const DynamicValue& currentValue, bool hasScene);
     void runIntervals (JSContext* ctx, JSValue globalObj, const std::string& bindingKeyString) const;
     DynamicValueUniquePtr fallbackTextValue (const std::string& scriptSource) const;
     void applyTextFallback (DynamicValue& value, const std::string& scriptSource) const;
-    void logTextEvaluationDebug (const ScriptBindingContext* bindingContext) const;
-    void logTextResultDebug (const ScriptBindingContext* bindingContext, const DynamicValueUniquePtr& dynResult) const;
-    JSValue callUpdate (JSContext* ctx, JSValue module, const DynamicValue& currentValue) const;
-    DynamicValueUniquePtr resolveUndefinedResult (
-	const std::string& scriptSource,
-	const DynamicValue& currentValue,
-	WallpaperEngine::Render::Wallpapers::CScene* scene,
-	const ScriptBindingContext* bindingContext
-    ) const;
-    DynamicValueUniquePtr resolveEvaluationResult (
-	JSContext* ctx,
-	JSValue result,
-	const std::string& scriptSource,
-	const DynamicValue& currentValue,
-	WallpaperEngine::Render::Wallpapers::CScene* scene,
-	const ScriptBindingContext* bindingContext
-    ) const;
 
     struct MediaState {
 	int playbackState = 0;
@@ -159,10 +161,22 @@ private:
 
     JSRuntime* m_runtime = nullptr;
     JSContext* m_context = nullptr;
+    JSValue m_globalThis;
+    Render::Wallpapers::CScene& m_scene;
+    std::unique_ptr<EngineObject> m_engineObject;
+    std::unique_ptr<InputObject> m_inputObject;
+    std::unique_ptr<SceneObject> m_sceneObject;
+    std::unique_ptr<ConsoleObject> m_consoleObject;
+    std::unique_ptr<ScriptPropertiesObject> m_scriptPropertiesObject;
+
+    std::map<std::string, std::unique_ptr<Modules::ScriptModule>> m_modules = {};
+    std::map<std::string, LoadedModule> m_scriptModules = {};
+
+    LoadedModule* m_runningModule = nullptr;
+
     ScriptLayerHandle m_nextLayerId = 1;
     bool m_layerRegistryReady = false;
     std::map<ScriptLayerHandle, bool> m_layerInitialized;
-    std::unordered_map<const void*, JSValue> m_modules = {};
     std::unordered_set<const void*> m_initializedModules = {};
     std::unordered_map<const void*, std::string> m_lastMediaProperties = {};
     std::unordered_map<const void*, std::string> m_lastMediaPlayback = {};
@@ -172,5 +186,7 @@ private:
     std::future<std::optional<MediaState>> m_mediaPollFuture = {};
     MediaState m_mediaState = {};
     bool m_builtinsInstalled = false;
+
+    JSObjectAdapters m_adapters;
 };
 } // namespace WallpaperEngine::Scripting

+ 185 - 0
src/WallpaperEngine/Scripting/ScriptPropertiesObject.cpp

@@ -0,0 +1,185 @@
+#include "ScriptPropertiesObject.h"
+
+#include "Adapters/ScriptableObjectAdapter.h"
+#include "EngineObject.h"
+#include "ScriptEngine.h"
+#include "WallpaperEngine/Data/Utils/ScopeGuard.h"
+#include "WallpaperEngine/Render/Wallpapers/CScene.h"
+
+using namespace WallpaperEngine::Scripting;
+
+static uint32_t ScriptPropertiesObjectInstanceId = 0;
+std::map<uint32_t, ScriptPropertiesObject&> scriptPropertiesObjectInstances;
+
+struct OpaqueScriptPropertiesInstance {
+    ScriptPropertiesObject& object;
+    DynamicValue& value;
+};
+
+struct OpaqueScriptProperties {
+    ScriptPropertiesObject& object;
+};
+
+JSValue scriptproperties_property_get (JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst receiver) {
+    JSClassID classId = 0;
+
+    auto* container = static_cast<OpaqueScriptPropertiesInstance*> (JS_GetAnyOpaque (obj_val, &classId));
+
+    const char* name = JS_AtomToCString (ctx, atom);
+
+    if (name == nullptr) {
+	return JS_EXCEPTION;
+    }
+
+    ScopeGuard guard ([=] { JS_FreeCString (ctx, name); });
+
+    try {
+	auto& properties = container->value.getProperties ();
+	const auto it = properties.find (name);
+
+	if (it == properties.end ()) {
+	    return JS_UNDEFINED;
+	}
+
+	return container->object.getEngine ().dynamicToJs (*it->second->value);
+    } catch (const std::exception& e) {
+	return JS_EXCEPTION;
+    }
+}
+
+int scriptproperties_property_set (
+    JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst val, JSValueConst receiver, int flags
+) {
+    // do not support setting properties
+    return -1;
+}
+
+JSValue scriptpropertiescreator_add (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    // no need to do anything, any add call should just return itself
+    // we'll set them either way as what comes in the DynamicValue
+    // TODO: PROPERLY IMPLEMENT THIS CHAIN AT SOME POINT
+    return this_val;
+}
+
+JSValue scriptpropertiescreator_finish (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
+    JSClassID classId = 0;
+    const auto container = static_cast<OpaqueScriptProperties*> (JS_GetAnyOpaque (this_val, &classId));
+
+    // get all the properties and set the right values
+    const auto* module = container->object.getEngine ().getRunningModule ();
+
+    if (module == nullptr) {
+	return JS_UNDEFINED;
+    }
+
+    // create a new object based off the properties in the dynamic value and call it a day
+    JSValue result = JS_NewObjectClass (ctx, container->object.getPropertiesClassId ());
+    JS_SetOpaque (
+	result,
+	new OpaqueScriptPropertiesInstance { .object = container->object,
+					     .value = container->object.getEngine ().getRunningModule ()->value }
+    );
+
+    return result;
+}
+
+void scriptpropertiescreator_finalizer (JSRuntime* rt, JSValueConst val) {
+    JSClassID classId = 0;
+
+    delete static_cast<OpaqueScriptProperties*> (JS_GetAnyOpaque (val, &classId));
+}
+
+void scriptproperties_finalizer (JSRuntime* rt, JSValueConst val) {
+    JSClassID classId = 0;
+
+    delete static_cast<OpaqueScriptPropertiesInstance*> (JS_GetAnyOpaque (val, &classId));
+}
+
+JSValue
+scriptpropertiescreator_create (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
+    const auto instance = scriptPropertiesObjectInstances.find (magic);
+
+    if (instance == scriptPropertiesObjectInstances.end ()) {
+	return JS_UNDEFINED;
+    }
+
+    JSValue creator = JS_NewObjectClass (ctx, instance->second.getCreatorClassId ());
+
+    // setup the current script properties creator
+    JS_SetOpaque (creator, new OpaqueScriptProperties { .object = instance->second });
+
+    return creator;
+}
+
+ScriptPropertiesObject::ScriptPropertiesObject (ScriptEngine& engine, Render::Wallpapers::CScene& scene) :
+    m_scene (scene), m_engine (engine), m_instanceId (++ScriptPropertiesObjectInstanceId), m_creatorClassId (0),
+    m_propertiesClassId (0) {
+    scriptPropertiesObjectInstances.emplace (this->m_instanceId, *this);
+
+    this->m_exoticMethods = {
+	.get_property = scriptproperties_property_get,
+	.set_property = scriptproperties_property_set,
+    };
+    this->m_creatorDefinition = {
+	.class_name = "IScriptPropertiesCreator",
+	.finalizer = scriptpropertiescreator_finalizer,
+    };
+    JS_NewClassID (this->m_engine.getRuntime (), &this->m_creatorClassId);
+    JS_NewClass (this->m_engine.getRuntime (), this->m_creatorClassId, &this->m_creatorDefinition);
+    this->m_creatorPrototype = JS_NewObject (this->m_engine.getContext ());
+
+    this->m_propertiesDefinition = {
+	.class_name = "IScriptProperties",
+	.finalizer = scriptproperties_finalizer,
+	.exotic = &this->m_exoticMethods,
+    };
+    JS_NewClassID (this->m_engine.getRuntime (), &this->m_propertiesClassId);
+    JS_NewClass (this->m_engine.getRuntime (), this->m_propertiesClassId, &this->m_propertiesDefinition);
+    this->m_propertiesPrototype = JS_NewObject (this->m_engine.getContext ());
+
+    JS_DupValue (this->m_engine.getContext (), this->m_propertiesPrototype);
+    JS_DupValue (this->m_engine.getContext (), this->m_creatorPrototype);
+
+    // set properties
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_creatorPrototype, "addSlider",
+	JS_NewCFunction (this->m_engine.getContext (), scriptpropertiescreator_add, "addSlider", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_creatorPrototype, "addCheckbox",
+	JS_NewCFunction (this->m_engine.getContext (), scriptpropertiescreator_add, "addCheckbox", 0),
+	JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_creatorPrototype, "addText",
+	JS_NewCFunction (this->m_engine.getContext (), scriptpropertiescreator_add, "addText", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_creatorPrototype, "addCombo",
+	JS_NewCFunction (this->m_engine.getContext (), scriptpropertiescreator_add, "addCombo", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_creatorPrototype, "addColor",
+	JS_NewCFunction (this->m_engine.getContext (), scriptpropertiescreator_add, "addColor", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_creatorPrototype, "finish",
+	JS_NewCFunction (this->m_engine.getContext (), scriptpropertiescreator_finish, "finish", 0), JS_PROP_ENUMERABLE
+    );
+    JS_DefinePropertyValueStr (
+	this->m_engine.getContext (), this->m_engine.getGlobalThis (), "createScriptProperties",
+	JS_NewCFunctionMagic (
+	    this->m_engine.getContext (), scriptpropertiescreator_create, "createScriptProperties", 0, JS_CFUNC_generic,
+	    m_instanceId
+	),
+	JS_PROP_ENUMERABLE
+    );
+
+    JS_SetClassProto (this->m_engine.getContext (), this->m_propertiesClassId, this->m_propertiesPrototype);
+    JS_SetClassProto (this->m_engine.getContext (), this->m_creatorClassId, this->m_creatorPrototype);
+}
+
+ScriptPropertiesObject::~ScriptPropertiesObject () {
+    JS_FreeValue (this->m_engine.getContext (), this->m_creatorPrototype);
+    JS_FreeValue (this->m_engine.getContext (), this->m_propertiesPrototype);
+}

+ 36 - 0
src/WallpaperEngine/Scripting/ScriptPropertiesObject.h

@@ -0,0 +1,36 @@
+#pragma once
+#include "quickjs.h"
+
+namespace WallpaperEngine::Render::Wallpapers {
+class CScene;
+}
+namespace WallpaperEngine::Scripting {
+class ScriptEngine;
+class ScriptPropertiesObject {
+public:
+    ScriptPropertiesObject (ScriptEngine& engine, Render::Wallpapers::CScene& scene);
+    ~ScriptPropertiesObject ();
+
+    [[nodiscard]] const Render::Wallpapers::CScene& getScene () const { return m_scene; }
+    [[nodiscard]] JSValue getCreatorPrototype () const { return m_creatorPrototype; }
+    [[nodiscard]] JSValue getPropertiesPrototype () const { return m_propertiesPrototype; }
+    [[nodiscard]] JSClassID getCreatorClassId () const { return m_creatorClassId; }
+    [[nodiscard]] JSClassID getPropertiesClassId () const { return m_propertiesClassId; }
+    [[nodiscard]] ScriptEngine& getEngine () const { return m_engine; }
+
+protected:
+    Render::Wallpapers::CScene& m_scene;
+    ScriptEngine& m_engine;
+
+    uint32_t m_instanceId;
+
+    JSClassID m_creatorClassId;
+    JSClassDef m_creatorDefinition;
+    JSValue m_creatorPrototype;
+
+    JSClassID m_propertiesClassId;
+    JSClassDef m_propertiesDefinition;
+    JSValue m_propertiesPrototype;
+    JSClassExoticMethods m_exoticMethods;
+};
+}

+ 43 - 0
src/WallpaperEngine/Scripting/ScriptableObject.cpp

@@ -0,0 +1,43 @@
+#include "ScriptableObject.h"
+
+#include "ScriptEngine.h"
+#include "WallpaperEngine/Data/Utils/ScopeGuard.h"
+
+#include <ranges>
+
+using namespace WallpaperEngine::Render;
+using namespace WallpaperEngine::Scripting;
+
+ScriptableObject::ScriptableObject (Wallpapers::CScene& scene, const Object& object) : CObject (scene, object) {
+    // register common dynamic values
+    this->registerProperty ("origin", *object.origin->value);
+    this->registerProperty ("scale", *object.groupScale->value);
+    this->registerProperty ("angles", *object.groupAngles->value);
+    this->registerProperty ("visible", *object.groupVisible->value);
+}
+
+DynamicValue& ScriptableObject::getProperty (const std::string& name) {
+    const auto it = this->m_properties.find (name);
+
+    if (it == this->m_properties.end ()) {
+	sLog.exception ("Property '" + name + "' not found on object '" + this->getObject ().name + "'");
+    }
+
+    return it->second.value;
+}
+
+const std::map<std::string, ScriptableObject::PropertyEntry>& ScriptableObject::getProperties () const {
+    return this->m_properties;
+}
+
+void ScriptableObject::registerProperty (const std::string& name, DynamicValue& value) {
+    auto inserted = this->m_properties.emplace (
+	name, PropertyEntry { .key = name + "_" + std::to_string (this->getId ()), .value = value }
+    );
+
+    if (!inserted.second) {
+	return;
+    }
+
+    this->getScene ().getScriptEngine ().queueScript (inserted.first->second.key, inserted.first->second.value, *this);
+}

+ 30 - 0
src/WallpaperEngine/Scripting/ScriptableObject.h

@@ -0,0 +1,30 @@
+#pragma once
+#include "WallpaperEngine/Data/Model/Types.h"
+#include "WallpaperEngine/Render/CObject.h"
+
+namespace WallpaperEngine::Render::Wallpapers {
+class CScene;
+}
+
+namespace WallpaperEngine::Scripting {
+class ScriptableObject : virtual public CObject {
+public:
+    struct PropertyEntry {
+	std::string key;
+	DynamicValue& value;
+    };
+
+    ScriptableObject (Wallpapers::CScene& scene, const Object& object);
+    virtual ~ScriptableObject () = default;
+
+    DynamicValue& getProperty (const std::string& name);
+
+    const std::map<std::string, PropertyEntry>& getProperties () const;
+
+protected:
+    void registerProperty (const std::string& name, DynamicValue& value);
+
+private:
+    std::map<std::string, PropertyEntry> m_properties;
+};
+}

+ 1 - 1
src/WallpaperEngine/Testing/Cases/PropertyParser.cpp

@@ -1,7 +1,7 @@
 #include <catch2/catch_test_macros.hpp>
 
-#include "WallpaperEngine/Data/Parsers/PropertyParser.h"
 #include "WallpaperEngine/Data/Model/Property.h"
+#include "WallpaperEngine/Data/Parsers/PropertyParser.h"
 
 using WallpaperEngine::Data::JSON::JSON;
 using WallpaperEngine::Data::Model::DynamicValue;

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.