Compare commits

..

No commits in common. "main" and "test-cdn" have entirely different histories.

47 changed files with 409 additions and 8429 deletions

1
.eslintignore Normal file
View file

@ -0,0 +1 @@
**/*.js

28
.eslintrc.json Normal file
View file

@ -0,0 +1,28 @@
{
"root": true,
"env": {
"browser": true,
"es2020": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"prettier/@typescript-eslint"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"plugins": ["unused-imports", "@typescript-eslint", "prettier"],
"rules": {
"prettier/prettier": "error",
"no-unused-vars": "off",
"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"
}
}

View file

@ -1,45 +0,0 @@
name: Check & deploy
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: Build
run: npm run build
- name: Copy build to host pages mount (sdf2d)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: http://forgejo:3000/andras/ci-actions/deploy-pages@main
with:
source: dist
target: sdf2d
- name: Copy build to host pages mount (sdf-2d)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: http://forgejo:3000/andras/ci-actions/deploy-pages@main
with:
source: dist
target: sdf-2d

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
dist
node_modules
package-lock.json
.vscode
.firebase

1
.nvmrc
View file

@ -1 +0,0 @@
22.13.0

View file

@ -1,7 +1,6 @@
<h1>
<img src="static/logo-colored.svg" width="64" height="64" alt="SDF-2D logo" />
SDF-2D demo
</h1>
<img src="static/logo-colored.svg" width=128 height=128 alt="logo" />
# SDF-2D demo
![Deploy everything](https://github.com/schmelczerandras/sdf-2d-demo/workflows/Deploy%20everything/badge.svg)

View file

@ -1,39 +0,0 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettierConfig from 'eslint-config-prettier';
import prettierPlugin from 'eslint-plugin-prettier';
import unusedImports from 'eslint-plugin-unused-imports';
import globals from 'globals';
export default tseslint.config(
{ ignores: ['dist/', 'node_modules/', '**/*.js'] },
js.configs.recommended,
...tseslint.configs.recommended,
prettierConfig,
{
files: ['src/**/*.ts'],
languageOptions: {
globals: { ...globals.browser },
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
},
plugins: {
'unused-imports': unusedImports,
prettier: prettierPlugin,
},
rules: {
'prettier/prettier': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
}
);

7007
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,13 @@
{
"name": "sdf-2d-demo",
"version": "0.0.0",
"description": "Some simple demos to showcase the possibilities of this library.",
"private": true,
"main": "index.html",
"scripts": {
"dev": "webpack serve --mode development",
"start": "webpack-dev-server --mode development",
"lint": "npx eslint --fix \"src/**/*.ts\" && npx prettier --write \"src/**/*.ts\"",
"build": "webpack --mode production && rm -f dist/*.js dist/*.css"
"build": "rm -rf dist/* && webpack --mode production && find dist -type f -not -regex \"dist\\/.*\\.\\(html\\|png\\|ico\\|svg\\|jpg\\)\" | xargs rm && sed -i 's/^\\/\\/#.*.map//' dist/index.html"
},
"keywords": [],
"author": "András Schmelczer <andras@schmelczer.dev> (https://schmelczer.dev/)",
@ -23,35 +24,35 @@
"*.scss"
],
"devDependencies": {
"@eslint/js": "^10.0.1",
"@plausible-analytics/tracker": "^0.4.5",
"autoprefixer": "^10.5.0",
"css-loader": "^7.1.4",
"css-minimizer-webpack-plugin": "^8.0.0",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-unused-imports": "^4.4.1",
"gl-matrix": "^3.4.4",
"globals": "^17.6.0",
"html-inline-css-webpack-plugin": "^1.11.2",
"html-inline-script-webpack-plugin": "^3.2.1",
"html-loader": "^5.1.0",
"html-webpack-plugin": "^5.6.7",
"mini-css-extract-plugin": "^2.10.2",
"postcss": "^8.5.15",
"postcss-loader": "^8.2.1",
"prettier": "^3.8.3",
"resolve-url-loader": "^5.0.0",
"sass": "^1.100.0",
"sass-loader": "^17.0.0",
"sdf-2d": "^0.8.0",
"source-map-loader": "^5.0.0",
"ts-loader": "^9.6.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.60.1",
"webpack": "^5.107.2",
"webpack-cli": "^7.0.3",
"webpack-dev-server": "^5.2.4"
"@typescript-eslint/eslint-plugin": "^3.10.1",
"@typescript-eslint/parser": "^3.10.1",
"@types/gl-matrix": "^2.4.5",
"gl-matrix": "^3.3.0",
"autoprefixer": "^9.8.6",
"css-loader": "^3.5.2",
"eslint": "^7.9.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-unused-imports": "^0.1.3",
"html-webpack-inline-source-plugin": "0.0.10",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.9.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss-loader": "^3.0.0",
"prettier": "^2.1.2",
"raw-loader": "^4.0.1",
"resolve-url-loader": "^3.1.1",
"sass": "^1.26.3",
"sass-loader": "^9.0.3",
"sdf-2d": "^0.1.0-alpha",
"source-map-loader": "^1.1.0",
"svg-url-loader": "^6.0.0",
"terser-webpack-plugin": "^2.3.8",
"ts-loader": "^8.0.3",
"typescript": "^3.9.7",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
}
}

View file

@ -1,38 +0,0 @@
import {
init as plausibleInit,
track as plausibleTrack,
type PlausibleEventOptions,
} from '@plausible-analytics/tracker';
const ANALYTICS_AUTO_CAPTURE_PAGEVIEWS = true;
const ANALYTICS_DOMAIN = 'schmelczer.dev/sdf2d';
const ANALYTICS_ENDPOINT = 'https://stats.schmelczer.dev/status';
const ANALYTICS_LOGGING = process.env.NODE_ENV !== 'production';
let isInitialized = false;
export const track = (eventName: string, options: PlausibleEventOptions = {}) => {
try {
plausibleTrack(eventName, options);
} catch (error) {
console.warn(`Could not track analytics event "${eventName}".`, error);
}
};
export const initAnalytics = () => {
if (isInitialized) {
return;
}
try {
plausibleInit({
domain: ANALYTICS_DOMAIN,
endpoint: ANALYTICS_ENDPOINT,
autoCapturePageviews: ANALYTICS_AUTO_CAPTURE_PAGEVIEWS,
logging: ANALYTICS_LOGGING,
});
isInitialized = true;
} catch (error) {
console.warn('Could not initialize analytics.', error);
}
};

View file

@ -1,16 +0,0 @@
import { vec2 } from 'gl-matrix';
export class Circle {
constructor(
public center: vec2,
public radius: number
) {}
public distance(target: vec2): number {
return vec2.distance(this.center, target) - this.radius;
}
public distanceBetween(target: Circle): number {
return vec2.distance(target.center, this.center) - this.radius - target.radius;
}
}

View file

@ -1,19 +0,0 @@
import { RendererInfo } from 'sdf-2d/lib/src/graphics/rendering/renderer/renderer-info';
export const extractInsights = (
insights?: RendererInfo
): {
vendor?: string;
renderer?: string;
fps?: number;
renderScale?: number;
lightScale?: number;
version?: string;
} => ({
fps: insights?.fps,
vendor: insights?.vendor,
renderer: insights?.renderer,
renderScale: insights?.renderPasses.distance.renderScale,
lightScale: insights?.renderPasses.lights.renderScale,
version: insights?.sdf2dVersion,
});

View file

@ -1,48 +0,0 @@
export const handleFullScreen = (
minimizeButton: HTMLElement,
maximizeButton: HTMLElement,
target: HTMLElement
) => {
if (!document.fullscreenEnabled) {
minimizeButton.style.visibility = 'hidden';
maximizeButton.style.visibility = 'hidden';
return;
}
const isInFullScreen = (): boolean => document.fullscreenElement !== null;
const showButtons = () => {
minimizeButton.style.visibility = isInFullScreen() ? 'visible' : 'hidden';
maximizeButton.style.visibility = isInFullScreen() ? 'hidden' : 'visible';
};
showButtons();
let currentWindowHeight = innerHeight;
const followToggle = () => {
showButtons();
currentWindowHeight = innerHeight;
};
const triggerToggle = async () => {
await (isInFullScreen() ? document.exitFullscreen() : target.requestFullscreen());
followToggle();
};
addEventListener('keydown', (e) => {
if (e.key === 'F11') {
triggerToggle();
e.preventDefault();
}
});
addEventListener('resize', () => {
if (isInFullScreen && currentWindowHeight > innerHeight) {
followToggle();
}
});
maximizeButton.addEventListener('click', triggerToggle);
minimizeButton.addEventListener('click', triggerToggle);
};

View file

@ -1,22 +0,0 @@
import { track } from '../analytics';
export const handleInsights = async (initialData: any): Promise<(data: any) => void> => {
track('Session Insights', { props: toStringProps(initialData) });
let performanceReported = false;
return (data) => {
if (performanceReported) {
return;
}
performanceReported = true;
track('Performance Insights', { props: toStringProps(data) });
};
};
const toStringProps = (data: Record<string, unknown>): Record<string, string> =>
Object.entries(data).reduce<Record<string, string>>((props, [key, value]) => {
if (value !== undefined && value !== null) {
props[key] = String(value);
}
return props;
}, {});

1
src/helper/mix.ts Normal file
View file

@ -0,0 +1 @@
export const mix = (from: number, to: number, q: number) => from + (to - from) * q;

View file

@ -1,4 +1,4 @@
export const prettyPrint = (o: any): string =>
JSON.stringify(o, (_, v) => (v?.toFixed ? Number(v.toFixed(3)) : v), ' ')
JSON.stringify(o, (_, v) => (v.toFixed ? Number(v.toFixed(3)) : v), ' ')
.replace(/("|,|{|^\n)/g, '')
.replace(/(\W*}\n?)+/g, '\n\n');

3
src/helper/rgb.ts Normal file
View file

@ -0,0 +1,3 @@
import { vec3 } from 'gl-matrix';
export const rgb = (r: number, g: number, b: number): vec3 => vec3.fromValues(r, g, b);

4
src/helper/rgb255.ts Normal file
View file

@ -0,0 +1,4 @@
import { vec3 } from 'gl-matrix';
export const rgb255 = (r: number, g: number, b: number): vec3 =>
vec3.fromValues(r / 255, g / 255, b / 255);

View file

@ -4,33 +4,33 @@
<meta charset="utf-8" />
<title>SDF-2D demo</title>
<meta property="og:image:width" content="1438" />
<meta property="og:image:height" content="638" />
<meta property="og:url" content="https://sdf2d.schmelczer.dev" />
<meta property="og:image" content="https://sdf2d.schmelczer.dev/og-image.png" />
<meta property="og:image:width" content="1607" />
<meta property="og:image:height" content="880" />
<meta
property="og:image:alt"
content="Merging vibrant red blobs lit from below by two flashlights."
/>
<meta property="og:image" content="https://sdf2d.schmelczer.dev/og-image.jpg" />
<meta name="theme-color" content="#103783" />
<meta
name="description"
content="Some simple demos to showcase the possibilities of the sdf-2d library. Click on the title to find out more."
content="Some simple demos to showcase the possibilities of the sdf-2d library library. Click on the title to find out more."
/>
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
<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="viewport" content="width=device-width" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<noscript>
<h1>Javascript is required for this website.</h1>
</noscript>
<noscript>Javascript is required for this website.</noscript>
<main id="canvas-container">
<canvas></canvas>
<a id="info" href="https://github.com/schmelczerandras/sdf-2d" target="_BLANK">
<p>SDF-2D</p>
<img src="static/logo-white.svg" alt="logo" />
</a>
<code id="overlay"></code>
<button id="toggle-text"></button>
<canvas id="main"></canvas>
<div id="errors-container">
<div id="errors">
@ -38,32 +38,5 @@
<p id="error-text"></p>
</div>
</div>
<img
src="../static/minimize.svg"
alt="minimize"
id="minimize"
class="full-screen-control"
/>
<img
src="../static/maximize.svg"
alt="maximize"
id="maximize"
class="full-screen-control"
/>
</main>
<a id="info" href="https://github.com/schmelczerandras/sdf-2d" target="_BLANK">
<p>SDF-2D</p>
<img
class="logo"
width="64"
height="64"
src="../static/logo-white.svg"
alt="logo"
/>
</a>
<code id="overlay"></code>
<button id="toggle-text">Loading…</button>
</body>
</html>

View file

@ -1,97 +1,74 @@
import { glMatrix } from 'gl-matrix';
import { compile } from 'sdf-2d';
import '../static/favicons/apple-touch-icon.png';
import '../static/favicons/favicon-16x16.png';
import '../static/favicons/favicon-32x32.png';
import '../static/favicons/favicon.ico';
import '../static/logo-white.svg';
import '../static/no-change/404.html';
import '../static/no-change/favicons/apple-touch-icon.png';
import '../static/no-change/favicons/favicon-16x16.png';
import '../static/no-change/favicons/favicon-32x32.png';
import '../static/no-change/favicons/favicon.ico';
import '../static/no-change/og-image.png';
import '../static/no-change/robots.txt';
import { initAnalytics } from './analytics';
import { extractInsights } from './helper/extract-insights';
import { handleFullScreen } from './helper/handle-full-screen';
import { handleInsights } from './helper/handle-insights';
import { Random } from './helper/random';
import '../static/og-image.jpg';
import { DeltaTimeCalculator } from './helper/delta-time-calculator';
import { removeUnnecessaryOutlines } from './helper/remove-unnecessary-outlines';
import { BlobScene } from './scenes/blob/blob-scene';
import { MetaballScene } from './scenes/metaball/metaball-scene';
import { OrbitScene } from './scenes/orbit/orbit-scene';
import { RainScene } from './scenes/rain/rain-scene';
import { Scene } from './scenes/scene';
import { TunnelScene } from './scenes/tunnel-scene';
import './styles/index.scss';
const scenes = [TunnelScene, MetaballScene, RainScene, BlobScene, OrbitScene];
Random.seed = 2;
const scenes = [TunnelScene, RainScene, BlobScene];
const sceneIntervalInSeconds = 8;
glMatrix.setMatrixArrayType(Array);
removeUnnecessaryOutlines();
initAnalytics();
const deltaTimeCalculator = new DeltaTimeCalculator();
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
const logo = document.querySelector('#info') as HTMLElement;
const canvasContainer = document.querySelector('#canvas-container') as HTMLCanvasElement;
const errorText = document.querySelector('#error-text') as HTMLParagraphElement;
const info = document.querySelector('#info') as HTMLDivElement;
const errorText = document.querySelector('#error-text') as HTMLParamElement;
const errors = document.querySelector('#errors') as HTMLDivElement;
const errorsContainer = document.querySelector('#errors-container') as HTMLDivElement;
const toggleButton = document.querySelector('#toggle-text') as HTMLElement;
const minimizeButton = document.querySelector('#minimize') as HTMLElement;
const maximizeButton = document.querySelector('#maximize') as HTMLElement;
const toggleButton = document.querySelector('#toggle-text');
const overlay = document.querySelector('#overlay') as HTMLDivElement;
let textVisible = true;
let textVisible = false;
const handleTextToggle = () => {
[info, overlay, errors].forEach(
(e) => (e.style.visibility = textVisible ? 'hidden' : 'inherit')
);
textVisible = !textVisible;
overlay.style.visibility = textVisible ? 'visible' : 'hidden';
toggleButton.innerHTML = textVisible ? 'Hide insights' : 'Show insights';
if (textVisible) {
toggleButton.classList.remove('off');
toggleButton.innerHTML = textVisible ? 'Hide text' : 'Show text';
};
toggleButton.addEventListener('click', handleTextToggle);
handleTextToggle();
const handleScene = async (SceneConstructor: new () => Scene) => {
const scene = new SceneConstructor();
await scene.initialize(canvas, overlay);
let triggerIsOver: () => void;
const isOver = new Promise((resolve) => (triggerIsOver = resolve));
let timeSinceStart = 0;
const handleFrame = (currentTime: DOMHighResTimeStamp) => {
const deltaTime = deltaTimeCalculator.getNextDeltaTime(currentTime);
scene.drawNextFrame(currentTime, deltaTime);
if ((timeSinceStart += deltaTime) > sceneIntervalInSeconds * 1000) {
triggerIsOver();
} else {
toggleButton.classList.add('off');
requestAnimationFrame(handleFrame);
}
};
const isInsidePortfolio =
new URLSearchParams(location.search).get('portfolioView') !== null;
if (!isInsidePortfolio) {
toggleButton.style.visibility = 'visible';
logo.style.visibility = 'visible';
toggleButton.addEventListener('click', handleTextToggle);
}
handleTextToggle();
const startInsightsSession = async (): Promise<(data: any) => unknown> => {
const dummyRenderer = await compile(document.createElement('canvas'), []);
const { vendor, renderer, version } = extractInsights(dummyRenderer.insights);
dummyRenderer.destroy();
return await handleInsights({
vendor,
renderer,
referrer: document.referrer,
connection: (navigator as any)?.connection?.effectiveType,
devicePixelRatio: devicePixelRatio,
height: innerHeight,
width: innerWidth,
version,
});
requestAnimationFrame(handleFrame);
await isOver;
scene.destroy();
};
const main = async () => {
const sendFramePromise = startInsightsSession();
handleFullScreen(minimizeButton, maximizeButton, canvasContainer);
try {
let i = 0;
for (;;) {
const currentScene = new scenes[i++ % scenes.length]();
await currentScene.run(canvas, overlay);
const { fps, renderScale, lightScale } = extractInsights(currentScene.insights);
(await sendFramePromise)({
fps,
renderScale,
lightScale,
});
await handleScene(scenes[i++ % scenes.length]);
}
} catch (e) {
console.error(e);

View file

@ -1,80 +1,78 @@
import { vec2 } from 'gl-matrix';
import { CircleFactory, CircleLight, Renderer, rgb, rgb255, runAnimation } from 'sdf-2d';
import { vec2, vec3 } from 'gl-matrix';
import { Circle, CircleLight, compile, InvertedTunnel, Renderer } from 'sdf-2d';
import { prettyPrint } from '../../helper/pretty-print';
import { settings } from '../../settings';
import { Scene } from '../scene';
import { Blob } from './blob';
const Circle = CircleFactory(rgb255(119, 143, 120));
export class BlobScene implements Scene {
private renderer: Renderer;
private canvas: HTMLCanvasElement;
private overlay: HTMLDivElement;
public insights?: any;
public async run(canvas: HTMLCanvasElement, overlay: HTMLDivElement): Promise<void> {
private tunnels: Array<InvertedTunnel> = [];
private lights: Array<CircleLight> = [];
public async initialize(
canvas: HTMLCanvasElement,
overlay: HTMLDivElement
): Promise<void> {
this.canvas = canvas;
this.overlay = overlay;
const { width, height } = this.canvas.getBoundingClientRect();
const length = vec2.length([width, height]);
this.blob = new Blob([width / 2, -length / 4 + length / 2]);
await runAnimation(
this.renderer = await compile(
canvas,
[
Circle.descriptor,
{
...CircleLight.descriptor,
shaderCombinationSteps: [0, 1, 2],
shaderCombinationSteps: [1, 2],
},
Blob.descriptor,
],
this.drawNextFrame.bind(this),
{
ambientLight: rgb255(89, 25, 115),
enableHighDpiRendering: true,
colorPalette: [
rgb255(0, 0, 0),
rgb255(119, 143, 120),
rgb255(119, 143, 120),
rgb255(224, 96, 126),
],
}
[
vec3.fromValues(0, 0, 0),
vec3.fromValues(224 / 255, 96 / 255, 126 / 255),
vec3.fromValues(119 / 255, 173 / 255, 120 / 255),
]
);
this.renderer.setRuntimeSettings({
ambientLight: vec3.fromValues(0.35, 0.1, 0.45),
shadowLength: 800,
});
const { width, height } = this.canvas.getBoundingClientRect();
const length = vec2.length([width, height]);
this.blob = new Blob([width / 2, -length / 4 + length / 2]);
}
private blob: Blob;
private drawNextFrame(
renderer: Renderer,
private deltaSinceStart = 0;
public drawNextFrame(
currentTime: DOMHighResTimeStamp,
_: DOMHighResTimeStamp
): boolean {
this.insights = renderer.insights;
deltaTime: DOMHighResTimeStamp
): void {
const { width, height } = this.canvas.getBoundingClientRect();
this.deltaSinceStart += deltaTime;
this.renderer.setViewArea([0, height], [width, height]);
const width = renderer.canvasSize.x;
const height = renderer.canvasSize.y;
renderer.setViewArea([0, height], [width, height]);
this.overlay.innerText = prettyPrint(renderer.insights);
this.renderer.autoscaleQuality(deltaTime);
this.overlay.innerText = prettyPrint(this.renderer.insights);
const length = vec2.length([width, height]);
const q = (currentTime % 8000) / 4300;
this.blob.animate(currentTime);
this.blob.position = [width / 2, -length / 4 + length / 2.5];
const q = (this.deltaSinceStart % 8000) / 4300;
this.blob.animate(this.deltaSinceStart);
[
new Circle([width / 2, -length / 4], length / 2.5),
new Circle([width / 2, -length / 4], length / 2),
this.blob,
new CircleLight(
[
(Math.cos((1 - q) * Math.PI) * length) / 2 + width / 2,
(Math.sin((1 - q) * Math.PI) * length) / 2,
],
rgb(1, 0.8, 0),
[1, 0.8, 0],
1
),
new CircleLight(
@ -82,11 +80,15 @@ export class BlobScene implements Scene {
(Math.cos(-q * Math.PI) * length) / 2 + width / 2,
(Math.sin(-q * Math.PI) * length) / 2,
],
rgb(0, 0.8, 1),
[0, 0.8, 1],
1
),
].forEach((d) => renderer.addDrawable(d));
].forEach((d) => this.renderer.addDrawable(d));
return currentTime < settings.sceneTimeInMilliseconds;
this.renderer.renderDrawables();
}
public destroy(): void {
this.renderer.destroy();
}
}

View file

@ -1,16 +1,18 @@
import { mat2d, vec2 } from 'gl-matrix';
import { Drawable, DrawableDescriptor } from 'sdf-2d';
import { Circle } from '../../helper/circle';
import { Circle, Drawable, DrawableDescriptor } from 'sdf-2d';
export class Blob extends Drawable {
public static descriptor: DrawableDescriptor = {
sdf: {
shader: `
uniform vec2 headCenters[BLOB_COUNT];
uniform vec2 leftFootCenters[BLOB_COUNT];
uniform vec2 rightFootCenters[BLOB_COUNT];
uniform float headRadii[BLOB_COUNT];
uniform float footRadii[BLOB_COUNT];
uniform struct {
vec2 headCenter;
vec2 leftFootCenter;
vec2 rightFootCenter;
float headRadius;
float footRadius;
float k;
}[BLOB_COUNT] blobs;
float smoothMin(float a, float b)
{
@ -19,18 +21,15 @@ export class Blob extends Drawable {
return -log2( res )/k;
}
float circleDistance(vec2 circleCenter, float radius, vec2 target) {
return distance(target, circleCenter) - radius;
float circleDistance(vec2 circleCenter, float radius) {
return distance(position, circleCenter) - radius;
}
float blobMinDistance(vec2 target, out vec4 color) {
float minDistance = 1000.0;
color = readFromPalette(3);
void blobMinDistance(inout float minDistance, inout float color) {
for (int i = 0; i < BLOB_COUNT; i++) {
float headDistance = circleDistance(headCenters[i], headRadii[i], target);
float leftFootDistance = circleDistance(leftFootCenters[i], footRadii[i], target);
float rightFootDistance = circleDistance(rightFootCenters[i], footRadii[i], target);
float headDistance = circleDistance(blobs[i].headCenter, blobs[i].headRadius);
float leftFootDistance = circleDistance(blobs[i].leftFootCenter, blobs[i].footRadius);
float rightFootDistance = circleDistance(blobs[i].rightFootCenter, blobs[i].footRadius);
float res = min(
smoothMin(headDistance, leftFootDistance),
@ -38,22 +37,15 @@ export class Blob extends Drawable {
);
minDistance = min(minDistance, res);
color = mix(1.0, color, step(distanceNdcPixelSize + SURFACE_OFFSET, res));
}
return minDistance;
}
`,
distanceFunctionName: 'blobMinDistance',
},
propertyUniformMapping: {
footRadius: 'footRadii',
headRadius: 'headRadii',
rightFootCenter: 'rightFootCenters',
leftFootCenter: 'leftFootCenters',
headCenter: 'headCenters',
},
uniformName: 'blobs',
uniformCountMacroName: 'BLOB_COUNT',
shaderCombinationSteps: [0, 1],
shaderCombinationSteps: [1],
empty: new Blob(vec2.fromValues(0, 0)),
};
@ -120,10 +112,12 @@ export class Blob extends Drawable {
Math.sin((q > 1 ? q - 1 : 0) * Math.PI) * 10
)
);
this.position = this.center;
}
public minDistance(target: vec2): number {
return this.boundingCircle.distance(target);
return this.boundingCircle.minDistance(target);
}
protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any {

View file

@ -1,99 +0,0 @@
import { vec2 } from 'gl-matrix';
import { Flashlight, Renderer, rgb, rgb255, runAnimation } from 'sdf-2d';
import { prettyPrint } from '../../helper/pretty-print';
import { Random } from '../../helper/random';
import { settings } from '../../settings';
import { Scene } from '../scene';
import { Metaball, MetaCircle } from './metaball';
export class MetaballScene implements Scene {
private circles: Array<Metaball> = [];
private overlay: HTMLDivElement;
public insights?: any;
public async run(canvas: HTMLCanvasElement, overlay: HTMLDivElement): Promise<void> {
this.overlay = overlay;
for (let i = 0; i < 16; i++) {
this.circles.push(
new Metaball(
vec2.fromValues(
Random.getRandomInRange(0.3, 0.6),
Random.getRandomInRange(0.3, 0.6)
),
vec2.fromValues(
Random.getRandomInRange(0.05, 0.5),
Random.getRandomInRange(0.05, 0.5)
)
)
);
}
await runAnimation(
canvas,
[
{
...Flashlight.descriptor,
shaderCombinationSteps: [0, 2],
},
{
...MetaCircle.descriptor,
shaderCombinationSteps: [0, 1, 2, 4, 8, 12, 16],
},
],
this.drawNextFrame.bind(this),
{
ambientLight: rgb(0.1, 0.1, 0.3),
enableHighDpiRendering: true,
}
);
}
private drawNextFrame(
renderer: Renderer,
currentTime: DOMHighResTimeStamp,
_: DOMHighResTimeStamp
): boolean {
this.insights = renderer.insights;
const width = renderer.canvasSize.x;
const height = renderer.canvasSize.y;
const viewAreaWidth = width / Math.max(width, height);
const viewAreaHeight = height / Math.max(width, height);
renderer.setViewArea(
vec2.fromValues(0, viewAreaHeight),
vec2.fromValues(viewAreaWidth, viewAreaHeight)
);
this.overlay.innerText = prettyPrint(renderer.insights);
this.circles.forEach((c) => {
c.animate(currentTime / 2000, viewAreaWidth, viewAreaHeight);
renderer.addDrawable(c.shape);
});
const sweep = Math.sin(currentTime / 1500) * 0.5;
const light1 = new Flashlight(
vec2.fromValues(-0.05, -0.05),
rgb255(104, 171, 212),
0.02,
vec2.fromValues(Math.cos(Math.PI / 4 + sweep), Math.sin(Math.PI / 4 + sweep))
);
const light2 = new Flashlight(
vec2.fromValues(viewAreaWidth + 0.05, -0.05),
rgb255(226, 90, 102),
0.02,
vec2.fromValues(
Math.cos((Math.PI * 3) / 4 - sweep),
Math.sin((Math.PI * 3) / 4 - sweep)
)
);
renderer.addDrawable(light1);
renderer.addDrawable(light2);
return currentTime < settings.sceneTimeInMilliseconds;
}
}

View file

@ -1,26 +0,0 @@
import { vec2 } from 'gl-matrix';
import { MetaCircleFactory, rgb255 } from 'sdf-2d';
import { Random } from '../../helper/random';
export const MetaCircle = MetaCircleFactory(rgb255(186, 59, 70));
export class Metaball {
public shape = new MetaCircle(vec2.create(), Random.getRandomInRange(0.025, 0.075));
private direction = Random.getRandom() > 0.5 ? 1 : -1;
private speed = Random.getRandomInRange(0.5, 2);
constructor(
private readonly center: vec2,
private readonly size: vec2
) {}
public animate(currentTime: DOMHighResTimeStamp, width: number, height: number) {
vec2.set(
this.shape.center,
this.size.x * Math.cos(currentTime * this.speed * this.direction) +
this.center.x * width,
this.size.y * Math.sin(currentTime * this.speed * this.direction) +
this.center.y * height
);
}
}

View file

@ -1,215 +0,0 @@
import { mat2d, vec2 } from 'gl-matrix';
import { Drawable, DrawableDescriptor } from 'sdf-2d';
/**
* Every visual style a body can take. The style selects both the outline
* (smooth disc, jagged asteroid, ring annulus) and the per-fragment surface
* colouring inside the SDF shader gradients, bands, continents instead of
* a single flat palette colour.
*/
export const BodyStyle = {
sun: 0, // radial white-hot → gold → ember gradient with simmering granules
rocky: 1, // two-tone continent blotches
banded: 2, // wavy gas-giant stripes
ice: 3, // swirling azure sheen
asteroid: 4, // jagged outline, speckled surface
rings: 5, // two concentric annuli (drawn around a planet)
moon: 6, // small cratered grey
star: 7, // tiny twinkle, colour boosted far above 1 to stay bright after
// being multiplied by the dim ambient lighting
} as const;
export type BodyStyleName = keyof typeof BodyStyle;
/**
* One celestial body: sun, planet, moon, ring system, asteroid, comet
* particle, or background star. A single descriptor renders all of them.
*
* Each instance carries two palette colour indices (mixed per fragment by its
* style), a `seed` that animates its surface, and `litAngle` the direction
* towards the sun used to shade a day/night terminator. Outline roughness is
* derived from the style inside the shader.
*/
export class Body extends Drawable {
// The outline harmonics can push the surface outward by at most
// 0.12 × roughness, and the roughest style (asteroid) uses 2.6.
private static readonly MAX_OUTLINE_GROWTH = 1.45;
public static descriptor: DrawableDescriptor = {
sdf: {
shader: `
uniform vec2 bodyCenters[BODY_COUNT];
uniform float bodyRadii[BODY_COUNT];
uniform float bodyColors[BODY_COUNT];
uniform float bodySeeds[BODY_COUNT];
uniform float bodyStyles[BODY_COUNT];
uniform float bodyLitAngles[BODY_COUNT];
float bodyNoise(vec2 p) {
return sin(p.x) * sin(p.y);
}
vec3 bodySurfaceColor(
int style,
vec2 d,
float radius,
float seed,
float litAngle,
vec3 colorA,
vec3 colorB
) {
vec2 q = d / radius;
float r01 = length(q);
if (style == ${BodyStyle.sun}) {
// Three drifting interference lattices; the first two sit at
// nearby frequencies and slide in opposite directions, so the
// convection cells continuously merge, split and boil instead of
// gliding across the disc as one rigid pattern.
float granules =
0.8 * bodyNoise(q * 8.0 + vec2(seed, -seed * 0.6)) +
0.55 * bodyNoise(q * 8.9 - vec2(seed * 0.8, seed * 1.1)) +
0.45 * bodyNoise(q * 15.0 + vec2(seed * 1.4, -seed * 0.9));
vec3 c = mix(vec3(1.4, 1.32, 1.1), colorA, smoothstep(0.0, 0.6, r01));
c = mix(c, colorB, smoothstep(0.5, 1.0, r01 + granules * 0.15));
return c * (1.0 + 0.2 * granules);
}
if (style == ${BodyStyle.star}) {
return colorA * (3.75 + 1.75 * sin(seed));
}
// Every world shares one recipe: its surface coordinates rotate
// with the body's spin (seed grows over time), so stripes and
// blotches visibly roll around the disc, then a single field —
// radial ripples for rings, stripes for the banded and ice giants,
// blotches for everything rocky — mixes the two colours. The
// terminator and limb shading below use the unrotated coordinates,
// keeping the lighting locked to the sun while the surface turns.
vec2 qr = vec2(
q.x * cos(seed) - q.y * sin(seed),
q.x * sin(seed) + q.y * cos(seed)
);
float field;
if (style == ${BodyStyle.rings}) {
field = sin(r01 * 26.0 + seed);
} else if (style == ${BodyStyle.banded} || style == ${BodyStyle.ice}) {
field = sin(qr.y * 5.0 + seed * 0.3);
} else {
field = bodyNoise(qr * 3.4) + 0.6 * bodyNoise(qr * 7.3 + vec2(2.7, 1.3));
}
vec3 c = mix(colorB, colorA, smoothstep(-0.6, 0.6, field));
vec2 toSun = vec2(cos(litAngle), sin(litAngle));
float dayside = smoothstep(-0.85, 0.55, dot(q / max(r01, 1e-4), toSun));
c *= mix(0.22, 1.1, dayside);
if (style != ${BodyStyle.rings}) {
c *= 1.0 - 0.45 * smoothstep(0.5, 1.0, r01);
}
return c;
}
float bodyMinDistance(vec2 target, out vec4 color) {
color = vec4(0.0, 0.0, 0.0, 1.0);
float minDistance = 1000.0;
for (int i = 0; i < BODY_COUNT; i++) {
vec2 d = target - bodyCenters[i];
float radius = max(bodyRadii[i], 1e-5);
int style = int(bodyStyles[i] + 0.5);
float seed = bodySeeds[i];
float dist;
if (style == ${BodyStyle.rings}) {
float len = length(d);
dist = min(
abs(len - radius * 0.68) - radius * 0.1,
abs(len - radius * 0.9) - radius * 0.06
);
} else {
float angle = atan(d.y, d.x);
float roughness = style == ${BodyStyle.sun}
? 0.5
: style == ${BodyStyle.asteroid}
? 2.6
: style == ${BodyStyle.rocky}
? 0.25
: style == ${BodyStyle.moon}
? 0.35
: 0.06;
float wobble =
sin(angle * 3.0 + seed) * 0.06 +
sin(angle * 7.0 + seed * 1.7) * 0.04 +
sin(angle * 17.0 - seed * 0.5) * 0.02;
dist = length(d) - radius * (1.0 + wobble * roughness);
}
if (dist < minDistance) {
minDistance = dist;
float packedColor = bodyColors[i];
float indexA = floor(packedColor / 32.0);
vec3 colorA = readFromPalette(int(indexA + 0.5)).rgb;
vec3 colorB = readFromPalette(int(packedColor - indexA * 32.0 + 0.5)).rgb;
color = vec4(
bodySurfaceColor(
style, d, radius, seed, bodyLitAngles[i], colorA, colorB
),
1.0
);
}
}
return minDistance;
}
`,
distanceFunctionName: 'bodyMinDistance',
},
propertyUniformMapping: {
center: 'bodyCenters',
radius: 'bodyRadii',
colors: 'bodyColors',
seed: 'bodySeeds',
style: 'bodyStyles',
litAngle: 'bodyLitAngles',
},
uniformCountMacroName: 'BODY_COUNT',
shaderCombinationSteps: [0, 4, 8, 16, 32],
empty: new Body(vec2.create(), 0, 'star', 0, 0, 0),
};
constructor(
public center: vec2,
public radius: number,
public style: BodyStyleName,
public colorA: number,
public colorB: number,
public seed: number,
public litAngle = 0
) {
super();
}
public minDistance(target: vec2): number {
// Conservative: bound by the most outward the bumpiest outline can reach
// so a body is never culled from a tile its surface might poke into.
return vec2.dist(this.center, target) - this.radius * Body.MAX_OUTLINE_GROWTH;
}
protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any {
// litAngle is a world-space direction: push it through the linear part of
// the transform so it survives the view mapping's scale and y-flip.
const dx = Math.cos(this.litAngle);
const dy = Math.sin(this.litAngle);
return {
center: vec2.transformMat2d(vec2.create(), this.center, transform2d),
radius: this.radius * transform1d,
colors: this.colorA * 32 + this.colorB,
seed: this.seed,
style: BodyStyle[this.style],
litAngle: Math.atan2(
transform2d[1] * dx + transform2d[3] * dy,
transform2d[0] * dx + transform2d[2] * dy
),
};
}
}

View file

@ -1,299 +0,0 @@
import { vec2 } from 'gl-matrix';
import { CircleLight, hsl, Renderer, rgb, runAnimation } from 'sdf-2d';
import { prettyPrint } from '../../helper/pretty-print';
import { Random } from '../../helper/random';
import { settings } from '../../settings';
import { Scene } from '../scene';
import { Body, BodyStyleName } from './body';
// Every colour the scene uses; bodies reference entries by name and mix their
// two colours per fragment in the shader, so nothing renders as a flat fill.
const palette = {
white: rgb(1, 1, 1),
sunGold: hsl(38, 100, 62),
sunEmber: hsl(14, 95, 46),
scorchedTan: hsl(32, 22, 60),
scorchedBrown: hsl(20, 25, 38),
venusCream: hsl(46, 85, 78),
venusAmber: hsl(33, 75, 58),
earthLand: hsl(135, 50, 45),
earthOcean: hsl(213, 85, 52),
marsRust: hsl(16, 80, 55),
marsShadow: hsl(22, 65, 34),
jovianCream: hsl(36, 70, 75),
jovianRust: hsl(16, 65, 52),
saturnSand: hsl(45, 65, 76),
saturnDust: hsl(36, 50, 58),
ringGold: hsl(44, 40, 70),
ringShadow: hsl(40, 28, 42),
iceAzure: hsl(200, 90, 68),
iceDeep: hsl(232, 70, 50),
moonGrey: hsl(220, 10, 68),
moonShadow: hsl(225, 12, 44),
};
type PaletteColor = keyof typeof palette;
const paletteIndex = (name: PaletteColor): number => Object.keys(palette).indexOf(name);
interface MoonSpec {
orbit: number; // around the parent planet, as a fraction of the system radius
size: number;
speed: number; // rad/s
}
interface PlanetSpec {
orbit: number; // fraction of the system radius
size: number;
style: BodyStyleName;
colors: [PaletteColor, PaletteColor];
spin: number; // how fast the surface pattern drifts
ringSize?: number;
moons?: Array<MoonSpec>;
}
// A miniature solar system: small rocky worlds inside, then the giants —
// banded, ringed, and an ice giant — spread roughly evenly from the sun's
// doorstep to the screen edge. Planets all orbit prograde at Kepler speeds
// (∝ orbit⁻¹·⁵).
const PLANETS: Array<PlanetSpec> = [
{
orbit: 0.165,
size: 0.026,
style: 'rocky',
colors: ['scorchedTan', 'scorchedBrown'],
spin: 0.55,
},
{
orbit: 0.27,
size: 0.036,
style: 'banded',
colors: ['venusCream', 'venusAmber'],
spin: 0.4,
},
{
orbit: 0.385,
size: 0.04,
style: 'rocky',
colors: ['earthLand', 'earthOcean'],
spin: 0.45,
moons: [{ orbit: 0.06, size: 0.01, speed: 2.4 }],
},
{
orbit: 0.49,
size: 0.03,
style: 'rocky',
colors: ['marsRust', 'marsShadow'],
spin: 0.5,
},
{
orbit: 0.7,
size: 0.06,
style: 'banded',
colors: ['jovianCream', 'jovianRust'],
spin: 0.3,
moons: [
{ orbit: 0.085, size: 0.011, speed: 1.8 },
{ orbit: 0.108, size: 0.009, speed: 1.2 },
],
},
{
orbit: 0.825,
size: 0.042,
style: 'banded',
colors: ['saturnSand', 'saturnDust'],
spin: 0.32,
ringSize: 0.082,
},
{ orbit: 0.95, size: 0.036, style: 'ice', colors: ['iceAzure', 'iceDeep'], spin: 0.45 },
];
const ORBITAL_SPEED = 0.26; // rad/s at orbit = 1; scaled by Kepler's third law
interface OrbitingBody {
drawable: Body;
orbit: number; // around the sun — or around `parent`, for moons and rings
phase: number;
speed: number;
size: number;
spin: number;
seedPhase: number;
parent?: OrbitingBody;
}
export class OrbitScene implements Scene {
private overlay: HTMLDivElement;
public insights?: any;
// The sun's visible disc carries its own white-hot → ember gradient; these
// lights add the blown-out core, the warm corona reflecting off the lifted
// background, and the sunlight (plus shadows) on everything orbiting.
private sunCore = new CircleLight(vec2.create(), rgb(1.0, 0.96, 0.88), 0.1);
private sunGlow = new CircleLight(vec2.create(), rgb(1.35, 0.58, 0.18), 0.5);
private sun: OrbitingBody = {
drawable: new Body(
vec2.create(),
0,
'sun',
paletteIndex('sunGold'),
paletteIndex('sunEmber'),
0
),
orbit: 0,
phase: 0,
speed: 0,
size: 0.115,
spin: 0.9,
seedPhase: Random.getRandom() * 10,
};
private bodies: Array<OrbitingBody> = [];
public async run(canvas: HTMLCanvasElement, overlay: HTMLDivElement): Promise<void> {
this.overlay = overlay;
this.bodies.push(this.sun);
PLANETS.forEach((spec) => {
const planet = this.createOrbitingBody(
spec.orbit,
spec.size,
spec.style,
spec.colors,
spec.spin
);
this.bodies.push(planet);
if (spec.ringSize) {
this.bodies.push({
...this.createOrbitingBody(
0,
spec.ringSize,
'rings',
['ringGold', 'ringShadow'],
0
),
parent: planet,
});
}
spec.moons?.forEach((moon) => {
this.bodies.push({
...this.createOrbitingBody(
moon.orbit,
moon.size,
'moon',
['moonGrey', 'moonShadow'],
0.8
),
speed: moon.speed,
parent: planet,
});
});
});
const bodyCount = this.bodies.length;
await runAnimation(
canvas,
[
{
...Body.descriptor,
shaderCombinationSteps: [...new Set([0, 4, 8, 16, 32, bodyCount])],
},
{
...CircleLight.descriptor,
shaderCombinationSteps: [0, 2],
},
],
this.drawNextFrame.bind(this),
{
enableHighDpiRendering: true,
motionBlur: 0.5,
lightPenetrationRatio: 0.8,
ambientLight: rgb(0.07, 0.075, 0.145),
backgroundColor: rgb(0.035, 0.035, 0.07),
colorPalette: Object.values(palette),
}
);
}
private createOrbitingBody(
orbit: number,
size: number,
style: BodyStyleName,
colors: [PaletteColor, PaletteColor],
spin: number
): OrbitingBody {
return {
drawable: new Body(
vec2.create(),
0,
style,
paletteIndex(colors[0]),
paletteIndex(colors[1]),
0
),
orbit,
phase: Random.getRandom() * Math.PI * 2,
speed: orbit > 0 ? ORBITAL_SPEED / Math.pow(orbit, 1.5) : 0,
size,
spin,
seedPhase: Random.getRandom() * 10,
};
}
private drawNextFrame(
renderer: Renderer,
currentTime: DOMHighResTimeStamp,
_: DOMHighResTimeStamp
): boolean {
this.insights = renderer.insights;
const width = renderer.canvasSize.x;
const height = renderer.canvasSize.y;
const maxSide = Math.max(width, height);
const viewAreaWidth = width / maxSide;
const viewAreaHeight = height / maxSide;
renderer.setViewArea(
vec2.fromValues(0, viewAreaHeight),
vec2.fromValues(viewAreaWidth, viewAreaHeight)
);
this.overlay.innerText = prettyPrint(renderer.insights);
const center = vec2.fromValues(viewAreaWidth / 2, viewAreaHeight / 2);
const maxRadius = Math.min(viewAreaWidth, viewAreaHeight) / 2;
const time = currentTime / 1000;
vec2.copy(this.sunCore.center, center);
vec2.copy(this.sunGlow.center, center);
this.sunGlow.intensity = 0.5 + 0.05 * Math.sin(time * 1.7);
// Parents are pushed before their moons and rings, so each parent's
// position is already up to date when its satellites read it.
this.bodies.forEach((body) => {
const angle = body.phase + time * body.speed;
const origin = body.parent?.drawable.center ?? center;
vec2.set(
body.drawable.center,
origin[0] + Math.cos(angle) * body.orbit * maxRadius,
origin[1] + Math.sin(angle) * body.orbit * maxRadius
);
// The sun breathes gently; everything else keeps its size.
const pulse = body === this.sun ? 1 + 0.015 * Math.sin(time * 2.4) : 1;
body.drawable.radius = body.size * maxRadius * pulse;
body.drawable.seed = body.seedPhase + time * body.spin;
body.drawable.litAngle = Math.atan2(
center[1] - body.drawable.center[1],
center[0] - body.drawable.center[0]
);
});
this.bodies.forEach((body) => renderer.addDrawable(body.drawable));
renderer.addDrawable(this.sunCore);
renderer.addDrawable(this.sunGlow);
return currentTime < settings.sceneTimeInMilliseconds;
}
}

View file

@ -1,46 +1,38 @@
import { ReadonlyVec2, vec2 } from 'gl-matrix';
import { DropletFactory, rgb255 } from 'sdf-2d';
import { vec2 } from 'gl-matrix';
import { Tunnel } from 'sdf-2d';
import { Random } from '../../helper/random';
export const Droplet = DropletFactory(rgb255(122, 122, 255));
export class DropletWrapper {
public readonly drawable: InstanceType<typeof Droplet>;
export class Droplet {
public readonly drawable: Tunnel;
private speed = Random.getRandom() * 0.45 + 0.45;
private speed = Random.getRandom() * 0.2 + 0.2;
private position = vec2.fromValues(
Random.getRandomInRange(0.1, 0.9),
Random.getRandom()
);
private length = Random.getRandom() * 14 + 8;
private length = Random.getRandom() * 20 + 4;
constructor() {
const size = Random.getRandom() * 1.2 + 1;
const size = Random.getRandom() * 2 + 2;
this.drawable = new Droplet(
this.drawable = new Tunnel(
vec2.create(),
vec2.create(),
size + Random.getRandom() + 1,
size + Random.getRandom() * 2 + 2,
size
);
}
public animate(currentTime: number, viewAreaSize: ReadonlyVec2, windSlant: number) {
public animate(currentTime: number, viewAreaSize: vec2) {
const heightOffset = 100;
const fall = this.speed * currentTime;
vec2.set(
this.drawable.from,
(this.position.x * viewAreaSize.x + fall * windSlant) % viewAreaSize.x,
this.position.x * viewAreaSize.x,
viewAreaSize.y -
((this.position.y * viewAreaSize.y + fall) % (viewAreaSize.y + heightOffset))
((this.position.y * viewAreaSize.y + this.speed * currentTime) %
(viewAreaSize.y + heightOffset))
);
// the tail points opposite to the direction of motion
const norm = this.length / Math.sqrt(1 + windSlant * windSlant);
vec2.add(
this.drawable.to,
this.drawable.from,
vec2.fromValues(-windSlant * norm, norm)
);
vec2.add(this.drawable.to, this.drawable.from, vec2.fromValues(0, this.length));
}
}

View file

@ -1,91 +1,74 @@
import { vec2 } from 'gl-matrix';
import { CircleLight, Renderer, rgb, rgb255, runAnimation } from 'sdf-2d';
import { vec2, vec3 } from 'gl-matrix';
import { CircleLight, compile, Renderer, Tunnel } from 'sdf-2d';
import { prettyPrint } from '../../helper/pretty-print';
import { settings } from '../../settings';
import { rgb255 } from '../../helper/rgb255';
import { Scene } from '../scene';
import { Droplet, DropletWrapper } from './droplet';
const WIND_SLANT = 0.2;
import { Droplet } from './droplet';
export class RainScene implements Scene {
private droplets: Array<DropletWrapper> = [];
// The moon: a single pale, cold light hanging high in the sky and the scene's
// only light source — the falling rain only shows where its streaks catch it.
private moon = new CircleLight(vec2.create(), rgb255(200, 214, 255), 2.5);
private droplets: Array<Droplet> = [];
private light1: CircleLight = new CircleLight(vec2.create(), rgb255(184, 41, 255), 2);
private light2: CircleLight = new CircleLight(vec2.create(), rgb255(255, 31, 109), 2);
private renderer: Renderer;
private canvas: HTMLCanvasElement;
private overlay: HTMLDivElement;
public insights?: any;
public async run(canvas: HTMLCanvasElement, overlay: HTMLDivElement): Promise<void> {
public async initialize(
canvas: HTMLCanvasElement,
overlay: HTMLDivElement
): Promise<void> {
this.canvas = canvas;
this.overlay = overlay;
for (
let i = 0;
i < Math.max(100, (canvas.getBoundingClientRect().width / 800) * 100);
i++
) {
this.droplets.push(new DropletWrapper());
}
await runAnimation(
this.renderer = await compile(
canvas,
[
{
...Droplet.descriptor,
// Tiles that contain more droplets than the largest step have no
// compiled shader to fall back on and flicker; together with the
// raised tileMultiplier, 48 keeps the worst-case tile comfortably
// covered.
shaderCombinationSteps: [0, 2, 4, 8, 16, 32, 48],
...Tunnel.descriptor,
shaderCombinationSteps: [0, 1, 2, 4, 8, 12, 16, 24],
},
{
...CircleLight.descriptor,
shaderCombinationSteps: [0, 1],
shaderCombinationSteps: [2],
},
],
this.drawNextFrame.bind(this),
{
// A dark night sky so the moon is the dominant light.
backgroundColor: rgb(0.09, 0.12, 0.19),
ambientLight: rgb(0.2, 0.22, 0.28),
enableHighDpiRendering: true,
// Keep most of the previous frame each render so the droplets smear
// into long streaks of rain.
motionBlur: 0.9,
// More, smaller tiles keep the droplet count per tile low.
tileMultiplier: 12,
}
[vec3.fromValues(0.4, 1, 0.6), vec3.fromValues(1, 1, 0), vec3.fromValues(0.3, 1, 1)]
);
this.renderer.setRuntimeSettings({
ambientLight: vec3.fromValues(0.2, 0.2, 0.2),
tileMultiplier: 10,
});
for (let i = 0; i < (canvas.getBoundingClientRect().width / 1000) * 20; i++) {
this.droplets.push(new Droplet());
}
}
private drawNextFrame(
renderer: Renderer,
public drawNextFrame(
currentTime: DOMHighResTimeStamp,
_: DOMHighResTimeStamp
): boolean {
this.insights = renderer.insights;
deltaTime: DOMHighResTimeStamp
): void {
const { width, height } = this.canvas.getBoundingClientRect();
this.renderer.setViewArea(vec2.fromValues(0, height), vec2.fromValues(width, height));
this.renderer.autoscaleQuality(deltaTime);
this.overlay.innerText = prettyPrint(this.renderer.insights);
const width = renderer.canvasSize.x;
const height = renderer.canvasSize.y;
const viewAreaSize = this.renderer.viewAreaSize;
renderer.setViewArea(vec2.fromValues(0, height), vec2.fromValues(width, height));
this.overlay.innerText = prettyPrint(renderer.insights);
vec2.set(this.light1.center, 0, viewAreaSize.y / 2);
vec2.set(this.light2.center, viewAreaSize.x, viewAreaSize.y / 2);
const viewAreaSize = renderer.viewAreaSize;
this.droplets.forEach((d) => d.animate(currentTime, viewAreaSize));
// The moon drifts back and forth along the top edge; the rain falls past
// it and lights up.
vec2.set(
this.moon.center,
viewAreaSize.x * (0.5 + 0.5 * Math.sin(currentTime / 2500)),
viewAreaSize.y * 0.95
[...this.droplets.map((d) => d.drawable), this.light1, this.light2].forEach((d) =>
this.renderer.addDrawable(d)
);
this.droplets.forEach((d) => d.animate(currentTime, viewAreaSize, WIND_SLANT));
this.renderer.renderDrawables();
}
[...this.droplets.map((d) => d.drawable), this.moon].forEach((d) =>
renderer.addDrawable(d)
);
return currentTime < settings.sceneTimeInMilliseconds;
public destroy(): void {
this.renderer.destroy();
}
}

View file

@ -1,4 +1,5 @@
export interface Scene {
run(canvas: HTMLCanvasElement, overlay: HTMLDivElement): Promise<void>;
readonly insights?: any;
initialize(canvas: HTMLCanvasElement, overlay: HTMLDivElement): Promise<void>;
drawNextFrame(currentTime: DOMHighResTimeStamp, deltaTime: DOMHighResTimeStamp): void;
destroy(): void;
}

View file

@ -1,49 +1,49 @@
import { vec2, vec3 } from 'gl-matrix';
import {
CircleLight,
FilteringOptions,
InvertedTunnelFactory,
Renderer,
renderNoise,
rgb,
runAnimation,
WrapOptions,
} from 'sdf-2d';
import { CircleLight, compile, InvertedTunnel, Renderer } from 'sdf-2d';
import { clamp } from '../helper/clamp';
import { last } from '../helper/last';
import { prettyPrint } from '../helper/pretty-print';
import { Random } from '../helper/random';
import { settings } from '../settings';
import { rgb } from '../helper/rgb';
import { Scene } from './scene';
const InvertedTunnel = InvertedTunnelFactory(3);
export class TunnelScene implements Scene {
private renderer: Renderer;
private canvas: HTMLCanvasElement;
private overlay: HTMLDivElement;
private tunnels: Array<InstanceType<typeof InvertedTunnel>> = [];
private tunnels: Array<InvertedTunnel> = [];
private lights: Array<CircleLight> = [];
public insights?: any;
private generateTunnel() {
let previousEnd = vec2.fromValues(0, 0.5);
let previousRadius = 0.1;
const canvasSize = this.canvas.getBoundingClientRect();
let previousEnd = vec2.fromValues(0, 200);
let previousRadius = 50;
if (this.tunnels.length > 0) {
previousEnd = last(this.tunnels).to;
previousRadius = last(this.tunnels).toRadius;
}
let height = previousEnd.y + Random.getRandomInRange(-0.4, 0.4);
height = clamp(height, 0.2, 0.8);
let height =
previousEnd.y +
Random.getRandomInRange(-canvasSize.height / 3, canvasSize.height / 3);
const currentEnd = vec2.fromValues(this.tunnels.length * 0.25, height);
const currentToRadius = Random.getRandom() * 0.1 + 0.1;
height = clamp(
height,
canvasSize.height / 6 + 50,
canvasSize.height - canvasSize.height / 6 + 50
);
const currentEnd = vec2.fromValues(this.tunnels.length * 300, height);
const currentToRadius = (Random.getRandom() * canvasSize.height) / 6 + 50;
this.tunnels.push(
new InvertedTunnel(previousEnd, currentEnd, previousRadius, currentToRadius)
);
if (this.tunnels.length % 4 == 0) {
if (this.tunnels.length % 3 == 0) {
this.lights.push(
new CircleLight(
previousEnd,
@ -52,22 +52,19 @@ export class TunnelScene implements Scene {
Random.getRandom(),
Random.getRandom(),
]),
0.00025
0.35
)
);
}
}
public async run(canvas: HTMLCanvasElement, overlay: HTMLDivElement): Promise<void> {
const noiseTexture = await renderNoise([1024, 1], 15, 0.5);
public async initialize(
canvas: HTMLCanvasElement,
overlay: HTMLDivElement
): Promise<void> {
this.canvas = canvas;
this.overlay = overlay;
for (let i = 0; i < 30; i++) {
this.generateTunnel();
}
await runAnimation(
this.renderer = await compile(
canvas,
[
{
@ -76,52 +73,51 @@ export class TunnelScene implements Scene {
},
{
...CircleLight.descriptor,
shaderCombinationSteps: [0, 1, 2, 3, 4, 5, 6, 7],
shaderCombinationSteps: [1, 2, 3, 4, 5, 6, 7],
},
],
this.drawNextFrame.bind(this),
{
lightPenetrationRatio: 0.5,
isWorldInverted: true,
enableHighDpiRendering: true,
ambientLight: rgb(0.35, 0.1, 0.45),
colorPalette: [rgb(0.4, 0.5, 0.6), rgb(0, 0, 0), rgb(0, 0, 0), rgb(0, 0, 0)],
textures: {
noiseTexture: {
source: noiseTexture,
overrides: {
maxFilter: FilteringOptions.LINEAR,
wrapS: WrapOptions.MIRRORED_REPEAT,
},
},
},
}
[rgb(0.4, 1, 0.6), rgb(1, 1, 0), rgb(0.3, 1, 1)]
);
this.renderer.setRuntimeSettings({
isWorldInverted: true,
ambientLight: vec3.fromValues(0.35, 0.1, 0.45),
shadowLength: 550,
});
for (let i = 0; i < 200; i++) {
this.generateTunnel();
}
}
private deltaSinceStart = 0;
private drawNextFrame(
renderer: Renderer,
public drawNextFrame(
currentTime: DOMHighResTimeStamp,
deltaTime: DOMHighResTimeStamp
): boolean {
this.insights = renderer.insights;
): void {
const { width, height } = this.canvas.getBoundingClientRect();
this.deltaSinceStart += deltaTime;
const startX = this.deltaSinceStart / 4 / 1000;
const width = renderer.canvasSize.x / renderer.canvasSize.y;
renderer.setViewArea(vec2.fromValues(startX, 1), vec2.fromValues(width, 1));
const startX = this.deltaSinceStart / 3;
const endX = startX + width;
this.renderer.setViewArea(
vec2.fromValues(startX, height),
vec2.fromValues(width, height)
);
this.overlay.innerText = prettyPrint(renderer.insights);
this.renderer.autoscaleQuality(deltaTime);
this.overlay.innerText = prettyPrint(this.renderer.insights);
[
...this.tunnels.filter(
(t) =>
startX < t.to.x + t.toRadius && t.from.x - t.fromRadius <= startX + width * 1
(t) => startX < t.to.x + t.toRadius && t.from.x - t.fromRadius <= endX
),
...this.lights,
].forEach((d) => renderer.addDrawable(d));
].forEach((d) => this.renderer.addDrawable(d));
return currentTime < settings.sceneTimeInMilliseconds;
this.renderer.renderDrawables();
}
public destroy(): void {
this.renderer.destroy();
}
}

View file

@ -1,3 +0,0 @@
export const settings = {
sceneTimeInMilliseconds: 8000,
};

View file

@ -1,5 +1,6 @@
@use './mixins' as *;
@import './mixins';
$bg: linear-gradient(45deg, #103783, #9bafd9);
$breakpoint: 800px;
* {
@ -10,14 +11,15 @@ $breakpoint: 800px;
}
html,
body {
body,
canvas#main {
height: 100%;
width: 100%;
overflow: hidden;
}
html {
background-color: #103783;
background: $bg;
@media (max-width: $breakpoint) {
font-size: 0.7rem;
@ -40,7 +42,7 @@ body {
#info {
@include card();
@include center-children();
visibility: hidden;
padding: 0.25rem 0.5rem;
text-decoration: none;
@ -48,7 +50,7 @@ body {
padding-top: 2px;
}
.logo {
img {
@include square(64px);
padding-left: 8px;
}
@ -56,66 +58,35 @@ body {
@media (max-width: $breakpoint) {
font-size: 2rem;
.logo {
img {
@include square(40px);
}
}
}
#canvas-container {
&,
canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.full-screen-control {
position: absolute;
right: 0;
bottom: 0;
box-sizing: content-box;
@include square(40px);
padding: 16px;
cursor: pointer;
}
}
#overlay {
right: 0;
white-space: pre-wrap;
overflow-wrap: break-word;
font-size: 0.75rem;
white-space: pre;
font-family: 'Lucida Console', Monaco, monospace;
font-size: 0.75rem;
max-width: 400px;
@media (max-width: $breakpoint) {
font-size: 0.6rem;
max-width: 200px;
}
}
#toggle-text {
bottom: 0;
@include card();
visibility: hidden;
font-size: 1.25rem;
width: 150px;
width: 120px;
cursor: pointer;
padding: 0.2em 0.5rem;
background: none;
user-select: none;
&.off:not(:hover) {
opacity: 0.5;
}
transition: opacity 200ms;
@media (max-width: $breakpoint) {
width: 110px;
width: 80px;
}
}

View file

@ -1,12 +1,8 @@
@mixin card {
backdrop-filter: blur(16px);
border: 2px solid white;
border-radius: 12px;
backdrop-filter: blur(24px);
@supports not (backdrop-filter: blur(24px)) {
background-color: rgba(0, 0, 0, 0.15);
}
&:focus {
outline: none;
border: 4px solid white;

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 440 B

After

Width:  |  Height:  |  Size: 440 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 978 B

After

Width:  |  Height:  |  Size: 978 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

@ -1,4 +1,4 @@
<svg viewBox="0 0 100 100" width="64" height="64" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg">
<stop offset="0%" stop-color="#103783" />
@ -7,18 +7,18 @@
<g id="ray-a">
<rect x="18" y="-3" height="6" rx="3">
<animate attributeName="width" values="20;25;20" dur="5s" repeatCount="indefinite" />
<animate attributeName="width" values="20;25;20" dur="4s" repeatCount="indefinite" />
</rect>
<circle cy="0" r="3">
<animate attributeName="cx" values="47;40;47" dur="5s" repeatCount="indefinite" />
<animate attributeName="cx" values="47;40;47" dur="4s" repeatCount="indefinite" />
</circle>
</g>
<g id="ray-b">
<rect x="18" y="-3" height="6" rx="3">
<animate attributeName="width" values="25;20;25" dur="5s" repeatCount="indefinite" />
<animate attributeName="width" values="25;20;25" dur="4s" repeatCount="indefinite" />
</rect>
<circle cy="0" r="3">
<animate attributeName="cx" values="40;47;40" dur="5s" repeatCount="indefinite" />
<animate attributeName="cx" values="40;47;40" dur="4s" repeatCount="indefinite" />
</circle>
</g>
</defs>
@ -38,7 +38,7 @@
attributeName="transform"
type="rotate"
values="0 50 50;90 50 50"
dur="10s"
dur="6s"
repeatCount="indefinite"
/>
</g>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

View file

@ -1,4 +1,4 @@
<svg viewBox="0 0 100 100" width="64" height="64" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<g id="ray-a">
<rect x="18" y="-3" height="6" rx="3">

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" stroke-width="1.5" stroke="#FFFFFF" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M4 8v-2a2 2 0 0 1 2 -2h2" />
<path d="M4 16v2a2 2 0 0 0 2 2h2" />
<path d="M16 4h2a2 2 0 0 1 2 2v2" />
<path d="M16 20h2a2 2 0 0 0 2 -2v-2" />
</svg>

Before

Width:  |  Height:  |  Size: 399 B

View file

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" stroke-width="1.5" stroke="#FFFFFF" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M15 19v-2a2 2 0 0 1 2 -2h2" />
<path d="M15 5v2a2 2 0 0 0 2 2h2" />
<path d="M5 15h2a2 2 0 0 1 2 2v2" />
<path d="M5 9h2a2 2 0 0 0 2 -2v-2" />
</svg>

Before

Width:  |  Height:  |  Size: 399 B

View file

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Not found</title>
<meta name="theme-color" content="#b7455e" />
<meta name="viewport" content="initial-scale=1.0" />
<style>
html,
body {
height: 100%;
}
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: #b7455e;
}
h1 {
font-family: "Roboto", "Helvetica Neue", sans-serif;
font-weight: 100;
font-size: 3rem;
color: white;
text-align: center;
padding: 0.5rem;
}
</style>
</head>
<body>
<h1>The requested resource cannot be found.</h1>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 KiB

View file

@ -1,2 +0,0 @@
User-agent: *
Allow: /

BIN
static/og-image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -1,18 +1,14 @@
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": false,
"strict": false,
"target": "ES2017",
"module": "ESNext",
"moduleResolution": "node10",
"ignoreDeprecations": "6.0",
"target": "es6",
"downlevelIteration": true,
"allowJs": true,
"experimentalDecorators": true,
"esModuleInterop": true,
"lib": ["ES2017", "DOM"],
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["node_modules"]
"moduleResolution": "Node",
"module": "es6",
"lib": ["es2016", "dom"]
}
}

View file

@ -1,58 +1,49 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const HtmlInlineScriptPlugin = require('html-inline-script-webpack-plugin');
const HTMLInlineCSSWebpackPlugin =
require('html-inline-css-webpack-plugin').default;
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
const Sass = require('sass');
const PATHS = {
entryPoint: path.resolve(__dirname, 'src/index.ts'),
entryHtml: path.resolve(__dirname, 'src/index.html'),
bundles: path.resolve(__dirname, 'dist'),
};
// Inline the UI SVGs referenced via <img src> into the HTML (replacing the
// abandoned html-webpack-inline-svg-plugin). Scope html-loader to <img src> so
// the favicon <link href> tags are left to resolve against the emitted files.
const htmlLoaderOptions = JSON.stringify({
sources: { list: [{ tag: 'img', attribute: 'src', type: 'src' }] },
minimize: false,
});
module.exports = (env, argv) => ({
module.exports = {
entry: {
index: PATHS.entryPoint,
},
target: 'web',
output: {
filename: '[name].[contenthash].js',
path: PATHS.bundles,
// The bundle is inlined into index.html (no <script src>), so webpack 5's
// default publicPath:'auto' cannot derive a path from document.currentScript
// and throws "Automatic publicPath is not supported". A static value (assets
// sit next to index.html at the dist root) avoids the runtime auto-detection.
publicPath: '',
clean: true,
},
//devtool: 'source-map',
devtool: 'source-map',
watchOptions: {
aggregateTimeout: 600,
ignored: /node_modules/,
},
devServer: {
host: '0.0.0.0',
port: 9999,
allowedHosts: 'all',
disableHostCheck: true,
},
optimization: {
minimize: true,
usedExports: true,
minimizer: [
new TerserJSPlugin({
sourceMap: true,
test: /\.js$/,
}),
new OptimizeCSSAssetsPlugin({}),
],
},
plugins: [
new MiniCssExtractPlugin(),
new HtmlWebpackPlugin({
xhtml: true,
template: `!!html-loader?${htmlLoaderOptions}!${PATHS.entryHtml}`,
// The bundle gets inlined, and `defer` is ignored on inline <script>s, so
// it must sit at the end of <body> (not head) to run after the DOM exists.
inject: 'body',
scriptLoading: 'blocking',
template: './src/index.html',
minify: {
collapseWhitespace: true,
removeComments: true,
@ -61,72 +52,72 @@ module.exports = (env, argv) => ({
removeStyleLinkTypeAttributes: true,
useShortDoctype: true,
},
inlineSource: '.(js|css)$',
}),
new HtmlWebpackInlineSourcePlugin(),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[id].[contenthash].css',
}),
...(argv.mode === 'production'
? [new HtmlInlineScriptPlugin(), new HTMLInlineCSSWebpackPlugin()]
: []),
],
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
// transpileOnly mirrors the project's prior de-facto behaviour: the old
// ts-config-webpack-plugin ran type-checking in a separate fork-ts-checker
// process that crashes on Node 22, so types were never actually enforced.
// (`npm run lint` still type-aware-lints; full type-checking can be
// re-enabled separately once the pre-existing type errors are addressed.)
use: {
loader: 'ts-loader',
options: { transpileOnly: true },
},
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
// resolve-url-loader v5 always retains the query/hash (the old
// `keepQuery` option was removed), so no options are needed.
'resolve-url-loader',
{
loader: 'resolve-url-loader',
options: {
keepQuery: true,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
implementation: require('sass'),
implementation: Sass,
},
},
],
},
{
// Favicons, og-image, 404.html and robots.txt: emit verbatim to the
// dist root (imported for their side effect in src/index.ts).
// sideEffects:true keeps these bare imports from being tree-shaken away
// under the package's "sideEffects": ["*.scss"] declaration.
test: /no-change.*$/i,
type: 'asset/resource',
sideEffects: true,
generator: {
filename: '[name][ext]',
test: /\.ts$/,
use: {
loader: 'ts-loader',
},
exclude: /node_modules/,
},
{
test: /\.(ico|png|jpg)$/,
use: {
loader: 'file-loader',
query: {
outputPath: '/',
name: '[name].[ext]',
},
},
},
{
test: /\.svg$/,
type: 'asset/inline',
test: /\.(svg)$/,
use: {
loader: 'file-loader',
query: {
outputPath: '/static',
name: '[name].[ext]',
},
},
},
{
test: /\.js$/,
enforce: 'pre',
exclude: /node_modules/,
use: ['source-map-loader'],
},
],
},
optimization: {
minimizer: ['...', new CssMinimizerPlugin()],
},
resolve: {
extensions: ['.ts', '.js'],
},
});
};