From ef3aab7284401dea45875870dcf5557e04d1b804 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:31:09 +0000 Subject: [PATCH 1/3] Initial plan From c3fa5e9c79285053f34e1f20af124535df00ba31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:53:05 +0000 Subject: [PATCH 2/3] feat: Complete 2D Boids simulation with WebGPU + Svelte 5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Svelte 5 + Vite 6 project scaffold - WebGPU compute pipeline (binning → boid-logic → render) - Ping-pong boid storage buffers with toroidal topology - Spatial grid binning shader (atomicAdd, 64 boids/cell max) - Boid logic shader: separation, cohesion, alignment, predator avoidance, goal seeking, SDF obstacle avoidance, MIC toroidal distances - Render shader: velocity-oriented triangles, blue boids / red predators - WebGL2 + CPU fallback (spatial hash, instanced rendering) - App.svelte: full-screen canvas, 2D overlay for grid/goals/obstacles, pointer events for goal/obstacle placement and drag - ControlPanel.svelte: all sliders, toolbox, FPS display, GPU status Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 2 + index.html | 16 + package-lock.json | 1422 +++++++++++++++++++++++++ package.json | 16 + src/App.svelte | 325 ++++++ src/components/ControlPanel.svelte | 260 +++++ src/main.js | 8 + src/simulation/BoidSimulation.js | 410 +++++++ src/simulation/WebGLFallback.js | 409 +++++++ src/simulation/shaders/binning.wgsl | 58 + src/simulation/shaders/boidLogic.wgsl | 246 +++++ src/simulation/shaders/render.wgsl | 76 ++ vite.config.js | 6 + 13 files changed, 3254 insertions(+) create mode 100644 .gitignore create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/App.svelte create mode 100644 src/components/ControlPanel.svelte create mode 100644 src/main.js create mode 100644 src/simulation/BoidSimulation.js create mode 100644 src/simulation/WebGLFallback.js create mode 100644 src/simulation/shaders/binning.wgsl create mode 100644 src/simulation/shaders/boidLogic.wgsl create mode 100644 src/simulation/shaders/render.wgsl create mode 100644 vite.config.js 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/index.html b/index.html new file mode 100644 index 0000000..1a3b65e --- /dev/null +++ b/index.html @@ -0,0 +1,16 @@ + + + + + + Boids Simulation + + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0412af0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1422 @@ +{ + "name": "boids", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "boids", + "version": "0.0.0", + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "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.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", + "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "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/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/devalue": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", + "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@typescript-eslint/types": "^8.2.0" + } + }, + "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/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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/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.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "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.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.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/svelte": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.0.tgz", + "integrity": "sha512-SThllKq6TRMBwPtat7ASnm/9CDXnIhBR0NPGw0ujn2DVYx9rVwsPZxDaDQcYGdUz/3BYVsCzdq7pZarRQoGvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.4", + "esm-env": "^1.2.1", + "esrap": "^2.2.2", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "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/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "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 + } + } + }, + "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/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9cbdbc4 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "boids", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } +} diff --git a/src/App.svelte b/src/App.svelte new file mode 100644 index 0000000..747f644 --- /dev/null +++ b/src/App.svelte @@ -0,0 +1,325 @@ + + +
+ + +
+ + +
+ + + { goals = [] }} + onclearObstacles={() => { obstacles = [] }} + /> +
+ + diff --git a/src/components/ControlPanel.svelte b/src/components/ControlPanel.svelte new file mode 100644 index 0000000..c45a647 --- /dev/null +++ b/src/components/ControlPanel.svelte @@ -0,0 +1,260 @@ + + + + + diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..37b4dbb --- /dev/null +++ b/src/main.js @@ -0,0 +1,8 @@ +import { mount } from 'svelte' +import App from './App.svelte' + +const app = mount(App, { + target: document.getElementById('app'), +}) + +export default app diff --git a/src/simulation/BoidSimulation.js b/src/simulation/BoidSimulation.js new file mode 100644 index 0000000..9359df3 --- /dev/null +++ b/src/simulation/BoidSimulation.js @@ -0,0 +1,410 @@ +import binningShader from './shaders/binning.wgsl?raw' +import boidLogicShader from './shaders/boidLogic.wgsl?raw' +import renderShader from './shaders/render.wgsl?raw' + +// ── Buffer-layout constants (must match WGSL structs) ── +const BOID_FLOATS = 6 // pos(2) vel(2) type(1) pad(1) +const OBSTACLE_FLOATS = 8 // type x y param1 param2 pad*3 +const GOAL_FLOATS = 4 // x y strength pad + +// ── Pre-allocated maximums ── +const MAX_TOTAL_BOIDS = 20_100 // 20 000 prey + 100 predators +const MAX_CELLS = 65_536 // 256 × 256 grid +const MAX_PER_CELL = 64 +const MAX_OBSTACLES = 64 +const MAX_GOALS = 16 +const MAX_GRID_DIM = 256 + +export class BoidSimulation { + constructor (canvas) { + this.canvas = canvas + this.device = null + this.context = null + this.format = null + + // GPU buffers + this.boidBufA = null + this.boidBufB = null + this.paramsBuf = null + this.cellCountBuf = null + this.cellBoidsBuf = null + this.obstacleBuf = null + this.goalBuf = null + this.renderParamsBuf = null + + // GPU pipelines + this.binningPipeline = null + this.logicPipeline = null + this.renderPipeline = null + + // Simulation state + this.numBoids = 500 + this.numPredators = 5 + this.width = 800 + this.height = 600 + this.gridWidth = 16 + this.gridHeight = 12 + this.params = { + separationWeight : 1.5, + cohesionWeight : 1.0, + alignmentWeight : 1.0, + perceptionRadius : 50, + maxSpeed : 3.0, + } + this.obstacles = [] + this.goals = [] + this.initialized = false + } + + // ── Public: initialise WebGPU ───────────────────────────────────────────── + async init () { + if (!navigator.gpu) throw new Error('WebGPU not supported') + + const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' }) + if (!adapter) throw new Error('No WebGPU adapter available') + + this.device = await adapter.requestDevice() + this.device.addEventListener('uncapturederror', ev => + console.error('[WebGPU device error]', ev.error)) + + this.context = this.canvas.getContext('webgpu') + if (!this.context) throw new Error('Cannot get WebGPU context from canvas') + + this.format = navigator.gpu.getPreferredCanvasFormat() + this.width = Math.max(1, this.canvas.width) + this.height = Math.max(1, this.canvas.height) + + this.context.configure({ + device : this.device, + format : this.format, + alphaMode : 'premultiplied', + }) + + this._createBuffers() + await this._createPipelines() + this._initBoids() + this.initialized = true + } + + // ── Private helpers ─────────────────────────────────────────────────────── + _createBuffers () { + const d = this.device + const bSz = MAX_TOTAL_BOIDS * BOID_FLOATS * 4 + + this.boidBufA = d.createBuffer({ + label : 'boids-A', + size : bSz, + usage : GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }) + this.boidBufB = d.createBuffer({ + label : 'boids-B', + size : bSz, + usage : GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }) + + // Params uniform (16 × f32/u32 = 64 bytes) + this.paramsBuf = d.createBuffer({ + label : 'params', + size : 64, + usage : GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }) + + // Spatial grid + this.cellCountBuf = d.createBuffer({ + label : 'cellCount', + size : MAX_CELLS * 4, + usage : GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }) + this.cellBoidsBuf = d.createBuffer({ + label : 'cellBoids', + size : MAX_CELLS * MAX_PER_CELL * 4, + usage : GPUBufferUsage.STORAGE, + }) + + this.obstacleBuf = d.createBuffer({ + label : 'obstacles', + size : Math.max(MAX_OBSTACLES * OBSTACLE_FLOATS * 4, 32), + usage : GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }) + this.goalBuf = d.createBuffer({ + label : 'goals', + size : Math.max(MAX_GOALS * GOAL_FLOATS * 4, 32), + usage : GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }) + + // Render params uniform (16 bytes) + this.renderParamsBuf = d.createBuffer({ + label : 'renderParams', + size : 16, + usage : GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }) + } + + async _createPipelines () { + const d = this.device + + // Binning + const binMod = d.createShaderModule({ label: 'binning', code: binningShader }) + this.binningPipeline = d.createComputePipeline({ + label : 'binning-pipeline', + layout : 'auto', + compute : { module: binMod, entryPoint: 'main' }, + }) + + // Boid logic + const logicMod = d.createShaderModule({ label: 'boidLogic', code: boidLogicShader }) + this.logicPipeline = d.createComputePipeline({ + label : 'boidLogic-pipeline', + layout : 'auto', + compute : { module: logicMod, entryPoint: 'main' }, + }) + + // Render + const renderMod = d.createShaderModule({ label: 'render', code: renderShader }) + this.renderPipeline = d.createRenderPipeline({ + label : 'render-pipeline', + layout : 'auto', + vertex : { module: renderMod, entryPoint: 'vs_main' }, + fragment : { + module : renderMod, + entryPoint : 'fs_main', + targets : [{ + format : this.format, + blend : { + color : { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' }, + alpha : { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' }, + }, + }], + }, + primitive : { topology: 'triangle-list' }, + }) + + // Surface any shader compilation errors + for (const [name, mod] of [['binning', binMod], ['boidLogic', logicMod], ['render', renderMod]]) { + const info = await mod.getCompilationInfo() + for (const msg of info.messages) { + if (msg.type === 'error') + throw new Error(`[${name}] shader error at line ${msg.lineNum}: ${msg.message}`) + if (msg.type === 'warning') + console.warn(`[${name}] shader warning at line ${msg.lineNum}: ${msg.message}`) + } + } + } + + _initBoids () { + const total = this.numBoids + this.numPredators + const data = new Float32Array(MAX_TOTAL_BOIDS * BOID_FLOATS) + for (let i = 0; i < total; i++) { + const base = i * BOID_FLOATS + const angle = Math.random() * Math.PI * 2 + const speed = (0.4 + Math.random() * 0.6) * this.params.maxSpeed + data[base + 0] = Math.random() * this.width + data[base + 1] = Math.random() * this.height + data[base + 2] = Math.cos(angle) * speed + data[base + 3] = Math.sin(angle) * speed + data[base + 4] = i < this.numBoids ? 0 : 1 + data[base + 5] = 0 + } + this.device.queue.writeBuffer(this.boidBufA, 0, data) + } + + _writeParamsBuffer (dt) { + const p = this.params + const pr = Math.max(1, p.perceptionRadius ?? 50) + + // Compute grid dimensions (capped to MAX_GRID_DIM) + this.gridWidth = Math.max(1, Math.min(MAX_GRID_DIM, Math.ceil(this.width / pr))) + this.gridHeight = Math.max(1, Math.min(MAX_GRID_DIM, Math.ceil(this.height / pr))) + + const buf = new ArrayBuffer(64) + const f = new Float32Array(buf) + const u = new Uint32Array(buf) + u[0] = this.numBoids + this.numPredators + u[1] = this.numPredators + f[2] = this.width + f[3] = this.height + f[4] = Math.min(dt, 0.05) + f[5] = p.separationWeight ?? 1.5 + f[6] = p.cohesionWeight ?? 1.0 + f[7] = p.alignmentWeight ?? 1.0 + f[8] = pr + f[9] = p.maxSpeed ?? 3.0 + u[10] = Math.min(this.obstacles.length, MAX_OBSTACLES) + u[11] = Math.min(this.goals.length, MAX_GOALS) + u[12] = this.gridWidth + u[13] = this.gridHeight + u[14] = 0 + u[15] = 0 + this.device.queue.writeBuffer(this.paramsBuf, 0, buf) + } + + _writeRenderParamsBuffer () { + const buf = new ArrayBuffer(16) + const f = new Float32Array(buf) + const u = new Uint32Array(buf) + f[0] = this.width + f[1] = this.height + u[2] = this.numBoids + this.numPredators + u[3] = 0 + this.device.queue.writeBuffer(this.renderParamsBuf, 0, buf) + } + + // ── Public: per-frame update + render ──────────────────────────────────── + update (dt, params) { + if (!this.initialized || !this.device) return + this.params = { ...this.params, ...params } + + const total = this.numBoids + this.numPredators + if (total === 0) return + + this._writeParamsBuffer(dt) + this._writeRenderParamsBuffer() + + const encoder = this.device.createCommandEncoder({ label: 'frame' }) + + // ── Pass 1a: clear cell counts ── + const clearSize = this.gridWidth * this.gridHeight * 4 + encoder.clearBuffer(this.cellCountBuf, 0, clearSize) + + // ── Pass 1b: spatial binning ── + { + const bg = this.device.createBindGroup({ + layout : this.binningPipeline.getBindGroupLayout(0), + entries : [ + { binding: 0, resource: { buffer: this.boidBufA } }, + { binding: 1, resource: { buffer: this.paramsBuf } }, + { binding: 2, resource: { buffer: this.cellCountBuf } }, + { binding: 3, resource: { buffer: this.cellBoidsBuf } }, + ], + }) + const pass = encoder.beginComputePass({ label: 'binning' }) + pass.setPipeline(this.binningPipeline) + pass.setBindGroup(0, bg) + pass.dispatchWorkgroups(Math.ceil(total / 64)) + pass.end() + } + + // ── Pass 2: boid logic (A → B) ── + { + const bg = this.device.createBindGroup({ + layout : this.logicPipeline.getBindGroupLayout(0), + entries : [ + { binding: 0, resource: { buffer: this.boidBufA } }, + { binding: 1, resource: { buffer: this.boidBufB } }, + { binding: 2, resource: { buffer: this.paramsBuf } }, + { binding: 3, resource: { buffer: this.cellCountBuf } }, + { binding: 4, resource: { buffer: this.cellBoidsBuf } }, + { binding: 5, resource: { buffer: this.obstacleBuf } }, + { binding: 6, resource: { buffer: this.goalBuf } }, + ], + }) + const pass = encoder.beginComputePass({ label: 'boidLogic' }) + pass.setPipeline(this.logicPipeline) + pass.setBindGroup(0, bg) + pass.dispatchWorkgroups(Math.ceil(total / 64)) + pass.end() + } + + // ── Pass 3: render boids from B (just written) ── + { + const bg = this.device.createBindGroup({ + layout : this.renderPipeline.getBindGroupLayout(0), + entries : [ + { binding: 0, resource: { buffer: this.boidBufB } }, + { binding: 1, resource: { buffer: this.renderParamsBuf } }, + ], + }) + const pass = encoder.beginRenderPass({ + label : 'render', + colorAttachments : [{ + view : this.context.getCurrentTexture().createView(), + clearValue : { r: 0.04, g: 0.04, b: 0.08, a: 1.0 }, + loadOp : 'clear', + storeOp : 'store', + }], + }) + pass.setPipeline(this.renderPipeline) + pass.setBindGroup(0, bg) + pass.draw(total * 3) + pass.end() + } + + this.device.queue.submit([encoder.finish()]) + + // Ping-pong: B is now current + ;[this.boidBufA, this.boidBufB] = [this.boidBufB, this.boidBufA] + } + + // ── Public: resize canvas ───────────────────────────────────────────────── + resize (w, h) { + this.width = Math.max(1, w) + this.height = Math.max(1, h) + if (this.context && this.device) { + this.context.configure({ + device : this.device, + format : this.format, + alphaMode : 'premultiplied', + }) + } + } + + // ── Public: update obstacle data ───────────────────────────────────────── + setObstacles (obstacles) { + this.obstacles = obstacles + if (!this.device) return + const data = new Float32Array(MAX_OBSTACLES * OBSTACLE_FLOATS) + const n = Math.min(obstacles.length, MAX_OBSTACLES) + for (let i = 0; i < n; i++) { + const o = obstacles[i] + const base = i * OBSTACLE_FLOATS + data[base + 0] = o.type === 0 ? 0 : 1 + data[base + 1] = o.x + data[base + 2] = o.y + data[base + 3] = o.type === 0 ? o.radius : o.halfW + data[base + 4] = o.type === 0 ? 0 : o.halfH + } + this.device.queue.writeBuffer(this.obstacleBuf, 0, data) + } + + // ── Public: update goal data ────────────────────────────────────────────── + setGoals (goals) { + this.goals = goals + if (!this.device) return + const data = new Float32Array(MAX_GOALS * GOAL_FLOATS) + const n = Math.min(goals.length, MAX_GOALS) + for (let i = 0; i < n; i++) { + const g = goals[i] + const base = i * GOAL_FLOATS + data[base + 0] = g.x + data[base + 1] = g.y + data[base + 2] = g.strength ?? 1.0 + } + this.device.queue.writeBuffer(this.goalBuf, 0, data) + } + + // ── Public: params (weights etc.) ───────────────────────────────────────── + updateParams (params) { + this.params = { ...this.params, ...params } + } + + // ── Public: change boid / predator count ────────────────────────────────── + setBoidCount (numBoids, numPredators) { + this.numBoids = Math.max(0, Math.min(numBoids, 20_000)) + this.numPredators = Math.max(0, Math.min(numPredators, 100)) + if (!this.device) return + // Re-scatter all agents in the scene + this._initBoids() + } + + // ── Public: cleanup ─────────────────────────────────────────────────────── + destroy () { + this.initialized = false + ;[ + this.boidBufA, this.boidBufB, this.paramsBuf, + this.cellCountBuf, this.cellBoidsBuf, + this.obstacleBuf, this.goalBuf, this.renderParamsBuf, + ].forEach(b => b?.destroy()) + this.context?.unconfigure() + this.device?.destroy() + } +} diff --git a/src/simulation/WebGLFallback.js b/src/simulation/WebGLFallback.js new file mode 100644 index 0000000..92bc349 --- /dev/null +++ b/src/simulation/WebGLFallback.js @@ -0,0 +1,409 @@ +// ─── WebGL2 + CPU Fallback ──────────────────────────────────────────────── +// Uses JavaScript for boid simulation and WebGL2 (or 2D Canvas) for rendering. + +const VERT_SRC = /* glsl */ `#version 300 es +precision highp float; + +// Per-vertex (triangle template, divisor = 0) +in vec2 aTriVert; + +// Per-instance boid data (divisor = 1) +in vec2 aPos; +in vec2 aVel; +in float aType; + +uniform vec2 uSize; + +out vec4 vColor; + +void main() { + vec2 fwd = length(aVel) > 0.001 ? normalize(aVel) : vec2(0.0, -1.0); + vec2 rgt = vec2(-fwd.y, fwd.x); + float sz = aType > 0.5 ? 7.0 : 4.5; + + vec2 world = aPos + fwd * (aTriVert.y * sz) + rgt * (aTriVert.x * sz); + gl_Position = vec4( + world.x / uSize.x * 2.0 - 1.0, + 1.0 - world.y / uSize.y * 2.0, + 0.0, 1.0 + ); + vColor = aType > 0.5 + ? vec4(0.95, 0.22, 0.22, 1.0) + : vec4(0.25, 0.55, 0.95, 1.0); +} +` + +const FRAG_SRC = /* glsl */ `#version 300 es +precision mediump float; +in vec4 vColor; +out vec4 fragColor; +void main() { fragColor = vColor; } +` + +// Triangle template in model space (tip forward, base behind) +const TRI_VERTS = new Float32Array([ + 0.0, 2.2, // tip + -1.0, -1.0, // rear-left + 1.0, -1.0, // rear-right +]) + +export class WebGLFallback { + constructor (canvas) { + this.canvas = canvas + this.gl = null + this.ctx2d = null // ultimate fallback + this.program = null + this.vao = null + this.triBuf = null + this.instanceBuf = null + this.uSizeLoc = null + + this.boids = [] // { x, y, vx, vy, type } + this.numBoids = 500 + this.numPredators = 5 + this.width = 800 + this.height = 600 + this.params = { + separationWeight : 1.5, + cohesionWeight : 1.0, + alignmentWeight : 1.0, + perceptionRadius : 50, + maxSpeed : 3.0, + } + this.obstacles = [] + this.goals = [] + } + + async init () { + const gl = this.canvas.getContext('webgl2') + if (gl) { + this.gl = gl + this._initGL() + } else { + // Ultimate fallback: plain 2D canvas + this.ctx2d = this.canvas.getContext('2d') + } + this.width = Math.max(1, this.canvas.width) + this.height = Math.max(1, this.canvas.height) + this._spawnBoids() + } + + // ── WebGL2 setup ────────────────────────────────────────────────────────── + _initGL () { + const gl = this.gl + const vs = this._compileShader(gl.VERTEX_SHADER, VERT_SRC) + const fs = this._compileShader(gl.FRAGMENT_SHADER, FRAG_SRC) + + this.program = gl.createProgram() + gl.attachShader(this.program, vs) + gl.attachShader(this.program, fs) + gl.linkProgram(this.program) + if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) + throw new Error('GL link error: ' + gl.getProgramInfoLog(this.program)) + + this.uSizeLoc = gl.getUniformLocation(this.program, 'uSize') + + this.vao = gl.createVertexArray() + gl.bindVertexArray(this.vao) + + // Triangle template buffer (shared vertices, divisor 0) + this.triBuf = gl.createBuffer() + gl.bindBuffer(gl.ARRAY_BUFFER, this.triBuf) + gl.bufferData(gl.ARRAY_BUFFER, TRI_VERTS, gl.STATIC_DRAW) + const triLoc = gl.getAttribLocation(this.program, 'aTriVert') + gl.enableVertexAttribArray(triLoc) + gl.vertexAttribPointer(triLoc, 2, gl.FLOAT, false, 0, 0) + gl.vertexAttribDivisor(triLoc, 0) // same for every instance vertex + + // Instance buffer (updated every frame, divisor 1) + this.instanceBuf = gl.createBuffer() + gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuf) + const stride = 6 * 4 // x y vx vy type pad + const posLoc = gl.getAttribLocation(this.program, 'aPos') + const velLoc = gl.getAttribLocation(this.program, 'aVel') + const typeLoc = gl.getAttribLocation(this.program, 'aType') + gl.enableVertexAttribArray(posLoc); gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, stride, 0); gl.vertexAttribDivisor(posLoc, 1) + gl.enableVertexAttribArray(velLoc); gl.vertexAttribPointer(velLoc, 2, gl.FLOAT, false, stride, 8); gl.vertexAttribDivisor(velLoc, 1) + gl.enableVertexAttribArray(typeLoc); gl.vertexAttribPointer(typeLoc, 1, gl.FLOAT, false, stride, 16); gl.vertexAttribDivisor(typeLoc, 1) + + gl.bindVertexArray(null) + + gl.enable(gl.BLEND) + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + } + + _compileShader (type, src) { + const gl = this.gl + const sh = gl.createShader(type) + gl.shaderSource(sh, src) + gl.compileShader(sh) + if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) + throw new Error('GL shader error: ' + gl.getShaderInfoLog(sh)) + return sh + } + + // ── Boid initialisation ─────────────────────────────────────────────────── + _spawnBoids () { + const total = this.numBoids + this.numPredators + this.boids = [] + for (let i = 0; i < total; i++) { + const angle = Math.random() * Math.PI * 2 + const speed = (0.4 + Math.random() * 0.6) * this.params.maxSpeed + this.boids.push({ + x : Math.random() * this.width, + y : Math.random() * this.height, + vx : Math.cos(angle) * speed, + vy : Math.sin(angle) * speed, + type : i < this.numBoids ? 0 : 1, + }) + } + } + + // ── Spatial grid (CPU) ──────────────────────────────────────────────────── + _buildGrid () { + const pr = Math.max(1, this.params.perceptionRadius) + const gw = Math.max(1, Math.ceil(this.width / pr)) + const gh = Math.max(1, Math.ceil(this.height / pr)) + const grid = new Array(gw * gh).fill(null).map(() => []) + for (let i = 0; i < this.boids.length; i++) { + const b = this.boids[i] + const cx = Math.floor(b.x / pr) % gw + const cy = Math.floor(b.y / pr) % gh + grid[cy * gw + cx].push(i) + } + return { grid, gw, gh, pr } + } + + // Minimum Image Convention + _minDiff (a, b, W) { + let d = b - a + if (d > W * 0.5) d -= W + if (d < -W * 0.5) d += W + return d + } + + // ── CPU boid update ─────────────────────────────────────────────────────── + _stepCPU (dt) { + const { separationWeight, cohesionWeight, alignmentWeight, perceptionRadius, maxSpeed } = this.params + const W = this.width + const H = this.height + const pr = Math.max(1, perceptionRadius) + const { grid, gw, gh } = this._buildGrid() + + const next = this.boids.map((b, i) => { + const cx = Math.floor(b.x / pr) % gw + const cy = Math.floor(b.y / pr) % gh + + let sepX = 0, sepY = 0 + let cohX = 0, cohY = 0 + let aliX = 0, aliY = 0 + let nn = 0 + + let predAvX = 0, predAvY = 0 + + let preyN = 0, preyX = 0, preyY = 0 + + for (let dy = -1; dy <= 1; dy++) { + for (let dx = -1; dx <= 1; dx++) { + const nx = ((cx + dx) % gw + gw) % gw + const ny = ((cy + dy) % gh + gh) % gh + const cell = grid[ny * gw + nx] + for (const j of cell) { + if (j === i) continue + const o = this.boids[j] + const ddx = this._minDiff(b.x, o.x, W) + const ddy = this._minDiff(b.y, o.y, H) + const dist = Math.hypot(ddx, ddy) + if (dist >= pr || dist < 0.0001) continue + + if (b.type === 0 && o.type === 0) { + const sepR = pr * 0.35 + if (dist < sepR) { + sepX -= ddx / (dist * dist + 0.01) + sepY -= ddy / (dist * dist + 0.01) + } + cohX += ddx; cohY += ddy + aliX += o.vx; aliY += o.vy + nn++ + } else if (b.type === 0 && o.type === 1 && dist < pr * 1.6) { + const w = pr * 1.6 - dist + predAvX -= (ddx / dist) * w + predAvY -= (ddy / dist) * w + } else if (b.type === 1 && o.type === 0) { + preyN++; preyX += b.x + ddx; preyY += b.y + ddy + } + } + } + } + + let ax = 0, ay = 0 + + if (b.type === 0) { + const nc = Math.max(nn, 1) + ax += sepX * separationWeight + ay += sepY * separationWeight + ax += (cohX / nc) * cohesionWeight * 0.015 + ay += (cohY / nc) * cohesionWeight * 0.015 + ax += (aliX / nc - b.vx) * alignmentWeight * 0.08 + ay += (aliY / nc - b.vy) * alignmentWeight * 0.08 + ax += predAvX * 2.5 + ay += predAvY * 2.5 + + // Goal seeking + for (const g of this.goals) { + const gdx = this._minDiff(b.x, g.x, W) + const gdy = this._minDiff(b.y, g.y, H) + const gd = Math.hypot(gdx, gdy) + if (gd > 1) { + ax += (gdx / gd) * (g.strength ?? 1) * 0.4 + ay += (gdy / gd) * (g.strength ?? 1) * 0.4 + } + } + } else { + // Predator pursuit + if (preyN > 0) { + const pcx = preyX / preyN + const pcy = preyY / preyN + const pdx = this._minDiff(b.x, pcx, W) + const pdy = this._minDiff(b.y, pcy, H) + const pd = Math.hypot(pdx, pdy) + if (pd > 1) { + const desired = { x: (pdx / pd) * maxSpeed * 1.25, y: (pdy / pd) * maxSpeed * 1.25 } + ax += (desired.x - b.vx) * 0.07 + ay += (desired.y - b.vy) * 0.07 + } + } else { + ax += b.vx * -0.005 + ay += b.vy * -0.005 + } + } + + // Obstacle avoidance + for (const obs of this.obstacles) { + let dist, nx2, ny2 + if (obs.type === 0) { + const dx2 = b.x - obs.x; const dy2 = b.y - obs.y + const d = Math.hypot(dx2, dy2) + dist = d - obs.radius + nx2 = d > 0.001 ? dx2 / d : 1 + ny2 = d > 0.001 ? dy2 / d : 0 + } else { + const dx2 = b.x - obs.x; const dy2 = b.y - obs.y + const qx = Math.abs(dx2) - obs.halfW; const qy = Math.abs(dy2) - obs.halfH + dist = Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) + Math.min(Math.max(qx, qy), 0) + nx2 = Math.abs(dx2) / Math.max(obs.halfW, 0.001) > Math.abs(dy2) / Math.max(obs.halfH, 0.001) ? Math.sign(dx2) : 0 + ny2 = nx2 === 0 ? Math.sign(dy2) : 0 + } + const ar = pr * 0.7 + if (dist < ar) { + const t = (ar - dist) / ar + ax += nx2 * t * t * 5 + ay += ny2 * t * t * 5 + } + } + + let nvx = b.vx + ax + let nvy = b.vy + ay + const maxSpd = b.type === 1 ? maxSpeed * 1.3 : maxSpeed + const spd = Math.hypot(nvx, nvy) + if (spd > maxSpd && spd > 0.0001) { nvx = (nvx / spd) * maxSpd; nvy = (nvy / spd) * maxSpd } + const minSpd = maxSpd * 0.08 + if (spd < minSpd && spd > 0.0001) { nvx = (nvx / spd) * minSpd; nvy = (nvy / spd) * minSpd } + + let nx3 = b.x + nvx * dt + let ny3 = b.y + nvy * dt + nx3 = ((nx3 % W) + W) % W + ny3 = ((ny3 % H) + H) % H + + return { x: nx3, y: ny3, vx: nvx, vy: nvy, type: b.type } + }) + + this.boids = next + } + + // ── Rendering (WebGL2) ──────────────────────────────────────────────────── + _renderGL () { + const gl = this.gl + const total = this.boids.length + gl.viewport(0, 0, this.width, this.height) + gl.clearColor(0.04, 0.04, 0.08, 1) + gl.clear(gl.COLOR_BUFFER_BIT) + + if (total === 0) return + + // Pack instance data + const data = new Float32Array(total * 6) + for (let i = 0; i < total; i++) { + const b = this.boids[i] + data[i * 6 + 0] = b.x; data[i * 6 + 1] = b.y + data[i * 6 + 2] = b.vx; data[i * 6 + 3] = b.vy + data[i * 6 + 4] = b.type + } + + gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuf) + gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW) + + gl.useProgram(this.program) + gl.uniform2f(this.uSizeLoc, this.width, this.height) + gl.bindVertexArray(this.vao) + gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, total) + gl.bindVertexArray(null) + } + + // ── Rendering (2D Canvas fallback) ──────────────────────────────────────── + _render2D () { + const ctx = this.ctx2d + ctx.fillStyle = 'rgba(10, 10, 20, 1)' + ctx.fillRect(0, 0, this.width, this.height) + + for (const b of this.boids) { + const spd = Math.hypot(b.vx, b.vy) + const fwdX = spd > 0.001 ? b.vx / spd : 0 + const fwdY = spd > 0.001 ? b.vy / spd : -1 + const sz = b.type === 1 ? 7 : 4.5 + + ctx.save() + ctx.translate(b.x, b.y) + ctx.beginPath() + ctx.moveTo( fwdX * sz * 2.2, fwdY * sz * 2.2) + ctx.lineTo(-fwdY * sz - fwdX * sz, fwdX * sz - fwdY * sz) + ctx.lineTo( fwdY * sz - fwdX * sz, -fwdX * sz - fwdY * sz) + ctx.closePath() + ctx.fillStyle = b.type === 1 ? '#f43' : '#4af' + ctx.fill() + ctx.restore() + } + } + + // ── Public API (mirrors BoidSimulation) ─────────────────────────────────── + update (dt, params) { + this.params = { ...this.params, ...params } + this._stepCPU(Math.min(dt, 0.05)) + if (this.gl) this._renderGL() + else if (this.ctx2d) this._render2D() + } + + resize (w, h) { + this.width = Math.max(1, w) + this.height = Math.max(1, h) + } + + setObstacles (obstacles) { this.obstacles = obstacles } + setGoals (goals) { this.goals = goals } + updateParams (params) { this.params = { ...this.params, ...params } } + + setBoidCount (numBoids, numPredators) { + this.numBoids = Math.max(0, Math.min(numBoids, 5_000)) + this.numPredators = Math.max(0, Math.min(numPredators, 50)) + this._spawnBoids() + } + + destroy () { + const gl = this.gl + if (!gl) return + gl.deleteProgram(this.program) + gl.deleteBuffer(this.triBuf) + gl.deleteBuffer(this.instanceBuf) + gl.deleteVertexArray(this.vao) + } +} diff --git a/src/simulation/shaders/binning.wgsl b/src/simulation/shaders/binning.wgsl new file mode 100644 index 0000000..58206ec --- /dev/null +++ b/src/simulation/shaders/binning.wgsl @@ -0,0 +1,58 @@ +// ─── Spatial-Grid Binning Pass ────────────────────────────────────────────── +// Clears are done via encoder.clearBuffer before dispatch. +// Each boid atomically claims a slot in its grid cell. + +struct Boid { + pos : vec2f, + vel : vec2f, + boidType : f32, + _pad : f32, +} + +struct Params { + numBoids : u32, // total = boids + predators + numPredators : u32, + width : f32, + height : f32, + dt : f32, + separationWeight: f32, + cohesionWeight : f32, + alignmentWeight : f32, + perceptionRadius: f32, + maxSpeed : f32, + numObstacles : u32, + numGoals : u32, + gridWidth : u32, + gridHeight : u32, + _pad1 : u32, + _pad2 : u32, +} + +@group(0) @binding(0) var boids : array; +@group(0) @binding(1) var params : Params; +@group(0) @binding(2) var cellCount : array>; +@group(0) @binding(3) var cellBoids : array; + +const MAX_PER_CELL : u32 = 64u; + +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) gid : vec3u) { + let i = gid.x; + if i >= params.numBoids { return; } + + let pos = boids[i].pos; + let cs = params.perceptionRadius; + + // Clamp to valid cell range + let rawX = floor(pos.x / cs); + let rawY = floor(pos.y / cs); + let cx = u32(clamp(rawX, 0.0, f32(params.gridWidth - 1u))); + let cy = u32(clamp(rawY, 0.0, f32(params.gridHeight - 1u))); + + let cellIdx = cy * params.gridWidth + cx; + + let slot = atomicAdd(&cellCount[cellIdx], 1u); + if slot < MAX_PER_CELL { + cellBoids[cellIdx * MAX_PER_CELL + slot] = i; + } +} diff --git a/src/simulation/shaders/boidLogic.wgsl b/src/simulation/shaders/boidLogic.wgsl new file mode 100644 index 0000000..8f06b22 --- /dev/null +++ b/src/simulation/shaders/boidLogic.wgsl @@ -0,0 +1,246 @@ +// ─── Boid Logic Pass ──────────────────────────────────────────────────────── +// Reads from boidsIn + spatial grid, writes updated state to boidsOut. +// Uses Minimum Image Convention for toroidal (wrap-around) topology. + +struct Boid { + pos : vec2f, + vel : vec2f, + boidType : f32, + _pad : f32, +} + +struct Params { + numBoids : u32, + numPredators : u32, + width : f32, + height : f32, + dt : f32, + separationWeight: f32, + cohesionWeight : f32, + alignmentWeight : f32, + perceptionRadius: f32, + maxSpeed : f32, + numObstacles : u32, + numGoals : u32, + gridWidth : u32, + gridHeight : u32, + _pad1 : u32, + _pad2 : u32, +} + +struct Obstacle { + obsType : f32, // 0 = circle, 1 = box + x : f32, + y : f32, + param1 : f32, // radius (circle) | halfW (box) + param2 : f32, // 0 (circle) | halfH (box) + _p1 : f32, + _p2 : f32, + _p3 : f32, +} + +struct Goal { + x : f32, + y : f32, + strength : f32, + _pad : f32, +} + +@group(0) @binding(0) var boidsIn : array; +@group(0) @binding(1) var boidsOut : array; +@group(0) @binding(2) var params : Params; +@group(0) @binding(3) var cellCount : array; +@group(0) @binding(4) var cellBoids : array; +@group(0) @binding(5) var obstacles : array; +@group(0) @binding(6) var goals : array; + +const MAX_PER_CELL : u32 = 64u; + +// Minimum Image Convention – shortest vector on torus +fn toroidalDiff(from_p : vec2f, to_p : vec2f, sz : vec2f) -> vec2f { + var d = to_p - from_p; + if d.x > sz.x * 0.5 { d.x -= sz.x; } + else if d.x < -sz.x * 0.5 { d.x += sz.x; } + if d.y > sz.y * 0.5 { d.y -= sz.y; } + else if d.y < -sz.y * 0.5 { d.y += sz.y; } + return d; +} + +// Returns a repulsion vector away from all obstacles +fn obstacleForce(pos : vec2f) -> vec2f { + var force = vec2f(0.0); + let avoidR = params.perceptionRadius * 0.7; + + for (var i = 0u; i < params.numObstacles; i = i + 1u) { + let obs = obstacles[i]; + let center = vec2f(obs.x, obs.y); + var sdf : f32; + var normal : vec2f; + + if obs.obsType < 0.5 { + // Circle SDF + let dp = pos - center; + let d = length(dp); + sdf = d - obs.param1; + normal = select(vec2f(1.0, 0.0), dp / d, d > 0.001); + } else { + // Box SDF + let dp = pos - center; + let q = abs(dp) - vec2f(obs.param1, obs.param2); + let ext = length(max(q, vec2f(0.0))); + sdf = ext + min(max(q.x, q.y), 0.0); + // Approximate outward normal + if abs(dp.x) / max(obs.param1, 0.001) > abs(dp.y) / max(obs.param2, 0.001) { + normal = vec2f(sign(dp.x), 0.0); + } else { + normal = vec2f(0.0, sign(dp.y)); + } + } + + if sdf < avoidR { + let t = (avoidR - sdf) / avoidR; + force += normal * t * t * 5.0; + } + } + return force; +} + +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) gid : vec3u) { + let i = gid.x; + let total = params.numBoids; + if i >= total { return; } + + let boid = boidsIn[i]; + let pos = boid.pos; + let vel = boid.vel; + let isBoid = boid.boidType < 0.5; + let sz = vec2f(params.width, params.height); + let cs = params.perceptionRadius; + let gw = i32(params.gridWidth); + let gh = i32(params.gridHeight); + let cx = i32(floor(pos.x / cs)); + let cy = i32(floor(pos.y / cs)); + + // Per-rule accumulators + var sepForce = vec2f(0.0); + var cohSum = vec2f(0.0); + var aliSum = vec2f(0.0); + var predAvoid= vec2f(0.0); + var neighborN= 0u; + var predN = 0u; + + // Predator: track nearby prey centre + var preyN = 0u; + var preySum = vec2f(0.0); + + // 3 × 3 grid neighbourhood with toroidal wrapping + for (var dy : i32 = -1; dy <= 1; dy = dy + 1) { + for (var dx : i32 = -1; dx <= 1; dx = dx + 1) { + let nx = ((cx + dx) % gw + gw) % gw; + let ny = ((cy + dy) % gh + gh) % gh; + let cellIdx = u32(ny) * params.gridWidth + u32(nx); + let cnt = min(cellCount[cellIdx], MAX_PER_CELL); + + for (var k = 0u; k < cnt; k = k + 1u) { + let j = cellBoids[cellIdx * MAX_PER_CELL + k]; + if j == i { continue; } + + let other = boidsIn[j]; + let diff = toroidalDiff(pos, other.pos, sz); + let dist = length(diff); + + if dist >= cs || dist < 0.0001 { continue; } + + let otherIsBoid = other.boidType < 0.5; + + if isBoid && otherIsBoid { + // ── Flocking rules ── + let sepR = cs * 0.35; + if dist < sepR { + sepForce -= diff / (dist * dist + 0.01); + } + cohSum += diff; + aliSum += other.vel; + neighborN = neighborN + 1u; + + } else if isBoid && !otherIsBoid { + // ── Boid evades predator (extended radius) ── + if dist < cs * 1.6 { + let w = cs * 1.6 - dist; + predAvoid -= normalize(diff) * w; + predN = predN + 1u; + } + + } else if !isBoid && otherIsBoid { + // ── Predator tracks prey ── + preyN = preyN + 1u; + preySum = preySum + (pos + diff); // world-space prey pos + } + } + } + } + + var newVel = vel; + + if isBoid { + let nc = f32(max(neighborN, 1u)); + + // Separation + newVel += sepForce * params.separationWeight; + // Cohesion: steer toward local centre + newVel += (cohSum / nc) * params.cohesionWeight * 0.015; + // Alignment: match neighbours' velocity + newVel += (aliSum / nc - vel) * params.alignmentWeight * 0.08; + + // Predator avoidance + if predN > 0u { + newVel += predAvoid * 2.5; + } + + // Goal seeking + for (var g = 0u; g < params.numGoals; g = g + 1u) { + let gp = vec2f(goals[g].x, goals[g].y); + let gd = toroidalDiff(pos, gp, sz); + let gdst = length(gd); + if gdst > 1.0 { + newVel += normalize(gd) * goals[g].strength * 0.4; + } + } + + } else { + // ── Predator: pursue prey flock centre ── + if preyN > 0u { + let preyCenter = preySum / f32(preyN); + let pd = toroidalDiff(pos, preyCenter, sz); + let pdst = length(pd); + if pdst > 1.0 { + let desired = normalize(pd) * params.maxSpeed * 1.25; + newVel += (desired - vel) * 0.07; + } + } else { + newVel *= 0.995; // slow wandering + } + } + + // Obstacle avoidance for all agents + newVel += obstacleForce(pos); + + // ── Speed clamping ── + let maxSpd = select(params.maxSpeed, params.maxSpeed * 1.3, !isBoid); + let spd = length(newVel); + if spd > maxSpd && spd > 0.0001 { + newVel = (newVel / spd) * maxSpd; + } + let minSpd = maxSpd * 0.08; + if spd < minSpd && spd > 0.0001 { + newVel = (newVel / spd) * minSpd; + } + + // ── Toroidal position update ── + var newPos = pos + newVel * params.dt; + newPos.x = ((newPos.x % params.width) + params.width) % params.width; + newPos.y = ((newPos.y % params.height) + params.height) % params.height; + + boidsOut[i] = Boid(newPos, newVel, boid.boidType, 0.0); +} diff --git a/src/simulation/shaders/render.wgsl b/src/simulation/shaders/render.wgsl new file mode 100644 index 0000000..e5d9532 --- /dev/null +++ b/src/simulation/shaders/render.wgsl @@ -0,0 +1,76 @@ +// ─── Render Shader ─────────────────────────────────────────────────────────── +// Vertex-pulling from storage buffer; each boid = one triangle (3 verts). + +struct Boid { + pos : vec2f, + vel : vec2f, + boidType : f32, + _pad : f32, +} + +struct RenderParams { + width : f32, + height : f32, + total : u32, + _pad : u32, +} + +@group(0) @binding(0) var boids : array; +@group(0) @binding(1) var rp : RenderParams; + +struct VOut { + @builtin(position) pos : vec4f, + @location(0) color : vec4f, +} + +@vertex +fn vs_main(@builtin(vertex_index) vi : u32) -> VOut { + let boidIdx = vi / 3u; + let triVtx = vi % 3u; + + let b = boids[boidIdx]; + let spd = length(b.vel); + + // Forward direction (velocity normalised, default up) + var fwd = vec2f(0.0, -1.0); + if spd > 0.001 { + fwd = b.vel / spd; + } + let rgt = vec2f(-fwd.y, fwd.x); + + // Predators are slightly larger + let sz = select(4.5, 7.0, b.boidType > 0.5); + + // Triangle vertices in model space (tip forward, base behind) + var lp : vec2f; + if triVtx == 0u { + lp = fwd * sz * 2.2; // tip + } else if triVtx == 1u { + lp = fwd * (-sz) + rgt * sz; // rear-left + } else { + lp = fwd * (-sz) - rgt * sz; // rear-right + } + + let wp = b.pos + lp; + let ndc = vec2f( + wp.x / rp.width * 2.0 - 1.0, + 1.0 - wp.y / rp.height * 2.0, + ); + + // Blue for boids, red for predators + let color = select( + vec4f(0.25, 0.55, 0.95, 1.0), // boid (false) + vec4f(0.95, 0.22, 0.22, 1.0), // predator (true) + b.boidType > 0.5, + ); + + var o : VOut; + o.pos = vec4f(ndc, 0.0, 1.0); + o.color = color; + return o; +} + +@fragment +fn fs_main(i : VOut) -> @location(0) vec4f { + return i.color; +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..951a9ba --- /dev/null +++ b/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +export default defineConfig({ + plugins: [svelte()], +}) From ee29821bde88cd8a22e9205da466703fc620f472 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:56:12 +0000 Subject: [PATCH 3/3] Add GitHub Actions workflow to deploy to GitHub Pages Co-authored-by: LowWeiLin <3910365+LowWeiLin@users.noreply.github.com> Agent-Logs-Url: https://github.com/LowWeiLin/Boids/sessions/89c0fa34-980e-4f1b-b44f-0a5ac0bf916a --- .github/workflows/deploy.yml | 51 ++++++++++++++++++++++++++++++++++++ vite.config.js | 1 + 2 files changed, 52 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..db01d6c --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,51 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/vite.config.js b/vite.config.js index 951a9ba..f2c13fc 100644 --- a/vite.config.js +++ b/vite.config.js @@ -2,5 +2,6 @@ import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' export default defineConfig({ + base: '/Boids/', plugins: [svelte()], })