Add tiny UI
This commit is contained in:
parent
e3d0af56e2
commit
6a752a57e9
30 changed files with 627 additions and 210 deletions
BIN
assets/fonts/comfortaa-v40-latin-regular.woff
Normal file
BIN
assets/fonts/comfortaa-v40-latin-regular.woff
Normal file
Binary file not shown.
BIN
assets/fonts/comfortaa-v40-latin-regular.woff2
Normal file
BIN
assets/fonts/comfortaa-v40-latin-regular.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/open-sans-v34-latin-regular.woff
Normal file
BIN
assets/fonts/open-sans-v34-latin-regular.woff
Normal file
Binary file not shown.
BIN
assets/fonts/open-sans-v34-latin-regular.woff2
Normal file
BIN
assets/fonts/open-sans-v34-latin-regular.woff2
Normal file
Binary file not shown.
6
assets/icons/info.svg
Normal file
6
assets/icons/info.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" 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"/>
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<line x1="12" y1="8" x2="12.01" y2="8" />
|
||||||
|
<polyline points="11 12 12 12 12 16 13 16" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 344 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<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">
|
<svg xmlns="http://www.w3.org/2000/svg" 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 stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M4 8v-2a2 2 0 0 1 2 -2h2" />
|
<path d="M4 8v-2a2 2 0 0 1 2 -2h2" />
|
||||||
<path d="M4 16v2a2 2 0 0 0 2 2h2" />
|
<path d="M4 16v2a2 2 0 0 0 2 2h2" />
|
||||||
|
Before Width: | Height: | Size: 399 B After Width: | Height: | Size: 376 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<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">
|
<svg xmlns="http://www.w3.org/2000/svg" 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 stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M15 19v-2a2 2 0 0 1 2 -2h2" />
|
<path d="M15 19v-2a2 2 0 0 1 2 -2h2" />
|
||||||
<path d="M15 5v2a2 2 0 0 0 2 2h2" />
|
<path d="M15 5v2a2 2 0 0 0 2 2h2" />
|
||||||
|
Before Width: | Height: | Size: 399 B After Width: | Height: | Size: 376 B |
5
assets/icons/restart.svg
Normal file
5
assets/icons/restart.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" 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="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
|
||||||
|
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 326 B |
43
assets/no-change/404.html
Normal file
43
assets/no-change/404.html
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<!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;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
a {
|
||||||
|
font-family: 'Roboto', 'Helvetica Neue', sans-serif;
|
||||||
|
font-weight: 100;
|
||||||
|
font-size: 3rem;
|
||||||
|
color: white;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<h1>Page not found.</h1>
|
||||||
|
<a href="/">Go back</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -6,36 +6,45 @@ import { RenderPipeline } from '../pipelines/render/render-pipeline';
|
||||||
import { settings } from '../settings';
|
import { settings } from '../settings';
|
||||||
import { DeltaTimeCalculator } from '../utils/delta-time-calculator';
|
import { DeltaTimeCalculator } from '../utils/delta-time-calculator';
|
||||||
import { Random } from '../utils/random';
|
import { Random } from '../utils/random';
|
||||||
import { sleep } from '../utils/sleep';
|
|
||||||
|
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
export default class GameLoop {
|
export default class GameLoop {
|
||||||
private context: GPUCanvasContext;
|
private readonly deltaTimeCalculator = new DeltaTimeCalculator();
|
||||||
private device: GPUDevice;
|
|
||||||
|
|
||||||
private agentPipeline: AgentPipeline;
|
private readonly agentPipeline: AgentPipeline;
|
||||||
private renderPipeline: RenderPipeline;
|
private readonly renderPipeline: RenderPipeline;
|
||||||
private brushPipeline: BrushPipeline;
|
private readonly brushPipeline: BrushPipeline;
|
||||||
private diffusionPipeline: DiffusionPipeline;
|
private readonly diffusionPipeline: DiffusionPipeline;
|
||||||
|
|
||||||
private trailMapA?: GPUTexture;
|
private trailMapA?: GPUTexture;
|
||||||
private trailMapB?: GPUTexture;
|
private trailMapB?: GPUTexture;
|
||||||
|
|
||||||
|
private hasFinished = false;
|
||||||
|
private readonly hasFinishedPromise: Promise<void> = new Promise(
|
||||||
|
(resolve) => (this.resolveHasFinished = resolve)
|
||||||
|
);
|
||||||
|
private resolveHasFinished: () => void;
|
||||||
|
|
||||||
private isSwipeActive = false;
|
private isSwipeActive = false;
|
||||||
private readonly deltaTimeCalculator = new DeltaTimeCalculator();
|
|
||||||
|
|
||||||
public constructor(private canvas: HTMLCanvasElement) {}
|
public constructor(
|
||||||
|
private readonly canvas: HTMLCanvasElement,
|
||||||
async start() {
|
private readonly device: GPUDevice
|
||||||
await this.initializeDevice();
|
) {
|
||||||
|
const context = this.canvas.getContext('webgpu') as any;
|
||||||
|
context.configure({
|
||||||
|
device: this.device,
|
||||||
|
format: navigator.gpu.getPreferredCanvasFormat(),
|
||||||
|
alphaMode: 'premultiplied',
|
||||||
|
});
|
||||||
|
|
||||||
this.resize();
|
this.resize();
|
||||||
|
|
||||||
this.agentPipeline = new AgentPipeline(this.device, this.spawnAgents());
|
this.agentPipeline = new AgentPipeline(this.device, this.spawnAgents());
|
||||||
this.brushPipeline = new BrushPipeline(this.device);
|
this.brushPipeline = new BrushPipeline(this.device);
|
||||||
this.diffusionPipeline = new DiffusionPipeline(this.device);
|
this.diffusionPipeline = new DiffusionPipeline(this.device);
|
||||||
this.renderPipeline = new RenderPipeline(this.context, this.device);
|
this.renderPipeline = new RenderPipeline(context, this.device);
|
||||||
|
|
||||||
window.addEventListener('resize', this.resize.bind(this));
|
window.addEventListener('resize', this.resize.bind(this));
|
||||||
window.addEventListener('mousemove', this.onSwipe.bind(this));
|
window.addEventListener('mousemove', this.onSwipe.bind(this));
|
||||||
|
|
@ -44,8 +53,11 @@ export default class GameLoop {
|
||||||
this.isSwipeActive = false;
|
this.isSwipeActive = false;
|
||||||
this.brushPipeline.clearSwipes();
|
this.brushPipeline.clearSwipes();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start(): Promise<void> {
|
||||||
requestAnimationFrame(this.render.bind(this));
|
requestAnimationFrame(this.render.bind(this));
|
||||||
|
return this.hasFinishedPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSwipe(event: MouseEvent) {
|
private onSwipe(event: MouseEvent) {
|
||||||
|
|
@ -112,24 +124,11 @@ export default class GameLoop {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initializeDevice(): Promise<void> {
|
private render(time: DOMHighResTimeStamp) {
|
||||||
const gpu = navigator.gpu;
|
if (this.hasFinished) {
|
||||||
if (!gpu) {
|
return;
|
||||||
throw new Error('WebGPU is not supported');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const adapter = await gpu.requestAdapter();
|
|
||||||
this.device = await adapter.requestDevice(); // could request more resources
|
|
||||||
|
|
||||||
this.context = this.canvas.getContext('webgpu') as any;
|
|
||||||
this.context.configure({
|
|
||||||
device: this.device,
|
|
||||||
format: gpu.getPreferredCanvasFormat(),
|
|
||||||
alphaMode: 'premultiplied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async render(time: DOMHighResTimeStamp) {
|
|
||||||
const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time);
|
const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time);
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
|
|
@ -161,4 +160,18 @@ export default class GameLoop {
|
||||||
// await sleep(200);
|
// await sleep(200);
|
||||||
requestAnimationFrame(this.render.bind(this));
|
requestAnimationFrame(this.render.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.hasFinished = true;
|
||||||
|
|
||||||
|
this.agentPipeline?.destroy();
|
||||||
|
this.brushPipeline?.destroy();
|
||||||
|
this.diffusionPipeline?.destroy();
|
||||||
|
this.renderPipeline?.destroy();
|
||||||
|
|
||||||
|
this.trailMapA?.destroy();
|
||||||
|
this.trailMapB?.destroy();
|
||||||
|
|
||||||
|
this.resolveHasFinished();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,37 @@
|
||||||
<link inline inline-asset="index.css" inline-asset-delete />
|
<link inline inline-asset="index.css" inline-asset-delete />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>JavaScript is required for this website.</noscript>
|
<main class="canvas-container">
|
||||||
<canvas></canvas>
|
<canvas></canvas>
|
||||||
<section class="errors-container"><pre class="errors"></pre></section>
|
|
||||||
|
<section class="errors-container">
|
||||||
|
<pre class="errors">
|
||||||
|
<noscript>JavaScript is required for this website.</noscript>
|
||||||
|
</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button class="minimize-full-screen"></button>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<aside>
|
||||||
|
<nav class="buttons">
|
||||||
|
<button class="info"></button>
|
||||||
|
<button class="maximize-full-screen"></button>
|
||||||
|
<button class="restart"></button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="pages">
|
||||||
|
<section class="info-page">
|
||||||
|
<h1>Title</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>
|
||||||
|
<section class="info-page"></section>
|
||||||
|
</main>
|
||||||
|
</aside>
|
||||||
<script inline inline-asset="index.js" inline-asset-delete></script>
|
<script inline inline-asset="index.js" inline-asset-delete></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
92
src/index.scss
Normal file
92
src/index.scss
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
@use 'style/vars';
|
||||||
|
@use 'style/fonts';
|
||||||
|
@use 'style/mixins' as *;
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion) {
|
||||||
|
transition: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
-webkit-font-smooth: antialiased;
|
||||||
|
|
||||||
|
> body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> .canvas-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> canvas {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> button.minimize-full-screen {
|
||||||
|
@include image-button(url('../assets/icons/minimize.svg'));
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--small-margin);
|
||||||
|
right: var(--small-margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .errors-container {
|
||||||
|
color: red;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> aside {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
|
||||||
|
@include blurred-background;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin: var(--small-margin);
|
||||||
|
|
||||||
|
> nav.buttons {
|
||||||
|
@include center-children;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--normal-margin);
|
||||||
|
margin: var(--small-margin);
|
||||||
|
|
||||||
|
> button.info {
|
||||||
|
@include image-button(url('../assets/icons/info.svg'));
|
||||||
|
}
|
||||||
|
|
||||||
|
> button.maximize-full-screen {
|
||||||
|
@include image-button(url('../assets/icons/maximize.svg'));
|
||||||
|
}
|
||||||
|
|
||||||
|
> button.restart {
|
||||||
|
@include image-button(url('../assets/icons/restart.svg'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> main.pages {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/index.ts
43
src/index.ts
|
|
@ -1,6 +1,9 @@
|
||||||
|
import '../assets/icons/info.svg';
|
||||||
import GameLoop from './game-loop/game-loop';
|
import GameLoop from './game-loop/game-loop';
|
||||||
import './styles/index.scss';
|
import './index.scss';
|
||||||
import { applyArrayPlugins } from './utils/array';
|
import { applyArrayPlugins } from './utils/array';
|
||||||
|
import { handleFullScreen } from './utils/handle-full-screen';
|
||||||
|
import { initializeGPU } from './utils/webgpu/initialize-gpu';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Array<T> {
|
interface Array<T> {
|
||||||
|
|
@ -19,17 +22,43 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getElements = () => ({
|
||||||
|
infoButton: document.querySelector('button.info') as HTMLButtonElement,
|
||||||
|
minimizeFullScreenButton: document.querySelector(
|
||||||
|
'button.minimize-full-screen'
|
||||||
|
) as HTMLButtonElement,
|
||||||
|
maximizeFullScreenButton: document.querySelector(
|
||||||
|
'button.maximize-full-screen'
|
||||||
|
) 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') as HTMLDivElement,
|
||||||
|
});
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
applyArrayPlugins();
|
applyArrayPlugins();
|
||||||
|
const elements = getElements();
|
||||||
|
|
||||||
const errorContainer = document.querySelector('.errors');
|
handleFullScreen({
|
||||||
|
minimizeButton: elements.minimizeFullScreenButton,
|
||||||
|
maximizeButton: elements.maximizeFullScreenButton,
|
||||||
|
target: elements.canvasContainer,
|
||||||
|
});
|
||||||
|
|
||||||
const main = () => {
|
const gpu = await initializeGPU();
|
||||||
|
|
||||||
|
let game: GameLoop | null = null;
|
||||||
|
|
||||||
|
elements.restartButton.addEventListener('click', () => game?.destroy());
|
||||||
|
|
||||||
|
while (true) {
|
||||||
try {
|
try {
|
||||||
const canvas = document.querySelector('canvas');
|
game = new GameLoop(elements.canvas, gpu);
|
||||||
const game = new GameLoop(canvas);
|
await game.start();
|
||||||
game.start();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorContainer.innerHTML = e.message;
|
elements.errorContainer.innerHTML = e.message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { smartCompile } from '../../utils/smart-compile';
|
import { smartCompile } from '../../utils/webgpu/smart-compile';
|
||||||
import { CommonParameters } from '../common-parameters';
|
import { CommonParameters } from '../common-parameters';
|
||||||
import { AGENT_SIZE_IN_BYTES, Agent } from './agent';
|
import { AGENT_SIZE_IN_BYTES, Agent } from './agent';
|
||||||
import { AgentSettings } from './agent-settings';
|
import { AgentSettings } from './agent-settings';
|
||||||
|
|
@ -133,4 +133,9 @@ export class AgentPipeline {
|
||||||
this.previousTrailMapOut = trailMapOut;
|
this.previousTrailMapOut = trailMapOut;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.uniforms.destroy();
|
||||||
|
this.agentsBuffer.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { generateNoise } from '../../utils/graphics/noise/noise';
|
import { generateNoise } from '../../utils/graphics/noise/noise';
|
||||||
import { smartCompile } from '../../utils/smart-compile';
|
import { smartCompile } from '../../utils/webgpu/smart-compile';
|
||||||
import { CommonParameters } from '../common-parameters';
|
import { CommonParameters } from '../common-parameters';
|
||||||
import { BrushSettings } from './brush-settings';
|
import { BrushSettings } from './brush-settings';
|
||||||
import shader from './brush.wgsl';
|
import shader from './brush.wgsl';
|
||||||
|
|
@ -15,7 +15,7 @@ export class BrushPipeline {
|
||||||
private readonly pipeline: GPURenderPipeline;
|
private readonly pipeline: GPURenderPipeline;
|
||||||
private readonly uniforms: GPUBuffer;
|
private readonly uniforms: GPUBuffer;
|
||||||
private readonly vertexBuffer: GPUBuffer;
|
private readonly vertexBuffer: GPUBuffer;
|
||||||
private readonly noise: GPUTexture;
|
private readonly noise: GPUTextureView;
|
||||||
private linePoints: Array<vec2> = [];
|
private linePoints: Array<vec2> = [];
|
||||||
private previousPoints: Array<vec2> = [];
|
private previousPoints: Array<vec2> = [];
|
||||||
private nextPoint: vec2 | null = null;
|
private nextPoint: vec2 | null = null;
|
||||||
|
|
@ -116,7 +116,7 @@ export class BrushPipeline {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
binding: 2,
|
binding: 2,
|
||||||
resource: this.noise.createView(),
|
resource: this.noise,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
@ -234,6 +234,11 @@ export class BrushPipeline {
|
||||||
|
|
||||||
this.linePoints.splice(0, this.linePoints.length - 1);
|
this.linePoints.splice(0, this.linePoints.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.vertexBuffer.destroy();
|
||||||
|
this.uniforms.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const catmullRomInterpolation = (
|
const catmullRomInterpolation = (
|
||||||
|
|
|
||||||
|
|
@ -29,15 +29,14 @@ fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||||
let mixedTrails = mix(
|
let mixedTrails = mix(
|
||||||
current.rgb,
|
current.rgb,
|
||||||
neighbours.rgb,
|
neighbours.rgb,
|
||||||
settings.diffusionRateTrails + (noise.rgb - vec3(0.5)) * 0.1
|
settings.diffusionRateTrails
|
||||||
) * (1.0 - settings.decayRateTrails);
|
) * (1.0 - settings.decayRateTrails);
|
||||||
|
|
||||||
let mixedBrush = mix(
|
let mixedBrush = mix(
|
||||||
current.a,
|
current.a + (noise.a - 0.5) * 0.1,
|
||||||
neighbours.a ,
|
neighbours.a ,
|
||||||
settings.diffusionRateBrush + (noise.a - 0.5) * 0.5
|
settings.diffusionRateBrush
|
||||||
) * (1.0 - settings.decayRateBrush - (noise.a - 0.5) * 0.1);
|
) * (1.0 - settings.decayRateBrush);
|
||||||
|
|
||||||
|
|
||||||
return clamp(vec4(mixedTrails, mixedBrush), vec4(0), vec4(1));
|
return clamp(vec4(mixedTrails, mixedBrush), vec4(0), vec4(1));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad/full-screen-quad';
|
import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad/full-screen-quad';
|
||||||
import { generateNoise } from '../../utils/graphics/noise/noise';
|
import { generateNoise } from '../../utils/graphics/noise/noise';
|
||||||
import { smartCompile } from '../../utils/smart-compile';
|
import { smartCompile } from '../../utils/webgpu/smart-compile';
|
||||||
import { CommonParameters } from '../common-parameters';
|
import { CommonParameters } from '../common-parameters';
|
||||||
import shader from './diffuse.wgsl';
|
import shader from './diffuse.wgsl';
|
||||||
import { DiffusionSettings } from './diffusion-settings';
|
import { DiffusionSettings } from './diffusion-settings';
|
||||||
|
|
@ -11,7 +11,7 @@ export class DiffusionPipeline {
|
||||||
private readonly pipeline: GPURenderPipeline;
|
private readonly pipeline: GPURenderPipeline;
|
||||||
private readonly uniforms: GPUBuffer;
|
private readonly uniforms: GPUBuffer;
|
||||||
private readonly quadVertexBuffer: GPUBuffer;
|
private readonly quadVertexBuffer: GPUBuffer;
|
||||||
private readonly noise: GPUTexture;
|
private readonly noise: GPUTextureView;
|
||||||
|
|
||||||
private bindGroup?: GPUBindGroup;
|
private bindGroup?: GPUBindGroup;
|
||||||
private previousTrailMapIn?: GPUTexture;
|
private previousTrailMapIn?: GPUTexture;
|
||||||
|
|
@ -128,7 +128,7 @@ export class DiffusionPipeline {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
binding: 3,
|
binding: 3,
|
||||||
resource: this.noise.createView(),
|
resource: this.noise,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
@ -136,4 +136,9 @@ export class DiffusionPipeline {
|
||||||
this.previousTrailMapIn = trailMapIn;
|
this.previousTrailMapIn = trailMapIn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.quadVertexBuffer.destroy();
|
||||||
|
this.uniforms.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad/full-screen-quad';
|
import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad/full-screen-quad';
|
||||||
import { smartCompile } from '../../utils/smart-compile';
|
import { smartCompile } from '../../utils/webgpu/smart-compile';
|
||||||
import { CommonParameters } from '../common-parameters';
|
import { CommonParameters } from '../common-parameters';
|
||||||
import { RenderSettings } from './render-settings';
|
import { RenderSettings } from './render-settings';
|
||||||
import shader from './render.wgsl';
|
import shader from './render.wgsl';
|
||||||
|
|
@ -118,4 +118,9 @@ export class RenderPipeline {
|
||||||
this.previousColorTexture = colorTexture;
|
this.previousColorTexture = colorTexture;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.quadVertexBuffer.destroy();
|
||||||
|
this.uniforms.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
src/style/fonts.scss
Normal file
23
src/style/fonts.scss
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/* comfortaa-regular - latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Comfortaa';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
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+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open-sans-regular - latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
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+ */
|
||||||
|
}
|
||||||
160
src/style/mixins.scss
Normal file
160
src/style/mixins.scss
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
@use 'sass:math';
|
||||||
|
|
||||||
|
$breakpoint-width: 700px !default;
|
||||||
|
|
||||||
|
@mixin on-small-screen() {
|
||||||
|
@media (max-width: ($breakpoint-width - 1px)) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin on-large-screen() {
|
||||||
|
@media (min-width: $breakpoint-width) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin in-dark-mode() {
|
||||||
|
html[theme='dark'] {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin title-fragment-link() {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '#';
|
||||||
|
position: absolute;
|
||||||
|
left: -0.5ch;
|
||||||
|
top: 50%;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-100%) translateY(-50%);
|
||||||
|
transition: opacity var(--transition-time);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:before {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin image-button($background-image) {
|
||||||
|
@include square(var(--icon-size));
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
background-color: transparent;
|
||||||
|
background-image: $background-image;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
|
||||||
|
transition: transform var(--transition-time);
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin center-children() {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin absolute-center() {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateX(-50%) translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin blurred-background() {
|
||||||
|
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: var(--card-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin square($size) {
|
||||||
|
width: $size;
|
||||||
|
height: $size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin title-font() {
|
||||||
|
font: 400 3rem 'Comfortaa', sans-serif;
|
||||||
|
color: var(--normal-text-color);
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
@include on-small-screen {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin sub-title-font() {
|
||||||
|
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;
|
||||||
|
color: var(--normal-text-color);
|
||||||
|
line-height: 1.8;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin special-text-font() {
|
||||||
|
font: 400 1rem 'Open Sans', sans-serif;
|
||||||
|
color: var(--special-text-color);
|
||||||
|
hyphens: auto;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin link {
|
||||||
|
$border-shift: 10px;
|
||||||
|
$line-width: 2px;
|
||||||
|
|
||||||
|
@include special-text-font();
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
padding: 0 3px $line-width 0;
|
||||||
|
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
width: calc(100% + #{$border-shift});
|
||||||
|
border-bottom: $line-width dashed var(--accent-color);
|
||||||
|
transition: transform var(--transition-time);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
width: 100%;
|
||||||
|
height: $line-width;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--card-color) 0,
|
||||||
|
transparent 4px,
|
||||||
|
transparent calc(100% - 4px),
|
||||||
|
var(--card-color) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:before {
|
||||||
|
transform: translateX(-$border-shift);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/style/vars.scss
Normal file
44
src/style/vars.scss
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
@use 'mixins' as *;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--transition-time: 200ms;
|
||||||
|
--transition-time-long: 350ms;
|
||||||
|
--line-width: 4px;
|
||||||
|
--line-height: 1.125rem;
|
||||||
|
--accent-color: #b7455e;
|
||||||
|
--sun-color: #f7f78c;
|
||||||
|
--very-light-text-color: #ffffff;
|
||||||
|
--background: #ffffff;
|
||||||
|
--normal-text-color: #31343f;
|
||||||
|
--card-color: #ffffff;
|
||||||
|
--blurred-card-color: transparent;
|
||||||
|
--blur-radius: 12px;
|
||||||
|
--special-text-color: var(--accent-color);
|
||||||
|
--inset-shadow: inset 0 0 4px 1px rgba(0, 0, 0, 0.05), inset 0 0 5px rgba(0, 0, 0, 0.2);
|
||||||
|
--border-radius: 0.85rem;
|
||||||
|
|
||||||
|
--large-margin: 4.6rem;
|
||||||
|
--normal-margin: 2rem;
|
||||||
|
--small-margin: 1rem;
|
||||||
|
--shadow: 0 0 5px 2px rgba(0, 0, 0, 0.15), 0 0 1px rgba(0, 0, 0, 0.2);
|
||||||
|
--icon-size: 2.8rem;
|
||||||
|
--large-icon-size: 3.75rem;
|
||||||
|
--body-width: min(80%, 60rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include on-small-screen {
|
||||||
|
:root {
|
||||||
|
--body-width: 90%;
|
||||||
|
--large-margin: 2.8rem;
|
||||||
|
--normal-margin: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include in-dark-mode {
|
||||||
|
--background: #242638;
|
||||||
|
--normal-text-color: #ffffff;
|
||||||
|
--card-color: #263551;
|
||||||
|
--blurred-card-color: #212f4a77;
|
||||||
|
--special-text-color: #ffffff;
|
||||||
|
--inset-shadow: inset 0 0 10px 2px rgba(0, 0, 0, 0.3), inset 0 0 4px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion) {
|
|
||||||
transition: none !important;
|
|
||||||
animation: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
height: 100%;
|
|
||||||
-webkit-font-smooth: antialiased;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errors {
|
|
||||||
color: red;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
@mixin card {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin center-children {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin square($size) {
|
|
||||||
width: $size;
|
|
||||||
height: $size;
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { smartCompile } from '../../smart-compile';
|
import { smartCompile } from '../../webgpu/smart-compile';
|
||||||
import shader from './full-screen-quad.wgsl';
|
import shader from './full-screen-quad.wgsl';
|
||||||
|
|
||||||
export const setUpFullScreenQuad = (
|
export const setUpFullScreenQuad = (
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { Random } from '../../random';
|
import { Random } from '../../random';
|
||||||
import { smartCompile } from '../../smart-compile';
|
import { smartCompile } from '../../webgpu/smart-compile';
|
||||||
import { setUpFullScreenQuad } from '../full-screen-quad/full-screen-quad';
|
import { setUpFullScreenQuad } from '../full-screen-quad/full-screen-quad';
|
||||||
import noise from './noise.wgsl';
|
import noise from './noise.wgsl';
|
||||||
|
|
||||||
|
const textureCache = new Map<string, GPUTexture>();
|
||||||
|
|
||||||
export const generateNoise = ({
|
export const generateNoise = ({
|
||||||
device,
|
device,
|
||||||
width = 1024,
|
width = 1024,
|
||||||
|
|
@ -19,7 +21,9 @@ export const generateNoise = ({
|
||||||
lacunarity?: number;
|
lacunarity?: number;
|
||||||
amplitude?: number;
|
amplitude?: number;
|
||||||
gain?: number;
|
gain?: number;
|
||||||
}) => {
|
}): GPUTextureView => {
|
||||||
|
const cacheKey = `${width}x${height}x${octaves}x${lacunarity}x${amplitude}x${gain}`;
|
||||||
|
if (!textureCache.has(cacheKey)) {
|
||||||
const { buffer, vertex } = setUpFullScreenQuad(device);
|
const { buffer, vertex } = setUpFullScreenQuad(device);
|
||||||
const quadVertexBuffer = buffer;
|
const quadVertexBuffer = buffer;
|
||||||
|
|
||||||
|
|
@ -80,6 +84,8 @@ export const generateNoise = ({
|
||||||
passEncoder.end();
|
passEncoder.end();
|
||||||
|
|
||||||
device.queue.submit([commandEncoder.finish()]);
|
device.queue.submit([commandEncoder.finish()]);
|
||||||
|
textureCache.set(cacheKey, colorTexture);
|
||||||
|
}
|
||||||
|
|
||||||
return colorTexture;
|
return textureCache.get(cacheKey).createView();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
export const handleFullScreen = (
|
export const handleFullScreen = ({
|
||||||
minimizeButton: HTMLElement,
|
minimizeButton,
|
||||||
maximizeButton: HTMLElement,
|
maximizeButton,
|
||||||
target: HTMLElement
|
target,
|
||||||
) => {
|
}: {
|
||||||
|
minimizeButton: HTMLElement;
|
||||||
|
maximizeButton: HTMLElement;
|
||||||
|
target: HTMLElement;
|
||||||
|
}) => {
|
||||||
if (!document.fullscreenEnabled) {
|
if (!document.fullscreenEnabled) {
|
||||||
minimizeButton.style.visibility = 'hidden';
|
minimizeButton.style.visibility = 'hidden';
|
||||||
maximizeButton.style.visibility = 'hidden';
|
maximizeButton.style.visibility = 'hidden';
|
||||||
|
|
@ -10,39 +14,23 @@ export const handleFullScreen = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const isInFullScreen = (): boolean => document.fullscreenElement !== null;
|
const isInFullScreen = (): boolean => document.fullscreenElement !== null;
|
||||||
|
const updateButtons = () => {
|
||||||
const showButtons = () => {
|
|
||||||
minimizeButton.style.visibility = isInFullScreen() ? 'visible' : 'hidden';
|
minimizeButton.style.visibility = isInFullScreen() ? 'visible' : 'hidden';
|
||||||
maximizeButton.style.visibility = isInFullScreen() ? 'hidden' : 'visible';
|
maximizeButton.style.visibility = isInFullScreen() ? 'hidden' : 'visible';
|
||||||
};
|
};
|
||||||
|
|
||||||
showButtons();
|
updateButtons();
|
||||||
|
|
||||||
let currentWindowHeight = innerHeight;
|
|
||||||
|
|
||||||
const followToggle = () => {
|
|
||||||
showButtons();
|
|
||||||
currentWindowHeight = innerHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerToggle = async () => {
|
|
||||||
await (isInFullScreen() ? document.exitFullscreen() : target.requestFullscreen());
|
|
||||||
followToggle();
|
|
||||||
};
|
|
||||||
|
|
||||||
addEventListener('keydown', (e) => {
|
addEventListener('keydown', (e) => {
|
||||||
|
// on full screen request, only apply it to the target
|
||||||
if (e.key === 'F11') {
|
if (e.key === 'F11') {
|
||||||
triggerToggle();
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
isInFullScreen() ? document.exitFullscreen() : target.requestFullscreen();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
addEventListener('resize', () => {
|
addEventListener('fullscreenchange', updateButtons);
|
||||||
if (isInFullScreen() && currentWindowHeight > innerHeight) {
|
|
||||||
followToggle();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
maximizeButton.addEventListener('click', triggerToggle);
|
maximizeButton.addEventListener('click', target.requestFullscreen.bind(target));
|
||||||
minimizeButton.addEventListener('click', triggerToggle);
|
minimizeButton.addEventListener('click', document.exitFullscreen.bind(document));
|
||||||
};
|
};
|
||||||
|
|
|
||||||
16
src/utils/webgpu/initialize-gpu.ts
Normal file
16
src/utils/webgpu/initialize-gpu.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
export const initializeGPU = async (): Promise<GPUDevice> => {
|
||||||
|
const gpu = navigator.gpu;
|
||||||
|
if (!gpu) {
|
||||||
|
throw new Error('WebGPU is not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapter = await gpu.requestAdapter({
|
||||||
|
powerPreference: 'high-performance',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!adapter) {
|
||||||
|
throw new Error('Could not request adatper');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await adapter.requestDevice(); // could request more resources
|
||||||
|
};
|
||||||
|
|
@ -39,13 +39,13 @@ module.exports = (env, argv) => ({
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.svg$/i,
|
test: /\.svg$/i,
|
||||||
use: 'svg-inline-loader',
|
type: 'asset/inline',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.wgsl$/i,
|
test: /\.woff2?$/i,
|
||||||
type: 'asset/source',
|
type: 'asset/resource',
|
||||||
generator: {
|
generator: {
|
||||||
filename: '[name][ext]',
|
filename: '[hash:8][ext]',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -55,6 +55,13 @@ module.exports = (env, argv) => ({
|
||||||
filename: '[name][ext]',
|
filename: '[name][ext]',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.wgsl$/i,
|
||||||
|
type: 'asset/source',
|
||||||
|
generator: {
|
||||||
|
filename: '[name][ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.scss$/i,
|
test: /\.scss$/i,
|
||||||
use: [
|
use: [
|
||||||
|
|
@ -70,16 +77,13 @@ module.exports = (env, argv) => ({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.ts$/,
|
test: /\.ts$/i,
|
||||||
use: 'ts-loader',
|
use: 'ts-loader',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [
|
extensions: ['.ts', '.js'],
|
||||||
'.ts',
|
|
||||||
'.js', // required for development
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
clean: true,
|
clean: true,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue