diff --git a/assets/fonts/comfortaa-v40-latin-regular.woff b/assets/fonts/comfortaa-v40-latin-regular.woff new file mode 100644 index 0000000..c54393b Binary files /dev/null and b/assets/fonts/comfortaa-v40-latin-regular.woff differ diff --git a/assets/fonts/comfortaa-v40-latin-regular.woff2 b/assets/fonts/comfortaa-v40-latin-regular.woff2 new file mode 100644 index 0000000..bc4da8b Binary files /dev/null and b/assets/fonts/comfortaa-v40-latin-regular.woff2 differ diff --git a/assets/fonts/open-sans-v34-latin-regular.woff b/assets/fonts/open-sans-v34-latin-regular.woff new file mode 100644 index 0000000..b083626 Binary files /dev/null and b/assets/fonts/open-sans-v34-latin-regular.woff differ diff --git a/assets/fonts/open-sans-v34-latin-regular.woff2 b/assets/fonts/open-sans-v34-latin-regular.woff2 new file mode 100644 index 0000000..15339ea Binary files /dev/null and b/assets/fonts/open-sans-v34-latin-regular.woff2 differ diff --git a/assets/icons/info.svg b/assets/icons/info.svg new file mode 100644 index 0000000..a047bf5 --- /dev/null +++ b/assets/icons/info.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/static/maximize.svg b/assets/icons/maximize.svg similarity index 55% rename from static/maximize.svg rename to assets/icons/maximize.svg index 7118488..9c8558e 100644 --- a/static/maximize.svg +++ b/assets/icons/maximize.svg @@ -1,4 +1,4 @@ - + diff --git a/static/minimize.svg b/assets/icons/minimize.svg similarity index 55% rename from static/minimize.svg rename to assets/icons/minimize.svg index 93a212a..05d54a8 100644 --- a/static/minimize.svg +++ b/assets/icons/minimize.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/restart.svg b/assets/icons/restart.svg new file mode 100644 index 0000000..f87e22b --- /dev/null +++ b/assets/icons/restart.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/no-change/404.html b/assets/no-change/404.html new file mode 100644 index 0000000..7c62e5c --- /dev/null +++ b/assets/no-change/404.html @@ -0,0 +1,43 @@ + + + + + + Not found + + + + + +
+

Page not found.

+ Go back +
+ + diff --git a/static/no-change/robots.txt b/assets/no-change/robots.txt similarity index 100% rename from static/no-change/robots.txt rename to assets/no-change/robots.txt diff --git a/src/game-loop/game-loop.ts b/src/game-loop/game-loop.ts index 0221917..833c632 100644 --- a/src/game-loop/game-loop.ts +++ b/src/game-loop/game-loop.ts @@ -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 = 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 { requestAnimationFrame(this.render.bind(this)); + return this.hasFinishedPromise; } private onSwipe(event: MouseEvent) { @@ -112,24 +124,11 @@ export default class GameLoop { }); } - private async initializeDevice(): Promise { - 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(); + } } diff --git a/src/index.html b/src/index.html index ed9b4a4..45c0470 100644 --- a/src/index.html +++ b/src/index.html @@ -30,9 +30,37 @@ - - -
+
+ + +
+
+            
+          
+
+ + +
+ + diff --git a/src/index.scss b/src/index.scss new file mode 100644 index 0000000..4e588f1 --- /dev/null +++ b/src/index.scss @@ -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; + } + } + } +} diff --git a/src/index.ts b/src/index.ts index 7abbbcf..66da258 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { @@ -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 = () => { - try { - const canvas = document.querySelector('canvas'); - const game = new GameLoop(canvas); - game.start(); - } catch (e) { - errorContainer.innerHTML = e.message; + 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 { + game = new GameLoop(elements.canvas, gpu); + await game.start(); + } catch (e) { + elements.errorContainer.innerHTML = e.message; + } } }; diff --git a/src/pipelines/agents/agent-pipeline.ts b/src/pipelines/agents/agent-pipeline.ts index 42e5d01..4b21465 100644 --- a/src/pipelines/agents/agent-pipeline.ts +++ b/src/pipelines/agents/agent-pipeline.ts @@ -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(); + } } diff --git a/src/pipelines/brush/brush-pipeline.ts b/src/pipelines/brush/brush-pipeline.ts index ada27d0..9707485 100644 --- a/src/pipelines/brush/brush-pipeline.ts +++ b/src/pipelines/brush/brush-pipeline.ts @@ -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 = []; private previousPoints: Array = []; 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 = ( diff --git a/src/pipelines/diffusion/diffuse.wgsl b/src/pipelines/diffusion/diffuse.wgsl index 154cd45..2ecb89f 100644 --- a/src/pipelines/diffusion/diffuse.wgsl +++ b/src/pipelines/diffusion/diffuse.wgsl @@ -29,15 +29,14 @@ fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { 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)); } diff --git a/src/pipelines/diffusion/diffusion-pipeline.ts b/src/pipelines/diffusion/diffusion-pipeline.ts index fe1fe48..c2dda95 100644 --- a/src/pipelines/diffusion/diffusion-pipeline.ts +++ b/src/pipelines/diffusion/diffusion-pipeline.ts @@ -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(); + } } diff --git a/src/pipelines/render/render-pipeline.ts b/src/pipelines/render/render-pipeline.ts index a0384d3..44b7be3 100644 --- a/src/pipelines/render/render-pipeline.ts +++ b/src/pipelines/render/render-pipeline.ts @@ -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(); + } } diff --git a/src/style/fonts.scss b/src/style/fonts.scss new file mode 100644 index 0000000..2b02a7b --- /dev/null +++ b/src/style/fonts.scss @@ -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+ */ +} diff --git a/src/style/mixins.scss b/src/style/mixins.scss new file mode 100644 index 0000000..1f18d08 --- /dev/null +++ b/src/style/mixins.scss @@ -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); + } +} diff --git a/src/style/vars.scss b/src/style/vars.scss new file mode 100644 index 0000000..2a4c1a0 --- /dev/null +++ b/src/style/vars.scss @@ -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); +} diff --git a/src/styles/index.scss b/src/styles/index.scss deleted file mode 100644 index 3a6e755..0000000 --- a/src/styles/index.scss +++ /dev/null @@ -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; -} diff --git a/src/styles/mixins.scss b/src/styles/mixins.scss deleted file mode 100644 index 207b151..0000000 --- a/src/styles/mixins.scss +++ /dev/null @@ -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; -} diff --git a/src/utils/graphics/full-screen-quad/full-screen-quad.ts b/src/utils/graphics/full-screen-quad/full-screen-quad.ts index 1afd862..0858a5a 100644 --- a/src/utils/graphics/full-screen-quad/full-screen-quad.ts +++ b/src/utils/graphics/full-screen-quad/full-screen-quad.ts @@ -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 = ( diff --git a/src/utils/graphics/noise/noise.ts b/src/utils/graphics/noise/noise.ts index 05ece8b..656e83c 100644 --- a/src/utils/graphics/noise/noise.ts +++ b/src/utils/graphics/noise/noise.ts @@ -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(); + export const generateNoise = ({ device, width = 1024, @@ -19,67 +21,71 @@ export const generateNoise = ({ lacunarity?: number; amplitude?: number; gain?: number; -}) => { - const { buffer, vertex } = setUpFullScreenQuad(device); - const quadVertexBuffer = buffer; +}): 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; - const pipeline = device.createRenderPipeline({ - layout: 'auto', - vertex, - fragment: { - module: smartCompile(device, noise), - entryPoint: 'fragment', - constants: { - octaves, - lacunarity, - amplitude, - gain, - seedR: Random.getRandom(), - seedG: Random.getRandom(), - seedB: Random.getRandom(), - seedA: Random.getRandom(), + const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex, + fragment: { + module: smartCompile(device, noise), + entryPoint: 'fragment', + constants: { + octaves, + lacunarity, + amplitude, + gain, + seedR: Random.getRandom(), + seedG: Random.getRandom(), + seedB: Random.getRandom(), + seedA: Random.getRandom(), + }, + targets: [ + { + format: 'rgba16float', + }, + ], }, - targets: [ + primitive: { + topology: 'triangle-strip', + }, + }); + + const colorTexture = device.createTexture({ + size: { + width, + height, + depthOrArrayLayers: 1, + }, + format: 'rgba16float', + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ { - format: 'rgba16float', + view: colorTexture.createView(), + clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', }, ], - }, - primitive: { - topology: 'triangle-strip', - }, - }); + }; - const colorTexture = device.createTexture({ - size: { - width, - height, - depthOrArrayLayers: 1, - }, - format: 'rgba16float', - usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT, - }); + const commandEncoder = device.createCommandEncoder(); - const renderPassDescriptor: GPURenderPassDescriptor = { - colorAttachments: [ - { - view: colorTexture.createView(), - clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, - loadOp: 'clear', - storeOp: 'store', - }, - ], - }; + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(pipeline); + passEncoder.setVertexBuffer(0, quadVertexBuffer); + passEncoder.draw(4, 1); + passEncoder.end(); - const commandEncoder = device.createCommandEncoder(); + device.queue.submit([commandEncoder.finish()]); + textureCache.set(cacheKey, colorTexture); + } - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); - passEncoder.setPipeline(pipeline); - passEncoder.setVertexBuffer(0, quadVertexBuffer); - passEncoder.draw(4, 1); - passEncoder.end(); - - device.queue.submit([commandEncoder.finish()]); - - return colorTexture; + return textureCache.get(cacheKey).createView(); }; diff --git a/src/utils/handle-full-screen.ts b/src/utils/handle-full-screen.ts index 8fc7fd1..db91163 100644 --- a/src/utils/handle-full-screen.ts +++ b/src/utils/handle-full-screen.ts @@ -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)); }; diff --git a/src/utils/webgpu/initialize-gpu.ts b/src/utils/webgpu/initialize-gpu.ts new file mode 100644 index 0000000..54a63ef --- /dev/null +++ b/src/utils/webgpu/initialize-gpu.ts @@ -0,0 +1,16 @@ +export const initializeGPU = async (): Promise => { + 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 +}; diff --git a/src/utils/smart-compile.ts b/src/utils/webgpu/smart-compile.ts similarity index 100% rename from src/utils/smart-compile.ts rename to src/utils/webgpu/smart-compile.ts diff --git a/webpack.config.js b/webpack.config.js index 55462d4..db7a51d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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,