diff --git a/sling/.gitignore b/sling/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/sling/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/sling/index.html b/sling/index.html
new file mode 100644
index 0000000..1f015b9
--- /dev/null
+++ b/sling/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ sling
+
+
+
+
+
+
diff --git a/sling/package-lock.json b/sling/package-lock.json
new file mode 100644
index 0000000..141bd22
--- /dev/null
+++ b/sling/package-lock.json
@@ -0,0 +1,971 @@
+{
+ "name": "sling",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "sling",
+ "version": "0.0.0",
+ "dependencies": {
+ "canvas-common": "file:../canvas-common"
+ },
+ "devDependencies": {
+ "typescript": "~5.7.2",
+ "vite": "^6.2.0"
+ }
+ },
+ "../canvas-common": {
+ "version": "1.0.0",
+ "license": "MIT"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
+ "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
+ "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
+ "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
+ "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
+ "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
+ "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
+ "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
+ "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
+ "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
+ "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
+ "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
+ "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
+ "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
+ "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
+ "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
+ "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
+ "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
+ "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
+ "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
+ "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
+ "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
+ "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
+ "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
+ "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
+ "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz",
+ "integrity": "sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.35.0.tgz",
+ "integrity": "sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.35.0.tgz",
+ "integrity": "sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.35.0.tgz",
+ "integrity": "sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.35.0.tgz",
+ "integrity": "sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.35.0.tgz",
+ "integrity": "sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.35.0.tgz",
+ "integrity": "sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.35.0.tgz",
+ "integrity": "sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.35.0.tgz",
+ "integrity": "sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.35.0.tgz",
+ "integrity": "sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.35.0.tgz",
+ "integrity": "sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.35.0.tgz",
+ "integrity": "sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.35.0.tgz",
+ "integrity": "sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.35.0.tgz",
+ "integrity": "sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.35.0.tgz",
+ "integrity": "sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.35.0.tgz",
+ "integrity": "sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.35.0.tgz",
+ "integrity": "sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.35.0.tgz",
+ "integrity": "sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.35.0.tgz",
+ "integrity": "sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/canvas-common": {
+ "resolved": "../canvas-common",
+ "link": true
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
+ "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.0",
+ "@esbuild/android-arm": "0.25.0",
+ "@esbuild/android-arm64": "0.25.0",
+ "@esbuild/android-x64": "0.25.0",
+ "@esbuild/darwin-arm64": "0.25.0",
+ "@esbuild/darwin-x64": "0.25.0",
+ "@esbuild/freebsd-arm64": "0.25.0",
+ "@esbuild/freebsd-x64": "0.25.0",
+ "@esbuild/linux-arm": "0.25.0",
+ "@esbuild/linux-arm64": "0.25.0",
+ "@esbuild/linux-ia32": "0.25.0",
+ "@esbuild/linux-loong64": "0.25.0",
+ "@esbuild/linux-mips64el": "0.25.0",
+ "@esbuild/linux-ppc64": "0.25.0",
+ "@esbuild/linux-riscv64": "0.25.0",
+ "@esbuild/linux-s390x": "0.25.0",
+ "@esbuild/linux-x64": "0.25.0",
+ "@esbuild/netbsd-arm64": "0.25.0",
+ "@esbuild/netbsd-x64": "0.25.0",
+ "@esbuild/openbsd-arm64": "0.25.0",
+ "@esbuild/openbsd-x64": "0.25.0",
+ "@esbuild/sunos-x64": "0.25.0",
+ "@esbuild/win32-arm64": "0.25.0",
+ "@esbuild/win32-ia32": "0.25.0",
+ "@esbuild/win32-x64": "0.25.0"
+ }
+ },
+ "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/nanoid": {
+ "version": "3.3.9",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz",
+ "integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==",
+ "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/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/postcss": {
+ "version": "8.5.3",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
+ "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
+ "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.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.35.0.tgz",
+ "integrity": "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.6"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.35.0",
+ "@rollup/rollup-android-arm64": "4.35.0",
+ "@rollup/rollup-darwin-arm64": "4.35.0",
+ "@rollup/rollup-darwin-x64": "4.35.0",
+ "@rollup/rollup-freebsd-arm64": "4.35.0",
+ "@rollup/rollup-freebsd-x64": "4.35.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.35.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.35.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.35.0",
+ "@rollup/rollup-linux-arm64-musl": "4.35.0",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.35.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.35.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.35.0",
+ "@rollup/rollup-linux-x64-gnu": "4.35.0",
+ "@rollup/rollup-linux-x64-musl": "4.35.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.35.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.35.0",
+ "@rollup/rollup-win32-x64-msvc": "4.35.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "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/typescript": {
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
+ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz",
+ "integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "postcss": "^8.5.3",
+ "rollup": "^4.30.1"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "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
+ }
+ }
+ }
+ }
+}
diff --git a/sling/package.json b/sling/package.json
new file mode 100644
index 0000000..8dee9ba
--- /dev/null
+++ b/sling/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "sling",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "canvas-common": "file:../canvas-common"
+ },
+ "devDependencies": {
+ "typescript": "~5.7.2",
+ "vite": "^6.2.0"
+ }
+}
diff --git a/sling/src/abstracts.ts b/sling/src/abstracts.ts
new file mode 100644
index 0000000..fdc26c5
--- /dev/null
+++ b/sling/src/abstracts.ts
@@ -0,0 +1,97 @@
+import {World} from "./data.ts";
+import {PointGravitySource} from "./objects.ts";
+import {BoundingBox, Circle, Line, Vector} from "./vector.ts";
+
+export interface Drawable extends Item, Positionable {
+ draw(ctx)
+}
+
+export interface Actable extends Item {
+ act(world: World)
+}
+
+export enum CollisionBehaviour {
+ CIRCLE,
+ LINE
+}
+
+export interface Collidable extends Item {
+ collide(world: World, collidable: Collidable);
+ boundingBox(): BoundingBox;
+ geometricCollisionBehaviour(): CollisionBehaviour;
+}
+
+export interface Gravitatable extends Item {
+ affect(gravitySource: PointGravitySource)
+}
+
+export interface Circable {
+ getCircle(): Circle;
+}
+
+export interface Lineable {
+ getLine(): Line;
+}
+
+export interface Positionable {
+ get position(): Vector;
+ set position(vector: Vector);
+}
+
+export interface MassOwning {
+ get mass(): number;
+ set mass(mass: number);
+}
+
+export interface Item {
+ id(): number;
+}
+
+export abstract class AbstractItem implements Item {
+ private _id: number;
+ private _flag: boolean;
+
+ protected constructor() {
+ this._id = ~~(Math.random() * 10000000)
+ this._flag = false;
+ }
+
+ id(): number {
+ return this._id;
+ }
+
+}
+
+export abstract class MovingItem extends AbstractItem implements Positionable, Actable {
+
+ private _speed: Vector;
+ private _position: Vector;
+
+ constructor(_x?: number,
+ _y?: number,
+ _x_speed?: number,
+ _y_speed?: number) {
+ super();
+ this._position = new Vector(_x?? 0, _y?? 0)
+ this._speed = new Vector(_x_speed?? 0, _y_speed?? 0)
+ }
+
+ act(world: World) {
+ this._position = this.position.plus(this._speed)
+ }
+
+ get speed(): Vector {
+ return this._speed;
+ }
+ set speed(vector: Vector){
+ this._speed = vector;
+ }
+ accelerate(vector: Vector) {
+ this._speed = this.speed.plus(vector)
+ }
+
+ get position(): Vector {
+ return this._position;
+ }
+
+}
\ No newline at end of file
diff --git a/sling/src/collision.ts b/sling/src/collision.ts
new file mode 100644
index 0000000..46f53dc
--- /dev/null
+++ b/sling/src/collision.ts
@@ -0,0 +1,246 @@
+import {Circle, Line, Vector} from "./vector.ts";
+import {Circable, Collidable, CollisionBehaviour, Drawable, Lineable, MassOwning, MovingItem} from "./abstracts.ts";
+import {InstanceOfUtils} from "./instances.ts";
+
+export enum CollisionType {
+ MISS = 0,
+ HIT = 1,
+}
+
+export class CollisionResult {
+ constructor(private _type: CollisionType, private _collisionLocation?: Vector) {
+
+ }
+
+ static miss(): CollisionResult {
+ return new CollisionResult(CollisionType.MISS)
+ }
+
+ static hit(point: Vector): CollisionResult {
+ return new CollisionResult(CollisionType.HIT, point)
+ }
+
+ get type(): CollisionType {
+ return this._type;
+ }
+
+ get collision(): Vector | undefined {
+ return this._collisionLocation;
+ }
+}
+
+class CollisionPair {
+ constructor(private _first: CollisionBehaviour, private _second: CollisionBehaviour) {
+ }
+
+
+ get first(): CollisionBehaviour {
+ return this._first;
+ }
+
+ get second(): CollisionBehaviour {
+ return this._second;
+ }
+}
+
+export class CollisionManager {
+
+ private collTypes = new Map CollisionResult>();
+ private collReactions = new Map void>();
+
+ constructor() {
+ this.addCollisionMapper(CollisionBehaviour.LINE, CollisionBehaviour.CIRCLE, this.circleLine)
+ this.addCollisionMapper(CollisionBehaviour.CIRCLE, CollisionBehaviour.CIRCLE, this.circleCircle)
+ this.addCollisionReactions(CollisionBehaviour.LINE, CollisionBehaviour.CIRCLE, this.circleLineAngleOut)
+ this.addCollisionReactions(CollisionBehaviour.CIRCLE, CollisionBehaviour.CIRCLE, this.circleCircleAngleOut)
+ }
+
+ private addCollisionMapper(first: CollisionBehaviour, second: CollisionBehaviour, method: (first: Collidable, second: Collidable) => CollisionResult) {
+ this.collTypes.set(new CollisionPair(first, second), method)
+ this.collTypes.set(new CollisionPair(second, first), method)
+ }
+
+ private addCollisionReactions(first: CollisionBehaviour, second: CollisionBehaviour, method: (first: Collidable, second: Collidable, point: Vector) => void) {
+ this.collReactions.set(new CollisionPair(first, second), method)
+ this.collReactions.set(new CollisionPair(second, first), method)
+ }
+
+ private getCollisionPair(first: CollisionBehaviour, second: CollisionBehaviour): CollisionPair | undefined {
+ for (let [key, value] of this.collTypes.entries()) {
+ if(key.first === first && key.second === second) {
+ return key;
+ }
+ }
+ return undefined;
+ }
+
+ private getCollisionReactionPair(first: CollisionBehaviour, second: CollisionBehaviour): CollisionPair | undefined {
+ for (let [key, value] of this.collReactions.entries()) {
+ if(key.first === first && key.second === second) {
+ return key;
+ }
+ }
+ return undefined;
+ }
+
+ collide(collidable: Collidable, secondCollidable: Collidable): CollisionResult {
+ if(collidable.boundingBox().intersect(secondCollidable.boundingBox())) {
+ let collisionPair = this.getCollisionPair(collidable.geometricCollisionBehaviour(), secondCollidable.geometricCollisionBehaviour());
+ if(collisionPair) {
+ let functionToExecute = this.collTypes.get(collisionPair);
+ let collision = functionToExecute(collidable, secondCollidable);
+ if(collision.type === CollisionType.HIT) {
+ let reactionPair = this.getCollisionReactionPair(collidable.geometricCollisionBehaviour(), secondCollidable.geometricCollisionBehaviour());
+ if(reactionPair) {
+ let functionToExecute = this.collReactions.get(reactionPair);
+ functionToExecute(collidable, secondCollidable, collision.collision!);
+ return collision;
+ } else {
+ console.log(`Did not find a collision reaction pair between ${collidable.geometricCollisionBehaviour()} and ${secondCollidable.geometricCollisionBehaviour()}`)
+ }
+ }
+ } else {
+ console.log(`Did not find a collision pair between ${collidable.geometricCollisionBehaviour()} and ${secondCollidable.geometricCollisionBehaviour()}`)
+ }
+ }
+ return CollisionResult.miss();
+ }
+
+ circleLineAngleOut(first: Collidable, second: Collidable, collisionPoint: Vector): void {
+ let firstCircle = first instanceof MovingItem;
+ let secondCircle = first instanceof MovingItem;
+ if(firstCircle || secondCircle) {
+ let res = CollisionManager.getCircleLine(first, second);
+ let circle = res.circle;
+ let line= res.line;
+
+ let movingItem: MovingItem;
+ if(firstCircle) {
+ movingItem = first as MovingItem;
+ } else {
+ movingItem = second as MovingItem;
+ }
+ let vector = Vector.between(collisionPoint, circle.center).normalize()
+ let lineNormal = line.toVector().normal();
+ let secondLineNormal = line.toVector().otherNormal();
+ let normalToUse = secondLineNormal;
+ if(movingItem.speed.angleBetween(lineNormal) < movingItem.speed.angleBetween(secondLineNormal)) {
+ normalToUse = lineNormal;
+ }
+ let normal = normalToUse.normalize()
+ let distanceAlongNormal = vector.x * normal.x + vector.y * normal.y
+ let x = - 2.0 * distanceAlongNormal * normal.x
+ let y = - 2.0 * distanceAlongNormal * normal.y
+ movingItem.speed = new Vector(x, y)
+ }
+ }
+
+ circleCircleAngleOut(first: Collidable, second: Collidable, collisionPoint: Vector): void {
+ if(first instanceof MovingItem && second instanceof MovingItem) {
+ let firstMass = 1;
+ let firstHasMass = false;
+ if(InstanceOfUtils.instanceOfMassOwning(first)) {
+ firstMass = (first as MassOwning).mass;
+ firstHasMass = true;
+ }
+
+ let secondMass = 1;
+ let secondHasMass = false;
+ if(InstanceOfUtils.instanceOfMassOwning(second)) {
+ secondMass = (second as MassOwning).mass;
+ secondHasMass = true;
+ }
+ let useMass = firstHasMass && secondHasMass;
+ let firstMassFactor = useMass ? 2 * secondMass / (firstMass + secondMass) : 1;
+ let secondMassFactor = useMass ? 2 * firstMass / (firstMass + secondMass) : 1;
+ let v1MinV2 = first.speed.minus(second.speed)
+ let x1MinX2 = first.position.minus(second.position)
+ let v1 = x1MinX2.multNumber(v1MinV2.dot(x1MinX2) / (x1MinX2.secondNorm() ** 2) * firstMassFactor)
+ first.speed = first.speed.minus(v1);
+
+ let v2MinV1 = second.speed.minus(first.speed)
+ let x2Minx1 = second.position.minus(first.position)
+ let v2 = x2Minx1.multNumber(v2MinV1.dot(x2Minx1) / (x2Minx1.secondNorm() ** 2) * secondMassFactor)
+ second.speed = second.speed.minus(v2);
+ } else {
+ let movingItem;
+ let notMovingItem;
+ if(first instanceof MovingItem) {
+ movingItem = first;
+ notMovingItem = second;
+ } else if(second instanceof MovingItem) {
+ movingItem = second;
+ notMovingItem = first;
+ }
+ let movingCircle = (movingItem as Circable).getCircle();
+ let fixedCircle = (notMovingItem as Circable).getCircle();
+ let directionVector = Vector.between(collisionPoint, fixedCircle.center).normalize();
+ let vector = directionVector.normal();
+ let circleNormal = vector.normal()
+ let otherCircleNormal = vector.otherNormal()
+ let normalToUse = circleNormal;
+ if(movingItem.speed.angleBetween(otherCircleNormal) < movingItem.speed.angleBetween(circleNormal)) {
+ normalToUse = otherCircleNormal;
+ }
+ let normal = normalToUse.normalize()
+ let distanceAlongNormal = movingItem.speed.x * normal.x + movingItem.speed.y * normal.y
+ let x = movingItem.speed.x - 2.0 * distanceAlongNormal * normal.x
+ let y = movingItem.speed.y - 2.0 * distanceAlongNormal * normal.y
+ movingItem.speed = new Vector(x, y)
+ }
+ }
+
+ private circleCircle(first: Collidable, second: Collidable) {
+ let firstCircle = (first as Circable).getCircle();
+ let secondCircle = (second as Circable).getCircle();
+ if(!firstCircle.circleCollision(secondCircle)) {
+ return CollisionResult.miss();
+ }
+
+ let vectorBetween = Vector.between(firstCircle.center, secondCircle.center);
+ let collisionPoint = secondCircle.center.plus(vectorBetween.normalize().multNumber(secondCircle.radius));
+ return CollisionResult.hit(collisionPoint)
+ }
+
+ private circleLine(first: Collidable, second: Collidable): CollisionResult {
+ let res = CollisionManager.getCircleLine(first, second);
+ let circle = res.circle;
+ let line= res.line;
+ let collisionPoint = Vector.zero();
+ let dot = ((circle.center.x - line.start.x) * (line.end.x - line.start.x) + (circle.center.y - line.start.y) * (line.end.y - line.start.y)) / Math.pow(line.len, 2)
+ if (circle.pointInside(line.start) || circle.pointInside(line.end)) {
+ collisionPoint = circle.center;
+ } else {
+ let closestX = line.start.x + dot * (line.end.x - line.start.x)
+ let closestY = line.start.y + dot * (line.end.y - line.start.y)
+ let closestPoint = new Vector(closestX, closestY);
+ if (!line.pointCollision(closestPoint)) {
+ return CollisionResult.miss();
+ }
+ let distance = closestPoint.distanceTo(circle.center)
+ if (distance <= circle.radius) {
+ collisionPoint = closestPoint;
+ } else {
+ return CollisionResult.miss();
+ }
+ }
+ return CollisionResult.hit(collisionPoint)
+ }
+
+
+ private static getCircleLine(first: Collidable, second: Collidable): {circle: Circle, line: Line} {
+ let circle;
+ if (InstanceOfUtils.instanceOfCircable(first)) {
+ circle = (first as Circable).getCircle();
+ } else if (InstanceOfUtils.instanceOfCircable(second)) {
+ circle = (second as Circable).getCircle();
+ }
+ let line;
+ if (InstanceOfUtils.instanceOfLineable(first)) {
+ line = (first as Lineable).getLine();
+ } else if (InstanceOfUtils.instanceOfLineable(second)) {
+ line = (second as Lineable).getLine();
+ }
+ return {circle, line};
+ }
+}
\ No newline at end of file
diff --git a/sling/src/data.ts b/sling/src/data.ts
new file mode 100644
index 0000000..365735c
--- /dev/null
+++ b/sling/src/data.ts
@@ -0,0 +1,87 @@
+import {Actable, Collidable, Drawable, Gravitatable, Item} from "./abstracts.ts";
+import {PointGravitySource} from "./objects.ts";
+import {InstanceOfUtils} from "./instances.ts";
+import {CollisionManager} from "./collision.ts";
+
+export class World {
+ private _items: Item[] = [];
+ private _drawable: Drawable[] = [];
+ private _actable: Actable[] = [];
+ private _collidable: Collidable[] = [];
+ private _gravitatable: Gravitatable[] = [];
+
+ private _collisionManager: CollisionManager = new CollisionManager();
+
+ constructor() {
+ }
+
+ addItem(item: Item) {
+ this._items.push(item)
+ if(InstanceOfUtils.instanceOfDrawable(item)) {
+ this._drawable.push(item)
+ }
+
+ if(InstanceOfUtils.instanceOfActable(item)) {
+ this._actable.push(item)
+ }
+
+ if(InstanceOfUtils.instanceOfGravitatable(item)) {
+ this._gravitatable.push(item)
+ }
+ if(InstanceOfUtils.instanceOfCollidable(item)) {
+ this._collidable.push(item)
+ }
+ }
+
+ removeItem(itemToRemove: Item) {
+ this._items = this._items.filter(item => item.id() !== itemToRemove.id())
+
+ if(InstanceOfUtils.instanceOfDrawable(itemToRemove)) {
+ this._drawable = this._drawable.filter(item => item.id() !== itemToRemove.id())
+ }
+
+ if(InstanceOfUtils.instanceOfActable(itemToRemove)) {
+ this._actable = this._actable.filter(item => item.id() !== itemToRemove.id())
+ }
+
+ if(InstanceOfUtils.instanceOfGravitatable(itemToRemove)) {
+ this._gravitatable = this._gravitatable.filter(item => item.id() !== itemToRemove.id())
+ }
+
+ if(InstanceOfUtils.instanceOfCollidable(itemToRemove)) {
+ this._collidable = this._collidable.filter(item => item.id() !== itemToRemove.id())
+ }
+ }
+
+ act() {
+ this.collide()
+ this._actable.forEach(value => value.act(this))
+ }
+
+ collide() {
+ let collisionsDone = {}
+ this._collidable.forEach(value => {
+ this._collidable.forEach(innerCollidable => {
+ let collidableKey = Math.min(value.id(), innerCollidable.id()) + '_' + Math.max(value.id(), innerCollidable.id());
+ if(value.id() !== innerCollidable.id() && !(collidableKey in collisionsDone)) {
+ value.collide(this, innerCollidable);
+ collisionsDone[collidableKey] = 1;
+ }
+ });
+ })
+ }
+
+ draw(ctx) {
+ this._drawable.forEach(value => value.draw(ctx))
+ }
+
+ applyGravity(gravitySource: PointGravitySource) {
+ this._gravitatable.forEach(value => value.affect(gravitySource))
+ }
+
+
+ get collisionManager(): CollisionManager {
+ return this._collisionManager;
+ }
+}
+
diff --git a/sling/src/generic.ts b/sling/src/generic.ts
new file mode 100644
index 0000000..f1c1629
--- /dev/null
+++ b/sling/src/generic.ts
@@ -0,0 +1,8 @@
+export class Color {
+ constructor(private _r: number, private _g: number, private _b: number, private _a?: number) {
+ }
+
+ repr(): string {
+ return `rgb(${this._r}, ${this._g}, ${this._b})`
+ }
+}
\ No newline at end of file
diff --git a/sling/src/instances.ts b/sling/src/instances.ts
new file mode 100644
index 0000000..328f0ea
--- /dev/null
+++ b/sling/src/instances.ts
@@ -0,0 +1,31 @@
+import {Actable, Circable, Collidable, Drawable, Gravitatable, Lineable, MassOwning} from "./abstracts.ts";
+
+export class InstanceOfUtils {
+ static instanceOfCircable(object: any): object is Circable{
+ return 'getCircle' in object;
+ }
+
+ static instanceOfLineable(object: any): object is Lineable {
+ return 'getLine' in object;
+ }
+
+ static instanceOfDrawable(object: any): object is Drawable {
+ return 'draw' in object;
+ }
+
+ static instanceOfGravitatable(object: any): object is Gravitatable {
+ return 'affect' in object;
+ }
+
+ static instanceOfCollidable(object: any): object is Collidable {
+ return 'collide' in object;
+ }
+
+ static instanceOfActable(object: any): object is Actable {
+ return 'act' in object;
+ }
+
+ static instanceOfMassOwning(object: any): object is MassOwning {
+ return 'mass' in object;
+ }
+}
\ No newline at end of file
diff --git a/sling/src/main.ts b/sling/src/main.ts
new file mode 100644
index 0000000..04a006f
--- /dev/null
+++ b/sling/src/main.ts
@@ -0,0 +1,72 @@
+import {docReady} from "canvas-common";
+
+import './style.css'
+import {World} from "./data.ts";
+import {Bubble, CircleBarrier, PointGravitySource, LineBarrier, DirectionalGravitySource} from "./objects.ts";
+import {Vector} from "./vector.ts";
+
+let canvas;
+let ctx;
+let animationId;
+
+let world = new World();
+
+let config = {
+ general: {
+ size: {
+ height: window.innerHeight,
+ width: window.innerWidth
+ },
+ fps: 60,
+ debug: true
+ }
+};
+
+declare global {
+ interface Window { config: any; }
+}
+
+window.config = config;
+
+function loadWorld() {
+ let borderSize = 0;
+ world.addItem(new PointGravitySource(config.general.size.width / 2, config.general.size.height / 2, 10))
+ for (let i = 0; i < 12; i++) {
+ world.addItem(new Bubble(config.general.size.width * Math.random(), config.general.size.height * Math.random(), Math.random() * 25))
+ }
+
+ // diamond shape
+ //world.addItem(new LineBarrier(new Vector(config.general.size.width / 2, 0), new Vector(config.general.size.width, config.general.size.height / 2)))
+ //world.addItem(new LineBarrier(new Vector(config.general.size.width, config.general.size.height / 2), new Vector(config.general.size.width / 2, config.general.size.height)))
+ //world.addItem(new LineBarrier(new Vector(config.general.size.width / 2, config.general.size.height), new Vector(0, config.general.size.height / 2)))
+ //world.addItem(new LineBarrier(new Vector(0, config.general.size.height / 2), new Vector(config.general.size.width / 2, 0)))
+
+ world.addItem(new CircleBarrier(new Vector(config.general.size.width / 2, config.general.size.height / 2), 150));
+ // borders
+ world.addItem(new LineBarrier(new Vector(borderSize, borderSize), new Vector(config.general.size.width - borderSize, borderSize)))
+ world.addItem(new LineBarrier(new Vector(config.general.size.width - borderSize, borderSize), new Vector(config.general.size.width - borderSize, config.general.size.height - borderSize)))
+ world.addItem(new LineBarrier(new Vector(borderSize, config.general.size.height - borderSize), new Vector(config.general.size.width - borderSize, config.general.size.height - borderSize)))
+ world.addItem(new LineBarrier(new Vector(borderSize, config.general.size.height - borderSize), new Vector(borderSize, borderSize)))
+}
+
+docReady(function() {
+ canvas = document.getElementById('canvas')
+ canvas.width = config.general.size.width;
+ canvas.height = config.general.size.height;
+ ctx = canvas.getContext("2d");
+ ctx.translate(0.5, 0.5); // to make better anti-aliasing
+ loadWorld();
+ requestAnimationFrame(render);
+});
+
+
+function render() {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ world.draw(ctx);
+ world.act()
+ setTimeout(function () {
+ animationId = requestAnimationFrame(render);
+ }, 1000 / config.general.fps)
+}
+
+
diff --git a/sling/src/objects.ts b/sling/src/objects.ts
new file mode 100644
index 0000000..1a9cc74
--- /dev/null
+++ b/sling/src/objects.ts
@@ -0,0 +1,331 @@
+import {
+ AbstractItem,
+ Actable,
+ Circable,
+ Collidable,
+ CollisionBehaviour,
+ Drawable,
+ Gravitatable,
+ Lineable,
+ MassOwning,
+ MovingItem,
+ Positionable
+} from "./abstracts.ts";
+import {Color} from "./generic.ts";
+import {World} from "./data.ts";
+import {BoundingBox, Circle, Line, Vector} from "./vector.ts";
+import {CollisionType} from "./collision.ts";
+
+export class Bubble extends MovingItem implements Gravitatable, Drawable, Collidable, Circable, MassOwning {
+ private _radius: number;
+ private _color: Color;
+ private _score: number;
+ private _mass: number
+
+ constructor(_x: number,
+ _y: number,
+ radius?: number,
+ color?: Color,
+ _speed?: Vector,
+ _score?: number) {
+ super(_x, _y, _speed?.x ?? 0, _speed?.y ?? 0)
+ this._radius = radius ?? 10;
+ this._color = color ?? new Color(120, 120, 120);
+ this._score = _score ?? 1;
+ this._mass = 1;
+ }
+
+
+ act(world: World) {
+ super.act(world);
+ }
+
+ draw(ctx) {
+ ctx.beginPath();
+ if (this.color) {
+ ctx.fillStyle = this.color.repr();
+ }
+ ctx.arc(this.x, this.y, this._radius, 0, 2 * Math.PI);
+ ctx.stroke()
+ if(window.config.general.debug) {
+ let box = this.boundingBox();
+ ctx.beginPath();
+ ctx.rect(box.topLeft.x, box.topLeft.y, box.len.x, box.len.y)
+ ctx.stroke();
+ }
+ ctx.beginPath();
+ ctx.fillStyle = 'red'
+ ctx.fillText(this._score, this.x, this.y)
+ ctx.stroke()
+ ctx.fillStyle = 'black'
+ }
+
+ get x(): number {
+ return this.position.x;
+ }
+
+ get y(): number {
+ return this.position.y;
+ }
+
+ get radius(): number {
+ return this._radius;
+ }
+
+ get color(): Color {
+ return this._color;
+ }
+
+ affect(gravitySource: PointGravitySource) {
+ let vector = Vector.between(gravitySource.position, this.position);
+ let force = gravitySource.getForce(vector);
+ this.accelerate(force)
+ }
+
+ boundingBox(): BoundingBox {
+ let topLeft = this.position.minus(new Vector(this._radius, this._radius))
+ let len = new Vector(this._radius * 2, this._radius * 2)
+ return new BoundingBox(topLeft, len);
+ }
+
+ collide(world: World, collidable: Collidable) {
+ let collisionResult = world.collisionManager.collide(this, collidable);
+ if(collisionResult.type === CollisionType.HIT) {
+ if(collidable instanceof Bubble) {
+ let collidingBubble = (collidable as Bubble)
+ if(this._score > collidingBubble._score && this.radius > collidingBubble.radius) {
+ this._score += collidingBubble._score;
+ this._radius += collidingBubble._radius;
+ this._mass += collidingBubble._mass;
+ world.removeItem(collidable)
+ } else {
+ collidingBubble._score += this._score;
+ collidingBubble._radius += this._radius;
+ collidingBubble._mass += this._mass;
+ world.removeItem(this)
+ }
+ }
+ }
+ }
+
+ geometricCollisionBehaviour(): CollisionBehaviour {
+ return CollisionBehaviour.CIRCLE;
+ }
+
+ getCircle(): Circle {
+ return new Circle(this.position, this._radius);
+ }
+
+
+ get score(): number {
+ return this._score;
+ }
+
+
+ set score(value: number) {
+ this._score = value;
+ }
+
+ get mass(): number {
+ return this._mass;
+ }
+
+ set mass(mass: number) {
+ this._mass = mass;
+ }
+}
+
+export class PointGravitySource extends AbstractItem implements Actable, Positionable, Drawable {
+
+ private _force: number;
+ private _position: Vector;
+
+ constructor(_x: number,
+ _y: number,
+ force?: number) {
+ super();
+ this._position = new Vector(_x, _y)
+ this._force = force ?? 10;
+ }
+
+ act(world: World) {
+ world.applyGravity(this)
+ }
+
+ getForce(distanceVector: Vector): Vector {
+ let distance = distanceVector.len();
+ return new Vector(distanceVector.x * this._force / (distance * distance), distanceVector.y * this._force / (distance * distance))
+ }
+
+ get x() {
+ return this._position.x;
+ }
+
+ get y() {
+ return this._position.y;
+ }
+
+ get position(): Vector {
+ return this._position;
+ }
+
+ draw(ctx) {
+ if(window.config.general.debug) {
+ ctx.beginPath()
+ ctx.strokeStyle = 'red'
+ ctx.arc(this.x, this.y, 10, 0, 2 * Math.PI);
+ ctx.stroke()
+ ctx.strokeStyle= 'black'
+ }
+ }
+}
+
+export class DirectionalGravitySource extends AbstractItem implements Actable, Positionable, Drawable {
+ private _force: number;
+ private _position: Vector;
+ private _direction: Vector;
+ private _start: Vector;
+ private _end: Vector;
+
+
+ constructor(_x: number,
+ _y: number,
+ _direction?: Vector,
+ force?: number) {
+ super();
+ this._position = new Vector(_x, _y)
+ this._direction = _direction?? new Vector(1, 0);
+ this._start = this._position.minus(this._direction.normal().multNumber(25))
+ this._end = this._position.minus(this._direction.otherNormal().multNumber(25))
+ this._force = force ?? 10;
+ }
+
+ act(world: World) {
+ world.applyGravity(this)
+ }
+
+ getForce(distanceVector: Vector): Vector {
+ let distance = distanceVector.len();
+ return new Vector(distanceVector.x * this._force / (distance * distance) * this._direction.x, distanceVector.y * this._force / (distance * distance) * this._direction.y)
+ }
+
+ get x() {
+ return this._position.x;
+ }
+
+ get y() {
+ return this._position.y;
+ }
+
+ get position(): Vector {
+ return this._position;
+ }
+
+ draw(ctx) {
+ if(window.config.general.debug) {
+ ctx.beginPath();
+ ctx.strokeStyle = 'red'
+ ctx.moveTo(this._start.x, this._start.y);
+ ctx.lineTo(this._end.x, this._end.y);
+ ctx.stroke();
+ ctx.strokeStyle = 'black'
+ }
+ }
+}
+
+export abstract class Barrier extends AbstractItem implements Collidable, Positionable, Drawable {
+ private _position: Vector;
+
+ constructor(position: Vector) {
+ super();
+ this._position = position;
+ }
+
+ abstract collide(world: World, collidable: Collidable);
+ abstract boundingBox(): BoundingBox;
+ abstract geometricCollisionBehaviour(): CollisionBehaviour;
+
+
+ get position(): Vector {
+ return this._position;
+ }
+
+ set position(value: Vector) {
+ this._position = value;
+ }
+
+ abstract draw(ctx);
+}
+
+export class CircleBarrier extends Barrier implements Circable {
+
+ private _radius: number;
+
+ constructor(position: Vector, radius: number) {
+ super(position);
+ this._radius = radius;
+ }
+
+ boundingBox(): BoundingBox {
+ let topLeft = this.position.minus(new Vector(this._radius, this._radius))
+ let len = new Vector(this._radius * 2, this._radius * 2)
+ return new BoundingBox(topLeft, len);
+ }
+
+ collide(world: World, collidable: Collidable) {
+ }
+
+ geometricCollisionBehaviour(): CollisionBehaviour {
+ return CollisionBehaviour.CIRCLE;
+ }
+
+ getCircle(): Circle {
+ return new Circle(this.position, this._radius);
+ }
+
+ draw(ctx) {
+ ctx.beginPath();
+ ctx.arc(this.position.x, this.position.y, this._radius, 0, 2 * Math.PI);
+ ctx.stroke()
+ }
+}
+
+export class LineBarrier extends Barrier implements Lineable {
+ private _line: Line;
+
+ constructor(start: Vector, end: Vector) {
+ super(start)
+ this._line = new Line(start, end);
+ }
+
+ draw(ctx) {
+ ctx.beginPath();
+ ctx.moveTo(this._line.start.x, this._line.start.y);
+ ctx.lineTo(this._line.end.x, this._line.end.y);
+ ctx.stroke();
+ if(window.config.general.debug) {
+ let box = this.boundingBox();
+ ctx.beginPath();
+ ctx.rect(box.topLeft.x, box.topLeft.y, box.len.x, box.len.y)
+ ctx.stroke()
+ }
+ }
+
+ boundingBox(): BoundingBox {
+ let topLeft = new Vector(Math.min(this._line.start.x, this._line.end.x), Math.min(this._line.start.y, this._line.end.y))
+ let len = new Vector(this._line.start.x - this._line.end.x, this._line.start.y - this._line.end.y)
+ return new BoundingBox(topLeft, len.abs());
+ }
+
+ collide(world: World, collidable: Collidable) {
+ }
+
+ geometricCollisionBehaviour(): CollisionBehaviour {
+ return CollisionBehaviour.LINE;
+ }
+
+ getLine(): Line {
+ return this._line;
+ }
+
+}
\ No newline at end of file
diff --git a/sling/src/style.css b/sling/src/style.css
new file mode 100644
index 0000000..8e56c29
--- /dev/null
+++ b/sling/src/style.css
@@ -0,0 +1,20 @@
+:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+html, body { width:100%; height:100%; }
+
+html, body, div, canvas {
+ margin: 0;
+ padding: 0;
+}
+
+canvas { display:block; }
\ No newline at end of file
diff --git a/sling/src/vector.ts b/sling/src/vector.ts
new file mode 100644
index 0000000..1a4bb65
--- /dev/null
+++ b/sling/src/vector.ts
@@ -0,0 +1,171 @@
+export class Vector {
+ constructor(private _x: number, private _y: number) {
+ }
+
+ static zero(): Vector {
+ return new Vector(0, 0)
+ }
+
+ static between(pointy: Vector, shaft : Vector) {
+ return new Vector(pointy.x - shaft.x, pointy.y - shaft.y)
+ }
+
+ plus(vector: Vector) {
+ return new Vector(this._x + vector.x, this._y + vector.y)
+ }
+
+ minus(vector: Vector) {
+ return new Vector(this._x - vector.x, this._y - vector.y)
+ }
+
+ dot(vector: Vector): number {
+ return this._x * vector._x + this._y * vector._y
+ }
+
+ normal() {
+ return new Vector(-this.y, this.x)
+ }
+
+ otherNormal() {
+ return new Vector(this.y, -this.x)
+ }
+
+ normalize() {
+ let length = this.len();
+ return new Vector(this.x / length, this.y / length)
+ }
+
+ secondNorm() {
+ return Math.sqrt(this._x * this._x + this._y * this._y)
+ }
+
+ divide(factor: number) {
+ return new Vector(this._x / factor, this._y / factor)
+ }
+
+ angleBetween(vector: Vector) {
+ return Math.atan2(this._x * vector._y - this._y * vector._x, this._x * vector._x + this._y * vector._y) * 180 / Math.PI;
+ }
+
+ multNumber(factor: number) {
+ return new Vector(this._x * factor, this._y * factor)
+ }
+
+ mult(vector: Vector) {
+ return new Vector(this._x * vector.x, this._y * vector.y)
+ }
+
+ len() {
+ return Math.sqrt(this.x * this.x + this.y * this.y)
+ }
+
+ isZero() {
+ return this.x === 0 && this.y === 0;
+ }
+
+ distanceTo(vector: Vector): number {
+ return Math.sqrt(Math.pow(vector.x - this.x, 2) + Math.pow(vector.y - this.y, 2))
+ }
+
+ abs() {
+ return new Vector(Math.abs(this.x), Math.abs(this.y))
+ }
+
+ invert() {
+ return new Vector(-this.x, -this.y)
+ }
+
+ get x(): number {
+ return this._x;
+ }
+
+ get y(): number {
+ return this._y;
+ }
+}
+
+export class Line {
+ constructor(private _start: Vector, private _end: Vector) {
+ }
+
+ get start(): Vector {
+ return this._start;
+ }
+
+ get end(): Vector {
+ return this._end;
+ }
+
+ toVector() {
+ return Vector.between(this._end, this._start)
+ }
+
+ get len(): number {
+ let distX = this.end.x - this.start.x;
+ let distY = this.end.y - this.start.y;
+ return Math.sqrt(distX * distX + distY * distY)
+ }
+
+ pointCollision(point: Vector) {
+ // https://www.jeffreythompson.org/collision-detection/line-point.php
+ let d1 = point.distanceTo(this.start)
+ let d2 = point.distanceTo(this.end)
+ let len = this.len;
+ const buffer = 0.1
+ if((d1 + d2) >= (len - buffer) && (d1 + d2) <= (len + buffer)) {
+ return true
+ } else {
+ return false;
+ }
+ }
+}
+
+export class Circle {
+ constructor(private _center: Vector, private _radius: number) {
+ }
+
+ pointInside(point: Vector) {
+ return this._center.distanceTo(point) <= this._radius;
+ }
+
+ circleCollision(circle: Circle) {
+ return this._center.distanceTo(circle.center) <= this._radius + circle.radius;
+ }
+
+ get center(): Vector {
+ return this._center;
+ }
+
+
+ get radius(): number {
+ return this._radius;
+ }
+}
+
+export class BoundingBox {
+ constructor(private _topLeft: Vector, private _len: Vector) {
+ }
+
+ // https://www.jeffreythompson.org/collision-detection/rect-rect.php
+ intersect(box: BoundingBox): boolean {
+ // r1 = this
+ // r2 = box
+ if (this._topLeft.x + this._len.x >= box._topLeft.x &&
+ this._topLeft.x <= box._topLeft.x + box._len.x &&
+ this._topLeft.y + this._len.y >= box._topLeft.y &&
+ this._topLeft.y <= box._topLeft.y + box._len.y) {
+ return true
+ } else {
+ return false;
+ }
+ }
+
+
+ get topLeft(): Vector {
+ return this._topLeft;
+ }
+
+ get len(): Vector {
+ return this._len;
+ }
+}
\ No newline at end of file
diff --git a/sling/src/vite-env.d.ts b/sling/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/sling/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/sling/tsconfig.json b/sling/tsconfig.json
new file mode 100644
index 0000000..a4883f2
--- /dev/null
+++ b/sling/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}