Add tiny UI

This commit is contained in:
Andras Schmelczer 2023-04-30 11:27:26 +01:00
parent e3d0af56e2
commit 6a752a57e9
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
30 changed files with 627 additions and 210 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

6
assets/icons/info.svg Normal file
View 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

View file

@ -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 d="M4 8v-2a2 2 0 0 1 2 -2h2" />
<path d="M4 16v2a2 2 0 0 0 2 2h2" />

Before

Width:  |  Height:  |  Size: 399 B

After

Width:  |  Height:  |  Size: 376 B

Before After
Before After

View file

@ -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 d="M15 19v-2a2 2 0 0 1 2 -2h2" />
<path d="M15 5v2a2 2 0 0 0 2 2h2" />

Before

Width:  |  Height:  |  Size: 399 B

After

Width:  |  Height:  |  Size: 376 B

Before After
Before After

5
assets/icons/restart.svg Normal file
View 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
View 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>

View file

@ -6,36 +6,45 @@ import { RenderPipeline } from '../pipelines/render/render-pipeline';
import { settings } from '../settings';
import { DeltaTimeCalculator } from '../utils/delta-time-calculator';
import { Random } from '../utils/random';
import { sleep } from '../utils/sleep';
import { vec2 } from 'gl-matrix';
export default class GameLoop {
private context: GPUCanvasContext;
private device: GPUDevice;
private readonly deltaTimeCalculator = new DeltaTimeCalculator();
private agentPipeline: AgentPipeline;
private renderPipeline: RenderPipeline;
private brushPipeline: BrushPipeline;
private diffusionPipeline: DiffusionPipeline;
private readonly agentPipeline: AgentPipeline;
private readonly renderPipeline: RenderPipeline;
private readonly brushPipeline: BrushPipeline;
private readonly diffusionPipeline: DiffusionPipeline;
private trailMapA?: 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 readonly deltaTimeCalculator = new DeltaTimeCalculator();
public constructor(private canvas: HTMLCanvasElement) {}
async start() {
await this.initializeDevice();
public constructor(
private readonly canvas: HTMLCanvasElement,
private readonly device: GPUDevice
) {
const context = this.canvas.getContext('webgpu') as any;
context.configure({
device: this.device,
format: navigator.gpu.getPreferredCanvasFormat(),
alphaMode: 'premultiplied',
});
this.resize();
this.agentPipeline = new AgentPipeline(this.device, this.spawnAgents());
this.brushPipeline = new BrushPipeline(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('mousemove', this.onSwipe.bind(this));
@ -44,8 +53,11 @@ export default class GameLoop {
this.isSwipeActive = false;
this.brushPipeline.clearSwipes();
});
}
public async start(): Promise<void> {
requestAnimationFrame(this.render.bind(this));
return this.hasFinishedPromise;
}
private onSwipe(event: MouseEvent) {
@ -112,24 +124,11 @@ export default class GameLoop {
});
}
private async initializeDevice(): Promise<void> {
const gpu = navigator.gpu;
if (!gpu) {
throw new Error('WebGPU is not supported');
private render(time: DOMHighResTimeStamp) {
if (this.hasFinished) {
return;
}
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 params = {
@ -161,4 +160,18 @@ export default class GameLoop {
// await sleep(200);
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();
}
}

View file

@ -30,9 +30,37 @@
<link inline inline-asset="index.css" inline-asset-delete />
</head>
<body>
<noscript>JavaScript is required for this website.</noscript>
<main class="canvas-container">
<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>
</body>
</html>

92
src/index.scss Normal file
View 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;
}
}
}
}

View file

@ -1,6 +1,9 @@
import '../assets/icons/info.svg';
import GameLoop from './game-loop/game-loop';
import './styles/index.scss';
import './index.scss';
import { applyArrayPlugins } from './utils/array';
import { handleFullScreen } from './utils/handle-full-screen';
import { initializeGPU } from './utils/webgpu/initialize-gpu';
declare global {
interface Array<T> {
@ -19,17 +22,43 @@ declare global {
}
}
applyArrayPlugins();
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 errorContainer = document.querySelector('.errors');
const main = async () => {
applyArrayPlugins();
const elements = getElements();
const main = () => {
handleFullScreen({
minimizeButton: elements.minimizeFullScreenButton,
maximizeButton: elements.maximizeFullScreenButton,
target: elements.canvasContainer,
});
const gpu = await initializeGPU();
let game: GameLoop | null = null;
elements.restartButton.addEventListener('click', () => game?.destroy());
while (true) {
try {
const canvas = document.querySelector('canvas');
const game = new GameLoop(canvas);
game.start();
game = new GameLoop(elements.canvas, gpu);
await game.start();
} catch (e) {
errorContainer.innerHTML = e.message;
elements.errorContainer.innerHTML = e.message;
}
}
};

View file

@ -1,4 +1,4 @@
import { smartCompile } from '../../utils/smart-compile';
import { smartCompile } from '../../utils/webgpu/smart-compile';
import { CommonParameters } from '../common-parameters';
import { AGENT_SIZE_IN_BYTES, Agent } from './agent';
import { AgentSettings } from './agent-settings';
@ -133,4 +133,9 @@ export class AgentPipeline {
this.previousTrailMapOut = trailMapOut;
}
}
public destroy() {
this.uniforms.destroy();
this.agentsBuffer.destroy();
}
}

View file

@ -1,5 +1,5 @@
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 { BrushSettings } from './brush-settings';
import shader from './brush.wgsl';
@ -15,7 +15,7 @@ export class BrushPipeline {
private readonly pipeline: GPURenderPipeline;
private readonly uniforms: GPUBuffer;
private readonly vertexBuffer: GPUBuffer;
private readonly noise: GPUTexture;
private readonly noise: GPUTextureView;
private linePoints: Array<vec2> = [];
private previousPoints: Array<vec2> = [];
private nextPoint: vec2 | null = null;
@ -116,7 +116,7 @@ export class BrushPipeline {
},
{
binding: 2,
resource: this.noise.createView(),
resource: this.noise,
},
],
});
@ -234,6 +234,11 @@ export class BrushPipeline {
this.linePoints.splice(0, this.linePoints.length - 1);
}
public destroy() {
this.vertexBuffer.destroy();
this.uniforms.destroy();
}
}
const catmullRomInterpolation = (

View file

@ -29,15 +29,14 @@ fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
let mixedTrails = mix(
current.rgb,
neighbours.rgb,
settings.diffusionRateTrails + (noise.rgb - vec3(0.5)) * 0.1
settings.diffusionRateTrails
) * (1.0 - settings.decayRateTrails);
let mixedBrush = mix(
current.a,
neighbours.a,
settings.diffusionRateBrush + (noise.a - 0.5) * 0.5
) * (1.0 - settings.decayRateBrush - (noise.a - 0.5) * 0.1);
current.a + (noise.a - 0.5) * 0.1,
neighbours.a ,
settings.diffusionRateBrush
) * (1.0 - settings.decayRateBrush);
return clamp(vec4(mixedTrails, mixedBrush), vec4(0), vec4(1));
}

View file

@ -1,6 +1,6 @@
import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad/full-screen-quad';
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 shader from './diffuse.wgsl';
import { DiffusionSettings } from './diffusion-settings';
@ -11,7 +11,7 @@ export class DiffusionPipeline {
private readonly pipeline: GPURenderPipeline;
private readonly uniforms: GPUBuffer;
private readonly quadVertexBuffer: GPUBuffer;
private readonly noise: GPUTexture;
private readonly noise: GPUTextureView;
private bindGroup?: GPUBindGroup;
private previousTrailMapIn?: GPUTexture;
@ -128,7 +128,7 @@ export class DiffusionPipeline {
},
{
binding: 3,
resource: this.noise.createView(),
resource: this.noise,
},
],
});
@ -136,4 +136,9 @@ export class DiffusionPipeline {
this.previousTrailMapIn = trailMapIn;
}
}
public destroy() {
this.quadVertexBuffer.destroy();
this.uniforms.destroy();
}
}

View file

@ -1,5 +1,5 @@
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 { RenderSettings } from './render-settings';
import shader from './render.wgsl';
@ -118,4 +118,9 @@ export class RenderPipeline {
this.previousColorTexture = colorTexture;
}
}
public destroy() {
this.quadVertexBuffer.destroy();
this.uniforms.destroy();
}
}

23
src/style/fonts.scss Normal file
View 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
View 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
View 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);
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -1,4 +1,4 @@
import { smartCompile } from '../../smart-compile';
import { smartCompile } from '../../webgpu/smart-compile';
import shader from './full-screen-quad.wgsl';
export const setUpFullScreenQuad = (

View file

@ -1,8 +1,10 @@
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 noise from './noise.wgsl';
const textureCache = new Map<string, GPUTexture>();
export const generateNoise = ({
device,
width = 1024,
@ -19,7 +21,9 @@ export const generateNoise = ({
lacunarity?: number;
amplitude?: 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 quadVertexBuffer = buffer;
@ -80,6 +84,8 @@ export const generateNoise = ({
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
textureCache.set(cacheKey, colorTexture);
}
return colorTexture;
return textureCache.get(cacheKey).createView();
};

View file

@ -1,8 +1,12 @@
export const handleFullScreen = (
minimizeButton: HTMLElement,
maximizeButton: HTMLElement,
target: HTMLElement
) => {
export const handleFullScreen = ({
minimizeButton,
maximizeButton,
target,
}: {
minimizeButton: HTMLElement;
maximizeButton: HTMLElement;
target: HTMLElement;
}) => {
if (!document.fullscreenEnabled) {
minimizeButton.style.visibility = 'hidden';
maximizeButton.style.visibility = 'hidden';
@ -10,39 +14,23 @@ export const handleFullScreen = (
}
const isInFullScreen = (): boolean => document.fullscreenElement !== null;
const showButtons = () => {
const updateButtons = () => {
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();
};
updateButtons();
addEventListener('keydown', (e) => {
// on full screen request, only apply it to the target
if (e.key === 'F11') {
triggerToggle();
e.preventDefault();
isInFullScreen() ? document.exitFullscreen() : target.requestFullscreen();
}
});
addEventListener('resize', () => {
if (isInFullScreen() && currentWindowHeight > innerHeight) {
followToggle();
}
});
addEventListener('fullscreenchange', updateButtons);
maximizeButton.addEventListener('click', triggerToggle);
minimizeButton.addEventListener('click', triggerToggle);
maximizeButton.addEventListener('click', target.requestFullscreen.bind(target));
minimizeButton.addEventListener('click', document.exitFullscreen.bind(document));
};

View 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
};

View file

@ -39,13 +39,13 @@ module.exports = (env, argv) => ({
rules: [
{
test: /\.svg$/i,
use: 'svg-inline-loader',
type: 'asset/inline',
},
{
test: /\.wgsl$/i,
type: 'asset/source',
test: /\.woff2?$/i,
type: 'asset/resource',
generator: {
filename: '[name][ext]',
filename: '[hash:8][ext]',
},
},
{
@ -55,6 +55,13 @@ module.exports = (env, argv) => ({
filename: '[name][ext]',
},
},
{
test: /\.wgsl$/i,
type: 'asset/source',
generator: {
filename: '[name][ext]',
},
},
{
test: /\.scss$/i,
use: [
@ -70,16 +77,13 @@ module.exports = (env, argv) => ({
],
},
{
test: /\.ts$/,
test: /\.ts$/i,
use: 'ts-loader',
},
],
},
resolve: {
extensions: [
'.ts',
'.js', // required for development
],
extensions: ['.ts', '.js'],
},
output: {
clean: true,