Refactor
This commit is contained in:
parent
de7fcc15d0
commit
9e582110ea
27 changed files with 248 additions and 123 deletions
19
README.md
19
README.md
|
|
@ -1,18 +1,3 @@
|
||||||
# 🔺 WebGPU Seed
|
## todo
|
||||||
|
|
||||||
[![License][license-img]][license-url]
|
-
|
||||||
|
|
||||||
A WebGPU repo you can use to get started with your own renderer.
|
|
||||||
|
|
||||||
- [🔳 Codepen Example](https://codepen.io/alaingalvan/pen/GRgvLGw)
|
|
||||||
|
|
||||||
- [💬 Blog Post](https://alain.xyz/blog/raw-webgpu)
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
> Refer to [this blog post on designing web libraries and apps](https://alain.xyz/blog/designing-a-web-app) for more details on Node.js, packages, etc.
|
|
||||||
|
|
||||||
As your project becomes more complex, you'll want to separate files and organize your application to something more akin to a game or renderer, check out this post on [game engine architecture](https://alain.xyz/blog/game-engine-architecture) and this one on [real time renderer architecture](https://alain.xyz/blog/realtime-renderer-architectures) for more details.
|
|
||||||
|
|
||||||
[license-img]: https://img.shields.io/:license-unlicense-blue.svg?style=flat-square
|
|
||||||
[license-url]: https://unlicense.org/
|
|
||||||
|
|
|
||||||
5
src/game-loop/game-loop-settings.ts
Normal file
5
src/game-loop/game-loop-settings.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface GameLoopSettings {
|
||||||
|
agentCount: number;
|
||||||
|
renderSpeed: number;
|
||||||
|
startingRadius: number;
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { Agent } from './pipelines/agents/agent';
|
import { Agent } from '../pipelines/agents/agent';
|
||||||
import { AgentPipeline } from './pipelines/agents/agent-pipeline';
|
import { AgentPipeline } from '../pipelines/agents/agent-pipeline';
|
||||||
import { BrushPipeline } from './pipelines/brush/brush-pipeline';
|
import { BrushPipeline } from '../pipelines/brush/brush-pipeline';
|
||||||
import { DiffusionPipeline } from './pipelines/diffusion/diffusion-pipeline';
|
import { DiffusionPipeline } from '../pipelines/diffusion/diffusion-pipeline';
|
||||||
import { RenderPipeline } from './pipelines/render/render-pipeline';
|
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 { randomBetween } from './utils/random-between';
|
import { Random } from '../utils/random';
|
||||||
import { sleep } from './utils/sleep';
|
|
||||||
|
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
|
|
@ -77,8 +76,8 @@ export default class Renderer {
|
||||||
);
|
);
|
||||||
vec2.normalize(size, size);
|
vec2.normalize(size, size);
|
||||||
return new Array(settings.agentCount).fill(0).map(() => {
|
return new Array(settings.agentCount).fill(0).map(() => {
|
||||||
const radius = randomBetween(0, settings.startingRadius / ratio);
|
const radius = Random.randomBetween(0, settings.startingRadius / ratio);
|
||||||
const angle = randomBetween(0, Math.PI * 2);
|
const angle = Random.randomBetween(0, Math.PI * 2);
|
||||||
const center = vec2.fromValues(0.5, 0.5);
|
const center = vec2.fromValues(0.5, 0.5);
|
||||||
|
|
||||||
const delta = vec2.fromValues(Math.cos(angle) * radius, Math.sin(angle) * radius);
|
const delta = vec2.fromValues(Math.cos(angle) * radius, Math.sin(angle) * radius);
|
||||||
|
|
@ -142,24 +141,20 @@ export default class Renderer {
|
||||||
private async render(time: DOMHighResTimeStamp) {
|
private async render(time: DOMHighResTimeStamp) {
|
||||||
const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time);
|
const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time);
|
||||||
|
|
||||||
this.agentPipeline.setParameters({
|
const params = {
|
||||||
...settings,
|
canvasSize: vec2.fromValues(this.canvas.width, this.canvas.height),
|
||||||
width: this.canvas.width,
|
|
||||||
height: this.canvas.height,
|
|
||||||
time,
|
time,
|
||||||
deltaTime,
|
deltaTime,
|
||||||
});
|
|
||||||
this.brushPipeline.setParameters({
|
|
||||||
width: this.canvas.width,
|
|
||||||
height: this.canvas.height,
|
|
||||||
});
|
|
||||||
this.diffusionPipeline.setParameters({
|
|
||||||
...settings,
|
...settings,
|
||||||
width: this.canvas.width,
|
};
|
||||||
height: this.canvas.height,
|
|
||||||
deltaTime,
|
[
|
||||||
time,
|
this.agentPipeline,
|
||||||
});
|
this.brushPipeline,
|
||||||
|
this.diffusionPipeline,
|
||||||
|
this.renderPipeline,
|
||||||
|
].forEach((pipeline) => pipeline.setParameters(params));
|
||||||
|
|
||||||
const commandEncoder = this.device.createCommandEncoder();
|
const commandEncoder = this.device.createCommandEncoder();
|
||||||
|
|
||||||
for (let i = 0; i < settings.renderSpeed; i++) {
|
for (let i = 0; i < settings.renderSpeed; i++) {
|
||||||
|
|
@ -172,7 +167,7 @@ export default class Renderer {
|
||||||
|
|
||||||
this.queue.submit([commandEncoder.finish()]);
|
this.queue.submit([commandEncoder.finish()]);
|
||||||
|
|
||||||
await sleep(200);
|
// await sleep(200);
|
||||||
requestAnimationFrame(this.render.bind(this));
|
requestAnimationFrame(this.render.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import './index.scss';
|
import Renderer from './game-loop/game-loop';
|
||||||
import Renderer from './renderer';
|
import './styles/index.scss';
|
||||||
import './utils/mulberry32';
|
|
||||||
|
|
||||||
const main = () => {
|
const main = () => {
|
||||||
const canvas = document.querySelector('canvas');
|
const canvas = document.querySelector('canvas');
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
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 shader from './agent.wgsl';
|
import shader from './agent.wgsl';
|
||||||
|
|
||||||
export class AgentPipeline {
|
export class AgentPipeline {
|
||||||
|
|
@ -54,34 +56,24 @@ export class AgentPipeline {
|
||||||
}
|
}
|
||||||
|
|
||||||
public setParameters({
|
public setParameters({
|
||||||
width,
|
canvasSize,
|
||||||
height,
|
|
||||||
trailWeight,
|
|
||||||
deltaTime,
|
deltaTime,
|
||||||
time,
|
time,
|
||||||
|
trailWeight,
|
||||||
moveSpeed,
|
moveSpeed,
|
||||||
turnSpeed,
|
turnSpeed,
|
||||||
sensorAngleDegrees,
|
sensorAngleDegrees,
|
||||||
sensorOffsetDst,
|
sensorOffsetDst,
|
||||||
}: {
|
}: CommonParameters & AgentSettings) {
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
trailWeight: number;
|
|
||||||
deltaTime: number;
|
|
||||||
time: number;
|
|
||||||
moveSpeed: number;
|
|
||||||
turnSpeed: number;
|
|
||||||
sensorAngleDegrees: number;
|
|
||||||
sensorOffsetDst: number;
|
|
||||||
}) {
|
|
||||||
this.device.queue.writeBuffer(
|
this.device.queue.writeBuffer(
|
||||||
this.uniforms,
|
this.uniforms,
|
||||||
0,
|
0,
|
||||||
new Float32Array([
|
new Float32Array([
|
||||||
width,
|
canvasSize[0],
|
||||||
height,
|
canvasSize[1],
|
||||||
trailWeight,
|
deltaTime,
|
||||||
time,
|
time,
|
||||||
|
trailWeight,
|
||||||
moveSpeed * deltaTime,
|
moveSpeed * deltaTime,
|
||||||
turnSpeed * deltaTime,
|
turnSpeed * deltaTime,
|
||||||
(sensorAngleDegrees * Math.PI) / 180,
|
(sensorAngleDegrees * Math.PI) / 180,
|
||||||
|
|
|
||||||
7
src/pipelines/agents/agent-settings.ts
Normal file
7
src/pipelines/agents/agent-settings.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface AgentSettings {
|
||||||
|
trailWeight: number;
|
||||||
|
moveSpeed: number;
|
||||||
|
turnSpeed: number;
|
||||||
|
sensorAngleDegrees: number;
|
||||||
|
sensorOffsetDst: number;
|
||||||
|
}
|
||||||
|
|
@ -5,12 +5,12 @@ struct Agent {
|
||||||
|
|
||||||
struct Settings {
|
struct Settings {
|
||||||
size: vec2<f32>,
|
size: vec2<f32>,
|
||||||
trailWeight : f32,
|
deltaTime : f32,
|
||||||
time : f32,
|
time : f32,
|
||||||
|
|
||||||
|
trailWeight : f32,
|
||||||
moveRate : f32,
|
moveRate : f32,
|
||||||
turnRate : f32,
|
turnRate : f32,
|
||||||
|
|
||||||
sensorAngle : f32,
|
sensorAngle : f32,
|
||||||
sensorOffsetDst : f32,
|
sensorOffsetDst : f32,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
import { CommonParameters } from '../common-parameters';
|
||||||
|
import { BrushSettings } from './brush-settings';
|
||||||
import shader from './brush.wgsl';
|
import shader from './brush.wgsl';
|
||||||
|
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
export class BrushPipeline {
|
export class BrushPipeline {
|
||||||
private static readonly UNIFORM_COUNT = 2;
|
private static readonly UNIFORM_COUNT = 4;
|
||||||
private static readonly MAX_LINE_COUNT = 100;
|
private static readonly MAX_LINE_COUNT = 100;
|
||||||
private static readonly VERTICES_PER_LINE_SEGMENT = 6;
|
private static readonly VERTICES_PER_LINE_SEGMENT = 6;
|
||||||
|
|
||||||
|
|
@ -85,8 +87,16 @@ export class BrushPipeline {
|
||||||
this.linePoints.length = 0;
|
this.linePoints.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setParameters({ width, height }: { width: number; height: number }) {
|
public setParameters({
|
||||||
this.device.queue.writeBuffer(this.uniforms, 0, new Float32Array([width, height]));
|
canvasSize,
|
||||||
|
deltaTime,
|
||||||
|
time,
|
||||||
|
}: CommonParameters & BrushSettings) {
|
||||||
|
this.device.queue.writeBuffer(
|
||||||
|
this.uniforms,
|
||||||
|
0,
|
||||||
|
new Float32Array([canvasSize[0], canvasSize[1], deltaTime, time])
|
||||||
|
);
|
||||||
|
|
||||||
this.device.queue.writeBuffer(
|
this.device.queue.writeBuffer(
|
||||||
this.vertexBuffer,
|
this.vertexBuffer,
|
||||||
|
|
|
||||||
1
src/pipelines/brush/brush-settings.ts
Normal file
1
src/pipelines/brush/brush-settings.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export interface BrushSettings {}
|
||||||
|
|
@ -12,7 +12,9 @@ fn vertex(
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Settings {
|
struct Settings {
|
||||||
size : vec2<f32>
|
size : vec2<f32>,
|
||||||
|
deltaTime : f32,
|
||||||
|
time : f32
|
||||||
};
|
};
|
||||||
|
|
||||||
@group(0) @binding(0) var<uniform> settings : Settings;
|
@group(0) @binding(0) var<uniform> settings : Settings;
|
||||||
|
|
|
||||||
7
src/pipelines/common-parameters.ts
Normal file
7
src/pipelines/common-parameters.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
|
export interface CommonParameters {
|
||||||
|
canvasSize: vec2;
|
||||||
|
deltaTime: number;
|
||||||
|
time: number;
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
struct Settings {
|
struct Settings {
|
||||||
size : vec2<f32>,
|
size : vec2<f32>,
|
||||||
diffusionRate : f32,
|
|
||||||
decayRate : f32,
|
|
||||||
deltaTime : f32,
|
deltaTime : f32,
|
||||||
time : f32,
|
time : f32,
|
||||||
|
|
||||||
|
diffusionRate : f32,
|
||||||
|
decayRate : f32,
|
||||||
swipeRadius : f32,
|
swipeRadius : f32,
|
||||||
swipeBlur : f32,
|
swipeBlur : f32,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { setUpFullScreenQuad } from '../../utils/full-screen-quad';
|
import { setUpFullScreenQuad } from '../../utils/full-screen-quad';
|
||||||
|
import { CommonParameters } from '../common-parameters';
|
||||||
import shader from './diffuse.wgsl';
|
import shader from './diffuse.wgsl';
|
||||||
|
import { DiffusionSettings } from './diffusion-settings';
|
||||||
|
|
||||||
export class DiffusionPipeline {
|
export class DiffusionPipeline {
|
||||||
private static readonly UNIFORM_COUNT = 16;
|
private static readonly UNIFORM_COUNT = 16;
|
||||||
|
|
@ -41,34 +43,24 @@ export class DiffusionPipeline {
|
||||||
}
|
}
|
||||||
|
|
||||||
public setParameters({
|
public setParameters({
|
||||||
width,
|
canvasSize,
|
||||||
height,
|
|
||||||
diffusionRate,
|
|
||||||
decayRate,
|
|
||||||
deltaTime,
|
deltaTime,
|
||||||
time,
|
time,
|
||||||
|
diffusionRate,
|
||||||
|
decayRate,
|
||||||
swipeRadius,
|
swipeRadius,
|
||||||
swipeBlur,
|
swipeBlur,
|
||||||
}: {
|
}: CommonParameters & DiffusionSettings) {
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
diffusionRate: number;
|
|
||||||
decayRate: number;
|
|
||||||
deltaTime: number;
|
|
||||||
time: number;
|
|
||||||
swipeRadius: number;
|
|
||||||
swipeBlur: number;
|
|
||||||
}) {
|
|
||||||
this.device.queue.writeBuffer(
|
this.device.queue.writeBuffer(
|
||||||
this.uniforms,
|
this.uniforms,
|
||||||
0,
|
0,
|
||||||
new Float32Array([
|
new Float32Array([
|
||||||
width,
|
canvasSize[0],
|
||||||
height,
|
canvasSize[1],
|
||||||
diffusionRate,
|
|
||||||
decayRate,
|
|
||||||
deltaTime,
|
deltaTime,
|
||||||
time,
|
time,
|
||||||
|
diffusionRate,
|
||||||
|
decayRate,
|
||||||
swipeRadius,
|
swipeRadius,
|
||||||
swipeBlur,
|
swipeBlur,
|
||||||
])
|
])
|
||||||
|
|
|
||||||
6
src/pipelines/diffusion/diffusion-settings.ts
Normal file
6
src/pipelines/diffusion/diffusion-settings.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface DiffusionSettings {
|
||||||
|
diffusionRate: number;
|
||||||
|
decayRate: number;
|
||||||
|
swipeRadius: number;
|
||||||
|
swipeBlur: number;
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
import { setUpFullScreenQuad } from '../../utils/full-screen-quad';
|
import { setUpFullScreenQuad } from '../../utils/full-screen-quad';
|
||||||
|
import { CommonParameters } from '../common-parameters';
|
||||||
|
import { RenderSettings } from './render-settings';
|
||||||
import shader from './render.wgsl';
|
import shader from './render.wgsl';
|
||||||
|
|
||||||
export class RenderPipeline {
|
export class RenderPipeline {
|
||||||
|
private static readonly UNIFORM_COUNT = 4;
|
||||||
|
|
||||||
private readonly pipeline: GPURenderPipeline;
|
private readonly pipeline: GPURenderPipeline;
|
||||||
|
private readonly uniforms: GPUBuffer;
|
||||||
private readonly quadVertexBuffer: GPUBuffer;
|
private readonly quadVertexBuffer: GPUBuffer;
|
||||||
|
|
||||||
private bindGroup?: GPUBindGroup;
|
private bindGroup?: GPUBindGroup;
|
||||||
|
|
@ -34,6 +39,23 @@ export class RenderPipeline {
|
||||||
topology: 'triangle-strip',
|
topology: 'triangle-strip',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.uniforms = this.device.createBuffer({
|
||||||
|
size: RenderPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
|
||||||
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setParameters({
|
||||||
|
canvasSize,
|
||||||
|
deltaTime,
|
||||||
|
time,
|
||||||
|
}: CommonParameters & RenderSettings) {
|
||||||
|
this.device.queue.writeBuffer(
|
||||||
|
this.uniforms,
|
||||||
|
0,
|
||||||
|
new Float32Array([canvasSize[0], canvasSize[1], deltaTime, time])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public execute(commandEncoder: GPUCommandEncoder, colorTexture: GPUTexture) {
|
public execute(commandEncoder: GPUCommandEncoder, colorTexture: GPUTexture) {
|
||||||
|
|
@ -64,13 +86,19 @@ export class RenderPipeline {
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
binding: 0,
|
binding: 0,
|
||||||
|
resource: {
|
||||||
|
buffer: this.uniforms,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
binding: 1,
|
||||||
resource: this.device.createSampler({
|
resource: this.device.createSampler({
|
||||||
magFilter: 'linear',
|
magFilter: 'linear',
|
||||||
minFilter: 'linear',
|
minFilter: 'linear',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
binding: 1,
|
binding: 2,
|
||||||
resource: colorTexture.createView(),
|
resource: colorTexture.createView(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
1
src/pipelines/render/render-settings.ts
Normal file
1
src/pipelines/render/render-settings.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export interface RenderSettings {}
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
@group(0) @binding(0) var mySampler: sampler;
|
struct Settings {
|
||||||
@group(0) @binding(1) var TargetTexture : texture_2d<f32>;
|
size : vec2<f32>,
|
||||||
|
deltaTime : f32,
|
||||||
|
time : f32,
|
||||||
|
};
|
||||||
|
|
||||||
|
@group(0) @binding(0) var<uniform> settings : Settings;
|
||||||
|
@group(0) @binding(1) var mySampler: sampler;
|
||||||
|
@group(0) @binding(2) var TargetTexture : texture_2d<f32>;
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||||
return vec4(textureSample(TargetTexture, mySampler, uv).rgb * 1.0, 1);
|
return vec4(textureSample(TargetTexture, mySampler, uv).r * 1.0, settings.deltaTime * 0.0, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,27 @@
|
||||||
interface Settings {
|
import { GameLoopSettings } from './game-loop/game-loop-settings';
|
||||||
agentCount: number;
|
import { AgentSettings } from './pipelines/agents/agent-settings';
|
||||||
renderSpeed: number;
|
import { BrushSettings } from './pipelines/brush/brush-settings';
|
||||||
startingRadius: number;
|
import { DiffusionSettings } from './pipelines/diffusion/diffusion-settings';
|
||||||
trailWeight: number;
|
import { RenderSettings } from './pipelines/render/render-settings';
|
||||||
decayRate: number;
|
|
||||||
diffusionRate: number;
|
|
||||||
moveSpeed: number;
|
|
||||||
turnSpeed: number;
|
|
||||||
sensorAngleDegrees: number;
|
|
||||||
sensorOffsetDst: number;
|
|
||||||
swipeRadius: number;
|
|
||||||
swipeBlur: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const settings: Settings = {
|
export const settings: GameLoopSettings &
|
||||||
|
AgentSettings &
|
||||||
|
BrushSettings &
|
||||||
|
DiffusionSettings &
|
||||||
|
RenderSettings = {
|
||||||
agentCount: 1_000,
|
agentCount: 1_000,
|
||||||
renderSpeed: 1,
|
renderSpeed: 1,
|
||||||
startingRadius: 0.15,
|
startingRadius: 0.15,
|
||||||
|
|
||||||
decayRate: 0.02,
|
|
||||||
diffusionRate: 0.8,
|
|
||||||
|
|
||||||
trailWeight: 5,
|
trailWeight: 5,
|
||||||
moveSpeed: 0.025,
|
moveSpeed: 0.025,
|
||||||
turnSpeed: 6,
|
turnSpeed: 6,
|
||||||
sensorAngleDegrees: 30,
|
sensorAngleDegrees: 30,
|
||||||
sensorOffsetDst: 0.025,
|
sensorOffsetDst: 0.025,
|
||||||
|
|
||||||
|
decayRate: 0.02,
|
||||||
|
diffusionRate: 0.8,
|
||||||
|
|
||||||
swipeRadius: 0.003,
|
swipeRadius: 0.003,
|
||||||
swipeBlur: 0.002,
|
swipeBlur: 0.002,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
25
src/styles/mixins.scss
Normal file
25
src/styles/mixins.scss
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
@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;
|
||||||
|
}
|
||||||
4
src/utils/clamp.ts
Normal file
4
src/utils/clamp.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const clamp = (value: number, min: number, max: number): number =>
|
||||||
|
Math.min(max, Math.max(min, value));
|
||||||
|
|
||||||
|
export const clamp01 = (value: number): number => Math.min(1, Math.max(0, value));
|
||||||
48
src/utils/handle-full-screen.ts
Normal file
48
src/utils/handle-full-screen.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
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);
|
||||||
|
};
|
||||||
3
src/utils/last.ts
Normal file
3
src/utils/last.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function last<T>(a: Array<T>): T | null {
|
||||||
|
return a.length > 0 ? a[a.length - 1] : null;
|
||||||
|
}
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
export const mulberry32 = (seed) => () => {
|
|
||||||
let t = (seed += 0x6d2b79f5);
|
|
||||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
||||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
||||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
||||||
};
|
|
||||||
|
|
||||||
Math.random = mulberry32(123);
|
|
||||||
4
src/utils/pretty-print.ts
Normal file
4
src/utils/pretty-print.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const prettyPrint = (o: any): string =>
|
||||||
|
JSON.stringify(o, (_, v) => (v?.toFixed ? Number(v.toFixed(3)) : v), ' ')
|
||||||
|
.replace(/("|,|{|^\n)/g, '')
|
||||||
|
.replace(/(\W*}\n?)+/g, '\n\n');
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export const randomBetween = (min: number, max: number) => {
|
|
||||||
return Math.random() * (max - min) + min;
|
|
||||||
};
|
|
||||||
20
src/utils/random.ts
Normal file
20
src/utils/random.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
export abstract class Random {
|
||||||
|
// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript, Mulberry32
|
||||||
|
|
||||||
|
private static _seed = 42;
|
||||||
|
|
||||||
|
public static set seed(value: number) {
|
||||||
|
Random._seed = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getRandom(): number {
|
||||||
|
let t = (Random._seed += 0x6d2b79f5);
|
||||||
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||||
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||||
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static randomBetween(from: number, to: number): number {
|
||||||
|
return from + Random.getRandom() * (to - from);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue