Compare commits
7 commits
5f0f500725
...
00371703ad
| Author | SHA1 | Date | |
|---|---|---|---|
| 00371703ad | |||
| c2a9d0a826 | |||
| 7e0f3d0303 | |||
| d73c3a8695 | |||
| be0a49a11f | |||
| 0735dd764f | |||
| f350b1ff37 |
5
.claude/settings.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"enabledPlugins": {
|
||||
"frontend-design@claude-plugins-official": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
webpack.config.js
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 11,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["unused-imports", "@typescript-eslint", "prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"unused-imports/no-unused-imports-ts": "error",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-empty-function": "off"
|
||||
}
|
||||
}
|
||||
43
.forgejo/workflows/deploy.yml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
name: Deploy to Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: 'pages'
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint -- --check || true
|
||||
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Copy build to host pages mount
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
mkdir -p /pages
|
||||
rsync -a --delete dist/ /pages/
|
||||
51
.github/workflows/deploy.yml
vendored
|
|
@ -1,51 +0,0 @@
|
|||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
npm ci && npm run build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: 'dist'
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
22
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"endOfLine": "lf",
|
||||
"importOrder": ["^[./]", ".*", ".scss$"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
|
||||
"importOrder": ["<BUILTIN_MODULES>", "<THIRD_PARTY_MODULES>", "", "^[./]"],
|
||||
"importOrderTypeScriptVersion": "5.0.0"
|
||||
}
|
||||
|
|
|
|||
12
definitions.d.ts
vendored
|
|
@ -1,14 +1,4 @@
|
|||
declare module '*.wgsl' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.html' {
|
||||
declare module '*.wgsl?raw' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
|
|
|||
82
index.html
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,viewport-fit=cover"
|
||||
/>
|
||||
<meta name="theme-color" content="#b7455e" />
|
||||
<meta
|
||||
name="description"
|
||||
content="A WebGPU agent simulation: a million blobs leave trails, infect each other across generations, and react to your brush."
|
||||
/>
|
||||
|
||||
<meta property="og:title" content="Just a bunch of blobs" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A WebGPU agent simulation: a million blobs leave trails, infect each other across generations, and react to your brush."
|
||||
/>
|
||||
<meta property="og:url" content="https://schmelczer.dev" />
|
||||
<meta property="og:image" content="https://schmelczer.dev/og-image.jpg" />
|
||||
<meta property="og:image:width" content="1920" />
|
||||
<meta property="og:image:height" content="1920" />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
|
||||
<title>Just a bunch of blobs</title>
|
||||
</head>
|
||||
<body>
|
||||
<main class="canvas-container">
|
||||
<canvas></canvas>
|
||||
|
||||
<section class="errors-container">
|
||||
<noscript>JavaScript is required for this website.</noscript>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<aside>
|
||||
<nav class="buttons">
|
||||
<button class="info" aria-label="About"></button>
|
||||
<button class="maximize-full-screen" aria-label="Enter fullscreen"></button>
|
||||
<button class="minimize-full-screen" aria-label="Exit fullscreen"></button>
|
||||
<button class="settings" aria-label="Settings"></button>
|
||||
<button class="restart" aria-label="Restart simulation"></button>
|
||||
</nav>
|
||||
|
||||
<main class="pages hidden info-page">
|
||||
<section>
|
||||
<h1>Just a bunch of blobs</h1>
|
||||
<p>
|
||||
A million autonomous agents wander a 2D field. Each one lays down a faint
|
||||
trail and follows trails it senses ahead. Two generations are competing for
|
||||
territory: the older one fades, the newer one spreads.
|
||||
</p>
|
||||
<p>
|
||||
Drag your finger or mouse anywhere on the canvas to paint a wall. Walls slow
|
||||
the new generation down and let the old one breathe a little longer. Open
|
||||
<em>Settings</em> to retune sensors, decay rates and aggression.
|
||||
</p>
|
||||
<p>
|
||||
Runs entirely on your GPU via WebGPU compute shaders — no servers, no
|
||||
tracking, no analytics. Source on
|
||||
<a href="https://github.com/schmelczer/webgpu" target="_blank" rel="noopener"
|
||||
>GitHub</a
|
||||
>.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<main class="pages hidden settings-page">
|
||||
<section>
|
||||
<div class="settings-content"></div>
|
||||
<button id="apply-defaults" class="large-button">Apply defaults</button>
|
||||
</section>
|
||||
</main>
|
||||
</aside>
|
||||
<script type="module" src="/src/index.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
24867
package-lock.json
generated
118
package.json
|
|
@ -1,61 +1,61 @@
|
|||
{
|
||||
"name": "webgpu-seed",
|
||||
"version": "0.1.0",
|
||||
"description": "🔺 A simple hello triangle example introducing WebGPU.",
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"start": "webpack serve --open --mode development",
|
||||
"lint": "eslint --fix \"src/**/*.ts\" && prettier --write \"src/**/*.(ts|scss|json|html)\"",
|
||||
"build": "webpack --mode production",
|
||||
"update": "ncu"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/alaingalvan/webgpu-seed.git"
|
||||
},
|
||||
"keywords": [
|
||||
"webgpu",
|
||||
"webgl",
|
||||
"example",
|
||||
"seed",
|
||||
"types",
|
||||
"typescript"
|
||||
],
|
||||
"author": "Alain Galvan",
|
||||
"license": "Unlicense",
|
||||
"bugs": {
|
||||
"url": "https://github.com/alaingalvan/webgpu-seed/issues"
|
||||
},
|
||||
"homepage": "https://github.com/alaingalvan/webgpu-seed#readme",
|
||||
"devDependencies": {
|
||||
"@webgpu/types": "^0.1.30",
|
||||
"@trivago/prettier-plugin-sort-imports": "^3.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
||||
"css-loader": "^6.7.1",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"inline-source-webpack-plugin": "^2.0.1",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"npm-check-updates": "^16.3.2",
|
||||
"prettier": "^2.7.1",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"responsive-loader": "^3.1.1",
|
||||
"sass": "^1.55.0",
|
||||
"sass-loader": "^13.0.2",
|
||||
"sharp": "^0.31.0",
|
||||
"string-replace-loader": "^3.1.0",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"ts-loader": "^9.4.1",
|
||||
"typescript": "^4.8.3",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"gl-matrix": "^3.4.3"
|
||||
}
|
||||
"name": "webgpu-seed",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "A WebGPU-powered slime-mold-meets-territory-control simulation.",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint --fix \"src/**/*.ts\" && prettier --write \"src/**/*.{ts,scss,json,html}\"",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"generate-icons": "pwa-assets-generator",
|
||||
"update": "ncu"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/schmelczer/webgpu.git"
|
||||
},
|
||||
"keywords": [
|
||||
"webgpu",
|
||||
"simulation",
|
||||
"physarum",
|
||||
"generative-art"
|
||||
],
|
||||
"author": "Andras Schmelczer",
|
||||
"license": "Unlicense",
|
||||
"browserslist": [
|
||||
"supports webgpu and last 2 years"
|
||||
],
|
||||
"dependencies": {
|
||||
"gl-matrix": "^3.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
||||
"@types/node": "^25.6.0",
|
||||
"@vite-pwa/assets-generator": "^1.0.2",
|
||||
"@webgpu/types": "^0.1.69",
|
||||
"browserslist": "^4.28.2",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-unused-imports": "^4.4.1",
|
||||
"globals": "^17.6.0",
|
||||
"lightningcss": "^1.32.0",
|
||||
"npm-check-updates": "^22.1.0",
|
||||
"prettier": "^3.8.3",
|
||||
"sass": "^1.99.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.1",
|
||||
"vite": "^8.0.10",
|
||||
"vite-plugin-singlefile": "^2.3.3",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
public/apple-touch-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 908 B |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 587 B |
6
public/favicon.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="14" fill="#b7455e" />
|
||||
<circle cx="22" cy="26" r="9" fill="#fff" opacity="0.95" />
|
||||
<circle cx="42" cy="32" r="11" fill="#fff" opacity="0.85" />
|
||||
<circle cx="28" cy="44" r="7" fill="#fff" opacity="0.75" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 312 B |
43
public/manifest.webmanifest
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "Just a bunch of blobs",
|
||||
"short_name": "Blobs",
|
||||
"description": "A WebGPU agent simulation: a million blobs leave trails, infect each other across generations, and react to your brush.",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "fullscreen",
|
||||
"display_override": ["fullscreen", "standalone", "minimal-ui"],
|
||||
"orientation": "any",
|
||||
"background_color": "#b7455e",
|
||||
"theme_color": "#b7455e",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/pwa-64x64.png",
|
||||
"sizes": "64x64",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/pwa-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/pwa-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/maskable-icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"categories": ["entertainment", "graphics"],
|
||||
"lang": "en"
|
||||
}
|
||||
BIN
public/maskable-icon-512x512.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/pwa-192x192.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/pwa-512x512.png
Normal file
|
After Width: | Height: | Size: 3 KiB |
BIN
public/pwa-64x64.png
Normal file
|
After Width: | Height: | Size: 488 B |
9
pwa-assets.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import {
|
||||
defineConfig,
|
||||
minimal2023Preset as preset,
|
||||
} from '@vite-pwa/assets-generator/config';
|
||||
|
||||
export default defineConfig({
|
||||
preset,
|
||||
images: ['public/favicon.svg'],
|
||||
});
|
||||
|
|
@ -1,4 +1 @@
|
|||
// @ts-ignore: injected by webpack
|
||||
export const isProduction: boolean = __IS_PRODUCTION__;
|
||||
// @ts-ignore: injected by webpack
|
||||
export const lastEdit = new Date(__CURRENT_DATE__);
|
||||
export const isProduction: boolean = import.meta.env.PROD;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
import { AgentGenerationPipeline } from '../pipelines/agents/agent-generation/agent-generation-pipeline';
|
||||
import { AgentPipeline } from '../pipelines/agents/agent-pipeline';
|
||||
import { BrushPipeline } from '../pipelines/brush/brush-pipeline';
|
||||
|
|
@ -13,8 +15,6 @@ import { sleep } from '../utils/sleep';
|
|||
import { GamePresentation } from './game-presentation';
|
||||
import { GameRules } from './game-rules';
|
||||
|
||||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export default class GameLoop {
|
||||
private readonly trailMapA: ResizableTexture;
|
||||
private readonly trailMapB: ResizableTexture;
|
||||
|
|
@ -28,12 +28,9 @@ export default class GameLoop {
|
|||
private readonly diffusionPipeline: DiffusionPipeline;
|
||||
|
||||
private hasFinished = false;
|
||||
private readonly hasFinishedPromise: Promise<void> = new Promise(
|
||||
(resolve) => (this.resolveHasFinished = resolve)
|
||||
);
|
||||
private resolveHasFinished: () => void;
|
||||
private readonly finished = Promise.withResolvers<void>();
|
||||
|
||||
private isSwipeActive = false;
|
||||
private activePointerId: number | null = null;
|
||||
|
||||
public constructor(
|
||||
private readonly canvas: HTMLCanvasElement,
|
||||
|
|
@ -73,44 +70,54 @@ export default class GameLoop {
|
|||
this.renderPipeline = new RenderPipeline(context, this.device, this.commonState);
|
||||
|
||||
window.addEventListener('resize', this.resize.bind(this));
|
||||
canvas.addEventListener('mousemove', this.onSwipe.bind(this));
|
||||
canvas.addEventListener('touchmove', this.onSwipe.bind(this));
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
if (!this.isSwipeActive) {
|
||||
this.brushPipeline.clearSwipes();
|
||||
this.isSwipeActive = true;
|
||||
}
|
||||
this.onSwipe(e);
|
||||
});
|
||||
canvas.addEventListener('pointerdown', this.onPointerDown.bind(this));
|
||||
canvas.addEventListener('pointermove', this.onPointerMove.bind(this));
|
||||
canvas.addEventListener('pointerup', this.onPointerUp.bind(this));
|
||||
canvas.addEventListener('pointercancel', this.onPointerUp.bind(this));
|
||||
}
|
||||
|
||||
canvas.addEventListener('touchstart', (e) => {
|
||||
if (!this.isSwipeActive) {
|
||||
this.brushPipeline.clearSwipes();
|
||||
this.isSwipeActive = true;
|
||||
}
|
||||
this.onSwipe(e);
|
||||
});
|
||||
private onPointerDown(event: PointerEvent) {
|
||||
if (this.activePointerId !== null) {
|
||||
return;
|
||||
}
|
||||
this.activePointerId = event.pointerId;
|
||||
this.canvas.setPointerCapture(event.pointerId);
|
||||
this.brushPipeline.clearSwipes();
|
||||
this.addSwipeAt(event);
|
||||
}
|
||||
|
||||
window.addEventListener('mouseup', (e) => {
|
||||
this.onSwipe(e);
|
||||
this.isSwipeActive = false;
|
||||
});
|
||||
private onPointerMove(event: PointerEvent) {
|
||||
if (event.pointerId !== this.activePointerId) {
|
||||
return;
|
||||
}
|
||||
this.addSwipeAt(event);
|
||||
}
|
||||
|
||||
window.addEventListener('touchend', (e) => {
|
||||
this.onSwipe(e);
|
||||
this.isSwipeActive = false;
|
||||
});
|
||||
private onPointerUp(event: PointerEvent) {
|
||||
if (event.pointerId !== this.activePointerId) {
|
||||
return;
|
||||
}
|
||||
this.addSwipeAt(event);
|
||||
this.canvas.releasePointerCapture(event.pointerId);
|
||||
this.activePointerId = null;
|
||||
}
|
||||
|
||||
window.addEventListener('touchcancel', (e) => {
|
||||
this.onSwipe(e);
|
||||
this.isSwipeActive = false;
|
||||
});
|
||||
private addSwipeAt(event: PointerEvent) {
|
||||
const position = vec2.fromValues(
|
||||
event.clientX * this.devicePixelRatio,
|
||||
this.canvas.height - event.clientY * this.devicePixelRatio
|
||||
);
|
||||
this.brushPipeline.addSwipe(position);
|
||||
}
|
||||
|
||||
private get isSwipeActive(): boolean {
|
||||
return this.activePointerId !== null;
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
requestAnimationFrame(this.render.bind(this));
|
||||
requestAnimationFrame(this.updateCounts.bind(this));
|
||||
return this.hasFinishedPromise;
|
||||
return this.finished.promise;
|
||||
}
|
||||
|
||||
private async updateCounts(): Promise<void> {
|
||||
|
|
@ -135,25 +142,6 @@ export default class GameLoop {
|
|||
return this.agentGenerationPipeline.maxAgentCount;
|
||||
}
|
||||
|
||||
private onSwipe(event: MouseEvent | TouchEvent) {
|
||||
if (!this.isSwipeActive || !event) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event instanceof TouchEvent && event.touches.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
|
||||
const y = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY;
|
||||
|
||||
const position = vec2.fromValues(
|
||||
x * this.devicePixelRatio,
|
||||
this.canvas.height - y * this.devicePixelRatio
|
||||
);
|
||||
this.brushPipeline.addSwipe(position);
|
||||
}
|
||||
|
||||
private resize() {
|
||||
this.canvas.width = this.canvas.clientWidth * this.devicePixelRatio;
|
||||
this.canvas.height = this.canvas.clientHeight * this.devicePixelRatio;
|
||||
|
|
@ -161,7 +149,7 @@ export default class GameLoop {
|
|||
|
||||
private async render(time: DOMHighResTimeStamp) {
|
||||
if (this.hasFinished) {
|
||||
this.resolveHasFinished();
|
||||
this.finished.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -170,7 +158,7 @@ export default class GameLoop {
|
|||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--accent-color',
|
||||
`rgb(${accentColor.map((v: number) => v * 255).join(',')})`
|
||||
`rgb(${accentColor[0] * 255},${accentColor[1] * 255},${accentColor[2] * 255})`
|
||||
);
|
||||
|
||||
const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time);
|
||||
|
|
@ -254,7 +242,7 @@ export default class GameLoop {
|
|||
|
||||
public async destroy() {
|
||||
this.hasFinished = true;
|
||||
await this.hasFinishedPromise;
|
||||
await this.finished.promise;
|
||||
|
||||
this.copyPipeline?.destroy();
|
||||
this.agentGenerationPipeline?.destroy();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import { vec3 } from 'gl-matrix';
|
||||
|
||||
import { settings } from '../settings';
|
||||
import { hsl } from '../utils/hsl';
|
||||
import { last } from '../utils/last';
|
||||
import { Random } from '../utils/random';
|
||||
|
||||
import { vec3 } from 'gl-matrix';
|
||||
|
||||
const hues = [settings.startColorHue];
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
hues.push((last(hues) + Random.randomBetween(90, 240)) % 360);
|
||||
hues.push((hues[hues.length - 1] + Random.randomBetween(90, 240)) % 360);
|
||||
}
|
||||
|
||||
const colors = hues.map((hue) =>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
import { GenerationCounts } from '../pipelines/agents/agent-generation/generation-counts';
|
||||
import { settings } from '../settings';
|
||||
import { clamp, clamp01 } from '../utils/clamp';
|
||||
import { mix } from '../utils/mix';
|
||||
import { Random } from '../utils/random';
|
||||
|
||||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export interface SpawnAction {
|
||||
generation: number;
|
||||
position: vec2;
|
||||
|
|
@ -13,8 +13,8 @@ export interface SpawnAction {
|
|||
}
|
||||
|
||||
export class GameRules {
|
||||
private static readonly DEAFULT_SPAWN_INTERVAL = 8;
|
||||
private static readonly DEAFULT_SPAWN_TIME_LENGTH = 2;
|
||||
private static readonly DEFAULT_SPAWN_INTERVAL = 8;
|
||||
private static readonly DEFAULT_SPAWN_TIME_LENGTH = 2;
|
||||
private static readonly DEFAULT_SPAWN_RADIUS = 20;
|
||||
|
||||
private lastSpawnTimeInSeconds = 0;
|
||||
|
|
@ -41,14 +41,14 @@ export class GameRules {
|
|||
public getSpawnAction(timeInSeconds: number, canvasSize: vec2): SpawnAction {
|
||||
if (
|
||||
this.lastSpawnAction &&
|
||||
timeInSeconds - this.lastSpawnTimeInSeconds < GameRules.DEAFULT_SPAWN_TIME_LENGTH
|
||||
timeInSeconds - this.lastSpawnTimeInSeconds < GameRules.DEFAULT_SPAWN_TIME_LENGTH
|
||||
) {
|
||||
return this.lastSpawnAction;
|
||||
}
|
||||
|
||||
this.currentSpawnInterval = mix(
|
||||
GameRules.DEAFULT_SPAWN_INTERVAL,
|
||||
GameRules.DEAFULT_SPAWN_INTERVAL / 5,
|
||||
GameRules.DEFAULT_SPAWN_INTERVAL,
|
||||
GameRules.DEFAULT_SPAWN_INTERVAL / 5,
|
||||
clamp01((timeInSeconds - this.lastGenerationChangeTimeInSeconds) / 120)
|
||||
);
|
||||
|
||||
|
|
@ -76,8 +76,8 @@ export class GameRules {
|
|||
this.lastSpawnAction = {
|
||||
generation: this.nextGenerationId,
|
||||
position: vec2.fromValues(
|
||||
Random.randomBetween(0, canvasSize.x),
|
||||
Random.randomBetween(0, canvasSize.y)
|
||||
Random.randomBetween(0, canvasSize[0]),
|
||||
Random.randomBetween(0, canvasSize[1])
|
||||
),
|
||||
radius: this.currentSpawnRadius,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<meta property="og:title" content="Just a bunch of blobs" />
|
||||
<meta property="og:description" content="Discover my projects." />
|
||||
<meta property="og:url" content="https://schmelczer.dev" />
|
||||
|
||||
<meta property="og:image:width" content="1920" />
|
||||
<meta property="og:image:height" content="1920" />
|
||||
<meta property="og:image" content="https://schmelczer.dev/og-image.jpg" />
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<meta
|
||||
name="description"
|
||||
content="I'm Andras Schmelczer, and this is my portfolio. Discover some of my projects. I'm passionate about solving challenging problems and designing large-scale systems, especially in the context of machine learning."
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,viewport-fit=cover"
|
||||
/>
|
||||
<meta name="theme-color" content="#b7455e" />
|
||||
|
||||
<title>Just a bunch of blobs</title>
|
||||
|
||||
<link inline inline-asset="index.css" inline-asset-delete />
|
||||
</head>
|
||||
<body>
|
||||
<main class="canvas-container">
|
||||
<canvas></canvas>
|
||||
|
||||
<section class="errors-container">
|
||||
<noscript>JavaScript is required for this website.</noscript>
|
||||
</section>
|
||||
|
||||
<!-- <div class="counters"><pre></pre></div> -->
|
||||
</main>
|
||||
|
||||
<aside>
|
||||
<nav class="buttons">
|
||||
<button class="info"></button>
|
||||
<button class="maximize-full-screen"></button>
|
||||
<button class="minimize-full-screen"></button>
|
||||
<button class="settings"></button>
|
||||
<button class="restart"></button>
|
||||
</nav>
|
||||
|
||||
<main class="pages hidden info-page">
|
||||
<section>
|
||||
<h1>Just a bunch of blobs</h1>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Eaque itaque
|
||||
perspiciatis nesciunt, molestiae officiis dignissimos porro! Provident totam
|
||||
sit enim, dolores dicta possimus ex assumenda earum, ea tempore, aut quidem?
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<main class="pages hidden settings-page">
|
||||
<section>
|
||||
<div class="settings-content"></div>
|
||||
<button id="apply-defaults" class="large-button">Apply defaults</button>
|
||||
</section>
|
||||
</main>
|
||||
</aside>
|
||||
<script inline inline-asset="index.js" inline-asset-delete></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -15,8 +15,10 @@ html > body {
|
|||
> canvas {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
cursor: url('../assets/icons/brush.svg') 0 24, auto;
|
||||
touch-action: none;
|
||||
cursor:
|
||||
url('../assets/icons/brush.svg') 0 24,
|
||||
auto;
|
||||
}
|
||||
|
||||
> .errors-container {
|
||||
|
|
@ -107,7 +109,8 @@ html > body {
|
|||
@include on-large-screen {
|
||||
width: 0;
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
transition: background-color var(--transition-time),
|
||||
transition:
|
||||
background-color var(--transition-time),
|
||||
width var(--transition-time);
|
||||
left: calc(-1 * var(--small-margin));
|
||||
height: 140%;
|
||||
|
|
@ -118,7 +121,8 @@ html > body {
|
|||
@include on-small-screen {
|
||||
height: 0;
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
transition: background-color var(--transition-time),
|
||||
transition:
|
||||
background-color var(--transition-time),
|
||||
height var(--transition-time);
|
||||
top: calc(-1 * var(--small-margin));
|
||||
width: 140%;
|
||||
|
|
@ -130,10 +134,10 @@ html > body {
|
|||
&::after {
|
||||
background-color: var(--accent-color);
|
||||
|
||||
transition: transform var(--transition-time),
|
||||
transition:
|
||||
transform var(--transition-time),
|
||||
background-color var(--transition-time);
|
||||
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
|
||||
@include square(var(--icon-size));
|
||||
|
|
@ -160,27 +164,18 @@ html > body {
|
|||
}
|
||||
|
||||
&.info::after {
|
||||
-webkit-mask-image: url('../assets/icons/info.svg');
|
||||
mask-image: url('../assets/icons/info.svg');
|
||||
}
|
||||
|
||||
&.maximize-full-screen::after {
|
||||
-webkit-mask-image: url('../assets/icons/maximize.svg');
|
||||
mask-image: url('../assets/icons/maximize.svg');
|
||||
}
|
||||
|
||||
&.minimize-full-screen::after {
|
||||
-webkit-mask-image: url('../assets/icons/minimize.svg');
|
||||
mask-image: url('../assets/icons/minimize.svg');
|
||||
}
|
||||
|
||||
&.settings::after {
|
||||
-webkit-mask-image: url('../assets/icons/settings.svg');
|
||||
mask-image: url('../assets/icons/settings.svg');
|
||||
}
|
||||
|
||||
&.restart::after {
|
||||
-webkit-mask-image: url('../assets/icons/restart.svg');
|
||||
mask-image: url('../assets/icons/restart.svg');
|
||||
}
|
||||
}
|
||||
|
|
@ -189,6 +184,8 @@ html > body {
|
|||
> main.pages {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--main-color) transparent;
|
||||
&::-webkit-scrollbar-track,
|
||||
&::-webkit-scrollbar {
|
||||
background-color: transparent;
|
||||
|
|
@ -201,7 +198,9 @@ html > body {
|
|||
|
||||
&,
|
||||
> * {
|
||||
transition: width var(--transition-time-long), height var(--transition-time-long);
|
||||
transition:
|
||||
width var(--transition-time-long),
|
||||
height var(--transition-time-long);
|
||||
|
||||
@include on-large-screen {
|
||||
width: max(500px, 10vw);
|
||||
|
|
|
|||
40
src/index.ts
|
|
@ -1,36 +1,19 @@
|
|||
import '../assets/icons/info.svg';
|
||||
import { isProduction, lastEdit } from './constants';
|
||||
import { isProduction } from './constants';
|
||||
import GameLoop from './game-loop/game-loop';
|
||||
import { GameRules } from './game-loop/game-rules';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
import { CollapsiblePanelAnimator } from './page/collapsible-panel-animator';
|
||||
import { FullScreenHandler } from './page/full-screen-handler';
|
||||
import { MenuHider } from './page/menu-hider';
|
||||
import { setUpSettingsPage } from './page/set-up-settings-page';
|
||||
import { SettingsSlider } from './page/settings-slider';
|
||||
import { resetSettings } from './settings';
|
||||
import { applyArrayPlugins } from './utils/array';
|
||||
import { DeltaTimeCalculator } from './utils/delta-time-calculator';
|
||||
import { ErrorHandler, Severity } from './utils/error-handler';
|
||||
import { initializeGpu } from './utils/graphics/initialize-gpu';
|
||||
|
||||
declare global {
|
||||
interface Array<T> {
|
||||
x: T;
|
||||
y: T;
|
||||
}
|
||||
|
||||
interface ReadonlyArray<T> {
|
||||
x: T;
|
||||
y: T;
|
||||
}
|
||||
|
||||
interface Float32Array {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
}
|
||||
|
||||
const elements = {
|
||||
aside: document.querySelector('aside') as HTMLDivElement,
|
||||
infoButton: document.querySelector('button.info') as HTMLButtonElement,
|
||||
|
|
@ -47,9 +30,7 @@ const elements = {
|
|||
settingsButton: document.querySelector('button.settings') as HTMLButtonElement,
|
||||
restartButton: document.querySelector('button.restart') as HTMLButtonElement,
|
||||
canvas: document.querySelector('canvas') as HTMLCanvasElement,
|
||||
canvasContainer: document.querySelector('main.canvas-container') as HTMLCanvasElement,
|
||||
errorContainer: document.querySelector('.errors-container') as HTMLDivElement,
|
||||
// counters: document.querySelector('.counters > pre') as HTMLPreElement,
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
|
|
@ -57,8 +38,6 @@ const main = async () => {
|
|||
let shouldStop = false;
|
||||
let game: GameLoop | null = null;
|
||||
|
||||
applyArrayPlugins();
|
||||
|
||||
ErrorHandler.addOnErrorListener((error, _metadata) => {
|
||||
elements.errorContainer.innerHTML += `
|
||||
<pre class="${error.severity}">${error.message}</div>
|
||||
|
|
@ -109,16 +88,6 @@ const main = async () => {
|
|||
sliders.forEach((slider) => slider.updateSliderValueBasedOnSource());
|
||||
});
|
||||
|
||||
console.log({ lastEdit });
|
||||
|
||||
// const updateCounters = () => {
|
||||
// elements.counters.innerHTML = `FPS: ${deltaTimeCalculator.fps.toFixed(2)}
|
||||
// current gen: ${formatNumber(game?.aliveAgentCounts.currentGenerationCount ?? 0)}
|
||||
// next gen: ${formatNumber(game?.aliveAgentCounts.nextGenerationCount ?? 0)}`;
|
||||
// window.requestAnimationFrame(updateCounters);
|
||||
// };
|
||||
// updateCounters();
|
||||
|
||||
while (!shouldStop) {
|
||||
const gameRules = new GameRules(performance.now() / 1000);
|
||||
game = new GameLoop(elements.canvas, gpu, deltaTimeCalculator, gameRules);
|
||||
|
|
@ -130,7 +99,8 @@ const main = async () => {
|
|||
await game.start();
|
||||
}
|
||||
} catch (e) {
|
||||
ErrorHandler.addError(Severity.ERROR, e.stack);
|
||||
const message = e instanceof Error ? (e.stack ?? e.message) : String(e);
|
||||
ErrorHandler.addError(Severity.ERROR, message);
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,14 +16,16 @@ export class FullScreenHandler {
|
|||
// on full screen request, only apply it to the target
|
||||
if (e.key === 'F11') {
|
||||
e.preventDefault();
|
||||
FullScreenHandler.isInFullScreenMode()
|
||||
? document.exitFullscreen()
|
||||
: target.requestFullscreen();
|
||||
if (FullScreenHandler.isInFullScreenMode()) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
target.requestFullscreen();
|
||||
}
|
||||
}
|
||||
});
|
||||
addEventListener('fullscreenchange', this.updateButtons.bind(this));
|
||||
maximizeButton.addEventListener('click', target.requestFullscreen.bind(target));
|
||||
minimizeButton.addEventListener('click', document.exitFullscreen.bind(document));
|
||||
maximizeButton.addEventListener('click', () => target.requestFullscreen());
|
||||
minimizeButton.addEventListener('click', () => document.exitFullscreen());
|
||||
}
|
||||
|
||||
public static isInFullScreenMode(): boolean {
|
||||
|
|
|
|||
|
|
@ -6,13 +6,16 @@ export const setUpSettingsPage = (
|
|||
settingsPage: HTMLDivElement,
|
||||
maxAgentCount: number
|
||||
): Array<SettingsSlider<any>> => {
|
||||
const sliders = [
|
||||
!isProduction &&
|
||||
new SettingsSlider(settings, 'renderSpeed', {
|
||||
min: 1,
|
||||
max: 10,
|
||||
rounding: Math.round,
|
||||
}),
|
||||
const sliders: Array<SettingsSlider<any>> = [
|
||||
...(isProduction
|
||||
? []
|
||||
: [
|
||||
new SettingsSlider(settings, 'renderSpeed', {
|
||||
min: 1,
|
||||
max: 10,
|
||||
rounding: Math.round,
|
||||
}),
|
||||
]),
|
||||
|
||||
new SettingsSlider(settings, 'agentCount', {
|
||||
min: 1,
|
||||
|
|
@ -102,11 +105,9 @@ export const setUpSettingsPage = (
|
|||
|
||||
const sliderContainerElement = document.createElement('div');
|
||||
|
||||
sliders
|
||||
.filter((v) => v)
|
||||
.forEach((slider) => {
|
||||
sliderContainerElement.appendChild(slider.element);
|
||||
});
|
||||
sliders.forEach((slider) => {
|
||||
sliderContainerElement.appendChild(slider.element);
|
||||
});
|
||||
|
||||
settingsPage.appendChild(sliderContainerElement);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import { getWorkgroupCounts } from '../../../utils/graphics/get-workgroup-counts
|
|||
import { smartCompile } from '../../../utils/graphics/smart-compile';
|
||||
import { CommonState } from '../../common-state/common-state';
|
||||
import { AGENT_SIZE_IN_BYTES } from './agent';
|
||||
import countingShader from './agent-counting.wgsl';
|
||||
import firstGenerationShader from './agent-first-generation.wgsl';
|
||||
import agentSchema from './agent-schema.wgsl';
|
||||
import countingShader from './agent-counting.wgsl?raw';
|
||||
import firstGenerationShader from './agent-first-generation.wgsl?raw';
|
||||
import agentSchema from './agent-schema.wgsl?raw';
|
||||
import { GenerationCounts } from './generation-counts';
|
||||
|
||||
export class AgentGenerationPipeline {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
import { getWorkgroupCounts } from '../../utils/graphics/get-workgroup-counts';
|
||||
import { smartCompile } from '../../utils/graphics/smart-compile';
|
||||
import { CommonState } from '../common-state/common-state';
|
||||
import agentSchme from './agent-generation/agent-schema.wgsl';
|
||||
import agentSchema from './agent-generation/agent-schema.wgsl?raw';
|
||||
import { AgentSettings } from './agent-settings';
|
||||
import shader from './agent.wgsl';
|
||||
|
||||
import { vec2 } from 'gl-matrix';
|
||||
import shader from './agent.wgsl?raw';
|
||||
|
||||
export class AgentPipeline {
|
||||
private static readonly WORKGROUP_SIZE = 64;
|
||||
|
|
@ -32,7 +32,7 @@ export class AgentPipeline {
|
|||
bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout],
|
||||
}),
|
||||
compute: {
|
||||
module: smartCompile(device, CommonState.shaderCode, agentSchme, shader),
|
||||
module: smartCompile(device, CommonState.shaderCode, agentSchema, shader),
|
||||
entryPoint: 'main',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
import { clamp } from '../../utils/clamp';
|
||||
import { smartCompile } from '../../utils/graphics/smart-compile';
|
||||
import { last } from '../../utils/last';
|
||||
import { CommonState } from '../common-state/common-state';
|
||||
import { BrushSettings } from './brush-settings';
|
||||
import shader from './brush.wgsl';
|
||||
|
||||
import { vec2 } from 'gl-matrix';
|
||||
import shader from './brush.wgsl?raw';
|
||||
|
||||
export class BrushPipeline {
|
||||
private static readonly UNIFORM_COUNT = 2;
|
||||
|
|
@ -188,7 +187,7 @@ export class BrushPipeline {
|
|||
result.push(position);
|
||||
}
|
||||
|
||||
result.push(last(points));
|
||||
result.push(points[points.length - 1]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { generateNoise } from '../../utils/graphics/noise';
|
||||
|
||||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
import { generateNoise } from '../../utils/graphics/noise';
|
||||
|
||||
export class CommonState {
|
||||
private static readonly UNIFORM_COUNT = 4;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { smartCompile } from '../../utils/graphics/smart-compile';
|
||||
import shader from './copy.wgsl';
|
||||
|
||||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
import { smartCompile } from '../../utils/graphics/smart-compile';
|
||||
import shader from './copy.wgsl?raw';
|
||||
|
||||
export class CopyPipeline {
|
||||
private static readonly UNIFORM_COUNT = 2;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad';
|
||||
import { smartCompile } from '../../utils/graphics/smart-compile';
|
||||
import { CommonState } from '../common-state/common-state';
|
||||
import shader from './diffuse.wgsl';
|
||||
import shader from './diffuse.wgsl?raw';
|
||||
import { DiffusionSettings } from './diffusion-settings';
|
||||
|
||||
export class DiffusionPipeline {
|
||||
|
|
@ -11,7 +11,6 @@ export class DiffusionPipeline {
|
|||
private readonly pipeline: GPURenderPipeline;
|
||||
private readonly uniforms: GPUBuffer;
|
||||
private readonly vertexBuffer: GPUBuffer;
|
||||
private readonly noise: GPUTextureView;
|
||||
|
||||
private bindGroup?: GPUBindGroup;
|
||||
private previousTrailMapIn?: GPUTextureView;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { vec3 } from 'gl-matrix';
|
||||
|
||||
import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad';
|
||||
import { smartCompile } from '../../utils/graphics/smart-compile';
|
||||
import { CommonState } from '../common-state/common-state';
|
||||
import { RenderSettings } from './render-settings';
|
||||
import shader from './render.wgsl';
|
||||
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import shader from './render.wgsl?raw';
|
||||
|
||||
export class RenderPipeline {
|
||||
private static readonly UNIFORM_COUNT = 13;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ p {
|
|||
|
||||
html {
|
||||
height: 100%;
|
||||
-webkit-font-smooth: antialiased;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.large-button {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local(''),
|
||||
src:
|
||||
local(''),
|
||||
url('../../assets/fonts/comfortaa-v40-latin-regular.woff2') format('woff2'),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('../../assets/fonts/comfortaa-v40-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
|
|
@ -16,7 +17,8 @@
|
|||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local(''),
|
||||
src:
|
||||
local(''),
|
||||
url('../../assets/fonts/open-sans-v34-latin-regular.woff2') format('woff2'),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('../../assets/fonts/open-sans-v34-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
|
|
|
|||
|
|
@ -49,14 +49,6 @@ $breakpoint-width: 600px !default;
|
|||
@mixin blurred-background($color: transparent) {
|
||||
background-color: color.adjust($color, $alpha: -0.66);
|
||||
backdrop-filter: blur(var(--blur-radius));
|
||||
-webkit-backdrop-filter: blur(var(--blur-radius));
|
||||
|
||||
@supports not (
|
||||
(backdrop-filter: blur(var(--blur-radius))) or
|
||||
(-webkit-backdrop-filter: blur(var(--blur-radius)))
|
||||
) {
|
||||
background-color: $color;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin square($size) {
|
||||
|
|
@ -65,7 +57,9 @@ $breakpoint-width: 600px !default;
|
|||
}
|
||||
|
||||
@mixin title-font() {
|
||||
font: 400 3rem 'Comfortaa', sans-serif;
|
||||
font:
|
||||
400 3rem 'Comfortaa',
|
||||
sans-serif;
|
||||
color: var(--normal-text-color);
|
||||
line-height: 1;
|
||||
|
||||
|
|
@ -76,20 +70,26 @@ $breakpoint-width: 600px !default;
|
|||
}
|
||||
|
||||
@mixin sub-title-font() {
|
||||
font: 400 1.75rem 'Comfortaa', sans-serif;
|
||||
font:
|
||||
400 1.75rem 'Comfortaa',
|
||||
sans-serif;
|
||||
color: var(--normal-text-color);
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
@mixin main-font() {
|
||||
font: 400 1.1rem 'Open Sans', sans-serif;
|
||||
font:
|
||||
400 1.1rem 'Open Sans',
|
||||
sans-serif;
|
||||
color: var(--normal-text-color);
|
||||
line-height: 1.8;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
@mixin special-text-font() {
|
||||
font: 400 1rem 'Open Sans', sans-serif;
|
||||
font:
|
||||
400 1rem 'Open Sans',
|
||||
sans-serif;
|
||||
color: var(--special-text-color);
|
||||
hyphens: auto;
|
||||
font-style: italic;
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
const setIndexAlias = (name: string, index: number, type: any) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(type.prototype, name)) {
|
||||
Object.defineProperty(type.prototype, name, {
|
||||
get() {
|
||||
return this[index];
|
||||
},
|
||||
set(value) {
|
||||
this[index] = value;
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const applyArrayPlugins = () => {
|
||||
setIndexAlias('x', 0, Array);
|
||||
setIndexAlias('y', 1, Array);
|
||||
setIndexAlias('x', 0, Float32Array);
|
||||
setIndexAlias('y', 1, Float32Array);
|
||||
};
|
||||
|
|
@ -41,6 +41,6 @@ export class DeltaTimeCalculator {
|
|||
}
|
||||
|
||||
public get fps() {
|
||||
return 1 / this.deltaTimeAccumulator;
|
||||
return this.deltaTimeAccumulator ? 1 / this.deltaTimeAccumulator : 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export class ErrorHandler {
|
|||
}
|
||||
|
||||
public static addMetadata(key: string, value: any) {
|
||||
const serialized = {};
|
||||
const serialized: Record<string, any> = {};
|
||||
for (const k in value) {
|
||||
serialized[k] = value[k];
|
||||
}
|
||||
|
|
|
|||
22
src/utils/format-number.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { formatNumber } from './format-number';
|
||||
|
||||
describe('formatNumber', () => {
|
||||
it('renders integers without decimals', () => {
|
||||
expect(formatNumber(42)).toBe('42 ');
|
||||
});
|
||||
it('renders fractional values with two decimals', () => {
|
||||
expect(formatNumber(3.14159)).toBe('3.14 ');
|
||||
});
|
||||
it('renders thousands compactly', () => {
|
||||
expect(formatNumber(2500)).toBe('2.5 thousand ');
|
||||
});
|
||||
it('renders millions compactly', () => {
|
||||
expect(formatNumber(1_500_000)).toBe('1.5 million ');
|
||||
});
|
||||
it('appends the unit when provided', () => {
|
||||
expect(formatNumber(5, 'agents')).toBe('5 agents');
|
||||
expect(formatNumber(2_000_000, 'agents')).toBe('2.0 million agents');
|
||||
});
|
||||
});
|
||||
|
|
@ -13,75 +13,77 @@ export const generateNoise = ({
|
|||
height: number;
|
||||
}): GPUTextureView => {
|
||||
const cacheKey = `${width}x${height}`;
|
||||
if (!textureCache.has(cacheKey)) {
|
||||
const { buffer, vertex } = setUpFullScreenQuad(device);
|
||||
const vertexBuffer = buffer;
|
||||
|
||||
const pipeline = device.createRenderPipeline({
|
||||
layout: 'auto',
|
||||
vertex,
|
||||
fragment: {
|
||||
module: smartCompile(
|
||||
device,
|
||||
/* wgsl */ `
|
||||
fn random_with_seed(uv: vec2<f32>, seed: f32) -> f32 {
|
||||
return fract(sin(dot(uv, vec2(12.9898 + seed, 78.233 + seed)))* 43758.5453123 + seed);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
return vec4(
|
||||
random_with_seed(uv, 0),
|
||||
random_with_seed(uv, 1),
|
||||
random_with_seed(uv, 2),
|
||||
random_with_seed(uv, 3),
|
||||
);
|
||||
}`
|
||||
),
|
||||
entryPoint: 'fragment',
|
||||
targets: [
|
||||
{
|
||||
format: 'rgba16float',
|
||||
},
|
||||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: 'triangle-strip',
|
||||
},
|
||||
});
|
||||
|
||||
const colorTexture = device.createTexture({
|
||||
size: {
|
||||
width,
|
||||
height,
|
||||
depthOrArrayLayers: 1,
|
||||
},
|
||||
format: 'rgba16float',
|
||||
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
const renderPassDescriptor: GPURenderPassDescriptor = {
|
||||
colorAttachments: [
|
||||
{
|
||||
view: colorTexture.createView(),
|
||||
clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const commandEncoder = device.createCommandEncoder();
|
||||
|
||||
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
|
||||
passEncoder.setPipeline(pipeline);
|
||||
passEncoder.setVertexBuffer(0, vertexBuffer);
|
||||
passEncoder.draw(4, 1);
|
||||
passEncoder.end();
|
||||
|
||||
device.queue.submit([commandEncoder.finish()]);
|
||||
textureCache.set(cacheKey, colorTexture);
|
||||
const cached = textureCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached.createView();
|
||||
}
|
||||
|
||||
return textureCache.get(cacheKey).createView();
|
||||
const { buffer, vertex } = setUpFullScreenQuad(device);
|
||||
const vertexBuffer = buffer;
|
||||
|
||||
const pipeline = device.createRenderPipeline({
|
||||
layout: 'auto',
|
||||
vertex,
|
||||
fragment: {
|
||||
module: smartCompile(
|
||||
device,
|
||||
/* wgsl */ `
|
||||
fn random_with_seed(uv: vec2<f32>, seed: f32) -> f32 {
|
||||
return fract(sin(dot(uv, vec2(12.9898 + seed, 78.233 + seed)))* 43758.5453123 + seed);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
return vec4(
|
||||
random_with_seed(uv, 0),
|
||||
random_with_seed(uv, 1),
|
||||
random_with_seed(uv, 2),
|
||||
random_with_seed(uv, 3),
|
||||
);
|
||||
}`
|
||||
),
|
||||
entryPoint: 'fragment',
|
||||
targets: [
|
||||
{
|
||||
format: 'rgba16float',
|
||||
},
|
||||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: 'triangle-strip',
|
||||
},
|
||||
});
|
||||
|
||||
const colorTexture = device.createTexture({
|
||||
size: {
|
||||
width,
|
||||
height,
|
||||
depthOrArrayLayers: 1,
|
||||
},
|
||||
format: 'rgba16float',
|
||||
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
const renderPassDescriptor: GPURenderPassDescriptor = {
|
||||
colorAttachments: [
|
||||
{
|
||||
view: colorTexture.createView(),
|
||||
clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const commandEncoder = device.createCommandEncoder();
|
||||
|
||||
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
|
||||
passEncoder.setPipeline(pipeline);
|
||||
passEncoder.setVertexBuffer(0, vertexBuffer);
|
||||
passEncoder.draw(4, 1);
|
||||
passEncoder.end();
|
||||
|
||||
device.queue.submit([commandEncoder.finish()]);
|
||||
textureCache.set(cacheKey, colorTexture);
|
||||
return colorTexture.createView();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,48 +1,40 @@
|
|||
import { CopyPipeline } from '../../pipelines/copy/copy-pipeline';
|
||||
|
||||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
import { CopyPipeline } from '../../pipelines/copy/copy-pipeline';
|
||||
|
||||
export class ResizableTexture {
|
||||
private texture: GPUTexture;
|
||||
private textureView: GPUTextureView;
|
||||
private size: vec2;
|
||||
private readonly copyPipeline: CopyPipeline;
|
||||
private size: vec2 | null = null;
|
||||
|
||||
public constructor(private readonly device: GPUDevice, size: vec2) {
|
||||
public constructor(
|
||||
private readonly device: GPUDevice,
|
||||
size: vec2
|
||||
) {
|
||||
this.copyPipeline = new CopyPipeline(this.device);
|
||||
this.resize(size);
|
||||
this.size = size;
|
||||
this.texture = this.createTexture(size);
|
||||
this.textureView = this.texture.createView();
|
||||
}
|
||||
|
||||
public resize(size: vec2): void {
|
||||
if (this.size !== null && vec2.equals(this.size, size)) {
|
||||
if (vec2.equals(this.size, size)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTexture = this.device.createTexture({
|
||||
format: 'rgba16float',
|
||||
size: {
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
},
|
||||
usage:
|
||||
GPUTextureUsage.STORAGE_BINDING |
|
||||
GPUTextureUsage.TEXTURE_BINDING |
|
||||
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
const newTexture = this.createTexture(size);
|
||||
const newTextureView = newTexture.createView();
|
||||
|
||||
if (this.textureView) {
|
||||
const commandEncoder = this.device.createCommandEncoder();
|
||||
this.copyPipeline.execute(
|
||||
commandEncoder,
|
||||
this.textureView,
|
||||
newTextureView,
|
||||
vec2.div(vec2.create(), this.size, size)
|
||||
);
|
||||
this.device.queue.submit([commandEncoder.finish()]);
|
||||
this.texture.destroy();
|
||||
}
|
||||
const commandEncoder = this.device.createCommandEncoder();
|
||||
this.copyPipeline.execute(
|
||||
commandEncoder,
|
||||
this.textureView,
|
||||
newTextureView,
|
||||
vec2.div(vec2.create(), this.size, size)
|
||||
);
|
||||
this.device.queue.submit([commandEncoder.finish()]);
|
||||
this.texture.destroy();
|
||||
|
||||
this.size = size;
|
||||
this.texture = newTexture;
|
||||
|
|
@ -57,4 +49,15 @@ export class ResizableTexture {
|
|||
this.texture.destroy();
|
||||
this.copyPipeline.destroy();
|
||||
}
|
||||
|
||||
private createTexture(size: vec2): GPUTexture {
|
||||
return this.device.createTexture({
|
||||
format: 'rgba16float',
|
||||
size: { width: size[0], height: size[1] },
|
||||
usage:
|
||||
GPUTextureUsage.STORAGE_BINDING |
|
||||
GPUTextureUsage.TEXTURE_BINDING |
|
||||
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
42
src/utils/hsl.test.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { hsl } from './hsl';
|
||||
|
||||
describe('hsl', () => {
|
||||
it('produces pure red at hue 0', () => {
|
||||
const [r, g, b] = hsl(0, 100, 50);
|
||||
expect(r).toBeCloseTo(1);
|
||||
expect(g).toBeCloseTo(0);
|
||||
expect(b).toBeCloseTo(0);
|
||||
});
|
||||
it('produces pure green at hue 120', () => {
|
||||
const [r, g, b] = hsl(120, 100, 50);
|
||||
expect(r).toBeCloseTo(0);
|
||||
expect(g).toBeCloseTo(1);
|
||||
expect(b).toBeCloseTo(0);
|
||||
});
|
||||
it('produces pure blue at hue 240', () => {
|
||||
const [r, g, b] = hsl(240, 100, 50);
|
||||
expect(r).toBeCloseTo(0);
|
||||
expect(g).toBeCloseTo(0);
|
||||
expect(b).toBeCloseTo(1);
|
||||
});
|
||||
it('produces gray at saturation 0', () => {
|
||||
const [r, g, b] = hsl(180, 0, 50);
|
||||
expect(r).toBeCloseTo(0.5);
|
||||
expect(g).toBeCloseTo(0.5);
|
||||
expect(b).toBeCloseTo(0.5);
|
||||
});
|
||||
it('produces black at lightness 0', () => {
|
||||
const [r, g, b] = hsl(0, 100, 0);
|
||||
expect(r).toBe(0);
|
||||
expect(g).toBe(0);
|
||||
expect(b).toBe(0);
|
||||
});
|
||||
it('produces white at lightness 100', () => {
|
||||
const [r, g, b] = hsl(0, 100, 100);
|
||||
expect(r).toBeCloseTo(1);
|
||||
expect(g).toBeCloseTo(1);
|
||||
expect(b).toBeCloseTo(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { rgb } from './rgb';
|
||||
|
||||
import { vec3 } from 'gl-matrix';
|
||||
|
||||
import { rgb } from './rgb';
|
||||
|
||||
export const hsl = (hue: number, saturation: number, lightness: number): vec3 => {
|
||||
hue /= 360;
|
||||
saturation /= 100;
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export function last<T>(a: Array<T>): T | null {
|
||||
return a.length > 0 ? a[a.length - 1] : null;
|
||||
}
|
||||
63
src/utils/math.test.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { clamp, clamp01 } from './clamp';
|
||||
import { exponentialDecay } from './exponential-decay';
|
||||
import { mix } from './mix';
|
||||
|
||||
describe('clamp', () => {
|
||||
it('returns value when within bounds', () => {
|
||||
expect(clamp(5, 0, 10)).toBe(5);
|
||||
});
|
||||
it('clamps below to lower bound', () => {
|
||||
expect(clamp(-3, 0, 10)).toBe(0);
|
||||
});
|
||||
it('clamps above to upper bound', () => {
|
||||
expect(clamp(42, 0, 10)).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clamp01', () => {
|
||||
it('passes through values in [0, 1]', () => {
|
||||
expect(clamp01(0.25)).toBe(0.25);
|
||||
});
|
||||
it('clamps negatives to 0', () => {
|
||||
expect(clamp01(-1)).toBe(0);
|
||||
});
|
||||
it('clamps above 1 to 1', () => {
|
||||
expect(clamp01(2)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mix', () => {
|
||||
it('returns from at q=0', () => {
|
||||
expect(mix(10, 20, 0)).toBe(10);
|
||||
});
|
||||
it('returns to at q=1', () => {
|
||||
expect(mix(10, 20, 1)).toBe(20);
|
||||
});
|
||||
it('interpolates at q=0.5', () => {
|
||||
expect(mix(10, 20, 0.5)).toBe(15);
|
||||
});
|
||||
it('extrapolates outside [0, 1]', () => {
|
||||
expect(mix(0, 10, 2)).toBe(20);
|
||||
expect(mix(0, 10, -1)).toBe(-10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exponentialDecay', () => {
|
||||
it('returns nextValue when bias is 1', () => {
|
||||
expect(exponentialDecay({ accumulator: 0, nextValue: 10, biasOfNextValue: 1 })).toBe(
|
||||
10
|
||||
);
|
||||
});
|
||||
it('returns accumulator when bias is 0', () => {
|
||||
expect(exponentialDecay({ accumulator: 5, nextValue: 10, biasOfNextValue: 0 })).toBe(
|
||||
5
|
||||
);
|
||||
});
|
||||
it('blends with given bias', () => {
|
||||
expect(
|
||||
exponentialDecay({ accumulator: 0, nextValue: 10, biasOfNextValue: 0.25 })
|
||||
).toBe(2.5);
|
||||
});
|
||||
});
|
||||
|
|
@ -2,7 +2,7 @@ export const persist = <T extends Record<string, number>>(wrapee: T): T => {
|
|||
const keys = Object.keys(wrapee);
|
||||
keys.sort();
|
||||
|
||||
const keysToShortKeys = Object.fromEntries(keys.map((key, i) => [key, key]));
|
||||
const keysToShortKeys = Object.fromEntries(keys.map((key) => [key, key]));
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const newParams = new URLSearchParams();
|
||||
|
|
|
|||
41
src/utils/random.test.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { Random } from './random';
|
||||
|
||||
describe('Random', () => {
|
||||
beforeEach(() => {
|
||||
Random.seed = 42;
|
||||
});
|
||||
|
||||
it('produces values in [0, 1)', () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const v = Random.getRandom();
|
||||
expect(v).toBeGreaterThanOrEqual(0);
|
||||
expect(v).toBeLessThan(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('is deterministic for the same seed', () => {
|
||||
Random.seed = 42;
|
||||
const a = Array.from({ length: 8 }, () => Random.getRandom());
|
||||
Random.seed = 42;
|
||||
const b = Array.from({ length: 8 }, () => Random.getRandom());
|
||||
expect(a).toEqual(b);
|
||||
});
|
||||
|
||||
it('produces different sequences for different seeds', () => {
|
||||
Random.seed = 1;
|
||||
const a = Array.from({ length: 4 }, () => Random.getRandom());
|
||||
Random.seed = 2;
|
||||
const b = Array.from({ length: 4 }, () => Random.getRandom());
|
||||
expect(a).not.toEqual(b);
|
||||
});
|
||||
|
||||
it('randomBetween stays within [from, to)', () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const v = Random.randomBetween(-10, 10);
|
||||
expect(v).toBeGreaterThanOrEqual(-10);
|
||||
expect(v).toBeLessThan(10);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,37 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"lib": [
|
||||
"es2017",
|
||||
"es2017.object",
|
||||
"es2017.sharedmemory",
|
||||
"es2016",
|
||||
"es2016.array.include",
|
||||
"es2015",
|
||||
"es2015.core",
|
||||
"es2015.promise",
|
||||
"es2015.collection",
|
||||
"es5",
|
||||
"dom"
|
||||
],
|
||||
"module": "commonjs",
|
||||
"declaration": false,
|
||||
"noImplicitAny": false,
|
||||
"removeComments": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"noEmitHelpers": false,
|
||||
"sourceMap": true,
|
||||
"strictNullChecks": false,
|
||||
"jsx": "react",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
||||
"types": ["@webgpu/types", "vite/client"],
|
||||
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false
|
||||
},
|
||||
"include": [
|
||||
"definitions.d.ts",
|
||||
"src/**/*",
|
||||
"node_modules/@webgpu/types/**/*"
|
||||
],
|
||||
"compileOnSave": false,
|
||||
"buildOnSave": false
|
||||
}
|
||||
"include": ["src/**/*", "definitions.d.ts", "vite.config.ts"]
|
||||
}
|
||||
|
|
|
|||
29
vite.config.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import browserslist from 'browserslist';
|
||||
import { browserslistToTargets } from 'lightningcss';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||
|
||||
const cssTargets = browserslistToTargets(browserslist());
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [viteSingleFile()],
|
||||
css: {
|
||||
transformer: 'lightningcss',
|
||||
lightningcss: {
|
||||
targets: cssTargets,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
target: 'es2022',
|
||||
cssCodeSplit: false,
|
||||
cssMinify: 'lightningcss',
|
||||
assetsInlineLimit: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
server: {
|
||||
open: true,
|
||||
},
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const InlineSourceWebpackPlugin = require('inline-source-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const DefinePlugin = require('webpack').DefinePlugin;
|
||||
|
||||
module.exports = (env, argv) => ({
|
||||
devtool: argv.mode === 'development' ? 'inline-source-map' : false,
|
||||
entry: {
|
||||
index: './src/index.ts',
|
||||
},
|
||||
watchOptions: {
|
||||
ignored: '**/node_modules',
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
module: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/index.html',
|
||||
}),
|
||||
new MiniCssExtractPlugin(),
|
||||
argv.mode === 'production' &&
|
||||
new InlineSourceWebpackPlugin({
|
||||
compress: true,
|
||||
}),
|
||||
new DefinePlugin({
|
||||
__CURRENT_DATE__: Date.now(),
|
||||
__IS_PRODUCTION__: argv.mode === 'production',
|
||||
}),
|
||||
].filter((v) => v),
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.svg$/i,
|
||||
type: 'asset/inline',
|
||||
},
|
||||
{
|
||||
test: /\.woff2?$/i,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: '[hash:8][ext]',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\/no-change\//i,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: '[name][ext]',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.wgsl$/i,
|
||||
type: 'asset/source',
|
||||
generator: {
|
||||
filename: '[name][ext]',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.scss$/i,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'resolve-url-loader',
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sourceMap: true, // required by resolve-url-loader
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.ts$/i,
|
||||
use: 'ts-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
},
|
||||
output: {
|
||||
clean: true,
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: '',
|
||||
},
|
||||
});
|
||||