commit 9e6e6666abf072ccd8a6e50feebb5cb3cd7a3eaa Author: Yura Dupyn <2153100+omedusyo@users.noreply.github.com> Date: Mon Mar 2 14:04:23 2026 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1eae0cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..2198622 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ + +strong nuclear force (quark confinement) +the force between particles stays constant +no matter what distance + + +spring universe (hooks law) +- when particles are far apart, it gives a huge attractive force + + +BAND +- when particles are far apart, it gives attraction, + but when particles are too close, it gives repulsion, + so you have this "band" in which the force kinda drops off... and is negligable +- molecular dynamics: + - when far apart, you have very small attractive force + - when close, strong repulsive force + + +Galactic Gravity +- instead of newtons 1/r^2, use 1/r +- orbits around a heavy body will move at constant speed + reg + + + +Boids +- don't crowd neighbours + - neighbours that are close have repulsive force +- alignment + + +What about different types of "masses"? +e.g. something like charge, or perhaps something weirder? + +====Implementation==== + +Should I track momentum or velocity? +Since I have constant mass particle, +I can always compute velocity easily. + +# Time and Evolution +Decoupling simulation step from `dt` given by the browser. + +# Global attributes +- net force (should be very close to 0) +- net momentum (should be very close to a constant) +What else? +- average kinetic energy (temperature?) +- center-of-mass (should be constant) + +- WTF potential energy? How to calculate that? Probably + always different for each of the systems? Does it always make sense? + + +# Camera & Culling + +- zoom & pan +- render only those particles that are on the screen + - note that this is gonna fuck up the trails, but whatever + + +# Brushes +## Spawning +- spray paint - bunch of particles near each other but with non-zero initial velocity +- single heavy click brush spawning particles +- brush that spawns particles orthogonal to the center of mass + +## Filtering +- delete particles in mass range +- delete particles in region + +## Mapping +- change mass of all particles based on a function. + +## Reduce +- Replace particles in a region by one particle that has combined mass + +# Local Attributes +- follow a single particle and highlight it, name it, display its attributes + +- Select a square/circle s.t. all particles within it + are now tracked... or perhaps those that enter/exit it? + + + +# Dimensions/Layers? +What about having quantities such as a `phase`? +Where basically particles of different phases don't interact... but then we could change phases of particles, and suddenly there is interaction etc... + + + + +# Symplectic vs Explicit Euler +Suppose we have `(pos0, vel0)`, and we have `dt`. We compute `acc` (acceleration). +Then the enxt state is: +- Explicit: + ``` + (pos0 + dt*vel0, vel0 + dt*acc) + ``` +- Symplectic: + ``` + let { + vel1 = vel0 + dt*acc, + pos1 = pos0 + dt*vel1, + . + (pos1, vel1) + } + ``` + I have no idea why symplectic works... + Does it work only for some systems or all? + + + +# Energy +- Kinetic Energy is the energy of motion... +- Potential Energy is the energy of position/configuration (relative position? idk...) + + +Compute? Potential Energy is integral of force over distance... + +Hmm. Let's have `f : M -> R` the "potential". +The force field that's generated by `f` is `d(f)` - but actually... the gradient?. +But given a force field, how can we attempt to +compute the potential energy? Atleast on the trajectory of the particle... + +There's no unique solution... so let's say we spawn a particle, and give it potential of 0. + +Then the acceleartion field tells us in which direction to move. I guess we're computing an integral over some 1-form. How do we get the 1-form? + +It is at a point, the inner product of the infinitesimal velocity with the infinitesimal force... +``` + +``` + + + + + + +# Angular Momentum? + + +What if our basic element would be like "rods" of two particles where the constraint is that the distance between them is fixed. Then we could naturally have +angular velocity etc... + + diff --git a/connections_and_accelerations_0.kra b/connections_and_accelerations_0.kra new file mode 100755 index 0000000..9b1a18d Binary files /dev/null and b/connections_and_accelerations_0.kra differ diff --git a/connections_and_accelerations_0.kra~ b/connections_and_accelerations_0.kra~ new file mode 100755 index 0000000..0c0b1df Binary files /dev/null and b/connections_and_accelerations_0.kra~ differ diff --git a/connections_and_accelerations_1.kra b/connections_and_accelerations_1.kra new file mode 100755 index 0000000..7aaa254 Binary files /dev/null and b/connections_and_accelerations_1.kra differ diff --git a/connections_and_accelerations_1.kra~ b/connections_and_accelerations_1.kra~ new file mode 100755 index 0000000..9a9a41f Binary files /dev/null and b/connections_and_accelerations_1.kra~ differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..2863fed --- /dev/null +++ b/index.html @@ -0,0 +1,11 @@ + + + + + Newtonian Dance + + + + + + diff --git a/notes_0.kra b/notes_0.kra new file mode 100755 index 0000000..7209456 Binary files /dev/null and b/notes_0.kra differ diff --git a/notes_0.kra~ b/notes_0.kra~ new file mode 100755 index 0000000..282fe57 Binary files /dev/null and b/notes_0.kra~ differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..301803c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1937 @@ +{ + "name": "newtonian_dance", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "newtonian_dance", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "solid-js": "^1.9.11" + }, + "devDependencies": { + "@types/node": "^25.3.0", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vite-plugin-solid": "^2.11.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions": { + "version": "0.40.5", + "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.5.tgz", + "integrity": "sha512-8TFKemVLDYezqqv4mWz+PhRrkryTzivTGu0twyLrOkVZ0P63COx2Y04eVsUjFlwSOXui1z3P3Pn209dokWnirg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "7.18.6", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.20.7", + "html-entities": "2.3.3", + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.20.12" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/babel-preset-solid": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.10.tgz", + "integrity": "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jsx-dom-expressions": "^0.40.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "solid-js": "^1.9.10" + }, + "peerDependenciesMeta": { + "solid-js": { + "optional": true + } + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge-anything": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", + "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seroval": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz", + "integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.0.tgz", + "integrity": "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/solid-js": { + "version": "1.9.11", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.11.tgz", + "integrity": "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.0", + "seroval": "~1.5.0", + "seroval-plugins": "~1.5.0" + } + }, + "node_modules/solid-refresh": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.6.3.tgz", + "integrity": "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.23.6", + "@babel/helper-module-imports": "^7.22.15", + "@babel/types": "^7.23.6" + }, + "peerDependencies": { + "solid-js": "^1.3" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-solid": { + "version": "2.11.10", + "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.10.tgz", + "integrity": "sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.3", + "@types/babel__core": "^7.20.4", + "babel-preset-solid": "^1.8.4", + "merge-anything": "^5.1.7", + "solid-refresh": "^0.6.3", + "vitefu": "^1.0.4" + }, + "peerDependencies": { + "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", + "solid-js": "^1.7.2", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@testing-library/jest-dom": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", + "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..391da23 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "newtonian_dance", + "version": "1.0.0", + "description": "", + "license": "MIT", + "author": "Yuriy Dupyn", + "type": "module", + "main": "index.js", + "scripts": { + "dev": "vite", + "build": "tsc && vite build --base ./", + "preview": "vite preview" + }, + "devDependencies": { + "@types/node": "^25.3.0", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vite-plugin-solid": "^2.11.10" + }, + "dependencies": { + "solid-js": "^1.9.11" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..daa2417 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,97 @@ +import { createSignal, For, Switch, Match } from "solid-js"; +import { FlatGravitySystem } from "./systems/flat_gravity"; +import { FlatGravityView, ViewConfig as FlatGravityViewConfig } from "./systems/flat_gravity_solid"; +import { SimpleKinematicsSystem } from "./systems/simple_kinematics"; +import { SimpleKinematicsView } from "./systems/simple_kinematics_solid"; + +const systemIds = [ + "simple-kinematics", + "flat-gravity", +] as const; +export type SystemId = (typeof systemIds)[number]; + +const defaultSystemId: SystemId = "flat-gravity"; + +const systemToLabel: Record = { + "simple-kinematics": "Simple Kinematics", + "flat-gravity": "Flat Gravity", +}; + +export function FlatGravityHost() { + const flatGravity = FlatGravitySystem({ + dt: 16, + topLeft: { x: 200, y: 200 }, + bottomRight: { x: 600, y: 600 }, + maxSpeed: 100 / 1000, + + countSmall: 200, + countBig: 3, + massRangeSmall: { min: 0.1, max: 0.5 }, + massRangeBig: { min: 200.0, max: 300.0 }, + }); + + const viewConfig: FlatGravityViewConfig = { + massRange: { min: 0, max: 300 }, + radiusRange: { min: 2, max: 20 }, + expectedNetMass: 20000, + }; + + return ; +} + +export function SimpleKinematicsHost() { + const simpleKinematics = SimpleKinematicsSystem({ + dt: 4, + count: 500, + topLeft: { x: 200, y: 200 }, + bottomRight: { x: 400, y: 400 }, + maxSpeed: 200 / 1000, + }); + + return ; +} + + +function SystemView(props: { systemId: SystemId }) { + return ( + Select a system}> + + + + + + + + ); +} + +export function App() { + const [systemId, setSystemId] = createSignal(defaultSystemId); + + return ( +
+
+
+ + +
+
+ +
+ +
+
+ ); +} + diff --git a/src/arithmetic.ts b/src/arithmetic.ts new file mode 100644 index 0000000..8492249 --- /dev/null +++ b/src/arithmetic.ts @@ -0,0 +1,217 @@ +// CONVENTIONS: +// - scalars are: A, B, C, ... +// - vectors are: V, W, U, ... +// - points are: P, Q, R +// - numbers are: a, b, c, ..., s, t,..., u, v, w,..., x, y, z, ... + +export type Scalar = { + val: Float64Array, +} + +export type Vector = { + x: Float64Array, + y: Float64Array, +} + +export type Config = { + capacity: number, + count: number, +} + +export function Arithmetic({ capacity: CAPACITY, count: COUNT }: Config) { + + const Scalar = { + allocate(): Scalar { + return { + val: new Float64Array(CAPACITY), + }; + }, + + // Out := 0 + zero(Out: Scalar) { + for (let i = 0; i < COUNT; i++) { + Out.val[i] = 0; + } + }, + + // Out := A + B + add(Out: Scalar, A: Scalar, B: Scalar) { + for (let i = 0; i < COUNT; i++) { + Out.val[i] = A.val[i] + B.val[i]; + } + }, + + // Out := A * B + mul(Out: Scalar, A: Scalar, B: Scalar) { + for (let i = 0; i < COUNT; i++) { + Out.val[i] = A.val[i] * B.val[i]; + } + }, + + // Out := A + s*B + addScaledByConstant(Out: Scalar, A: Scalar, s: number, B: Scalar) { + for (let i = 0; i < COUNT; i++) { + Out.val[i] = A.val[i] + s*B.val[i]; + } + }, + + // Out := A / B + div(Out: Scalar, A: Scalar, B: Scalar) { + for (let i = 0; i < COUNT; i++) { + Out.val[i] = A.val[i] / B.val[i]; + } + }, + + // Out := 1/A + reciprocal(Out: Scalar, A: Scalar) { + for (let i = 0; i < COUNT; i++) { + Out.val[i] = 1/A.val[i]; + } + }, + + // Out := A*s + scaleByNumber(Out: Scalar, A: Scalar, s: number) { + for (let i = 0; i < COUNT; i++) { + Out.val[i] = A.val[i] * s; + } + }, + + sum(A: Scalar): number { + let sum = 0; + for (let i = 0; i < COUNT; i++) { + sum += A.val[i]; + } + return sum; + }, + + average(A: Scalar): number { + // TODO: Perhaps not the best method... might cause overflows...? + return this.sum(A)/COUNT; + }, + + // Do I need a general `map`? Mapping one `number => number` over each scalar value in the scalar field + // It will not be performant. Better to write the loop by hand in JS. + }; + + const Vector = { + allocate(): Vector { + return { + x: new Float64Array(CAPACITY), + y: new Float64Array(CAPACITY), + }; + }, + + // Out := 0 + zero(Out: Vector) { + for (let i = 0; i < COUNT; i++) { + Out.x[i] = 0; + Out.y[i] = 0; + } + }, + + // Out := V + W + add(Out: Vector, V: Vector, W: Vector) { + // in js a single loop wins (bounds checking, dealing with `i` etc). But in something like C or WASM two loops might actually be better (better patterns of access to memory) + for (let i = 0; i < COUNT; i++) { + Out.x[i] = V.x[i] + W.x[i]; + Out.y[i] = V.y[i] + W.y[i]; + } + }, + + // Out := V + s*W + addScaledByConstant(Out: Vector, V: Vector, s: number, W: Vector) { + for (let i = 0; i < COUNT; i++) { + Out.x[i] = V.x[i] + s*W.x[i]; + Out.y[i] = V.y[i] + s*W.y[i]; + } + }, + + // Out := V*s + scaleByConstant(Out: Vector, V: Vector, s: number) { + for (let i = 0; i < COUNT; i++) { + Out.x[i] = V.x[i] * s; + Out.y[i] = V.y[i] * s; + } + }, + + // Out := V*S + scaleByScalar(Out: Vector, V: Vector, S: Scalar) { + for (let i = 0; i < COUNT; i++) { + Out.x[i] = V.x[i] * S.val[i]; + Out.y[i] = V.y[i] * S.val[i]; + } + }, + + // Out := V / S + divideByScalar(Out: Vector, V: Vector, S: Scalar) { + for (let i = 0; i < COUNT; i++) { + Out.x[i] = V.x[i] / S.val[i]; + Out.y[i] = V.y[i] / S.val[i]; + } + }, + + // Out := + innerProduct(Out: Scalar, V: Vector, W: Vector) { + for (let i = 0; i < COUNT; i++) { + Out.val[i] = V.x[i]*W.x[i] + V.y[i]*W.y[i]; + } + }, + + // Out := |V| + magnitude(Out: Scalar, V: Vector) { + for (let i = 0; i < COUNT; i++) { + const x = V.x[i]; + const y = V.y[i]; + Out.val[i] = Math.sqrt(x*x + y*y); + } + }, + + // Out := V/|V| + normalize(Out: Vector, V: Vector) { + for (let i = 0; i < COUNT; i++) { + const x = V.x[i]; + const y = V.y[i]; + const s = Math.sqrt(x*x + y*y); + + Out.x[i] = x / s; + Out.y[i] = y / s; + } + }, + + sum(V: Vector): { x: number, y: number } { + let x: number = 0; + let y: number = 0; + for (let i = 0; i < COUNT; i++) { + x += V.x[i]; + y += V.y[i]; + } + return { x, y }; + }, + + average(V: Vector): { x: number, y: number } { + const v = this.sum(V); + v.x = v.x/COUNT; + v.y = v.y/COUNT; + return v; + }, + + // TODO: Later we can consider other things like wedge product, or rotation by 90... or general rotation etc. + } + + function setCount(newCount: number) { + COUNT = newCount; + } + + function getCount(): number { + return COUNT; + } + + // TODO: Later also implement resizing capabilities that change Capacity. + return { + Scalar, + Vector, + setCount, + getCount, + } +} + diff --git a/src/draw.ts b/src/draw.ts new file mode 100644 index 0000000..b01c8f4 --- /dev/null +++ b/src/draw.ts @@ -0,0 +1,45 @@ + +export namespace Draw { + export function particle( + ctx: CanvasRenderingContext2D, + x: number, y: number, + radius: number, + fill: string, + stroke = "lightblue" + ) { + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fillStyle = fill; + ctx.fill(); + ctx.strokeStyle = stroke; + ctx.lineWidth = 1; + ctx.stroke(); + } + + export function crosshair( + ctx: CanvasRenderingContext2D, + x: number, y: number, + size: number, + color: string, + ) { + const circleRadius = size * 0.5; + + ctx.strokeStyle = color; + ctx.lineWidth = 2; // Fixed thickness + + // The Cross + ctx.beginPath(); + // Horizontal line + ctx.moveTo(x - size, y); + ctx.lineTo(x + size, y); + // Vertical line + ctx.moveTo(x, y - size); + ctx.lineTo(x, y + size); + ctx.stroke(); + + // The Circle + ctx.beginPath(); + ctx.arc(x, y, circleRadius, 0, Math.PI * 2); + ctx.stroke(); + } +} diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..2f7c3a0 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,57 @@ +import { createSignal } from "solid-js"; + +export function random(a: number, b: number) { + const intervalSize = b - a; + return a + Math.random()*intervalSize; +} + +export function projectToInterval(a: number, x: number, b: number) { + return Math.min(Math.max(a, x), b) +} + +export function mapRange(value: number, inMin: number, inMax: number, outMin: number, outMax: number) { + const t = (value - inMin) / (inMax - inMin); + const clampedT = projectToInterval(0, t, 1); + return outMin + clampedT * (outMax - outMin); +} + +// Snaps microscopic floating point errors to 0, and rounds to 2 decimals +export function formatNumber(x: number): string { + if (Math.abs(x) < 1e-10) return "0.00"; + return x.toFixed(2); +} + +// === Time === + +export type MicroDuration = number; +export type Duration = number; + + +// === Events === +export function usePointer(ref: () => HTMLCanvasElement) { + const [position, setPosition] = createSignal([0, 0]); + const [isDown, setIsDown] = createSignal(false); + + const handle = (e: PointerEvent) => { + const rect = ref().getBoundingClientRect(); + setPosition([e.clientX - rect.left, e.clientY - rect.top]); + setIsDown(e.buttons === 1); + }; + + return { position, isDown, handle }; +} + + +// === Generic System === + +// Observables contain derived attributes that can be computed purely from the `State`. +export interface System { + state: State, + observables: Observables, + flow(t: Duration): void, // Updates just the state, not observables! + refreshObservables(): void, // Updates just the observables from the current state. + totalTime(): Duration, // gets the time from the start of the simulation + reset(): void, + capabilities: Capabilities, +} + diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..c272485 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,9 @@ +import "./styles/main.css"; +import { render } from "solid-js/web"; +import { App } from "./App"; + +// === Coordinate Transformations === +// TODO: canvas to system coordinates and the other way + +render(() => , document.body); + diff --git a/src/styles/app.css b/src/styles/app.css new file mode 100644 index 0000000..779b525 --- /dev/null +++ b/src/styles/app.css @@ -0,0 +1,129 @@ + +main { + margin-top: 15px; +} + +.app-layout { + display: grid; + /* Columns: Brushes | Canvas (800px) | Observables */ + grid-template-columns: 100px 800px 400px; + /* Rows: Controls | Main content */ + grid-template-rows: min-content min-content; + + gap: 5px; + background-color: var(--bg-app); + min-height: 100vh; + + justify-content: center; + align-content: start; +} + +.controls { + grid-column: 2; /* Center column */ + display: flex; + align-items: center; + gap: 15px; + color: var(--text-main); + padding-bottom: 10px; +} + +.canvas-container { + grid-column: 2; + height: 600px; /* Force the canvas height here */ +} + +.canvas-container canvas { + background: #000; + cursor: crosshair; + border-radius: 4px; + border: 1px solid var(--border-subtle); + + user-select: none; + -webkit-user-select: none; + -webkit-user-drag: none; + touch-action: none; +} + +.brushes-panel { + grid-column: 1; + grid-row: 2; /* Sits next to the canvas */ + align-self: start; +} + +.side-panel { + grid-column: 3; + grid-row: 2; /* Sits next to the canvas */ + align-self: start; +} + +/* --- Buttons --- */ +.btn { + background: var(--bg-panel); + color: var(--text-main); + border: 1px solid var(--border-subtle); + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-size: 13px; +} + +/* --- Checkboxes & Labels --- */ +.toggle { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + cursor: pointer; + user-select: none; + color: var(--text-dim); +} + +.toggle input[type="checkbox"] { + cursor: pointer; + accent-color: var(--accent-sub-label); /* Modern browsers will use your blue for the check */ +} + + +/* --- Top Stats --- */ +.top-stats { + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + color: var(--text-dim); + font-variant-numeric: tabular-nums; + display: inline-block; + min-width: 50px; +} + + + +.top-nav { + display: flex; + align-items: center; + background-color: var(--bg-toolbar); + border-bottom: 1px solid var(--border-subtle); + padding: 0 10px; + font-size: 12px; + color: var(--text-toolbar); +} + +/* Simple Select Styling */ +.system-dropdown { + background-color: var(--bg-panel); + color: var(--text-main); + border: 1px solid var(--border-subtle); + border-radius: 3px; + padding: 1px 4px; + font-family: inherit; + font-size: 12px; + cursor: pointer; +} + +.system-dropdown:hover { + border-color: var(--accent-sub-label); +} + +.system-dropdown:focus { + outline: 1px solid var(--accent-sub-label); +} + diff --git a/src/styles/main.css b/src/styles/main.css new file mode 100644 index 0000000..7bd2234 --- /dev/null +++ b/src/styles/main.css @@ -0,0 +1,18 @@ +@import "./variables.css"; +@import "./observables.css"; +@import "./app.css"; + +*, *::before, *::after { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + background-color: var(--bg-app); + color: var(--text-main); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + diff --git a/src/styles/observables.css b/src/styles/observables.css new file mode 100644 index 0000000..d647f8a --- /dev/null +++ b/src/styles/observables.css @@ -0,0 +1,37 @@ + +.observables-container { + column-gap: 12px; + row-gap: 6px; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-size: 13px; + background: var(--bg-panel); + color: var(--text-main); + padding: 16px; + border-radius: 4px; + border: 1px solid var(--border-subtle); +} + +.observables-header { + padding-top: 12px; + padding-bottom: 4px; + border-bottom: 1px solid var(--border-subtle); + color: var(--text-dim); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.observables-label { + color: var(--accent-label); +} + +.observables-component-label { + color: var(--accent-sub-label); + text-align: right; +} + +.observables-value { + color: var(--color-number); + text-align: right; +} + diff --git a/src/styles/variables.css b/src/styles/variables.css new file mode 100644 index 0000000..89eb2ca --- /dev/null +++ b/src/styles/variables.css @@ -0,0 +1,20 @@ +:root { + /* Surface Colors */ + --bg-app: #121212; + --bg-panel: #1a1a1a; + --border-subtle: #333333; + + /* Typography & Accents */ + --text-main: #e0e0e0; + --text-dim: #888888; + --accent-label: #9cdcfe; /* Soft Blue */ + --accent-sub-label: #569cd6; /* Deeper Blue */ + + /* Data Colors */ + --color-number: #b5cea8; /* Pale Green */ + --color-critical: #f44747; /* Red */ +/* Toolbar specific */ + --bg-toolbar: #252526; + --text-toolbar: #cccccc; +} + diff --git a/src/systems/flat_gravity.ts b/src/systems/flat_gravity.ts new file mode 100644 index 0000000..0c08fdc --- /dev/null +++ b/src/systems/flat_gravity.ts @@ -0,0 +1,346 @@ +// A particle creates an attractive force field towards it s.t. the strength is proportional to the particle's mass. +// It doesn't depend on the distance - the force is constant. + +import { Arithmetic, Vector, Scalar } from "../arithmetic"; +import { Duration, MicroDuration, random, System } from "../helpers"; + +export type Config = { + dt: MicroDuration, // e.g. 5 miliseconds + // Box in which to generate random particles with uniform distribution, and random velocities + topLeft: {x: number, y: number}, + bottomRight: {x: number, y: number}, + maxSpeed: number, + + countSmall: number, + massRangeSmall: { min: number, max: number }, + countBig: number, + massRangeBig: { min: number, max: number }, +} + +export type State = { + position: Vector, + velocity: Vector, + mass: Scalar, +} + +export type Observables = { + particleCount: number, + averageSpeed: number, + netMass: number, + netForce: { fx: number, fy: number }, + netMomentum: { px: number, py: number }, + centerOfMass: { x: number, y: number }, + netKineticEnergy: number, + netPotentialEnergy: number, + netPotentialEnergyIntegralCalculation: number, + netEnergy: number, +} + +export type Capabilities = { + spawn(x: number, y: number, vx: number, vy: number, m: number): void, +} + +export type FlatGravitySystem = System; + +export function FlatGravitySystem(config: Config): FlatGravitySystem { + const arith = Arithmetic({ count: 0, capacity: 10000 }); + const { Vector, Scalar } = arith; + + // Responsible for computing the force/acceleration fields + function Force() { + const force = Vector.allocate(); + const acceleration = Vector.allocate(); + + function LOOM() { + const count = arith.getCount(); + + Vector.zero(force); + Vector.zero(acceleration); + for (let i = 0; i < count; i++) { + // Compute the net acceleration on i coming from interaction with all the other particles. + for (let j = i + 1; j < count; j++) { + const mi = mass.val[i]; + const mj = mass.val[j]; + const dx = position.x[j] - position.x[i]; + const dy = position.y[j] - position.y[i]; + const size = Math.sqrt(dx*dx + dy*dy); + if (size == 0) { continue } + const ux = dx/size; + const uy = dy/size; + // [ux, uy] is the unit vector pointing from i to j + + const ax_i = mj*G*ux; + const ay_i = mj*G*uy; + const fx_i = mi*ax_i; + const fy_i = mi*ay_i; + const ax_j = -mi*G*ux; + const ay_j = -mi*G*uy; + + acceleration.x[i] += ax_i; + acceleration.y[i] += ay_i; + acceleration.x[j] += ax_j; + acceleration.y[j] += ay_j; + + force.x[i] += fx_i; + force.y[i] += fy_i; + force.x[j] += -fx_i; + force.y[j] += -fy_i; + } + } + } + + return { force, acceleration, LOOM }; + } + + // TODO: Better name + // Responsible for stepping (position, velocity) + function MicroEvolution() { + const microDisplacement: Vector = Vector.allocate(); + + function LOOM(dt: MicroDuration) { + // Symplectic Euler: + // velocity = velocity + dt*acceleration + // position = position + dt*velocity + // + // Explicit Euler (seems to increase total energy over time): + // position = position + dt*velocity + // velocity = velocity + dt*acceleration + + Vector.addScaledByConstant(velocity, velocity, dt, acceleration); + + // If you don't need micro-displacement, you can just use: + // Vector.addScaledByConstant(position, position, dtInSeconds, velocity); + Vector.scaleByConstant(microDisplacement, velocity, dt); + Vector.add(position, position, microDisplacement); + } + return { microDisplacement, LOOM }; + } + + // Updates potential energy by current forces + function PotentialEnergyUpdate() { + const potentialEnergy: Scalar = Scalar.allocate(); + const microWork: Scalar = Scalar.allocate(); + + function LOOM(force: Vector, microDisplacement: Vector) { + // U := U - + Vector.innerProduct(microWork, force, microDisplacement); + Scalar.addScaledByConstant(potentialEnergy, potentialEnergy, -1, microWork); + } + return { potentialEnergy, LOOM }; + } + + function NetKineticEnergy() { + const kineticEnergy: Scalar = Scalar.allocate(); + const velocitySquared: Scalar = Scalar.allocate(); + const netKineticEnergy = { value: 0 }; + + function LOOM(mass: Scalar, velocity: Vector) { + // totalKineticEnergy = 1/2 * sum(m*v^2) + Vector.innerProduct(velocitySquared, velocity, velocity); + Scalar.mul(kineticEnergy, mass, velocitySquared); + netKineticEnergy.value = 0.5*Scalar.sum(kineticEnergy); + } + return { netKineticEnergy, LOOM }; + } + + function SetMomentumAndCenterOfMass() { + const momentum: Vector = Vector.allocate(); + const weightedPosition: Vector = Vector.allocate(); + const centerOfMass = { x: 0, y: 0 }; + const netMass = { value: 0 }; + + function LOOM(mass: Scalar, velocity: Vector) { + Vector.scaleByScalar(momentum, velocity, mass); + + netMass.value = Scalar.sum(mass); + + // centerOfMass := sum(mass * position) / totalMass + Vector.scaleByScalar(weightedPosition, position, mass); + const { x: mx, y: my } = Vector.sum(weightedPosition); + if (netMass.value !== 0) { + centerOfMass.x = mx/netMass.value; + centerOfMass.y = my/netMass.value; + } + } + return { momentum, centerOfMass, LOOM } + } + + let time: Duration = 0; + + const state: State = { + position: Vector.allocate(), // in pixels + velocity: Vector.allocate(), // in pixels per second (not miliseconds) + mass: Scalar.allocate(), + }; + const { velocity, position, mass } = state; + + const G = 0.05; + + const { force, acceleration, LOOM: forceLoom } = Force(); + const { microDisplacement, LOOM: microEvolutionLoom } = MicroEvolution(); + const { potentialEnergy, LOOM: potentialEnergyUpdateLoom } = PotentialEnergyUpdate(); + const { netKineticEnergy, LOOM: netKineticEnergyLoom } = NetKineticEnergy(); + const { momentum, centerOfMass, LOOM: setMomentumAndCenterOfMassLoom } = SetMomentumAndCenterOfMass(); + const weightedPosition: Vector = Vector.allocate(); + const speed: Scalar = Scalar.allocate(); + + function spawn(x: number, y: number, vx: number, vy: number, m: number) { + const idx = arith.getCount(); + + position.x[idx] = x; + position.y[idx] = y; + velocity.x[idx] = vx; + velocity.y[idx] = vy; + state.mass.val[idx] = m; + + arith.setCount(idx + 1); + } + + function reset() { + arith.setCount(0); + + for (let i = 0; i < config.countBig; i++) { + spawn( + // Random position inside the bounding box + random(config.topLeft.x, config.bottomRight.x), + random(config.topLeft.y, config.bottomRight.y), + // Random velocity between -maxVelocity and +maxVelocity + random(-1, 1) * config.maxSpeed, + random(-1, 1) * config.maxSpeed, + // Random mass + random(config.massRangeBig.min, config.massRangeBig.max), + ); + } + for (let i = 0; i < config.countSmall; i++) { + spawn( + // Random position inside the bounding box + random(config.topLeft.x, config.bottomRight.x), + random(config.topLeft.y, config.bottomRight.y), + // Random velocity between -maxVelocity and +maxVelocity + random(-1, 1) * config.maxSpeed, + random(-1, 1) * config.maxSpeed, + // Random mass + random(config.massRangeSmall.min, config.massRangeSmall.max), + ); + } + + Scalar.zero(potentialEnergy); + } + reset(); + + // TODO: how to initialize this without code-duplication? + const observables: Observables = { + particleCount: 0, + averageSpeed: 0, + netMass: 0, + netForce: { fx: 0, fy: 0 }, + netMomentum: { px: 0, py: 0 }, + centerOfMass: { x: 0, y: 0 }, + netKineticEnergy: 0, + netPotentialEnergy: 0, + netPotentialEnergyIntegralCalculation: 0, + netEnergy: 0, + }; + + + function refreshObservables() { + const count = arith.getCount(); + observables.particleCount = count; + + if (count === 0) return; + + Vector.magnitude(speed, velocity); + // Note that this would produce division by zero for `count === 0`. + observables.averageSpeed = Scalar.average(speed); + + // netMass := sum(mass) + const netMass = Scalar.sum(mass); + observables.netMass = netMass; + + // netForce := sum(force) + const { x: fx, y: fy } = Vector.sum(force); + observables.netForce.fx = fx; + observables.netForce.fy = fy; + + setMomentumAndCenterOfMassLoom(mass, velocity); + const { x: px, y: py } = Vector.sum(momentum); + observables.netMomentum.px = px; + observables.netMomentum.py = py; + + observables.centerOfMass.x = centerOfMass.x; + observables.centerOfMass.y = centerOfMass.y; + + netKineticEnergyLoom(mass, velocity); + observables.netKineticEnergy = netKineticEnergy.value; + + // Potential energy + let totalPotential = 0; + for (let i = 0; i < count; i++) { + for (let j = i + 1; j < count; j++) { + const dx = position.x[j] - position.x[i]; + const dy = position.y[j] - position.y[i]; + const distance = Math.sqrt(dx * dx + dy * dy); + + // U = G * m1 * m2 * distance + totalPotential += G * mass.val[i] * mass.val[j] * distance; + } + } + observables.netPotentialEnergy = totalPotential; + observables.netEnergy = observables.netKineticEnergy + observables.netPotentialEnergy; + observables.netPotentialEnergyIntegralCalculation = Scalar.sum(potentialEnergy); + } + + function microFlow(dt: MicroDuration) { + forceLoom(); + const dtInSeconds = dt / 1000.0; + microEvolutionLoom(dtInSeconds); + potentialEnergyUpdateLoom(force, microDisplacement); + } + + // Suppose that we need to flow for 43 miliseconds, and dt is 10 miliseconds. + // Then we do micro-flow 4 times totalling 40 miliseconds. We don't do micro-flow for 3 seconds + // - that could cause problems with integration. + // Instead we remember this leftover time, so the next time we flow, we add this leftover time to the total time to flow. + let leftover_time: Duration = 0; + // Integrates the micro-flow, updates global time, and recomputes the observables. + function flow(t: Duration) { + // TODO: They way we compute leftover time is not so great. There must be a better way. + const end_time = time + t + leftover_time; + + while (true) { + const next_time = time + config.dt; + if (next_time <= end_time) { + time = next_time; + microFlow(config.dt); + } else { + // assume at the start + // dt = 10 ms + // + // time = 0 + // leftover_time = 0 + // t = 43 ms + // then here + // next_time = 50 ms + // time = 40 ms + // end_time = 43 ms + // so actually we have 3 miliseconds leftover, i.e. we set + leftover_time = end_time - time; + break; + } + } + + } + + return { + state, + observables, + flow, + refreshObservables, + totalTime() { return time; }, + reset, + capabilities: { + spawn, + }, + }; +} + diff --git a/src/systems/flat_gravity_solid.tsx b/src/systems/flat_gravity_solid.tsx new file mode 100644 index 0000000..d5f7180 --- /dev/null +++ b/src/systems/flat_gravity_solid.tsx @@ -0,0 +1,245 @@ +import { Config, State, Observables, Capabilities, FlatGravitySystem } from "./flat_gravity"; +import { Duration, mapRange, random, usePointer } from "../helpers"; +import { createEffect, createSignal, onMount } from "solid-js"; +import { ObservableEntry, ObservablesPanel } from "../ui_helpers"; +import { Draw } from "../draw"; +import { useSimulation } from "../useSimulation"; + +export type ViewConfig = { + massRange: { min: number, max: number }, + radiusRange: { min: number, max: number }, + expectedNetMass: number, +} + +function entries(obs: Observables): ObservableEntry[] { + return [ + { type: "header", label: "General" }, + { type: "scalar", label: "Particles", value: obs.particleCount }, + { type: "scalar", label: "Net Mass", value: obs.netMass }, + + { type: "header", label: "Energy" }, + { type: "scalar", label: "Kinetic", value: obs.netKineticEnergy }, + { type: "scalar", label: "Potential (formula)", value: obs.netPotentialEnergy }, + { type: "scalar", label: "Potential (integral)", value: obs.netPotentialEnergyIntegralCalculation }, + { type: "scalar", label: "formula - integral", value: obs.netPotentialEnergy - obs.netPotentialEnergyIntegralCalculation }, + { type: "scalar", label: "Total (formula)", value: obs.netEnergy }, + { type: "scalar", label: "Total (integral)", value: obs.netKineticEnergy + obs.netPotentialEnergyIntegralCalculation }, + + { type: "header", label: "Vectors" }, + { + type: "vector", + label: "Center of Mass", + vector: [ obs.centerOfMass.x, obs.centerOfMass.y ], + components: ["x", "y"] + }, + { + type: "vector", + label: "Net Force", + vector: [ obs.netForce.fx, obs.netForce.fy ], + components: ["fx", "fy"] + }, + { + type: "vector", + label: "Net Momentum", + vector: [ obs.netMomentum.px, obs.netMomentum.py ], + components: ["px", "py"] + }, + ]; +} + +function render( + canvas: HTMLCanvasElement, + ctx: CanvasRenderingContext2D, + system: FlatGravitySystem, + { massRange, radiusRange, expectedNetMass }: ViewConfig, + showCenterOfMass: boolean, +) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const { particleCount, centerOfMass, netMass } = system.observables; + const { position, mass } = system.state; + + for (let i = 0; i < particleCount; i++) { + const m = mass.val[i]; + const radius = mapRange(m, massRange.min, massRange.max, radiusRange.min, radiusRange.max); + // Interpolate Color (Blue = 240, Red = 0) + const hue = mapRange(m, massRange.min, massRange.max, 240, 0); + + Draw.particle(ctx, position.x[i], position.y[i], radius, `hsl(${hue}, 100%, 50%)`); + } + + if (showCenterOfMass && particleCount > 0) { + const radius = mapRange(netMass, 0, expectedNetMass, 0, 300); + Draw.crosshair(ctx, centerOfMass.x, centerOfMass.y, radius, "red"); + } +} + +function renderTrails( + ctx: CanvasRenderingContext2D, + system: FlatGravitySystem, + config: ViewConfig, + opacity: number = 0.03 +) { + ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + const { particleCount } = system.observables; + const { position, mass } = system.state; + const { massRange } = config; + + ctx.fillStyle = "rgba(255, 255, 255, 0.5)"; // Faint white dots + for (let i = 0; i < particleCount; i++) { + const m = mass.val[i]; + + const trailSize = mapRange(m, massRange.min, massRange.max, 1, 3); + + ctx.fillRect( + position.x[i] - trailSize / 2, + position.y[i] - trailSize / 2, + trailSize, + trailSize + ); + } +} + +export function FlatGravityView(props: { system: FlatGravitySystem, viewConfig: ViewConfig }) { + + const [uiState, setUiState] = createSignal(structuredClone(props.system.observables)); + const [showCenterOfMass, setShowCenterOfMass] = createSignal(true); + const [showTrails, setShowTrails] = createSignal(true); + + const width = 800; + const height = 600; + + let canvasRef!: HTMLCanvasElement; + let ctx: CanvasRenderingContext2D; + let trailCanvasRef!: HTMLCanvasElement; + let trailCtx: CanvasRenderingContext2D; + + onMount(() => { + ctx = canvasRef.getContext("2d")!; + trailCtx = trailCanvasRef.getContext("2d")!; + + // Stop the 'ghost image' drag behavior + canvasRef.addEventListener('dragstart', (e) => e.preventDefault()); + + // prevent the context menu on right-click + // if you plan to use right-click for panning later + // canvasRef.addEventListener('contextmenu', (e) => e.preventDefault()); + }); + + const { fps, isPlaying, togglePlay, requestSyncedRender } = useSimulation({ + step(dt: Duration) { + props.system.flow(dt); + }, + render() { + props.system.refreshObservables(); + if (showTrails()) { + renderTrails(trailCtx, props.system, props.viewConfig); + } + render(canvasRef, ctx, props.system, props.viewConfig, showCenterOfMass()); + }, + sync() { + setUiState(structuredClone(props.system.observables)); + }, + isPlaying: true, + }); + + const { position, isDown, handle: handlePointer } = usePointer(() => canvasRef); + + function toggleTrails() { + if (showTrails()) { + // Clear the trail canvas entirely when turned off + trailCtx.clearRect(0, 0, trailCanvasRef.width, trailCanvasRef.height); + setShowTrails(false); + } else { + setShowTrails(true); + } + } + + // spawn new particles on mouse-down + createEffect(() => { + if (isDown()) { + if (props.system.observables.particleCount >= 10000) return; + + const [x, y] = position(); + + // Add a tiny bit of random scatter so they don't perfectly overlap. + const rx = x + random(-5, 5); + const ry = y + random(-5, 5); + + // Spawn a small particle with zero initial velocity (note that net-momentum is not gonna change) + const m = random(1, 5); + props.system.capabilities.spawn(rx, ry, 0, 0, m); + + requestSyncedRender(); + } + }); + + return ( +
+
+ + {/* === Top Controls === */} +
+ + + + + FPS: {fps()} + + + +
+ +
+ + {/* === Reserved for future brushes === */} +
+
+ + {/* === Canvas === */} +
+
+ + +
+
+ + {/* === Observables === */} +
+ +
+ +
+ ); +} + diff --git a/src/systems/simple_kinematics.ts b/src/systems/simple_kinematics.ts new file mode 100644 index 0000000..e20feff --- /dev/null +++ b/src/systems/simple_kinematics.ts @@ -0,0 +1,150 @@ + +import { Arithmetic, Vector, Scalar } from "../arithmetic"; +import { Duration, MicroDuration, random, System } from "../helpers"; + +// read-only config used to initialize the system +export type Config = { + dt: MicroDuration, // e.g. 5 miliseconds + count: number, + // Box in which to generate random particles with uniform distribution, and random velocities + topLeft: {x: number, y: number}, + bottomRight: {x: number, y: number}, + maxSpeed: number, +} + +export type State = { + position: Vector, + velocity: Vector, +} + +export type Observables = { + particleCount: number, + averageSpeed: number, + + // bounding-box + topLeft: {x: number, y: number}, + bottomRight: {x: number, y: number}, +} + +export type SimpleKinematicsSystem = System; + +export function SimpleKinematicsSystem(config: Config): SimpleKinematicsSystem { + const arith = Arithmetic({ count: 0, capacity: 1000 }); + const { Vector, Scalar } = arith; + + let time: Duration = 0; + + const state: State = { + position: Vector.allocate(), + velocity: Vector.allocate(), + }; + const { velocity, position } = state; + + function reset() { + arith.setCount(config.count); + const count = arith.getCount(); + + for (let i = 0; i < count; i++) { + // Random position inside the bounding box + position.x[i] = random(config.topLeft.x, config.bottomRight.x); + position.y[i] = random(config.topLeft.y, config.bottomRight.y); + + // Random velocity between -maxVelocity and +maxVelocity + velocity.x[i] = random(-1, 1) * config.maxSpeed; + velocity.y[i] = random(-1, 1) * config.maxSpeed; + } + } + reset(); + + // TODO: how to initialize this without code-duplication? + const observables: Observables = { + // TODO: Is there a better way of doing this? + // These are dummy values that will be overwritten + particleCount: 0, + averageSpeed: 0, + topLeft: {x: 0, y: 0}, + bottomRight: {x: 0, y: 0}, + }; + + const speed: Scalar = Scalar.allocate(); + function refreshObservables() { + const count = arith.getCount(); + observables.particleCount = count; + + if (count === 0) return; + + // bounding-box + let minX = Infinity, minY = Infinity; + let maxX = -Infinity, maxY = -Infinity; + + for (let i = 0; i < count; i++) { + const x = position.x[i]; + const y = position.y[i]; + if (x < minX) { minX = x; } + if (x > maxX) { maxX = x; } + if (y < minY) { minY = y; } + if (y > maxY) { maxY = y; } + } + observables.topLeft.x = minX; + observables.topLeft.y = minY; + observables.bottomRight.x = maxX; + observables.bottomRight.y = maxY; + + Vector.magnitude(speed, velocity); + // Note that this would produce division by zero for `count === 0`. + observables.averageSpeed = Scalar.average(speed); + } + + // const acceleration: Vector = Vector.allocate(); + // const force: Vector = Vector.allocate(); + // const momentum: Vector = Vector.allocate(); + // const kineticEnergy: Scalar = Vector.allocate(); + function microFlow(dt: MicroDuration) { + // position = position + dt*velocity + Vector.addScaledByConstant(position, position, dt, velocity) + } + + // Suppose that we need to flow for 43 miliseconds, and dt is 10 miliseconds. + // Then we do micro-flow 4 times totalling 40 miliseconds. We don't do micro-flow for 3 seconds + // - that could cause problems with integration. + // Instead we remember this leftover time, so the next time we flow, we add this leftover time to the total time to flow. + let leftover_time: Duration = 0; + // Integrates the micro-flow, updates global time, and recomputes the observables. + function flow(t: Duration) { + // TODO: They way we compute leftover time is not so great. There must be a better way. + const end_time = time + t + leftover_time; + + while (true) { + const next_time = time + config.dt; + if (next_time <= end_time) { + time = next_time; + microFlow(config.dt); + } else { + // assume at the start + // dt = 10 ms + // + // time = 0 + // leftover_time = 0 + // t = 43 ms + // then here + // next_time = 50 ms + // time = 40 ms + // end_time = 43 ms + // so actually we have 3 miliseconds leftover, i.e. we set + leftover_time = end_time - time; + break; + } + } + } + + return { + state, + observables, + flow, + refreshObservables, + totalTime() { return time; }, + reset, + capabilities: {}, + }; +} + diff --git a/src/systems/simple_kinematics_solid.tsx b/src/systems/simple_kinematics_solid.tsx new file mode 100644 index 0000000..3c30026 --- /dev/null +++ b/src/systems/simple_kinematics_solid.tsx @@ -0,0 +1,96 @@ +import { Config, State, Observables, SimpleKinematicsSystem } from "./simple_kinematics"; +import { Duration } from "../helpers"; +import { createSignal, onMount } from "solid-js"; +import { ObservableEntry, ObservablesPanel } from "../ui_helpers"; +import { Draw } from "../draw"; +import { useSimulation } from "../useSimulation"; + +function entries(obs: Observables): ObservableEntry[] { + return [ + { type: "header", label: "General" }, + { type: "scalar", label: "Particles", value: obs.particleCount }, + ]; +} + +function render( + canvas: HTMLCanvasElement, + ctx: CanvasRenderingContext2D, + system: SimpleKinematicsSystem, +) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const { particleCount } = system.observables; + const { position } = system.state; + + for (let i = 0; i < particleCount; i++) { + const radius = 2; + Draw.particle(ctx, position.x[i], position.y[i], radius, `hsl(30, 100%, 50%)`); + } +} + +export function SimpleKinematicsView(props: { system: SimpleKinematicsSystem }) { + + const [uiState, setUiState] = createSignal(structuredClone(props.system.observables)); + + let canvasRef!: HTMLCanvasElement; + let ctx: CanvasRenderingContext2D; + + onMount(() => { + ctx = canvasRef.getContext("2d")!; + }); + + const { fps, isPlaying, togglePlay } = useSimulation({ + step(dt: Duration) { + props.system.flow(dt); + }, + render() { + props.system.refreshObservables(); + render(canvasRef, ctx, props.system); + }, + sync() { + setUiState(structuredClone(props.system.observables)); + }, + isPlaying: true, + }); + + return ( +
+
+ + {/* === Top Controls === */} +
+ + + + + FPS: {fps()} + +
+ +
+ + {/* === Reserved for future brushes === */} +
+
+ + {/* === Canvas === */} +
+ +
+ + {/* === Observables === */} +
+ +
+ +
+ ); +} + diff --git a/src/ui_helpers.tsx b/src/ui_helpers.tsx new file mode 100644 index 0000000..6e7fe5e --- /dev/null +++ b/src/ui_helpers.tsx @@ -0,0 +1,83 @@ +import { formatNumber } from "./helpers"; +import { For, Switch, Match } from "solid-js"; + +type ScalarEntry = { + type: "scalar", + label: string, + value: number, +} +type VectorEntry = { + type: "vector", + label: string, + vector: [number, number] + components: [string, string], +} +type HeaderEntry = { + type: "header", + label: string, +} + +export type ObservableEntry = +| ScalarEntry +| VectorEntry +| HeaderEntry + + +export function ObservablesPanel(props: { data: ObservableEntry[] }) { + return ( +
+ + { (item: ObservableEntry) => ( + + + +
+ { item.label } +
+
+ + +
+ { item.label } + + { formatNumber((item as ScalarEntry).value) } + +
+
+ + +
+ { item.label } + + + { (item as VectorEntry).components[0] }: + + + { formatNumber((item as VectorEntry).vector[0]) } + + + { (item as VectorEntry).components[1] }: + + + { formatNumber((item as VectorEntry).vector[1]) } + +
+
+
+ )} +
+
+ ); +} + diff --git a/src/usePointer.ts b/src/usePointer.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/useSimulation.ts b/src/useSimulation.ts new file mode 100644 index 0000000..dc19477 --- /dev/null +++ b/src/useSimulation.ts @@ -0,0 +1,88 @@ +import { createSignal, onCleanup } from "solid-js"; +import { Duration } from "./helpers"; + +export type SimConfig = { + step(dt: Duration): void, // The physics step + render(): void, // The pure re-rendering + sync(): void, // The UI update (throttled) + isPlaying: boolean, // Should the simulation start immediately, or should it be paused? + syncEveryFrames?: number, // Default 10 + panicCap?: number, // Default 300 +} + +const syncEveryFramesDefault: number = 10; +const panicCapDefault: number = 300; + +// isPlaying == True: +// - the control of the rendering/stepping/syncing is in the hands of the simulation +// - client of the simulation has no control +// isPlaying == False: +// - the control of the rendering/stepping/syncing is in the hands of the client +// +// TODO: Perhaps you should create like two types for such objects, and think of them linearly... +// - one can become the other... but both can't really exist at once... Where are Linear Types when I need them? +export function useSimulation(config: SimConfig) { + const [fps, setFps] = createSignal(0); + const [isPlaying, setIsPlaying] = createSignal(config.isPlaying); + + const { step, render, sync } = config; + const syncEveryFrames = config.syncEveryFrames ?? syncEveryFramesDefault; + const panicCap = config.panicCap ?? panicCapDefault; + + let frameId: number = 0; + let lastTime = performance.now(); + let frameCounter = 0; + + function loop(now: number) { + frameCounter += 1; + + let dt = now - lastTime; + lastTime = now; + if (dt > panicCap) dt = panicCap; + + step(dt); + render(); + + if (frameCounter % syncEveryFrames === 0) { + setFps(Math.round(1000 / dt)); + sync(); + } + + frameId = requestAnimationFrame(loop); + } + + function togglePlay() { + if (isPlaying()) { + cancelAnimationFrame(frameId); + setIsPlaying(false); + } else { + // Reset lastTime so we don't calculate a massive time jump from while it was paused + lastTime = performance.now(); + frameId = requestAnimationFrame(loop); + setIsPlaying(true); + } + } + + function requestSyncedRender() { + if (!isPlaying()) { + render(); + sync(); + } + } + + function requestSimpleRender() { + if (!isPlaying()) { + render(); + } + } + + if (isPlaying()) { + frameId = requestAnimationFrame(loop); + } + + onCleanup(() => { + cancelAnimationFrame(frameId) + }); + + return { fps, isPlaying, togglePlay, requestSyncedRender, requestSimpleRender }; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..777e52f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + "jsx": "preserve", + "jsxImportSource": "solid-js", + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Strictness */ + "strict": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/units_and_types_0.kra b/units_and_types_0.kra new file mode 100755 index 0000000..3ef0687 Binary files /dev/null and b/units_and_types_0.kra differ diff --git a/units_and_types_0.kra~ b/units_and_types_0.kra~ new file mode 100755 index 0000000..33f2064 Binary files /dev/null and b/units_and_types_0.kra~ differ diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..e6c7e26 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite'; +import solidPlugin from 'vite-plugin-solid'; + +export default defineConfig({ + plugins: [solidPlugin()], + build: { + target: 'esnext', + minify: 'esbuild', + }, + server: { + // Required for SharedArrayBuffer or multi-threading + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, + }, +});